aboutsummaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
authorNicolas James <Eele1Ephe7uZahRie@tutanota.com>2025-02-12 21:57:46 +1100
committerNicolas James <Eele1Ephe7uZahRie@tutanota.com>2025-02-12 21:57:46 +1100
commite4483eca01b48b943cd0461e24a74ae1a3139ed4 (patch)
treeed58c3c246e3af1af337697695d780aa31f6ad9a /src/client
parent1cc08c51eb4b0f95c30c0a98ad1fc5ad3459b2df (diff)
Update to most recent version (old initial commit)
Diffstat (limited to 'src/client')
-rw-r--r--src/client/CMakeLists.txt60
-rw-r--r--src/client/client.cc690
-rw-r--r--src/client/client.hh21
-rw-r--r--src/client/draw.hh59
-rw-r--r--src/client/entity/animate.cc158
-rw-r--r--src/client/entity/animate.hh71
-rw-r--r--src/client/entity/entity.cc6
-rw-r--r--src/client/entity/entity.hh27
-rw-r--r--src/client/entity/moveable.cc86
-rw-r--r--src/client/entity/moveable.hh29
-rw-r--r--src/client/entity/player.cc54
-rw-r--r--src/client/entity/player.hh73
-rw-r--r--src/client/item/block.cc91
-rw-r--r--src/client/item/block.hh58
-rw-r--r--src/client/item/item.cc1
-rw-r--r--src/client/item/item.hh30
-rw-r--r--src/client/item/items.cc16
-rw-r--r--src/client/item/items.hh17
-rw-r--r--src/client/main.cc90
-rw-r--r--src/client/main.hh23
-rw-r--r--src/client/movement.hh23
-rw-r--r--src/client/movement/movement.cc (renamed from src/client/movement.cc)95
-rw-r--r--src/client/movement/movement.hh27
-rw-r--r--src/client/player.cc58
-rw-r--r--src/client/player.hh64
-rw-r--r--src/client/render/draw.cc (renamed from src/client/draw.cc)250
-rw-r--r--src/client/render/draw.hh30
-rw-r--r--src/client/render/render.cc147
-rw-r--r--src/client/render/render.hh24
-rw-r--r--src/client/render/struct.cc1
-rw-r--r--src/client/render/struct.hh29
-rw-r--r--src/client/settings.cc36
-rw-r--r--src/client/settings.hh23
-rw-r--r--src/client/shared.cc17
-rw-r--r--src/client/shared.hh24
-rw-r--r--src/client/state/chunks.cc1
-rw-r--r--src/client/state/chunks.hh18
-rw-r--r--src/client/state/entities.cc1
-rw-r--r--src/client/state/entities.hh21
-rw-r--r--src/client/state/state.cc15
-rw-r--r--src/client/state/state.hh41
-rw-r--r--src/client/window.cc449
-rw-r--r--src/client/window/basic_window.cc33
-rw-r--r--src/client/window/basic_window.hh52
-rw-r--r--src/client/window/button_window.cc71
-rw-r--r--src/client/window/button_window.hh40
-rw-r--r--src/client/window/hud_window.cc88
-rw-r--r--src/client/window/hud_window.hh32
-rw-r--r--src/client/window/inventory_window.cc196
-rw-r--r--src/client/window/inventory_window.hh40
-rw-r--r--src/client/window/text_input_window.cc81
-rw-r--r--src/client/window/text_input_window.hh43
-rw-r--r--src/client/window/window.cc318
-rw-r--r--src/client/window/window.hh (renamed from src/client/window.hh)16
-rw-r--r--src/client/world.cc429
-rw-r--r--src/client/world/block.cc117
-rw-r--r--src/client/world/block.hh53
-rw-r--r--src/client/world/chunk.cc255
-rw-r--r--src/client/world/chunk.hh (renamed from src/client/world.hh)44
-rw-r--r--src/client/world/world.datbin0 -> 20480 bytes
60 files changed, 3354 insertions, 1608 deletions
diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt
new file mode 100644
index 0000000..e6994ab
--- /dev/null
+++ b/src/client/CMakeLists.txt
@@ -0,0 +1,60 @@
+cmake_minimum_required(VERSION 3.18)
+
+project(blockgame_linux)
+
+file (GLOB_RECURSE SOURCE_FILES CONFIGURE_DEPENDS
+ "*.cc"
+)
+file (GLOB_RECURSE HEADER_FILES CONFIGURE_DEPENDS
+ "*.hh"
+ "../server/*.hh"
+ "../shared/*.hh"
+)
+add_executable(${PROJECT_NAME}
+ ${SOURCE_FILES}
+)
+
+find_library(LIB_SDL2 sdl2 SDL2 REQUIRED)
+find_library(LIB_EPOXY epoxy epoxy EPOXY libepoxy REQUIRED)
+find_library(LIB_SQLITE3 sqlite3 SQLITE3 REQUIRED)
+find_library(LIB_FREETYPE freetype libfreetype REQUIRED)
+find_library(LIB_ASSIMP assimp libassimp REQUIRED)
+find_library(LIB_PROTOBUF protobuf libprotobuf REQUIRED)
+find_package(Boost COMPONENTS iostreams REQUIRED)
+find_package(Threads REQUIRED)
+find_package(Backtrace REQUIRED)
+find_package(Freetype REQUIRED)
+
+target_compile_options(${PROJECT_NAME} PRIVATE
+ -Wall -Wextra -Wshadow -Wdouble-promotion -Wformat=2 -Wundef -fno-common
+ -Wconversion -Wpedantic -std=c++20 -O2
+ -Wno-exceptions
+ -Wno-missing-field-initializers -Wno-unknown-pragmas
+)
+target_compile_options(${PROJECT_NAME} PRIVATE
+ -fstack-protector-strong -fno-omit-frame-pointer #-fsanitize=undefined -fsanitize-trap=undefined
+)
+target_link_options(${PROJECT_NAME} PRIVATE
+ -fstack-protector-strong #-fsanitize=undefined -fsanitize-trap=undefined
+)
+target_include_directories(${PROJECT_NAME} PRIVATE
+ "../../src"
+ ${FREETYPE_INCLUDE_DIRS}
+)
+target_link_libraries(${PROJECT_NAME} PRIVATE
+ ${LIB_SDL2}
+ ${LIB_EPOXY}
+ ${LIB_FREETYPE}
+ ${LIB_ASSIMP}
+ ${LIB_PROTOBUF}
+ ${LIB_SQLITE3}
+ ${Backtrace_LIBRARIES}
+ ${Threads_LIBRARIES}
+ ${Boost_LIBRARIES}
+ ${FREETYPE_LIBRARIES}
+ shared
+ server
+)
+target_precompile_headers(${PROJECT_NAME} PRIVATE
+ ${HEADER_FILES}
+)
diff --git a/src/client/client.cc b/src/client/client.cc
index b4cacdc..28b3163 100644
--- a/src/client/client.cc
+++ b/src/client/client.cc
@@ -1,29 +1,29 @@
#include "client.hh"
namespace {
-bool should_send_move = false; // Ratelimit move packets by player packets.
+std::queue<proto::chunk> received_chunks;
-shared::world::block last_block = shared::world::block::type::dirt;
-
-client::world::chunk::map chunks{4096, shared::world::chunk::hash,
- shared::world::chunk::equal};
-
-client::players players;
} // namespace
namespace client {
-static auto get_player_it(const std::uint32_t index) {
- return std::ranges::find_if(
- ::players, [&](const auto& player) { return player.index == index; });
+static shared::net::connection make_connection(const std::string_view address,
+ const std::string_view port) {
+ constexpr addrinfo hints = {.ai_flags = AI_PASSIVE,
+ .ai_family = AF_INET,
+ .ai_socktype = SOCK_STREAM};
+ const auto info = shared::net::get_addr_info(address, port, &hints);
+ const auto sock = shared::net::make_socket(info.get());
+ shared::net::connect_socket(sock, info.get());
+ return shared::net::connection(sock);
}
static proto::packet
-make_get_chunk_packet(const shared::math::coords& coords) noexcept {
+make_request_chunk_packet(const shared::math::coords& coords) noexcept {
proto::packet packet;
const auto chunk_packet = packet.mutable_request_chunk_packet();
- shared::net::set_coords(*chunk_packet->mutable_chunk_pos(), coords);
+ shared::net::set_coords(chunk_packet->mutable_chunk_pos(), coords);
return packet;
}
@@ -39,14 +39,16 @@ static proto::packet make_auth_packet(const std::string& username,
return packet;
}
-static proto::packet make_add_block_packet(const shared::math::coords& coords,
- const glm::ivec3& pos) noexcept {
+static proto::packet
+make_add_block_packet(const shared::math::coords& coords, const glm::ivec3& pos,
+ const std::uint32_t& active) noexcept {
proto::packet packet;
- const auto interact_packet = packet.mutable_add_block_packet();
- shared::net::set_coords(*interact_packet->mutable_chunk_pos(), coords);
- shared::net::set_ivec3(*interact_packet->mutable_block_pos(), pos);
- interact_packet->set_block(static_cast<std::uint32_t>(::last_block.type));
+ const auto add_block_packet = packet.mutable_add_block_packet();
+ shared::net::set_coords(add_block_packet->mutable_chunk_pos(), coords);
+ shared::net::set_ivec3(add_block_packet->mutable_block_pos(), pos);
+
+ add_block_packet->set_active_item(active);
return packet;
}
@@ -56,9 +58,9 @@ make_remove_block_packet(const shared::math::coords& coords,
const glm::ivec3& pos) noexcept {
proto::packet packet;
- const auto interact_packet = packet.mutable_remove_block_packet();
- shared::net::set_coords(*interact_packet->mutable_chunk_pos(), coords);
- shared::net::set_ivec3(*interact_packet->mutable_block_pos(), pos);
+ const auto remove_block_packet = packet.mutable_remove_block_packet();
+ shared::net::set_coords(remove_block_packet->mutable_chunk_pos(), coords);
+ shared::net::set_ivec3(remove_block_packet->mutable_block_pos(), pos);
return packet;
}
@@ -69,93 +71,113 @@ make_remove_chunk_packet(const shared::math::coords& coords) noexcept {
proto::packet packet;
const auto remove_chunk_packet = packet.mutable_remove_chunk_packet();
- shared::net::set_coords(*remove_chunk_packet->mutable_chunk_pos(), coords);
+ shared::net::set_coords(remove_chunk_packet->mutable_chunk_pos(), coords);
+
+ return packet;
+}
+
+static proto::packet
+make_move_packet(const client::moveable& localplayer) noexcept {
+ proto::packet packet;
+
+ const auto move_packet = packet.mutable_move_packet();
+ move_packet->set_commands(localplayer.get_commands());
+ shared::net::set_angles(move_packet->mutable_viewangles(),
+ localplayer.get_angles());
+ move_packet->set_active_item(localplayer.get_active_item());
+ move_packet->set_sequence(localplayer.get_latest_sequence());
return packet;
}
static void handle_init_packet(const proto::init& packet) noexcept {
- client::state.seed = packet.seed();
- client::state.draw_distance =
- std::min(packet.draw_distance(),
- client::settings::get({"video", "draw_distance"}, 32));
- auto& localplayer = packet.localplayer();
- client::state.localplayer = localplayer.index();
+ // Draw distance is the std::min, but we make it more verbose.
+ if (const auto& wanted = state::draw_distance =
+ settings::get({"video", "draw_distance"}, 32);
+ wanted != packet.draw_distance()) {
+
+ if (wanted < packet.draw_distance()) {
+ state::draw_distance = wanted;
+ } else {
+ shared::print::warn << shared::print::time
+ << "client: server supported draw_distance ("
+ << packet.draw_distance()
+ << ") less than requested (" << wanted << ")\n";
+ state::draw_distance = packet.draw_distance();
+ }
+ }
+
+ state::seed = packet.seed();
+ state::tickrate = packet.tickrate();
+ state::tick = packet.tick();
+ state::delta_ticks = 0.0f; // amount of time passed (in ticks)
- ::players.emplace_back(shared::net::get_player(localplayer));
+ const auto& localplayer = packet.localplayer();
+ const auto& index = localplayer.animate().entity().index();
+
+ state::localplayer_index = index;
+ state::entities.emplace(index, std::make_unique<player>(localplayer));
}
-static void handle_player_packet(const proto::player& packet) noexcept {
- if (std::size(::players) <= 0) {
+static void
+handle_animate_update_packet(const proto::animate_update& packet) noexcept {
+ const auto& animate = packet.animate();
+ const auto& index = animate.entity().index();
+
+ const auto entity_it = get_entity_it(index);
+ if (entity_it == std::end(state::entities)) {
+ state::entities.emplace(index, std::make_unique<player>(animate));
return;
}
- const auto player_it = get_player_it(packet.index());
+ const auto animate_ptr = dynamic_cast<class animate*>(&*entity_it->second);
+ if (animate_ptr == nullptr) {
+ return;
+ }
- shared::player player = shared::net::get_player(packet);
+ const shared::tick_t tick = packet.tick();
- if (player_it == std::end(::players)) {
- ::players.emplace_back(player);
+ // Localplayer updates time factor and is update is called with the sequence
+ // number instead of the tick.
+ if (packet.has_sequence()) {
+ const shared::tick_t sequence = packet.sequence();
+ animate_ptr->update_time_factor(sequence, tick);
+ animate_ptr->notify(animate, sequence, true);
return;
}
- // If the player packet refers to us, we do not override our localplayer's
- // commands or viewangles. Also, we should send a new move packet.
- if (auto& lp = get_localplayer(::players); lp.index == packet.index()) {
- player.viewangles = lp.viewangles;
- player.commands = 0u;
- ::should_send_move = true;
- }
- player_it->update(player);
+ animate_ptr->notify(animate, tick, true);
}
// Remove the client whose element is equal to pkt.index.
-static void handle_remove_packet(const proto::remove_player& packet) noexcept {
- const auto player_it = get_player_it(packet.index());
- if (player_it == std::end(::players)) {
+static void
+handle_remove_entity_packet(const proto::remove_entity& packet) noexcept {
+ const auto entity_it = get_entity_it(packet.index());
+ if (entity_it == std::end(state::entities)) {
return;
}
- ::players.erase(player_it);
+
+ state::entities.erase(entity_it);
}
static void handle_hear_packet(const proto::hear_player& packet) noexcept {
- const auto player_it = get_player_it(packet.index());
- if (player_it == std::end(::players)) {
+ const auto entity_it = get_entity_it(packet.index());
+ if (entity_it == std::end(state::entities)) {
return;
}
- player_it->message.emplace(packet.text());
-}
-static void handle_chunk_packet(const proto::chunk& packet) noexcept {
- const shared::math::coords pos{.x = packet.chunk_pos().x(),
- .z = packet.chunk_pos().z()};
- const auto find_it = ::chunks.find(pos);
- if (find_it == std::end(::chunks)) {
+ const auto player_ptr = dynamic_cast<player*>(&*entity_it->second);
+ if (player_ptr == nullptr) {
return;
}
- find_it->second.emplace(
- client::state.seed, pos,
- shared::world::chunk::make_blocks_from_chunk(packet));
- // Force the surrounding chunks to regenerate their vbos.
- // It's possible we could only generate the ones that we actually need to
- // generate, but that's not really a priority atm.
- for (auto x = -1; x <= 1; ++x) {
- for (auto z = -1; z <= 1; ++z) {
- if (std::abs(x) == 1 && std::abs(z) == 1) {
- continue;
- }
- const auto find_update_it =
- ::chunks.find(pos + shared::math::coords{x, z});
- if (find_update_it == std::end(::chunks)) {
- continue;
- }
- if (!find_update_it->second.has_value()) {
- continue;
- }
- find_update_it->second->should_regenerate_vbo = true;
- }
- }
+
+ player_ptr->message.emplace(packet.text());
+}
+
+static void handle_chunk_packet(proto::chunk& packet) noexcept {
+ // Ratelimited parsing, moved into a vector.
+ ::received_chunks.push(std::move(packet));
}
static void
@@ -165,81 +187,111 @@ handle_server_message_packet(const proto::server_message& packet) noexcept {
"client: received " + std::string{fatal ? "fatal" : ""} +
" message from the server \"" + packet.message() + "\"\n";
if (!fatal) {
- shared::print::notify(message);
+ shared::print::notify << shared::print::time << message;
return;
}
- shared::print::warn(message);
+ shared::print::warn << shared::print::time << message;
shared::should_exit = true;
}
-static void parse_packet(const proto::packet& packet) noexcept {
-
+static void parse_packet(proto::packet&& packet) noexcept {
if (packet.has_init_packet()) {
handle_init_packet(packet.init_packet());
- } else if (packet.has_player_packet()) {
- handle_player_packet(packet.player_packet());
- } else if (packet.has_remove_player_packet()) {
- handle_remove_packet(packet.remove_player_packet());
+ } else if (packet.has_animate_update_packet()) {
+ handle_animate_update_packet(packet.animate_update_packet());
+ } else if (packet.has_remove_entity_packet()) {
+ handle_remove_entity_packet(packet.remove_entity_packet());
} else if (packet.has_hear_player_packet()) {
handle_hear_packet(packet.hear_player_packet());
} else if (packet.has_chunk_packet()) {
- handle_chunk_packet(packet.chunk_packet());
+ handle_chunk_packet(*packet.mutable_chunk_packet());
} else if (packet.has_server_message_packet()) {
handle_server_message_packet(packet.server_message_packet());
}
#ifndef NDEBUG
else {
- shared::print::warn("client: unhandled packet type\n");
+ shared::print::warn << shared::print::time
+ << "client: unhandled packet type\n";
}
#endif
}
-static shared::net::connection make_connection(const std::string_view address,
- const std::string_view port) {
- constexpr addrinfo hints = {.ai_flags = AI_PASSIVE,
- .ai_family = AF_INET,
- .ai_socktype = SOCK_STREAM};
- const auto info = shared::net::get_addr_info(address, port, &hints);
- const auto sock = shared::net::make_socket(info.get());
- shared::net::connect_socket(sock, info.get());
- return shared::net::connection(sock);
+static void regenerate_surrounding_chunks(const shared::math::coords& pos) {
+ // Force the surrounding chunks to regenerate their vbos.
+ // It's possible we could only generate the ones that we actually need
+ // to generate, but that's not really a priority atm.
+ for (auto x = -1; x <= 1; ++x) {
+ for (auto z = -1; z <= 1; ++z) {
+ if (std::abs(x) == 1 && std::abs(z) == 1) {
+ continue;
+ }
+ const auto find_update_it =
+ state::chunks.find(pos + shared::math::coords{x, z});
+ if (find_update_it == std::end(state::chunks)) {
+ continue;
+ }
+ if (!find_update_it->second.has_value()) {
+ continue;
+ }
+ find_update_it->second->should_regenerate_vbo = true;
+ }
+ }
}
-static void update_chunks(shared::net::connection& connection) noexcept {
+static void parse_new_chunks() {
+ // Add new chunks. We have to ratelimit these because the server responds
+ // so quickly that our frametime dives as it parses these new chunk packets.
+ constexpr int CHUNKS_PER_FRAME = 1;
+ for (int i = 0; i < CHUNKS_PER_FRAME && !::received_chunks.empty();
+ ++i, ::received_chunks.pop()) {
- const auto draw_distance = client::state.draw_distance;
- const shared::math::coords& lp_pos = get_localplayer(::players).chunk_pos;
+ const proto::chunk& packet = ::received_chunks.front();
+ const shared::math::coords pos{packet.chunk_pos().x(),
+ packet.chunk_pos().z()};
- // Remove bad chunks.
- std::erase_if(::chunks, [&](const auto& chunk) {
- const bool should_erase =
- !shared::math::is_inside_draw(chunk.first, lp_pos, draw_distance);
+ const auto find_it = state::chunks.find(pos);
+ if (find_it == std::end(state::chunks)) {
+ continue;
+ }
+ find_it->second.emplace(state::seed, packet);
+ regenerate_surrounding_chunks(pos);
+ }
+}
+
+static void erase_bad_chunks(const shared::math::coords& lp_pos) {
+ const auto draw_distance = state::draw_distance;
+ std::erase_if(state::chunks, [&](const auto& chunk) {
+ const bool should_erase = !shared::math::coords::is_inside_draw(
+ chunk.first, lp_pos, draw_distance);
if (should_erase) {
- connection.rsend_packet(make_remove_chunk_packet(chunk.first));
+ state::connection->rsend_packet(
+ make_remove_chunk_packet(chunk.first));
}
return should_erase;
});
+}
- for (int dist = 0; dist <= client::state.draw_distance; ++dist) {
+static void request_new_chunks(const shared::math::coords& lp_pos) {
+ for (int dist = 0; dist <= state::draw_distance; ++dist) {
+ const auto draw_distance = state::draw_distance;
const auto maybe_add_chunk = [&](const int x, const int z) -> bool {
const auto pos = shared::math::coords{x + lp_pos.x, z + lp_pos.z};
- if (!is_inside_draw(pos, lp_pos, draw_distance)) {
+ if (!shared::math::coords::is_inside_draw(pos, lp_pos,
+ draw_distance)) {
return false;
}
- if (::chunks.contains(pos)) {
+ if (state::chunks.contains(pos)) {
return false;
}
- connection.rsend_packet(make_get_chunk_packet(pos));
- ::chunks.emplace(pos, std::nullopt);
+ state::connection->rsend_packet(make_request_chunk_packet(pos));
+ state::chunks.emplace(pos, std::nullopt);
return true;
};
int x = -dist;
int z = dist;
-
- // Does a spiral pattern, but it's basically unnoticable :(
for (int i = 0; i < dist * 2 + 1; ++i) {
if (maybe_add_chunk(x, z)) {
return;
@@ -267,125 +319,162 @@ static void update_chunks(shared::net::connection& connection) noexcept {
}
}
-static void update_state() noexcept {
- client::state.player_count = ::players.size();
- client::state.requested_chunk_count =
- static_cast<std::uint32_t>(std::ranges::count_if(
- ::chunks, [](const auto& c) { return !c.second.has_value(); }));
- client::state.networked_chunk_count =
- std::size(::chunks) - client::state.requested_chunk_count;
-}
-
-static proto::packet make_say_packet(const std::string& text) noexcept {
- proto::packet packet;
-
- const auto sub_say_packet = packet.mutable_say_packet();
- sub_say_packet->set_text(text);
+static void update_chunks() {
+ parse_new_chunks();
- return packet;
-}
-
-// unfortunate non-static :(
-void send_say_packet(const std::string& text) noexcept {
- client::state.connection->rsend_packet(make_say_packet(text));
+ const shared::math::coords& lp_pos = get_localplayer().get_chunk_pos();
+ erase_bad_chunks(lp_pos);
+ request_new_chunks(lp_pos);
}
static void handle_button_input() noexcept {
- if (input::is_key_pressed(SDLK_z)) {
- client::render::camera::get_xfov() = 30.0f;
- } else {
- client::render::camera::get_xfov() =
- client::settings::get({"gameplay", "fov"}, 100.0f);
- }
// Don't build our movement commands if we're inputting text.
if (input::state.typing) {
return;
}
- auto& lp = get_localplayer(::players);
-
using spm = shared::player::mask;
- lp.commands |= spm::forward * input::is_key_pressed(SDLK_w);
- lp.commands |= spm::left * input::is_key_pressed(SDLK_a);
- lp.commands |= spm::backward * input::is_key_pressed(SDLK_s);
- lp.commands |= spm::right * input::is_key_pressed(SDLK_d);
- lp.commands |= spm::jump * input::is_key_pressed(SDLK_SPACE);
- lp.commands |= spm::crouch * input::is_key_pressed(SDLK_LCTRL);
- lp.commands |= spm::sprint * input::is_key_pressed(SDLK_LSHIFT);
- lp.commands |= spm::attack * input::is_key_pressed(SDL_BUTTON_LEFT);
+ auto& commands = get_localplayer().get_mutable_commands();
+ commands |= spm::forward * input::is_key_pressed(SDLK_w);
+ commands |= spm::left * input::is_key_pressed(SDLK_a);
+ commands |= spm::backward * input::is_key_pressed(SDLK_s);
+ commands |= spm::right * input::is_key_pressed(SDLK_d);
+ commands |= spm::jump * input::is_key_pressed(SDLK_SPACE);
+ commands |= spm::crouch * input::is_key_pressed(SDLK_LCTRL);
+ commands |= spm::sprint * input::is_key_pressed(SDLK_LSHIFT);
+ commands |= spm::attack * input::is_key_pressed(SDL_BUTTON_LEFT);
}
static void update_input() noexcept {
- client::input::update();
+ input::update();
+
+ if (input::is_key_pressed(SDLK_z)) {
+ render::camera::get_xfov() = 30.0f;
+ } else {
+ render::camera::get_xfov() = settings::get({"gameplay", "fov"}, 100.0f);
+ }
- if (!client::window::is_open()) {
+ if (!window::is_open()) {
handle_button_input();
}
- if (client::input::state.quit) {
+ if (input::state.quit) {
shared::should_exit = true;
}
}
-static proto::packet
-make_move_packet(const shared::player& localplayer) noexcept {
- proto::packet packet;
+static void update_delta_ticks() noexcept {
+ static shared::time_point_t last = std::chrono::steady_clock::now();
- const auto move_packet = packet.mutable_move_packet();
- move_packet->set_commands(localplayer.commands);
- shared::net::set_angles(*move_packet->mutable_viewangles(),
- localplayer.viewangles);
+ const auto now = std::chrono::steady_clock::now();
+ const auto delta_ticks =
+ shared::get_duration_seconds(now - last) /
+ shared::get_duration_seconds(state::get_time_per_tick());
- return packet;
+ state::delta_ticks += state::time_factor * delta_ticks;
+
+ last = now;
}
-static void send_move_packet(shared::net::connection& connection) noexcept {
- const auto& localplayer = get_localplayer(::players);
- connection.usend_packet(make_move_packet(localplayer));
+static void update_pre_move() {
+ state::player_count = state::entities.size();
+ state::requested_chunk_count = static_cast<std::uint32_t>(
+ std::ranges::count_if(state::chunks, [](const auto& c) {
+ return !c.second.has_value();
+ }));
+ state::networked_chunk_count =
+ std::size(state::chunks) - state::requested_chunk_count;
+ update_delta_ticks();
+ update_input();
+ update_chunks();
}
-static void update_players() noexcept {
- const auto lp_index = get_localplayer(::players).index;
+static void interp_entities() {
+ for (auto& [index, entity] : state::entities) {
+ const auto animate_ptr = dynamic_cast<animate*>(&*entity);
+ if (animate_ptr == nullptr) {
+ continue;
+ }
+ animate_ptr->interpolate();
+ }
+}
+
+static void remove_bad_entities() {
+ const auto lp_index = *state::localplayer_index;
// pvs, remove clients outside of chunks we own
- std::erase_if(::players, [&](const auto& player) {
- if (player.index == lp_index) {
+ std::erase_if(state::entities, [&](const auto& pair) {
+ const auto& [idx, entity] = pair;
+
+ const auto player_ptr = dynamic_cast<const player*>(&*entity);
+ if (player_ptr == nullptr) {
+ return false;
+ }
+
+ if (player_ptr->get_index() == lp_index) {
return false;
}
// Players should be removed if the chunk can't draw.
- const auto chunk_it = ::chunks.find(player.chunk_pos);
- if (chunk_it == std::end(::chunks)) {
+ const auto chunk_it = state::chunks.find(player_ptr->get_chunk_pos());
+ if (chunk_it == std::end(state::chunks)) {
return true;
}
const auto& chunk = chunk_it->second;
- if (!chunk.has_value()) {
- return false;
- }
-
- if (!chunk->can_draw()) {
+ if (chunk.has_value() && !chunk->can_draw()) {
return true;
}
return false;
});
}
+static void update_entities() noexcept {
+ interp_entities();
+ remove_bad_entities();
+}
+
+static void update_post_move() { update_entities(); }
+
+static void maybe_send_move() noexcept {
+
+ // An unknown amount of time has passed since we last rendered and this may
+ // be more than one tick. We do nothing if it's less than one tick.
+ const auto num_ticks_passed = static_cast<unsigned>(state::delta_ticks);
+
+ if (num_ticks_passed <= 0) {
+ return;
+ }
+
+ // Extrapolate for the number of ticks passed.
+ auto& localplayer = get_localplayer();
+ for (auto i = 0u; i < num_ticks_passed; ++i) {
+ ++state::tick; // possibly after?
+ state::delta_ticks -= 1.0f;
+ localplayer.extrapolate();
+ ++localplayer.get_mutable_latest_sequence();
+ state::connection->usend_packet(make_move_packet(localplayer));
+ }
+
+ // reset all commands except for flying, more later probably
+ const auto retained = shared::animate::flying;
+ localplayer.get_mutable_commands() &= retained;
+}
+
// requires SDL_MOUSEMOTION
static void handle_mousemotion(const SDL_Event& event) noexcept {
const float sens =
settings::get<float>({"gameplay", "mouse_sensitivity"}, 0.0235f);
- auto& lp = get_localplayer(::players);
- auto& angles = lp.viewangles;
+ auto& lp = get_localplayer();
+ auto& angles = lp.get_mutable_angles();
const float pitch_offset = static_cast<float>(event.motion.yrel) * sens;
const float yaw_offset = static_cast<float>(event.motion.xrel) * sens;
- angles.pitch = std::clamp(angles.pitch - glm::radians(pitch_offset),
- glm::radians(-89.0f), glm::radians(89.0f));
- angles.yaw =
- std::fmod(angles.yaw + glm::radians(yaw_offset), glm::radians(360.0f)) +
- (angles.yaw < 0.0f) * glm::radians(360.0f);
+ angles.pitch -= glm::radians(pitch_offset);
+ angles.yaw += glm::radians(yaw_offset);
+ angles.normalise();
+ angles.clamp();
}
// requires SDL_MOUSEBUTTONDOWN
@@ -395,37 +484,93 @@ static void handle_mousebuttons(const SDL_Event& event) noexcept {
return;
}
+ auto& lp = get_localplayer();
const auto mode = event.button.button == SDL_BUTTON_LEFT
- ? client::movement::interact_mode::remove
- : client::movement::interact_mode::add;
-
- const auto position =
- client::movement::interact(get_localplayer(::players), mode, ::chunks);
-
+ ? movement::interact_mode::remove
+ : movement::interact_mode::add;
+ const auto position = movement::interact(lp, mode, state::chunks);
if (!position.has_value()) {
return;
}
const auto& [chunk_pos, block_pos] = *position;
+ auto& block = state::chunks.find(chunk_pos)->second->get_block(block_pos);
+
+ if (mode == movement::interact_mode::remove) {
+ lp.inventory.maybe_add(shared::item::block::get_type(block.type), 1,
+ client::item::make_item);
+ block.type = shared::world::block::type::air;
+
+ state::connection->rsend_packet(
+ make_remove_block_packet(chunk_pos, block_pos));
+ } else {
+ // Adding is more complicated, needs the active item.
+ const auto& active = lp.get_active_item();
+ auto& item = lp.inventory.contents[active];
+ if (item == nullptr) {
+ return;
+ }
+
+ const auto block_ptr = dynamic_cast<const shared::item::block*>(&*item);
+ if (block_ptr == nullptr) {
+ return;
+ }
+ lp.inventory.decrement(active);
+ block.type = block_ptr->type;
+
+ state::connection->rsend_packet(
+ make_add_block_packet(chunk_pos, block_pos, active));
+ }
+
+ regenerate_surrounding_chunks(chunk_pos);
+}
- auto& connection = *client::state.connection;
- switch (mode) {
- case client::movement::interact_mode::add:
- connection.usend_packet(make_add_block_packet(chunk_pos, block_pos));
+static void handle_space(const SDL_Event& event) noexcept {
+ if (event.type == SDL_KEYUP) {
+ return;
+ }
+ auto& commands = get_localplayer().get_mutable_commands();
+ commands |= shared::animate::mask::jump;
+
+ if (event.key.repeat) {
+ return;
+ }
+ constexpr auto JUMP_FLY_DELAY = std::chrono::milliseconds(250);
+
+ static std::optional<shared::time_point_t> prev = std::nullopt;
+ const auto now = std::chrono::steady_clock::now();
+ if (prev.has_value() && now < *prev + JUMP_FLY_DELAY) {
+ commands ^= shared::animate::mask::flying;
+ prev.reset();
+ return;
+ }
+
+ prev.emplace(now);
+}
+
+static void handle_keys(const SDL_Event& event) noexcept {
+ switch (event.key.keysym.sym) {
+ case SDLK_SPACE:
+ handle_space(event);
break;
- case client::movement::interact_mode::remove:
- connection.usend_packet(make_remove_block_packet(chunk_pos, block_pos));
- ::last_block = ::chunks[chunk_pos]->get_block(block_pos);
+ default:
break;
}
}
static void handle_events(const SDL_Event& event) noexcept {
- if (client::window::is_open()) {
+ if (window::is_open()) {
+ return;
+ }
+ if (!state::localplayer_index.has_value()) {
return;
}
switch (event.type) {
+ case SDL_KEYDOWN:
+ case SDL_KEYUP:
+ handle_keys(event);
+ break;
case SDL_MOUSEBUTTONDOWN:
handle_mousebuttons(event);
break;
@@ -437,79 +582,124 @@ static void handle_events(const SDL_Event& event) noexcept {
}
}
-static void authenticate_client(shared::net::connection& connection) noexcept {
- const auto rand_alphanum_string = [](const int size) -> std::string {
- static const std::string allowed = "abcdefghijklmnopqrstuvwxyz"
- "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
- "1234567890";
- std::string ret;
- for (int i = 0; i < size; ++i) {
- std::ranges::sample(allowed, std::back_inserter(ret), 1,
- std::random_device{});
- }
+static std::string get_username() {
+ const settings::setting_pair_t loc = {"gameplay", "username"};
+ if (const auto username = settings::maybe_get_setting_str(loc);
+ username.has_value()) {
+ return *username;
+ }
- return ret;
- };
+ shared::print::notify << shared::print::time
+ << "first launch detected, username required\n";
+ std::string ret;
+ while (ret.empty() || !std::ranges::all_of(ret, isalnum)) {
+ std::cin.clear();
+ std::cout << "enter a valid username: ";
+ std::getline(std::cin, ret);
+ }
+ settings::set_setting_str(loc, ret);
+ return ret;
+}
- const std::string username = settings::get<std::string>(
- {"auth", "username"},
- rand_alphanum_string(shared::MAX_USER_PASS_LENGTH));
- const std::string password = settings::get<std::string>(
- {"auth", "password"},
- rand_alphanum_string(shared::MAX_USER_PASS_LENGTH));
+static std::string get_password(const std::string_view& address) {
+ const settings::setting_pair_t loc = {
+ "auth", std::string{address == "0.0.0.0" ? "localhost" : address}};
+ if (const auto password = settings::maybe_get_setting_str(loc);
+ password.has_value()) {
+ return *password;
+ }
- connection.rsend_packet(make_auth_packet(username, password));
+ // We generate a random string as our password. Our password has decent
+ // complexitity but considering we don't use any encryption it is worthless
+ // to any real attacker and just basic authentication.
+ std::string ret;
+ std::random_device rand{};
+ std::uniform_int_distribution<char> uniform{'a', 'z'};
+ for (int i = 0; i < 256; ++i) {
+ const char random_char = uniform(rand);
+ ret.push_back(random_char);
+ }
+ settings::set_setting_str(loc, ret);
+ return ret;
}
-void main(const std::string_view address, const std::string_view port) {
- client::state.address = address;
- client::state.port = port;
+static void send_auth_packet(const std::string_view& address,
+ shared::net::connection& connection) {
+ const std::string username = get_username();
+ const std::string password = get_password(address);
- shared::net::connection connection = make_connection(address, port);
- shared::print::notify("client: connected to " + std::string{address} + ':' +
- std::string{port} + '\n');
- client::state.connection = &connection;
+ connection.rsend_packet(make_auth_packet(username, password));
+}
+
+static bool should_do_loop(shared::net::connection& connection) noexcept {
+ if (shared::should_exit) {
+ return false;
+ }
- client::render::init();
- client::input::register_event_handler(&handle_events);
+ if (!connection.good()) {
+ shared::print::notify << shared::print::time
+ << "client: disconnected for \""
+ << connection.get_bad_reason() << "\"\n";
+ shared::should_exit = true; // cleanup server if necessary
+ return false;
+ }
- authenticate_client(connection);
+ return true;
+}
- while (!shared::should_exit) {
- // Parse all new packets.
- while (const auto packet = connection.recv_packet()) {
- parse_packet(packet.value());
+static void do_client_loop(shared::net::connection& connection) {
+ while (should_do_loop(connection)) {
+ connection.poll();
+ while (auto packet = connection.recv_packet()) {
+ parse_packet(std::move(*packet));
}
- // Wait for localplayer to be constructed before doing anything.
- if (::players.empty()) {
+ if (!state::has_initialised()) {
continue;
}
- // Handle input, which may prime text input -> send it to the
- // server.
- update_input();
- // Send our localplayer to the server.
- if (::should_send_move) {
- send_move_packet(connection);
- ::should_send_move = false;
- }
- update_chunks(connection);
- update_state();
- update_players();
-
- client::draw::draw(::players, ::chunks);
+ update_pre_move();
+ maybe_send_move();
+ update_post_move();
- if (!connection.good()) {
- shared::print::notify("client: disconnected for \"" +
- connection.get_bad_reason() + "\"\n");
- shared::should_exit = true;
- }
+ render::draw(state::entities, state::chunks);
}
- shared::print::notify("client: disconnecting from server\n");
+}
+
+static void init_client(const std::string_view& address,
+ const std::string_view& port,
+ shared::net::connection& connection) {
+ // Setup client state, some vars should be cleaned up later (raii lol).
+ state::address = address;
+ state::port = port;
+ state::connection = &connection;
+
+ input::register_event_handler(&handle_events);
+ send_auth_packet(address, connection);
+
+ render::init();
+}
+
+static void cleanup_client() {
+ state::connection = nullptr;
+ state::localplayer_index.reset();
+ state::chunks.clear();
+ state::entities.clear();
+ render::quit();
+ settings::save();
+}
+
+void main(const std::string_view address, const std::string_view port) {
+ shared::net::connection connection = make_connection(address, port);
+ init_client(address, port, connection);
+ shared::print::notify << shared::print::time << "client: connected to "
+ << address << ':' << port << '\n';
+
+ do_client_loop(connection);
- client::render::quit();
- client::settings::save();
+ shared::print::notify << shared::print::time
+ << "client: disconnecting from server\n";
+ cleanup_client();
}
} // namespace client
diff --git a/src/client/client.hh b/src/client/client.hh
index 6dbabc1..0eadfe4 100644
--- a/src/client/client.hh
+++ b/src/client/client.hh
@@ -3,25 +3,30 @@
#include <algorithm>
#include <cmath>
+#include <queue>
+#include <random>
#include <string_view>
#include <unordered_map>
-#include "client/draw.hh"
+#include "client/entity/player.hh"
#include "client/input.hh"
-#include "client/movement.hh"
-#include "client/player.hh"
+#include "client/item/items.hh"
+#include "client/movement/movement.hh"
#include "client/render/camera.hh"
+#include "client/render/draw.hh"
#include "client/settings.hh"
#include "client/shared.hh"
-#include "client/window.hh"
-#include "client/world.hh"
-#include "shared/math.hh"
+#include "client/state/chunks.hh"
+#include "client/state/entities.hh"
+#include "client/state/state.hh"
+#include "client/window/window.hh"
+#include "client/world/chunk.hh"
+#include "shared/math/math.hh"
+#include "shared/net/connection.hh"
#include "shared/net/net.hh"
#include "shared/net/proto.hh"
namespace client {
-void send_say_packet(const std::string& text) noexcept;
-
void main(const std::string_view address, const std::string_view port);
} // namespace client
diff --git a/src/client/draw.hh b/src/client/draw.hh
deleted file mode 100644
index 821d67b..0000000
--- a/src/client/draw.hh
+++ /dev/null
@@ -1,59 +0,0 @@
-#ifndef CLIENT_DRAW_HH_
-#define CLIENT_DRAW_HH_
-
-#include <string_view>
-
-#include <glm/glm.hpp>
-
-#include "client/movement.hh"
-#include "client/player.hh"
-#include "client/render/camera.hh"
-#include "client/render/render.hh"
-#include "client/render/program.hh"
-#include "client/shared.hh"
-#include "client/window.hh"
-#include "client/world.hh"
-
-namespace client {
-namespace draw {
-
-// Scale takes the range [0, 1] and represents an amount of the screen.
-// Offset is any value that is added after scale.
-struct relative_arg {
- glm::vec2 extent;
- glm::vec2 offset;
-
- glm::vec2 to_vec2() const noexcept {
- const glm::vec2& window = render::get_window_size();
- const float x = this->extent.x * window.x + this->offset.x;
- const float y = this->extent.y * window.y + this->offset.y;
- return {x, y};
- }
-};
-
-struct rectangle_args {
- relative_arg pos;
- relative_arg size;
- glm::vec4 colour;
-};
-
-struct text_args {
- relative_arg pos;
- float extent_height; // we don't get width here,
- float offset_height;
- glm::vec4 colour;
- bool has_backing;
- bool is_centered;
- bool is_vcentered;
-};
-
-void draw_rectangle(const rectangle_args& args) noexcept;
-void draw_colour(const glm::vec4& colour) noexcept;
-void draw_text(const std::string_view text, const text_args& args) noexcept;
-
-void draw(client::players& players, client::world::chunk::map& chunks) noexcept;
-
-} // namespace draw
-} // namespace client
-
-#endif
diff --git a/src/client/entity/animate.cc b/src/client/entity/animate.cc
new file mode 100644
index 0000000..9142b55
--- /dev/null
+++ b/src/client/entity/animate.cc
@@ -0,0 +1,158 @@
+#include "player.hh"
+
+namespace client {
+
+// We might start receiving data slower or faster. We compensate by adjusting
+// how fast time progresses on our client.;
+void animate::update_time_factor(const shared::tick_t& sequence,
+ const shared::tick_t& tick) noexcept {
+
+ // Check if we're older than the latest, don't update if so.
+ if (std::ranges::any_of(this->updates, [&](const auto& update) {
+ return update.from_server && update.tick_sequence >= sequence;
+ })) {
+ return;
+ }
+
+ // How many ticks in the future we prefer to be at. If the server is
+ // jittery, we would want this value to be greater, but this should be OK
+ // for most cases. A larger value decreases the max ping we can handle.
+ constexpr float TARGET_TICKS_AHEAD = 1.341519f;
+
+ if (const auto tick_dist = static_cast<std::int64_t>(tick) -
+ static_cast<std::int64_t>(state::tick);
+ tick_dist > 0) {
+
+ // We're behind, jump ahead. We only touch delta_ticks so extrapolate
+ // is called the correct number of times in client.cc.
+#ifndef NDEBUG
+ shared::print::debug << shared::print::time
+ << "client: time_factor jumped ahead, got tick "
+ << tick << ", was " << state::tick << " + "
+ << state::delta_ticks
+ << ", time_factor: " << state::time_factor << '\n';
+#endif
+ state::delta_ticks = static_cast<float>(tick_dist) + TARGET_TICKS_AHEAD;
+ state::time_factor = 1.0f;
+
+ return;
+ }
+
+ // Otherwise we try to move towards the value by adjusting how fast time
+ // progresses. This shouldn't be possible to notice, our constants should
+ // be adjusted if it is.
+ const float time_diff =
+ (static_cast<float>(tick) + TARGET_TICKS_AHEAD) -
+ (static_cast<float>(state::tick) + state::delta_ticks);
+
+ constexpr float MAX_TIME_FACTOR_DIFF = 0.1f;
+ constexpr float TIME_FACTOR_AGGRESSIVENESS = 0.25f;
+ state::time_factor =
+ std::clamp(1.0f + (time_diff * TIME_FACTOR_AGGRESSIVENESS),
+ 1.0f - MAX_TIME_FACTOR_DIFF, 1.0f + MAX_TIME_FACTOR_DIFF);
+}
+
+void animate::notify(const shared::animate& animate,
+ const shared::tick_t& tick_sequence,
+ const bool from_server) noexcept {
+ // If it's from the server we want to update previously emplaced predicted
+ // (and potentially predicted) values.
+ if (const auto it = std::ranges::find_if(this->updates,
+ [&](const auto& update) {
+ return update.tick_sequence ==
+ tick_sequence;
+ });
+ it != std::end(this->updates)) {
+
+ if (from_server) {
+ *it = animate_update{.tick_sequence = tick_sequence,
+ .animate = animate,
+ .from_server = from_server};
+ }
+
+ return;
+ }
+
+ this->updates.push_back({.tick_sequence = tick_sequence,
+ .animate = animate,
+ .from_server = from_server});
+
+ std::ranges::sort(this->updates, [](const auto& a, const auto& b) {
+ return a.tick_sequence < b.tick_sequence;
+ });
+}
+
+float animate::get_target_ticks_back() noexcept {
+ // The localplayer is interpolated via the latest tick.
+ const unsigned base_ticks_back = [&]() -> unsigned {
+ if (this->index == *state::localplayer_index) {
+ return 0u;
+ }
+ const float base = settings::get<float>({"engine", "interp"}, 0.25f);
+ const float ret = base * static_cast<float>(state::tickrate);
+ return std::clamp(static_cast<unsigned>(ret), 0u, state::tickrate);
+ }();
+
+ return state::delta_ticks + static_cast<float>(base_ticks_back);
+}
+
+void animate::interpolate() noexcept {
+ const float target_ticks_back = this->get_target_ticks_back();
+
+ const bool is_localplayer = this->index == *state::localplayer_index;
+ const auto b_it = [&, this]() {
+ if (is_localplayer) {
+ return std::rbegin(this->updates);
+ }
+ return std::ranges::find_if(
+ std::rbegin(this->updates), std::rend(this->updates),
+ [&](const auto& update) {
+ const unsigned target =
+ state::tick - static_cast<unsigned>(target_ticks_back);
+ return update.tick_sequence <= target;
+ });
+ }();
+ if (b_it == std::rend(this->updates)) {
+ return;
+ }
+
+ const auto a_it = std::next(b_it);
+ if (a_it == std::rend(this->updates)) {
+ return;
+ }
+
+ const glm::vec3 a_pos = a_it->animate.get_local_pos();
+ const glm::vec3 b_pos = shared::movement::make_relative(
+ a_it->animate.get_chunk_pos(), b_it->animate.get_local_pos(),
+ b_it->animate.get_chunk_pos());
+
+ const float b_interp = [&]() {
+ const float diff =
+ is_localplayer
+ ? 1.0f
+ : static_cast<float>(b_it->tick_sequence - a_it->tick_sequence);
+ const float base = std::fmod(target_ticks_back, 1.0f);
+ const float a_dist = diff - (1.0f - base);
+ return a_dist / diff;
+ }();
+ const float a_interp = (1.0f - b_interp);
+
+ const auto& a = a_it->animate;
+ const auto& b = b_it->animate;
+ this->local_pos = a_pos * a_interp + b_pos * b_interp;
+ this->chunk_pos = a.get_chunk_pos();
+ this->velocity = a.get_velocity() * a_interp + b.get_velocity() * b_interp;
+ // Update viewangles if we're not the localplayer, yaw requires special care
+ // because it should snap to the closest difference angle.
+ if (!is_localplayer) {
+ this->viewangles.pitch =
+ a.get_angles().pitch * a_interp + b.get_angles().pitch * b_interp;
+ const float yaw_delta = shared::math::angles::get_yaw_delta(
+ a.get_angles().yaw, b.get_angles().yaw);
+ this->viewangles.yaw = a.get_angles().yaw + yaw_delta * b_interp;
+ this->viewangles.normalise();
+ }
+ shared::movement::normalise_position(this->local_pos, this->chunk_pos);
+}
+
+} // namespace client
diff --git a/src/client/entity/animate.hh b/src/client/entity/animate.hh
new file mode 100644
index 0000000..79016ea
--- /dev/null
+++ b/src/client/entity/animate.hh
@@ -0,0 +1,71 @@
+#ifndef CLIENT_ENTITY_ANIMATE_HH_
+#define CLIENT_ENTITY_ANIMATE_HH_
+
+#include <algorithm>
+#include <chrono>
+#include <cmath>
+#include <ranges>
+#include <utility>
+#include <vector>
+
+#include <boost/circular_buffer.hpp>
+
+#include "client/entity/entity.hh"
+#include "client/movement/movement.hh"
+#include "client/settings.hh"
+#include "client/state/state.hh"
+#include "shared/entity/animate.hh"
+#include "shared/entity/entity.hh"
+#include "shared/movement/movement.hh"
+#include "shared/net/proto.hh"
+#include "shared/shared.hh"
+
+namespace client {
+
+class animate : virtual public shared::animate, virtual public client::entity {
+protected:
+ struct animate_update {
+ shared::tick_t tick_sequence; // tick or sequence, if LP then sequence.
+ shared::animate animate;
+ bool from_server;
+ };
+
+ // We have 1 second of potential player interpolation to use here when we
+ // initialise this with the server's tickrate.
+ boost::circular_buffer<animate_update> updates{state::tickrate};
+ shared::tick_t latest_sequence = 0;
+
+private:
+ float get_target_ticks_back() noexcept;
+
+public:
+ animate(shared::player&& p) noexcept
+ : shared::entity(p), shared::animate(p), client::entity(p) {}
+
+ animate(const proto::animate& proto) noexcept
+ : shared::entity(proto.entity()), shared::animate(proto),
+ client::entity(proto.entity()) {}
+
+public:
+ // Call when localplayer gets tick to update state timers.
+ void update_time_factor(const shared::tick_t& sequence,
+ const shared::tick_t& tick) noexcept;
+
+ virtual void notify(const shared::animate& animate,
+ const shared::tick_t& tick_sequence,
+ const bool from_server) noexcept;
+
+ shared::tick_t& get_mutable_latest_sequence() noexcept {
+ return this->latest_sequence;
+ }
+ shared::tick_t get_latest_sequence() const noexcept {
+ return this->latest_sequence;
+ }
+ // An animate may be interpolated, however it's the moveable class that is
+ // required for extrapolation and other prediction.
+ void interpolate() noexcept;
+};
+
+} // namespace client
+
+#endif
diff --git a/src/client/entity/entity.cc b/src/client/entity/entity.cc
new file mode 100644
index 0000000..c305a66
--- /dev/null
+++ b/src/client/entity/entity.cc
@@ -0,0 +1,6 @@
+#include "client/entity/entity.hh"
+
+namespace client {
+
+
+} // namespace client
diff --git a/src/client/entity/entity.hh b/src/client/entity/entity.hh
new file mode 100644
index 0000000..e8d6cc6
--- /dev/null
+++ b/src/client/entity/entity.hh
@@ -0,0 +1,27 @@
+#ifndef CLIENT_ENTITY_ENTITY_HH_
+#define CLIENT_ENTITY_ENTITY_HH_
+
+#include "shared/entity/entity.hh"
+#include "shared/entity/player.hh"
+
+namespace client {
+
+class player; // forward declaration
+
+// A client::entity is a renderable shared::entity.
+class entity : virtual public shared::entity {
+public:
+ entity(shared::entity& e) noexcept
+ : shared::entity(std::forward<shared::entity>(e)) {}
+ entity(const proto::entity& e) noexcept : shared::entity(e) {}
+
+ virtual ~entity() noexcept {}
+
+public:
+ virtual void draw(const client::player& localplayer) noexcept = 0;
+ virtual void draw_wts(const client::player& localplayer) noexcept = 0;
+};
+
+} // namespace client
+
+#endif
diff --git a/src/client/entity/moveable.cc b/src/client/entity/moveable.cc
new file mode 100644
index 0000000..480e6db
--- /dev/null
+++ b/src/client/entity/moveable.cc
@@ -0,0 +1,86 @@
+#include "client/entity/moveable.hh"
+
+namespace client {
+
+static auto find_sequence_it(auto& updates,
+ const shared::tick_t& sequence) noexcept {
+ return std::ranges::find_if(updates, [&sequence](const auto& update) {
+ return update.tick_sequence == sequence;
+ });
+}
+
+void moveable::repredict_from(const shared::tick_t& sequence) noexcept {
+ // find the animate update we want to repredict starting from
+ const auto find_it = find_sequence_it(this->updates, sequence);
+ if (find_it == std::end(this->updates)) {
+ return;
+ }
+
+ // backup current animate
+ const shared::animate before_move = *this;
+
+ // predict the next ticks starting from find_it until we run
+ // out of prediction history
+ for (auto it = find_it; it != std::end(this->updates); ++it) {
+ const auto next_it = std::next(it);
+ if (next_it == std::end(this->updates)) {
+ break;
+ }
+
+ const auto viewangles = next_it->animate.get_angles();
+ const auto commands = next_it->animate.get_commands();
+
+ this->shared::animate::operator=(it->animate);
+ this->commands = commands;
+ this->viewangles = viewangles;
+ const auto predicted = movement::move(*this, state::chunks);
+ if (!predicted.has_value()) {
+ break;
+ }
+ next_it->animate = *predicted;
+ next_it->animate.get_mutable_angles() = viewangles;
+ next_it->animate.get_mutable_commands() = commands;
+ }
+ // restore current animate
+ this->shared::animate::operator=(before_move);
+}
+
+void moveable::notify(const shared::animate& animate,
+ const shared::tick_t& sequence,
+ const bool from_server) noexcept {
+ this->animate::notify(animate, sequence, from_server);
+
+ if (!from_server) { // Only repredict if we're from the server.
+ return;
+ }
+ this->repredict_from(sequence);
+}
+
+void moveable::extrapolate() noexcept {
+ // can't extrapolate if no data
+ if (this->updates.empty()) {
+ return;
+ }
+
+ const auto& latest_it = std::rbegin(this->updates);
+ if (latest_it == std::rend(this->updates)) {
+ return;
+ }
+
+ // backup current animate, load latest animate
+ const shared::animate before_extrapolate = *this;
+ this->shared::animate::operator=(latest_it->animate);
+
+ // FIXED: we were writing our commands with the old commands, so we were
+ // always predicting the first commands, which were always for not moving!
+ this->commands = before_extrapolate.get_commands();
+ this->viewangles = before_extrapolate.get_angles();
+
+ const auto predicted = movement::move(*this, state::chunks);
+ if (predicted.has_value()) {
+ this->notify(*predicted, this->get_latest_sequence() + 1, false);
+ }
+ this->shared::animate::operator=(before_extrapolate);
+}
+
+} // namespace client
diff --git a/src/client/entity/moveable.hh b/src/client/entity/moveable.hh
new file mode 100644
index 0000000..fa08d22
--- /dev/null
+++ b/src/client/entity/moveable.hh
@@ -0,0 +1,29 @@
+#ifndef CLIENT_ENTITY_MOVEABLE_HH_
+#define CLIENT_ENTITY_MOVEABLE_HH_
+
+#include "client/entity/animate.hh"
+#include "client/state/chunks.hh"
+#include "client/state/state.hh"
+#include "client/world/chunk.hh"
+#include "shared/entity/moveable.hh"
+
+namespace client {
+
+class moveable : virtual public client::animate,
+ virtual public shared::moveable,
+ virtual public shared::animate {
+private:
+ void repredict_from(const shared::tick_t& sequence) noexcept;
+
+public:
+ // Notify repredicts if necessary.
+ virtual void notify(const shared::animate& animate,
+ const shared::tick_t& sequence,
+ const bool from_server) noexcept override;
+
+ void extrapolate() noexcept;
+};
+
+} // namespace client
+
+#endif
diff --git a/src/client/entity/player.cc b/src/client/entity/player.cc
new file mode 100644
index 0000000..550968f
--- /dev/null
+++ b/src/client/entity/player.cc
@@ -0,0 +1,54 @@
+#include "player.hh"
+namespace client {
+
+glm::vec3 player::get_world_pos(const client::player& lp) noexcept {
+ const auto& lp_cp = lp.get_chunk_pos();
+
+ // clang-format off
+ const float world_x = static_cast<float>(this->get_chunk_pos().x - lp_cp.x) * shared::world::chunk::WIDTH + this->local_pos.x;
+ const float world_y = static_cast<float>(this->get_local_pos().y) + shared::player::HEIGHT / 2.0f;
+ const float world_z = static_cast<float>(this->get_chunk_pos().z - lp_cp.z) * shared::world::chunk::WIDTH + this->local_pos.z;
+ // clang-format on
+ return {world_x, world_y, world_z};
+}
+
+void player::draw(const client::player& localplayer) noexcept {
+
+ static render::model playermodel{"res/models/player/player.obj"};
+ static render::program program{"res/shaders/model.vs",
+ "res/shaders/model.fs"};
+
+ const glm::vec3 world_pos = this->get_world_pos(localplayer);
+ const glm::mat4 rotate = glm::rotate(glm::mat4(1.0f), -this->viewangles.yaw,
+ glm::vec3(0.0f, 1.0f, 0.0f));
+ const glm::mat4 translate = glm::translate(glm::mat4(1.0f), world_pos);
+ const glm::mat4& view = render::camera::get_view();
+ const glm::mat4& proj = render::camera::get_proj();
+
+ playermodel.draw(program, proj * view * translate * rotate);
+}
+
+void player::draw_wts(const client::player& localplayer) noexcept {
+ const auto& msg = this->get_message();
+ if (!msg.has_value()) {
+ return;
+ }
+
+ const glm::vec3 world_pos = this->get_world_pos(localplayer);
+ const auto pos = math::world_to_screen(world_pos);
+
+ if (!pos.has_value()) {
+ return;
+ }
+
+ const glm::vec2& window = render::get_window_size();
+ const glm::mat4 proj = glm::ortho(0.0f, window.x, 0.0f, window.y);
+ const glm::mat4 tran =
+ glm::translate(glm::mat4(1.0f),
+ glm::vec3{std::floor(pos->x), std::floor(pos->y), 0.0f});
+
+ constexpr glm::vec4 white{1.0f, 1.0f, 1.0f, 1.0f};
+ render::render_text(msg->text, 30.0f, true, true, white, proj * tran);
+}
+
+} // namespace client
diff --git a/src/client/entity/player.hh b/src/client/entity/player.hh
new file mode 100644
index 0000000..d9bdc18
--- /dev/null
+++ b/src/client/entity/player.hh
@@ -0,0 +1,73 @@
+#ifndef CLIENT_ENTITY_PLAYER_HH_
+#define CLIENT_ENTITY_PLAYER_HH_
+
+#include <chrono>
+#include <optional>
+#include <string>
+#include <utility>
+
+#include "client/entity/moveable.hh"
+#include "client/entity/entity.hh"
+#include "client/item/block.hh"
+#include "client/item/items.hh"
+#include "client/math.hh"
+#include "client/render/camera.hh"
+#include "client/render/model.hh"
+#include "client/render/program.hh"
+
+namespace client {
+
+// Renderable player, similar to client::chunk. We also store the message the
+// player wants to say.
+class player : virtual public shared::player, public client::moveable {
+public:
+ static constexpr auto MSG_SHOW_TIME = std::chrono::seconds{15};
+ struct message {
+ std::string text;
+ std::chrono::time_point<std::chrono::steady_clock> receive_time;
+ message(const std::string& message) noexcept {
+ this->text = message;
+ this->receive_time = std::chrono::steady_clock::now();
+ }
+ };
+ std::optional<message> message;
+
+private:
+ glm::vec3 get_world_pos(const client::player& lp) noexcept;
+
+public:
+ player(shared::player&& p) noexcept
+ : shared::entity(p), shared::animate(p), shared::player(p),
+ client::entity(p), client::animate(std::move(p)) {
+
+ // upgrade our inventory contents to client::item::item
+ for (auto& item : this->inventory.contents) {
+ if (item == nullptr) {
+ continue;
+ }
+
+ item = client::item::make_item(item->get_type(), item->quantity);
+ }
+ }
+ player(const proto::animate& proto) noexcept
+ : shared::entity(proto.entity()), shared::animate(proto),
+ shared::player(proto), client::entity(proto.entity()),
+ client::animate(proto) {}
+
+ virtual void draw(const client::player& localplayer) noexcept override;
+ virtual void draw_wts(const client::player& localplayer) noexcept override;
+
+ const std::optional<struct message>& get_message() noexcept {
+ if (this->message.has_value()) {
+ const auto now = std::chrono::steady_clock::now();
+ if (now > this->message->receive_time + player::MSG_SHOW_TIME) {
+ this->message.reset();
+ }
+ }
+ return this->message;
+ }
+};
+
+} // namespace client
+
+#endif
diff --git a/src/client/item/block.cc b/src/client/item/block.cc
new file mode 100644
index 0000000..08f072b
--- /dev/null
+++ b/src/client/item/block.cc
@@ -0,0 +1,91 @@
+#include "client/item/block.hh"
+
+namespace client {
+namespace item {
+
+void block::regenerate_glo() noexcept {
+ const auto make_vbo = [](const auto& data) -> GLuint {
+ GLuint vbo = 0;
+ glGenBuffers(1, &vbo);
+ glBindBuffer(GL_ARRAY_BUFFER, vbo);
+ glBufferData(GL_ARRAY_BUFFER,
+ std::size(data) * sizeof(client::world::block::glvert),
+ std::data(data), GL_STATIC_DRAW);
+ return vbo;
+ };
+ const auto make_vao = []() -> GLuint {
+ GLuint vao = 0;
+ glGenVertexArrays(1, &vao);
+ glBindVertexArray(vao);
+ // posiiton
+ glEnableVertexAttribArray(0);
+ glVertexAttribPointer(0, sizeof(glm::vec3) / sizeof(float), GL_FLOAT,
+ GL_FALSE, sizeof(client::world::block::glvert),
+ nullptr);
+ // texture
+ glEnableVertexAttribArray(1);
+ glVertexAttribPointer(1, sizeof(glm::vec3) / sizeof(float), GL_FLOAT,
+ GL_FALSE, sizeof(client::world::block::glvert),
+ reinterpret_cast<void*>(sizeof(glm::vec3)));
+ return vao;
+ };
+ std::vector<client::world::block::glvert> data;
+
+ const float tex_yoff = static_cast<float>(this->type) - 1.0f;
+ for (const auto& face :
+ client::world::block::get_glfaces(this->shared::item::block::type)) {
+
+ std::ranges::transform(face, std::back_inserter(data), [&](auto vert) {
+ vert.texture.z += tex_yoff * 6.0f;
+ return vert;
+ });
+ }
+
+ const auto vbo = make_vbo(data);
+ const auto vao = make_vao();
+ this->glo.emplace(vbo, vao, std::size(data));
+}
+
+void block::draw(const glm::vec2& pos, const glm::vec2& size) noexcept {
+ const auto make_matrix = [&]() -> glm::mat4 {
+ const glm::vec2 window = client::render::get_window_size();
+
+ constexpr auto identity = glm::mat4{1.0f};
+ const auto proj = glm::ortho(0.0f, window.x, 0.0f, window.y);
+ const auto scal =
+ glm::scale(identity, glm::vec3{size.x * 0.5f, size.y * 0.5f, 0.0f});
+ const auto tran =
+ glm::translate(identity, glm::vec3{pos.x + size.x / 2.0f,
+ pos.y + size.y / 2.0f, 0.0f});
+
+ // hack for shurbs, which are already offset and look bad when rotated
+ const auto rotey = client::world::block::get_draw_type(this->type) ==
+ client::world::block::draw_type::block
+ ? glm::rotate(identity, glm::radians(45.0f),
+ glm::vec3(0.0f, 1.0f, 0.0f))
+ : identity;
+ const auto rotex = glm::rotate(identity, glm::radians(30.0f),
+ glm::vec3(1.0f, 0.0f, 0.0f));
+ return proj * tran * scal * rotex * rotey;
+ };
+
+ static const client::render::program program{"res/shaders/face.vs",
+ "res/shaders/face.fs"};
+ static const GLint u_matrix = glGetUniformLocation(program, "_u_matrix");
+
+ if (!this->glo.has_value()) {
+ this->regenerate_glo();
+ }
+
+ glDisable(GL_BLEND);
+ glDisable(GL_DEPTH_TEST);
+ glUseProgram(program);
+ glBindVertexArray(this->glo->vao);
+
+ glUniformMatrix4fv(u_matrix, 1, GL_FALSE, glm::value_ptr(make_matrix()));
+
+ glDrawArrays(GL_TRIANGLES, 0, this->glo->elements);
+}
+
+} // namespace item
+} // namespace client
diff --git a/src/client/item/block.hh b/src/client/item/block.hh
new file mode 100644
index 0000000..f259aad
--- /dev/null
+++ b/src/client/item/block.hh
@@ -0,0 +1,58 @@
+#ifndef CLIENT_ITEM_BLOCK_HH_
+#define CLIENT_ITEM_BLOCK_HH_
+
+#include <algorithm>
+#include <optional>
+
+#include "client/item/item.hh"
+#include "client/render/render.hh"
+#include "client/world/block.hh"
+#include "shared/item/block.hh"
+#include "shared/item/item.hh"
+
+namespace client {
+namespace item {
+
+class block : virtual public shared::item::block, public client::item::item {
+private:
+ struct gl_object {
+ GLuint vbo;
+ GLuint vao;
+ unsigned long elements;
+
+ gl_object(const GLuint& vbo, const GLuint& vao,
+ const unsigned long& elements) noexcept
+ : vbo(vbo), vao(vao), elements(elements) {}
+ gl_object(const gl_object&) = delete;
+ gl_object(gl_object&&) = delete;
+ ~gl_object() noexcept {
+ glDeleteBuffers(1, &vbo);
+ glDeleteVertexArrays(1, &vao);
+ }
+ };
+
+private:
+ std::optional<gl_object> glo = std::nullopt;
+ void regenerate_glo() noexcept;
+
+public:
+ template <typename... Args>
+ block(const enum shared::world::block::type& type, Args&&... args) noexcept
+ : shared::item::block(type, args...), client::item::item(
+ std::forward<Args>(args)...) {
+ }
+ template <typename... Args>
+ block(const shared::item::item::type_t& type, Args&&... args) noexcept
+ : shared::item::item(type, args...), shared::item::block(type, args...),
+ client::item::item(type, std::forward<Args>(args)...) {}
+ virtual ~block() noexcept {}
+
+public:
+ virtual void draw(const glm::vec2& pos,
+ const glm::vec2& size) noexcept override;
+};
+
+} // namespace item
+} // namespace client
+
+#endif
diff --git a/src/client/item/item.cc b/src/client/item/item.cc
new file mode 100644
index 0000000..64cc1f5
--- /dev/null
+++ b/src/client/item/item.cc
@@ -0,0 +1 @@
+#include "client/item/item.hh"
diff --git a/src/client/item/item.hh b/src/client/item/item.hh
new file mode 100644
index 0000000..ff80369
--- /dev/null
+++ b/src/client/item/item.hh
@@ -0,0 +1,30 @@
+#ifndef CLIENT_ITEM_ITEM_HH_
+#define CLIENT_ITEM_ITEM_HH_
+
+#include <algorithm>
+
+#include <glm/glm.hpp>
+
+#include "shared/item/item.hh"
+
+namespace client {
+namespace item {
+
+// client::item is a renderiable shared::item
+class item : virtual public shared::item::item {
+public:
+public:
+ template <typename... Args>
+ item(Args&&... args) noexcept
+ : shared::item::item(std::forward<Args>(args)...) {}
+ virtual ~item() noexcept {}
+
+public:
+ // 2d drawing of items
+ virtual void draw(const glm::vec2& pos, const glm::vec2& size) noexcept = 0;
+};
+
+} // namespace item
+} // namespace client
+
+#endif
diff --git a/src/client/item/items.cc b/src/client/item/items.cc
new file mode 100644
index 0000000..9ff25fe
--- /dev/null
+++ b/src/client/item/items.cc
@@ -0,0 +1,16 @@
+#include "client/item/items.hh"
+
+namespace client {
+namespace item {
+
+shared::item::item_t make_item(const shared::item::item::type_t& type,
+ const std::uint32_t& quantity) noexcept {
+ if (type >= shared::item::block::type_offset) {
+ return std::make_shared<client::item::block>(type, quantity);
+ }
+ // TODO non-block items
+ return std::make_shared<client::item::block>(type, quantity);
+}
+
+} // namespace item
+} // namespace client
diff --git a/src/client/item/items.hh b/src/client/item/items.hh
new file mode 100644
index 0000000..53cebfa
--- /dev/null
+++ b/src/client/item/items.hh
@@ -0,0 +1,17 @@
+#ifndef CLIENT_ITEM_ITEMS_HH_
+#define CLIENT_ITEM_ITEMS_HH_
+
+#include "client/item/block.hh"
+#include "shared/item/item.hh"
+#include "shared/item/items.hh"
+
+namespace client {
+namespace item {
+
+shared::item::item_t make_item(const shared::item::item::type_t& type,
+ const std::uint32_t& quantity) noexcept;
+
+} // namespace item
+} // namespace client
+
+#endif
diff --git a/src/client/main.cc b/src/client/main.cc
new file mode 100644
index 0000000..858c6df
--- /dev/null
+++ b/src/client/main.cc
@@ -0,0 +1,90 @@
+#include "client/main.hh"
+
+static bool& get_has_address() noexcept {
+ static bool ret = false;
+ return ret;
+}
+
+static bool& get_has_port() noexcept {
+ static bool ret = false;
+ return ret;
+}
+
+static std::string& get_address() noexcept {
+ static std::string address{shared::DEFAULT_ADDRESS};
+ return address;
+}
+
+static std::string& get_port() noexcept {
+ static std::string port{shared::DEFAULT_PORT};
+ return port;
+}
+
+static bool& get_is_headless() noexcept {
+ static bool is_headless = false;
+ return is_headless;
+}
+
+static bool parse_arg(const int& c, const char* const arg) {
+ switch (c) {
+ case 'a':
+ get_address() = boost::lexical_cast<std::string>(arg);
+ get_has_address() = true;
+ break;
+ case 'p':
+ get_port() = boost::lexical_cast<std::string>(arg);
+ get_has_port() = true;
+ break;
+ case 'H':
+ get_is_headless() = true;
+ break;
+ case 's':
+ get_address() = "localhost";
+ break;
+ default:
+ return false;
+ }
+ return true;
+}
+
+static const shared::args_t& get_options() {
+ static shared::args_t ret = []() -> shared::args_t {
+ shared::args_t ret{
+ {.name = "address", .desc = "connect elsewhere", .val = "a:"},
+ {.name = "port", .desc = "override the default port", .val = "p:"},
+ {.name = "headless", .desc = "run without a client ", .val = "H"},
+ {.name = "singleplayer",
+ .desc = "avoid hosting a lan server",
+ .val = "s"}};
+ std::ranges::copy(shared::get_options(), std::back_inserter(ret));
+ std::ranges::copy(server::get_options(), std::back_inserter(ret));
+ return ret;
+ }();
+
+ return ret;
+}
+
+int main(const int argc, char* const* argv) {
+ shared::init();
+
+ shared::parse_args(argc, argv, get_options(),
+ {&parse_arg, &shared::parse_arg, &server::parse_arg});
+
+ if (get_has_address()) {
+ shared::try_main(client::main, get_address(), get_port());
+ return EXIT_SUCCESS;
+ }
+
+ if (get_is_headless()) {
+ shared::try_main(server::main, get_address(), get_port());
+ return EXIT_SUCCESS;
+ }
+
+ const std::jthread server_thread{shared::try_main, server::main,
+ get_address(), get_port()};
+ while (!server::has_initialised) {
+ continue;
+ }
+ shared::try_main(client::main, get_address(), get_port());
+ return EXIT_SUCCESS;
+}
diff --git a/src/client/main.hh b/src/client/main.hh
new file mode 100644
index 0000000..8610763
--- /dev/null
+++ b/src/client/main.hh
@@ -0,0 +1,23 @@
+#ifndef CLIENT_MAIN_HH_
+#define CLIENT_MAIN_HH_
+
+#include <algorithm>
+#include <exception>
+#include <execinfo.h>
+#include <getopt.h>
+#include <iostream>
+#include <iterator>
+#include <signal.h>
+#include <string_view>
+#include <thread>
+#include <vector>
+
+#include <boost/lexical_cast.hpp>
+
+#include "client/client.hh"
+#include "server/server.hh"
+#include "server/shared.hh"
+#include "shared/init.hh"
+#include "shared/shared.hh"
+
+#endif
diff --git a/src/client/movement.hh b/src/client/movement.hh
deleted file mode 100644
index 82b56b1..0000000
--- a/src/client/movement.hh
+++ /dev/null
@@ -1,23 +0,0 @@
-#ifndef CLIENT_MOVEMENT_HH_
-#define CLIENT_MOVEMENT_HH_
-
-#include "client/world.hh"
-#include "shared/movement.hh"
-#include "shared/net/net.hh"
-#include "shared/player.hh"
-
-namespace client {
-namespace movement {
-
-void move(shared::player& player, client::world::chunk::map& chunks,
- const float deltatime) noexcept;
-
-enum class interact_mode { add, remove };
-std::optional<std::pair<shared::math::coords, glm::ivec3>>
-interact(const shared::player& player, const interact_mode& mode,
- const client::world::chunk::map& chunks) noexcept;
-
-} // namespace movement
-} // namespace client
-
-#endif
diff --git a/src/client/movement.cc b/src/client/movement/movement.cc
index e5898eb..8712f89 100644
--- a/src/client/movement.cc
+++ b/src/client/movement/movement.cc
@@ -1,11 +1,11 @@
-#include "client/movement.hh"
+#include "client/movement/movement.hh"
namespace client {
namespace movement {
static std::optional<shared::world::block>
maybe_get_block(const shared::math::coords& pos,
- const client::world::chunk::map& chunks,
+ const client::world::chunks_t& chunks,
const glm::ivec3& block_pos) noexcept {
const auto find_it = chunks.find(pos);
@@ -20,19 +20,20 @@ maybe_get_block(const shared::math::coords& pos,
return find_it->second->get_block(block_pos);
}
-static std::optional<std::vector<shared::movement::blockinfo>>
-make_blockinfos(const shared::player& player,
- const client::world::chunk::map& chunks, const int width,
- const int height) noexcept {
+static std::optional<shared::movement::blocks>
+maybe_make_blocks(const glm::vec3& local_pos,
+ const shared::math::coords& chunk_pos,
+ const client::world::chunks_t& chunks, const int width,
+ const int height) noexcept {
- std::vector<shared::movement::blockinfo> blockinfos;
+ shared::movement::blocks blocks;
for (int x = -width; x <= width; ++x) {
for (int y = -height; y <= height; ++y) {
for (int z = -width; z <= width; ++z) {
const glm::ivec3 rel_pos =
- glm::ivec3{x, y, z} + glm::ivec3{player.local_pos};
+ glm::ivec3{x, y, z} + glm::ivec3{local_pos};
if (rel_pos.y < 0 ||
rel_pos.y >= shared::world::chunk::HEIGHT) {
@@ -41,7 +42,7 @@ make_blockinfos(const shared::player& player,
const shared::math::coords norm_chunk_pos =
shared::world::chunk::get_normalised_chunk(
- player.chunk_pos, rel_pos.x, rel_pos.z);
+ chunk_pos, rel_pos.x, rel_pos.z);
const glm::ivec3 norm_pos =
shared::world::chunk::get_normalised_coords(rel_pos);
@@ -52,72 +53,68 @@ make_blockinfos(const shared::player& player,
return std::nullopt;
}
- const glm::vec3 pos = glm::vec3{rel_pos} - player.local_pos;
+ const glm::vec3 pos = glm::vec3{rel_pos} - local_pos;
const shared::movement::aabb aabb = {
.min = glm::vec3{0.0f, 0.0f, 0.0f} + pos,
.max = glm::vec3{1.0f, 1.0f, 1.0f} + pos};
- blockinfos.push_back(
- shared::movement::blockinfo{.block = block.value(),
- .aabb = aabb,
- .chunk_pos = norm_chunk_pos,
- .pos = norm_pos});
+ blocks.push_back(
+ shared::movement::block{.block = *block,
+ .aabb = aabb,
+ .chunk_pos = norm_chunk_pos,
+ .pos = norm_pos});
}
}
}
- return blockinfos;
+ return blocks;
}
-void move(shared::player& player, client::world::chunk::map& chunks,
- const float deltatime) noexcept {
- const auto blockinfos =
- make_blockinfos(player, chunks, shared::movement::move_width,
- shared::movement::move_height);
- if (!blockinfos.has_value()) {
- return;
+std::optional<shared::animate> move(const shared::moveable& moveable,
+ const world::chunks_t& chunks) noexcept {
+
+ const auto xy = shared::movement::get_move_xy(state::tickrate, moveable);
+ const auto blocks = maybe_make_blocks(
+ moveable.get_local_pos(), moveable.get_chunk_pos(), chunks, xy.x, xy.y);
+ if (!blocks.has_value()) {
+ return std::nullopt;
}
- shared::movement::move(player, blockinfos.value(), deltatime);
+ return shared::movement::move(moveable, *blocks, state::tickrate);
}
std::optional<std::pair<shared::math::coords, glm::ivec3>>
-interact(const shared::player& player, const interact_mode& mode,
- const client::world::chunk::map& chunks) noexcept {
- const auto blockinfos = make_blockinfos(player, chunks, 5, 5);
- if (!blockinfos.has_value()) {
+interact(const shared::moveable& moveable, const interact_mode& mode,
+ const client::world::chunks_t& chunks) noexcept {
+ const auto blocks = maybe_make_blocks(
+ moveable.get_local_pos(), moveable.get_chunk_pos(), chunks, 5, 5);
+ if (!blocks.has_value()) {
return std::nullopt;
}
constexpr float epsilon = 0.001f;
- constexpr shared::movement::aabb player_aabb = {
- .min = {-shared::player::HALFWIDTH + epsilon, 0.0f + epsilon,
- -shared::player::HALFWIDTH + epsilon},
- .max = {shared::player::HALFWIDTH - epsilon,
- shared::player::HEIGHT - epsilon,
- shared::player::HALFWIDTH - epsilon}};
+ const auto& aabb = moveable.get_aabb();
constexpr shared::movement::aabb ray_aabb = {
.min = {-epsilon, shared::player::EYE_HEIGHT - epsilon, -epsilon},
.max = {epsilon, shared::player::EYE_HEIGHT + epsilon, epsilon}};
const shared::movement::moving_aabb ray_moving_aabb{
- .aabb = ray_aabb,
- .velocity = shared::math::angle_to_dir(player.viewangles)};
+ .aabb = ray_aabb, .velocity = moveable.get_angles().to_dir()};
- std::optional<std::pair<shared::movement::moving_aabb_ret,
- shared::movement::blockinfo>>
+ std::optional<
+ std::pair<shared::movement::moving_aabb_ret, shared::movement::block>>
best;
- for (const auto& blockinfo : *blockinfos) {
- if (blockinfo.block == shared::world::block::type::air) {
+ for (const auto& block : *blocks) {
+ if (block.block == shared::world::block::type::air) {
continue;
}
- if (!shared::world::block::is_removable(blockinfo.block)) {
+ if (!shared::world::block::is_removable(block.block)) {
continue;
}
const auto intersect = shared::movement::intersect_moving_aabbs(
ray_moving_aabb,
- shared::movement::moving_aabb{.aabb = blockinfo.aabb,
+ shared::movement::moving_aabb{.aabb = block.aabb,
.velocity = {0.0f, 0.0f, 0.0f}});
constexpr float MAX_REACH = 3.8f;
@@ -128,18 +125,18 @@ interact(const shared::player& player, const interact_mode& mode,
continue;
}
- best = std::make_pair(*intersect, blockinfo);
+ best = std::make_pair(*intersect, block);
}
if (!best.has_value()) {
return std::nullopt;
}
- const auto& [intersect, blockinfo] = *best;
+ const auto& [intersect, block] = *best;
- const glm::ivec3 position = blockinfo.pos;
+ const glm::ivec3 position = block.pos;
if (mode == interact_mode::remove) {
- return std::make_pair(blockinfo.chunk_pos, position);
+ return std::make_pair(block.chunk_pos, position);
}
const glm::ivec3 normal_i = -intersect.normal;
@@ -147,15 +144,15 @@ interact(const shared::player& player, const interact_mode& mode,
shared::world::chunk::get_normalised_coords(normal_i + position);
const auto find_it =
- std::find_if(std::begin(*blockinfos), std::end(*blockinfos),
+ std::find_if(std::begin(*blocks), std::end(*blocks),
[&](const auto b) { return b.pos == normalised_pos; });
- if (find_it == std::end(*blockinfos)) {
+ if (find_it == std::end(*blocks)) {
return std::nullopt;
}
if (!shared::world::block::is_replaceable(find_it->block)) {
return std::nullopt;
}
- if (shared::movement::intersect_aabbs(player_aabb, find_it->aabb)) {
+ if (shared::movement::intersect_aabbs(aabb, find_it->aabb)) {
return std::nullopt;
}
return std::make_pair(find_it->chunk_pos, normalised_pos);
diff --git a/src/client/movement/movement.hh b/src/client/movement/movement.hh
new file mode 100644
index 0000000..19144ba
--- /dev/null
+++ b/src/client/movement/movement.hh
@@ -0,0 +1,27 @@
+#ifndef CLIENT_MOVEMENT_MOVEMENT_HH_
+#define CLIENT_MOVEMENT_MOVEMENT_HH_
+
+#include <cstdint>
+#include <optional>
+
+#include "client/state/state.hh"
+#include "client/world/chunk.hh"
+#include "shared/entity/moveable.hh"
+#include "shared/movement/movement.hh"
+#include "shared/net/net.hh"
+
+namespace client {
+namespace movement {
+
+std::optional<shared::animate> move(const shared::moveable& moveable,
+ const world::chunks_t& chunks) noexcept;
+
+enum class interact_mode { add, remove };
+std::optional<std::pair<shared::math::coords, glm::ivec3>>
+interact(const shared::moveable& player, const interact_mode& mode,
+ const world::chunks_t& chunks) noexcept;
+
+} // namespace movement
+} // namespace client
+
+#endif
diff --git a/src/client/player.cc b/src/client/player.cc
deleted file mode 100644
index 5ff4118..0000000
--- a/src/client/player.cc
+++ /dev/null
@@ -1,58 +0,0 @@
-#include "player.hh"
-
-glm::vec3 client::player::get_world_pos(const shared::player& lp) noexcept {
- // clang-format off
- const float world_x = static_cast<float>(this->chunk_pos.x - lp.chunk_pos.x) * shared::world::chunk::WIDTH + this->local_pos.x;
- const float world_y = static_cast<float>(this->local_pos.y) + shared::player::HEIGHT / 2.0f;
- const float world_z = static_cast<float>(this->chunk_pos.z - lp.chunk_pos.z) * shared::world::chunk::WIDTH + this->local_pos.z;
- // clang-format on
- return {world_x, world_y, world_z};
-}
-
-void client::player::draw_playermodel(
- const shared::player& localplayer) noexcept {
-
- static client::render::model playermodel{"res/models/player/player.obj"};
- static client::render::program program{"res/shaders/model.vs",
- "res/shaders/model.fs"};
-
- const glm::vec3 world_pos = this->get_world_pos(localplayer);
- const glm::mat4 rotate = glm::rotate(glm::mat4(1.0f), -this->viewangles.yaw,
- glm::vec3(0.0f, 1.0f, 0.0f));
- const glm::mat4 translate = glm::translate(glm::mat4(1.0f), world_pos);
- const glm::mat4& view = client::render::camera::get_view();
- const glm::mat4& proj = client::render::camera::get_proj();
-
- playermodel.draw(program, proj * view * translate * rotate);
-}
-
-void client::player::draw_message(const shared::player& localplayer) noexcept {
- // Clear the message if it's too old.
- if (this->message.has_value()) {
- const auto now = std::chrono::steady_clock::now();
- if (now > this->message->receive_time + player::MSG_SHOW_TIME) {
- this->message.reset();
- }
- }
-
- if (!this->message.has_value()) {
- return;
- }
-
- const glm::vec3 world_pos = this->get_world_pos(localplayer);
- const auto pos = client::math::world_to_screen(world_pos);
-
- if (!pos.has_value()) {
- return;
- }
-
- const glm::vec2& window = client::render::get_window_size();
- const glm::mat4 proj = glm::ortho(0.0f, window.x, 0.0f, window.y);
- const glm::mat4 tran =
- glm::translate(glm::mat4(1.0f),
- glm::vec3{std::floor(pos->x), std::floor(pos->y), 0.0f});
-
- constexpr glm::vec4 white{1.0f, 1.0f, 1.0f, 1.0f};
- client::render::render_text(this->message->text, 30.0f, true, true, white,
- proj * tran);
-}
diff --git a/src/client/player.hh b/src/client/player.hh
deleted file mode 100644
index c41ba23..0000000
--- a/src/client/player.hh
+++ /dev/null
@@ -1,64 +0,0 @@
-#ifndef CLIENT_PLAYER_HH_
-#define CLIENT_PLAYER_HH_
-
-#include <chrono>
-#include <optional>
-#include <string>
-#include <utility>
-
-#include "client/math.hh"
-#include "client/render/camera.hh"
-#include "client/render/model.hh"
-#include "client/render/program.hh"
-#include "shared/player.hh"
-
-namespace client {
-
-// Renderable player, similar to client::chunk. We also store the message the
-// player wants to say.
-class player : public shared::player {
-public:
- static constexpr auto MSG_SHOW_TIME = std::chrono::seconds(15);
- struct message {
- std::string text;
- std::chrono::time_point<std::chrono::steady_clock> receive_time;
- message(const std::string& message) noexcept {
- this->text = message;
- this->receive_time = std::chrono::steady_clock::now();
- }
- };
- std::optional<message> message;
-
-private:
- glm::vec3 get_world_pos(const shared::player& lp) noexcept;
-
-public:
- player(shared::player&& p) noexcept
- : shared::player(std::forward<shared::player>(p)) {}
- player(const shared::player& p) noexcept : shared::player(p) {}
-
- void update(const shared::player& p) noexcept {
- this->index = p.index; // there is no syntax for doing this in one line
- this->commands = p.commands;
- this->chunk_pos = p.chunk_pos;
- this->local_pos = p.local_pos;
- this->viewangles = p.viewangles;
- this->velocity = p.velocity;
- }
-
- void draw_playermodel(const shared::player& localplayer) noexcept;
- void draw_message(const shared::player& localplayer) noexcept;
-
- const std::optional<struct message>& get_message() const noexcept {
- return this->message;
- }
-};
-
-using players = std::vector<player>;
-static inline client::player& get_localplayer(players& players) noexcept {
- return players[0];
-}
-
-} // namespace client
-
-#endif
diff --git a/src/client/draw.cc b/src/client/render/draw.cc
index c549ffe..d9ea9c3 100644
--- a/src/client/draw.cc
+++ b/src/client/render/draw.cc
@@ -1,53 +1,7 @@
-#include "draw.hh"
+#include "client/render/draw.hh"
namespace client {
-namespace draw {
-
-void draw_rectangle(const rectangle_args& ra) noexcept {
- const glm::vec2 pos = ra.pos.to_vec2();
- const glm::vec2 size = ra.size.to_vec2();
-
- // We want to render from the bottom left corner.
- render::render_rectangle(pos + (size / 2.0f), size, ra.colour);
-}
-
-void draw_colour(const glm::vec4& colour) noexcept {
- const rectangle_args args = {
- .pos = {.extent = {0.0f, 0.0f}},
- .size = {.extent = {1.0f, 1.0f}},
- .colour = colour,
- };
- draw_rectangle(args);
-}
-
-void draw_text(const std::string_view text, const text_args& args) noexcept {
- const glm::vec2& window = render::get_window_size();
- const float x = floorf(window.x * args.pos.extent.x + args.pos.offset.x);
- const float y = floorf(window.y * args.pos.extent.y + args.pos.offset.y);
- const unsigned height = static_cast<unsigned>(
- args.extent_height * window.y + args.offset_height);
-
- const auto make_matrix = [&](const float xo, const float yo) -> glm::mat4 {
- constexpr auto identity = glm::mat4(1.0f);
- const auto proj = glm::ortho(0.f, window.x, 0.f, window.y);
- const auto tran =
- glm::translate(identity, glm::vec3{x + xo, y + yo, 0.0f});
- return proj * tran;
- };
-
- glDisable(GL_DEPTH_TEST);
- glDisable(GL_BLEND);
- if (args.has_backing) {
- constexpr glm::vec4 black{0.0f, 0.0f, 0.0f, 1.0f};
- render::render_text(text, height, args.is_centered, args.is_vcentered,
- black, make_matrix(1.0f, -2.0f));
- }
- glEnable(GL_BLEND);
-
- render::render_text(text, height, args.is_centered, args.is_vcentered,
- args.colour, make_matrix(0, 0));
- glEnable(GL_DEPTH_TEST);
-}
+namespace render {
static void draw_state_info(const shared::player& lp) noexcept {
@@ -58,14 +12,16 @@ static void draw_state_info(const shared::player& lp) noexcept {
};
const auto make_position = [&]() -> std::string {
- const auto chunk_x = lp.chunk_pos.x;
- const auto chunk_z = lp.chunk_pos.z;
- const auto x = static_cast<double>(lp.local_pos.x);
- const auto y = static_cast<double>(lp.local_pos.y);
- const auto z = static_cast<double>(lp.local_pos.z);
+ const auto chunk_x = lp.get_chunk_pos().x;
+ const auto chunk_z = lp.get_chunk_pos().z;
+ const auto x = static_cast<double>(lp.get_local_pos().x);
+ const auto y = static_cast<double>(lp.get_local_pos().y);
+ const auto z = static_cast<double>(lp.get_local_pos().z);
constexpr auto WIDTH = shared::world::chunk::WIDTH;
- const auto abs_x = static_cast<double>(lp.chunk_pos.x) * WIDTH + x;
- const auto abs_z = static_cast<double>(lp.chunk_pos.z) * WIDTH + z;
+ const auto abs_x =
+ static_cast<double>(lp.get_chunk_pos().x) * WIDTH + x;
+ const auto abs_z =
+ static_cast<double>(lp.get_chunk_pos().z) * WIDTH + z;
constexpr auto nstr = [](const double d) -> std::string {
auto str = std::to_string(d);
@@ -80,16 +36,16 @@ static void draw_state_info(const shared::player& lp) noexcept {
};
const auto make_direction = [&]() -> std::string {
- const float yaw = glm::degrees(lp.viewangles.yaw);
- const float pitch = glm::degrees(lp.viewangles.pitch);
+ const float yaw = glm::degrees(lp.get_angles().yaw);
+ const float pitch = glm::degrees(lp.get_angles().pitch);
const std::string bearing = [&]() -> std::string {
- if (yaw >= 315.0f || yaw < 45.0f) {
- return "North";
- } else if (yaw >= 45.0f && yaw < 135.0f) {
- return "East";
- } else if (yaw >= 135.0f && yaw < 225.0f) {
+ if (yaw > 135.0f || yaw < -135.0f) {
return "South";
+ } else if (yaw > 45.0f) {
+ return "East";
+ } else if (yaw > -45.0f) {
+ return "North";
} else {
return "West";
}
@@ -100,34 +56,65 @@ static void draw_state_info(const shared::player& lp) noexcept {
};
const auto make_address = []() -> std::string {
- const std::string address = std::string(client::state.address);
- const std::string port = std::string(client::state.port);
+ const std::string address = std::string(client::state::address);
+ const std::string port = std::string(client::state::port);
return "address: " + address + ":" + port;
};
+ const auto make_tickrate = [&]() -> std::string {
+ return "tickrate: " + std::to_string(state::tickrate);
+ };
+
const auto make_seed = []() -> std::string {
- return "seed: " + std::to_string(client::state.seed);
+ return "seed: " + std::to_string(client::state::seed);
};
- const auto make_players = []() -> std::string {
- const auto index = std::to_string(client::state.localplayer);
+ const auto make_entities = []() -> std::string {
+ const auto index = std::to_string(*client::state::localplayer_index);
return "index: " + index;
};
const auto make_chunks = []() -> std::string {
- return "chunks [req, rcv]: " +
- std::to_string(client::state.requested_chunk_count) + ", " +
- std::to_string(client::state.networked_chunk_count) + " @ " +
- std::to_string(client::state.draw_distance) + " draw distance";
+ return "chunks: " +
+ std::to_string(client::state::requested_chunk_count) + ", " +
+ std::to_string(client::state::networked_chunk_count) + " @ " +
+ std::to_string(client::state::draw_distance) + " draw distance";
};
const auto make_speed = [&]() -> std::string {
- return "speed: " + nstr(glm::length(lp.velocity)) + " b/s";
+ return "speed: " + nstr(glm::length(lp.get_velocity())) + " b/s";
};
const auto make_velocity = [&]() -> std::string {
- return "velocity: (" + nstr(lp.velocity.x) + ", " +
- nstr(lp.velocity.y) + ", " + nstr(lp.velocity.z) + ")";
+ return "velocity: (" + nstr(lp.get_velocity().x) + ", " +
+ nstr(lp.get_velocity().y) + ", " + nstr(lp.get_velocity().z) +
+ ")";
+ };
+
+ const auto make_msg = [&]() -> std::string {
+ const auto& message = get_localplayer().get_message();
+ return "message: " + (message.has_value() ? message->text : "");
+ };
+
+ const auto make_biome = [&]() -> std::string {
+ const int x = static_cast<int>(lp.get_local_pos().x);
+ const int z = static_cast<int>(lp.get_local_pos().z);
+ const auto is_inside = [](const int val) {
+ return std::clamp(val, 0, shared::world::chunk::WIDTH) == val;
+ };
+ if (!is_inside(x) || !is_inside(z)) {
+ return "?";
+ }
+
+ const auto find_it = client::state::chunks.find(lp.get_chunk_pos());
+ if (find_it == std::end(client::state::chunks)) {
+ return "?";
+ }
+ const auto& chunk = find_it->second;
+ if (!chunk.has_value()) {
+ return "?";
+ }
+ return std::string{"biome: "} + chunk->get_biome(x, z);
};
// Draws all of our strings and works its way down the top of the screen.
@@ -139,15 +126,18 @@ static void draw_state_info(const shared::player& lp) noexcept {
draw_text(str, args);
args.pos.extent.y -= 0.02f;
};
- draw_str_trail("blockgame_linux v0.20");
+ draw_str_trail("blockgame_linux v0.22");
draw_str_trail(make_address());
+ draw_str_trail(make_tickrate());
draw_str_trail(make_seed());
- draw_str_trail(make_players());
+ draw_str_trail(make_entities());
draw_str_trail(make_chunks());
draw_str_trail(make_position());
draw_str_trail(make_direction());
draw_str_trail(make_speed());
draw_str_trail(make_velocity());
+ draw_str_trail(make_biome());
+ draw_str_trail(make_msg());
}
static void draw_fps() noexcept {
@@ -180,26 +170,38 @@ static void draw_overlay(const shared::player& lp) noexcept {
draw_fps();
}
-static void draw_main_pass(const shared::player& localplayer,
- client::players& players,
- client::world::chunk::map& chunks) noexcept {
- for (auto& chunk : chunks) {
- if (!chunk.second.has_value()) {
- continue;
+static void draw_main_pass(const client::player& localplayer,
+ client::entities_t& entities,
+ client::world::chunks_t& chunks) noexcept {
+
+ {
+ // zero means no limit
+ const unsigned chunks_per_frame =
+ client::settings::get<unsigned>({"video", "chunks_per_frame"}, 0u);
+ unsigned chunks_regenerated = 0u;
+ for (auto& chunk : chunks) {
+ if (!chunk.second.has_value()) {
+ continue;
+ }
+
+ const bool skip = chunks_per_frame == 0
+ ? false
+ : chunks_regenerated >= chunks_per_frame;
+ chunks_regenerated += chunk.second->draw(
+ chunks, localplayer, world::chunk::pass::solid, skip);
}
- chunk.second->draw(chunks, localplayer,
- client::world::chunk::pass::solid);
}
- // Draw players while ignoring the localplayer, which is always 0'th!
- // This also handles drawing messages.
- for (auto i = 1u; i < std::size(players); ++i) {
- players[i].draw_playermodel(localplayer);
+ for (const auto& [index, entity_ptr] : entities) {
+ if (index == localplayer.get_index()) {
+ continue;
+ }
+ entity_ptr->draw(localplayer);
}
}
-static void draw_water_pass(const shared::player& localplayer,
- client::world::chunk::map& chunks) noexcept {
+static void draw_water_pass(const client::player& localplayer,
+ client::world::chunks_t& chunks) noexcept {
for (auto& chunk : chunks) {
if (!chunk.second.has_value()) {
continue;
@@ -209,37 +211,33 @@ static void draw_water_pass(const shared::player& localplayer,
}
}
-static void draw_wts_text(const shared::player& localplayer,
- client::players& players) noexcept {
- for (auto i = 1u; i < std::size(players); ++i) {
- players[i].draw_message(localplayer);
+static void draw_wts_text(const client::player& localplayer,
+ client::entities_t& entities) noexcept {
+ for (const auto& [index, entity_ptr] : entities) {
+ if (index == localplayer.get_index()) {
+ continue;
+ }
+ entity_ptr->draw_wts(localplayer);
}
}
static void draw_hud(const client::player& localplayer,
- const client::world::chunk::map& chunks) noexcept {
- if (client::input::is_key_toggled(SDLK_F3)) {
+ const client::world::chunks_t& chunks) noexcept {
+ const auto should_draw_hud = !client::input::is_key_toggled(SDLK_F1);
+ if (client::input::is_key_toggled(SDLK_F3) && should_draw_hud) {
draw_overlay(localplayer);
}
- if (const auto& msg = localplayer.get_message(); msg.has_value()) {
- if (msg->receive_time + player::MSG_SHOW_TIME >=
- std::chrono::steady_clock::now()) {
- draw_text(msg->text, {.pos = relative_arg{.extent = {0.5f, 0.0f},
- .offset = {0.0f, 2.0f}},
- .extent_height = 0.0165f,
- .colour = {1.0f, 1.0f, 1.0f, 1.0f},
- .has_backing = true,
- .is_centered = true});
- }
+ if (client::window::is_open()) {
+ return;
}
- if (client::window::is_open()) {
+ if (!should_draw_hud) {
return;
}
// crosshair
- client::draw::draw_rectangle(
+ client::render::draw_rectangle(
{.pos = {.extent = {0.5f, 0.5f}, .offset = {-3.0f, -3.0f}},
.size = {.offset = {6.0f, 6.0f}},
.colour = {1.0f, 1.0f, 1.0f, 1.0f}});
@@ -250,10 +248,10 @@ static void draw_hud(const client::player& localplayer,
interact.has_value()) {
const auto [world_x, world_z] = [&]() -> std::pair<float, float> {
- const float offset_x =
- static_cast<float>(interact->first.x - localplayer.chunk_pos.x);
- const float offset_z =
- static_cast<float>(interact->first.z - localplayer.chunk_pos.z);
+ const float offset_x = static_cast<float>(
+ interact->first.x - localplayer.get_chunk_pos().x);
+ const float offset_z = static_cast<float>(
+ interact->first.z - localplayer.get_chunk_pos().z);
return {offset_x * shared::world::chunk::WIDTH,
offset_z * shared::world::chunk::WIDTH};
}();
@@ -275,20 +273,19 @@ static void update_camera(const shared::player& localplayer) noexcept {
// bit of maths for calculating the max distance we could can see
client::render::camera::get_zfar() = std::hypot(
static_cast<float>(world::chunk::HEIGHT),
- float(client::state.draw_distance) * std::hypot(16.0f, 16.0f));
+ float(client::state::draw_distance) * std::hypot(16.0f, 16.0f));
client::render::camera::get_pos() =
- localplayer.local_pos +
+ localplayer.get_local_pos() +
glm::vec3{0.0f, shared::player::EYE_HEIGHT, 0.0f};
- client::render::camera::get_front() =
- shared::math::angle_to_dir(localplayer.viewangles);
+ client::render::camera::get_front() = localplayer.get_angles().to_dir();
client::render::camera::update();
}
// We create framebuffer here to enable postprocessing and solve some
// rendering problems.
-static void draw_buffers(const shared::player& localplayer,
- client::players& players,
- client::world::chunk::map& chunks) noexcept {
+static void draw_buffers(const client::player& localplayer,
+ client::entities_t& entities,
+ client::world::chunks_t& chunks) noexcept {
const auto make_vbo = [](const auto& vertices) -> GLuint {
GLuint vbo = 0;
glGenBuffers(1, &vbo);
@@ -367,7 +364,7 @@ static void draw_buffers(const shared::player& localplayer,
// We render our main scene on the main framebuffer.
glBindFramebuffer(GL_FRAMEBUFFER, fbo_main);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
- draw_main_pass(localplayer, players, chunks);
+ draw_main_pass(localplayer, entities, chunks);
// We render our water scene on the water framebuffer.
glBindFramebuffer(GL_FRAMEBUFFER, fbo_water);
@@ -375,7 +372,7 @@ static void draw_buffers(const shared::player& localplayer,
const bool is_underwater = [&]() -> bool {
const glm::ivec3 pos{client::render::camera::get_pos()};
- const auto find_it = chunks.find(localplayer.chunk_pos);
+ const auto find_it = chunks.find(localplayer.get_chunk_pos());
if (find_it == std::end(chunks) || !find_it->second.has_value()) {
return false;
}
@@ -412,21 +409,20 @@ static void draw_buffers(const shared::player& localplayer,
glEnable(GL_DEPTH_TEST);
}
-void draw(client::players& players,
- client::world::chunk::map& chunks) noexcept {
- const auto& localplayer = client::get_localplayer(players);
+void draw(entities_t& entities, world::chunks_t& chunks) noexcept {
+ const auto& localplayer = get_localplayer();
update_camera(localplayer);
client::render::update_uniforms();
- draw_buffers(localplayer, players, chunks);
+ draw_buffers(localplayer, entities, chunks);
draw_hud(localplayer, chunks);
- draw_wts_text(localplayer, players);
+ draw_wts_text(localplayer, entities);
client::window::draw();
client::render::swap_window();
}
-} // namespace draw
+} // namespace render
} // namespace client
diff --git a/src/client/render/draw.hh b/src/client/render/draw.hh
new file mode 100644
index 0000000..bf43224
--- /dev/null
+++ b/src/client/render/draw.hh
@@ -0,0 +1,30 @@
+#ifndef CLIENT_RENDER_DRAW_HH_
+#define CLIENT_RENDER_DRAW_HH_
+
+#include <string_view>
+
+#include <glm/glm.hpp>
+
+#include "client/entity/player.hh"
+#include "client/input.hh"
+#include "client/movement/movement.hh"
+#include "client/render/camera.hh"
+#include "client/render/program.hh"
+#include "client/render/render.hh"
+#include "client/render/struct.hh"
+#include "client/settings.hh"
+#include "client/shared.hh"
+#include "client/window/hud_window.hh"
+#include "client/window/window.hh"
+#include "client/world/chunk.hh"
+
+namespace client {
+namespace render {
+
+void draw(client::entities_t& players,
+ client::world::chunks_t& chunks) noexcept;
+
+} // namespace render
+} // namespace client
+
+#endif
diff --git a/src/client/render/render.cc b/src/client/render/render.cc
index 18aa32a..941988d 100644
--- a/src/client/render/render.cc
+++ b/src/client/render/render.cc
@@ -27,7 +27,7 @@ SDL_Window* const& get_sdl_window() noexcept {
std::bind(SDL_GL_SetAttribute, SDL_GL_CONTEXT_MINOR_VERSION, 2));
SDL_Window* const ret =
- SDL_CreateWindow("blockgame_linux", 0, 0, 0, 0,
+ SDL_CreateWindow("blockgame_linux", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 0, 0,
SDL_WINDOW_OPENGL | SDL_WINDOW_FULLSCREEN_DESKTOP |
SDL_WINDOW_BORDERLESS);
check_sdl_pointer(ret);
@@ -124,6 +124,71 @@ void swap_window() noexcept {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
+// Additional sanity checks for our atlas.
+static void check_atlas(const client::render::texture& texture) {
+ if (texture.width % 6) {
+ throw std::runtime_error("invalid atlas; WIDTH is not divisible by 6");
+ }
+ if (texture.height % (texture.width / 6)) {
+ throw std::runtime_error(
+ "invalid atlas, HEIGHT is not divisible by (WIDTH / 6)");
+ }
+}
+
+GLuint get_texture_atlas() noexcept {
+ static const auto atlas = []() -> GLuint {
+ GLuint texture = 0;
+ glActiveTexture(GL_TEXTURE1);
+ glGenTextures(1, &texture);
+ glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
+
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S,
+ GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T,
+ GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER,
+ GL_LINEAR_MIPMAP_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ glTexParameterf(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_ANISOTROPY, 16.0f);
+
+ const client::render::texture stbi{"res/textures/atlas.png"};
+ check_atlas(stbi);
+ const int face_size = stbi.width / 6;
+
+ // 2D texture array, where our depth is our block face.
+ glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, face_size, face_size,
+ 6 * (stbi.height / face_size), 0,
+ stbi.channels == 3 ? GL_RGB : GL_RGBA, GL_UNSIGNED_BYTE,
+ nullptr);
+
+ // Fill the 2D texture array.
+ // Because our image has multiple images on the x-axis and opengl
+ // expects a single image per axis, we must fill it in row by row.
+ const auto get_pixel_xy = [&stbi](const int x, const int y) {
+ return stbi.image + 4 * (y * stbi.width + x);
+ };
+ for (int x = 0; x < 6; ++x) {
+ const int x_pixel = x * face_size;
+
+ for (int y = 0; y < stbi.height / face_size; ++y) {
+ const int y_pixel = y * face_size;
+
+ for (auto row = 0; row < face_size; ++row) {
+ glTexSubImage3D(
+ GL_TEXTURE_2D_ARRAY, 0, 0, row, x + y * 6, face_size, 1,
+ 1, GL_RGBA, GL_UNSIGNED_BYTE,
+ get_pixel_xy(x_pixel, row + y_pixel)); // pixel
+ }
+ }
+ }
+
+ glGenerateMipmap(GL_TEXTURE_2D_ARRAY);
+
+ return texture;
+ }();
+ return atlas;
+}
+
void render_cube_outline(const glm::vec3& pos,
const glm::vec4& colour) noexcept {
const auto generate_vbo = [](const auto& vertices) -> GLuint {
@@ -427,6 +492,8 @@ public:
font_atlas(const unsigned size) noexcept
: font_size(size), texture(make_texture_info(size)) {}
~font_atlas() noexcept { glDeleteTextures(1, &this->texture.texture); }
+ font_atlas(font_atlas&&) = delete;
+ font_atlas(const font_atlas&) = delete;
GLint get_height() const noexcept { return this->texture.height; }
GLint get_width() const noexcept { return this->texture.width; }
@@ -578,25 +645,71 @@ void update_uniforms() noexcept {
};
static const GLuint ubo = make_ubo();
- ubo_data upload = {.proj = camera::get_proj(),
- .view = camera::get_view(),
- .frustum = camera::get_frustum(),
- .znear = camera::get_znear(),
- .zfar = camera::get_zfar(),
- .xfov = glm::radians(camera::get_xfov()),
- .yfov = glm::radians(camera::get_yfov()),
- .time = []() -> unsigned {
- static const auto start =
- std::chrono::steady_clock::now();
- const auto now = std::chrono::steady_clock::now();
- return static_cast<unsigned>(
- (now - start) / std::chrono::milliseconds(1));
- }(),
- .xwindow = static_cast<float>(get_window_size().x),
- .ywindow = static_cast<float>(get_window_size().y)};
+ const ubo_data upload = {
+ .proj = camera::get_proj(),
+ .view = camera::get_view(),
+ .frustum = camera::get_frustum(),
+ .znear = camera::get_znear(),
+ .zfar = camera::get_zfar(),
+ .xfov = glm::radians(camera::get_xfov()),
+ .yfov = glm::radians(camera::get_yfov()),
+ .time = []() -> unsigned {
+ static const auto start = std::chrono::steady_clock::now();
+ const auto now = std::chrono::steady_clock::now();
+ return static_cast<unsigned>((now - start) /
+ std::chrono::milliseconds(1));
+ }(),
+ .xwindow = static_cast<float>(get_window_size().x),
+ .ywindow = static_cast<float>(get_window_size().y)};
glBindBuffer(GL_UNIFORM_BUFFER, ubo);
glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(upload), &upload);
}
+void draw_rectangle(const rectangle_args& ra) noexcept {
+ const glm::vec2 pos = ra.pos.to_vec2();
+ const glm::vec2 size = ra.size.to_vec2();
+
+ // We want to render from the bottom left corner.
+ render::render_rectangle(pos + (size / 2.0f), size, ra.colour);
+}
+
+void draw_colour(const glm::vec4& colour) noexcept {
+ const rectangle_args args = {
+ .pos = {.extent = {0.0f, 0.0f}},
+ .size = {.extent = {1.0f, 1.0f}},
+ .colour = colour,
+ };
+ draw_rectangle(args);
+}
+
+void draw_text(const std::string_view text, const text_args& args) noexcept {
+ const glm::vec2& window = render::get_window_size();
+ const float x = floorf(window.x * args.pos.extent.x + args.pos.offset.x);
+ const float y = floorf(window.y * args.pos.extent.y + args.pos.offset.y);
+ const unsigned height = static_cast<unsigned>(
+ args.extent_height * window.y + args.offset_height);
+
+ const auto make_matrix = [&](const float xo, const float yo) -> glm::mat4 {
+ constexpr auto identity = glm::mat4(1.0f);
+ const auto proj = glm::ortho(0.f, window.x, 0.f, window.y);
+ const auto tran =
+ glm::translate(identity, glm::vec3{x + xo, y + yo, 0.0f});
+ return proj * tran;
+ };
+
+ glDisable(GL_DEPTH_TEST);
+ glDisable(GL_BLEND);
+ if (args.has_backing) {
+ constexpr glm::vec4 black{0.0f, 0.0f, 0.0f, 1.0f};
+ render_text(text, height, args.is_centered, args.is_vcentered, black,
+ make_matrix(2.0f, -2.0f));
+ }
+ glEnable(GL_BLEND);
+
+ render_text(text, height, args.is_centered, args.is_vcentered, args.colour,
+ make_matrix(0, 0));
+ glEnable(GL_DEPTH_TEST);
+}
+
} // namespace render
} // namespace client
diff --git a/src/client/render/render.hh b/src/client/render/render.hh
index ca98e6f..c98043a 100644
--- a/src/client/render/render.hh
+++ b/src/client/render/render.hh
@@ -27,8 +27,10 @@
#include "client/render/camera.hh"
#include "client/render/program.hh"
+#include "client/render/struct.hh"
+#include "client/render/texture.hh"
#include "shared/shared.hh"
-#include "shared/world.hh"
+#include "shared/world/chunk.hh"
namespace client {
namespace render {
@@ -40,6 +42,7 @@ void quit() noexcept;
int get_fps() noexcept;
SDL_Window* const& get_sdl_window() noexcept;
const glm::ivec2& get_window_size() noexcept;
+GLuint get_texture_atlas() noexcept;
// Update
void update_uniforms() noexcept;
@@ -54,7 +57,26 @@ void render_text(const std::string_view text, const unsigned int size,
void render_cube_outline(const glm::vec3& pos,
const glm::vec4& colour) noexcept;
+// Draw is similar to render but more abstracted.
+void draw_colour(const glm::vec4& colour) noexcept;
+struct rectangle_args {
+ relative_arg pos;
+ relative_arg size;
+ glm::vec4 colour;
+};
+void draw_rectangle(const rectangle_args& args) noexcept;
+
+struct text_args {
+ relative_arg pos;
+ float extent_height; // we don't get width here,
+ float offset_height;
+ glm::vec4 colour;
+ bool has_backing;
+ bool is_centered;
+ bool is_vcentered;
+};
+void draw_text(const std::string_view text, const text_args& args) noexcept;
} // namespace render
} // namespace client
diff --git a/src/client/render/struct.cc b/src/client/render/struct.cc
new file mode 100644
index 0000000..a7a1b5d
--- /dev/null
+++ b/src/client/render/struct.cc
@@ -0,0 +1 @@
+#include "client/render/struct.hh"
diff --git a/src/client/render/struct.hh b/src/client/render/struct.hh
new file mode 100644
index 0000000..fd00ffd
--- /dev/null
+++ b/src/client/render/struct.hh
@@ -0,0 +1,29 @@
+#ifndef CLIENT_RENDER_STRUCT_HH_
+#define CLIENT_RENDER_STRUCT_HH_
+
+//#include <client/render/render.hh>
+#include <glm/glm.hpp>
+
+namespace client {
+namespace render {
+
+const glm::ivec2& get_window_size() noexcept; // forward declaration
+
+// Scale takes the range [0, 1] and represents an amount of the screen.
+// Offset is any value that is added after scale.
+struct relative_arg {
+ glm::vec2 extent;
+ glm::vec2 offset;
+
+ glm::vec2 to_vec2() const noexcept {
+ const glm::vec2& window = render::get_window_size();
+ const float x = this->extent.x * window.x + this->offset.x;
+ const float y = this->extent.y * window.y + this->offset.y;
+ return {x, y};
+ }
+};
+
+} // namespace render
+} // namespace client
+
+#endif
diff --git a/src/client/settings.cc b/src/client/settings.cc
index 8e2d0c9..3253f60 100644
--- a/src/client/settings.cc
+++ b/src/client/settings.cc
@@ -9,7 +9,7 @@ const char* const config_name = "blockgame_linux.conf";
namespace client {
namespace settings {
-static std::string get_settings_file() noexcept {
+static std::string get_settings_file() {
std::filesystem::create_directories(home_dir + ::config_dir);
std::ifstream in{home_dir + ::config_dir + config_name,
std::fstream::in | std::fstream::app};
@@ -20,7 +20,7 @@ static std::string get_settings_file() noexcept {
return file.ends_with('\n') ? file : file + '\n';
}
-static void set_settings_file(const std::string_view contents) noexcept {
+static void set_settings_file(const std::string_view contents) {
std::filesystem::create_directories(home_dir + ::config_dir);
std::ofstream out{home_dir + ::config_dir + ::config_name,
std::fstream::out | std::fstream::trunc};
@@ -31,7 +31,7 @@ static void set_settings_file(const std::string_view contents) noexcept {
// Heading maps setting to headings (ie, "camera -> {map with fov, 100.0f}).
using value_map = std::unordered_map<std::string, std::string>;
using setting_map = std::unordered_map<std::string, value_map>;
-static setting_map& get_settings() noexcept {
+static setting_map& get_settings() {
static setting_map ret = []() -> setting_map {
setting_map settings{};
@@ -50,8 +50,9 @@ static setting_map& get_settings() noexcept {
const size_t split_pos = line.find_first_of(':');
if (split_pos == std::string::npos) {
- shared::print::warn("client: failed to parse line in settings "
- "file, consider manual intervention\n");
+ shared::print::warn
+ << "client: failed to parse line in settings file, "
+ "consider manual intervention\n";
continue;
}
@@ -72,10 +73,21 @@ static setting_map& get_settings() noexcept {
return ret;
}
-// TODO fix this bit of code duplication
-std::string
-get_setting_str(const std::pair<const std::string&, const std::string&> loc,
- const std::string default_value) noexcept {
+std::optional<std::string> maybe_get_setting_str(const setting_pair_t& loc) {
+ const auto& [heading, name] = loc;
+
+ setting_map& settings = get_settings();
+ value_map& values = settings[heading];
+
+ const auto it = values.find(name);
+ if (it == std::end(values)) {
+ return std::nullopt;
+ }
+ return it->second;
+}
+
+std::string get_setting_str(const setting_pair_t& loc,
+ const std::string default_value) {
const auto& [heading, name] = loc;
setting_map& settings = get_settings();
@@ -89,9 +101,7 @@ get_setting_str(const std::pair<const std::string&, const std::string&> loc,
return find_it->second;
}
-void set_setting_str(
- const std::pair<const std::string&, const std::string&> loc,
- const std::string& value) noexcept {
+void set_setting_str(const setting_pair_t& loc, const std::string& value) {
const auto& [heading, name] = loc;
setting_map& settings = get_settings();
@@ -105,7 +115,7 @@ void set_setting_str(
find_it->second = value;
}
-void save() noexcept {
+void save() {
std::string contents;
const setting_map& settings = get_settings();
diff --git a/src/client/settings.hh b/src/client/settings.hh
index d65022b..11b785c 100644
--- a/src/client/settings.hh
+++ b/src/client/settings.hh
@@ -9,7 +9,7 @@
#include <boost/lexical_cast.hpp>
-#include "client/shared.hh"
+//#include "client/shared.hh"
#include "shared/shared.hh"
// This settings file just provides functionality for persistent variables,
@@ -18,20 +18,20 @@
namespace client {
namespace settings {
-void set_setting_str(
- const std::pair<const std::string&, const std::string&> loc,
- const std::string& value) noexcept;
+using setting_pair_t = std::pair<std::string, std::string>;
-std::string
-get_setting_str(const std::pair<const std::string&, const std::string&> loc,
- const std::string default_value) noexcept;
+std::optional<std::string> maybe_get_setting_str(const setting_pair_t&);
+
+void set_setting_str(const setting_pair_t& loc, const std::string& value);
+
+std::string get_setting_str(const setting_pair_t& loc,
+ const std::string default_value);
// Attempts to read the setting in the file, returning it if it exists.
// If either name or category does not exist, writes the default value in
// the file under the expected headings.
template <typename T>
-T get(const std::pair<const std::string, const std::string>& loc,
- const T& default_value) noexcept {
+T get(const setting_pair_t& loc, const T& default_value) {
const std::string value =
get_setting_str(loc, boost::lexical_cast<std::string>(default_value));
return boost::lexical_cast<T>(value);
@@ -40,12 +40,11 @@ T get(const std::pair<const std::string, const std::string>& loc,
// Attempts to set the setting in the file, overwriting the pre-existing
// value or writing a new entry.
template <typename T>
-void set(const std::pair<const std::string, const std::string>& loc,
- const T& value) noexcept {
+void set(const setting_pair_t& loc, const T& value) {
set_setting_str(loc, boost::lexical_cast<std::string>(value));
}
-void save() noexcept;
+void save();
} // namespace settings
} // namespace client
diff --git a/src/client/shared.cc b/src/client/shared.cc
index 3c6a2ec..0346e3d 100644
--- a/src/client/shared.cc
+++ b/src/client/shared.cc
@@ -1 +1,18 @@
#include "shared.hh"
+
+namespace client {
+
+entities_t::iterator get_entity_it(const std::uint32_t& index) noexcept {
+ return client::state::entities.find(index);
+}
+
+shared::entity& get_entity(const std::uint32_t index) noexcept {
+ return *get_entity_it(index)->second;
+}
+
+client::player& get_localplayer() noexcept {
+ return dynamic_cast<client::player&>(
+ get_entity(*client::state::localplayer_index));
+}
+
+} // namespace client
diff --git a/src/client/shared.hh b/src/client/shared.hh
index 557d13a..31c0ded 100644
--- a/src/client/shared.hh
+++ b/src/client/shared.hh
@@ -1,28 +1,14 @@
#ifndef CLIENT_SHARED_HH_
#define CLIENT_SHARED_HH_
-#include <cstdint>
-#include <string_view>
-
-#include "shared/net/connection.hh"
+#include "client/entity/player.hh"
+#include "client/state/entities.hh"
namespace client {
-struct state {
- std::string_view address;
- std::string_view port;
- std::uint64_t seed;
- std::uint32_t localplayer;
- std::size_t player_count;
- std::size_t requested_chunk_count;
- std::size_t networked_chunk_count;
- std::int32_t draw_distance;
- std::uint16_t latency;
-
- shared::net::connection* connection = nullptr;
-};
-
-inline state state;
+entities_t::iterator get_entity_it(const std::uint32_t& index) noexcept;
+shared::entity& get_entity(const std::uint32_t index) noexcept;
+client::player& get_localplayer() noexcept;
}; // namespace client
diff --git a/src/client/state/chunks.cc b/src/client/state/chunks.cc
new file mode 100644
index 0000000..470c552
--- /dev/null
+++ b/src/client/state/chunks.cc
@@ -0,0 +1 @@
+#include "client/state/chunks.hh"
diff --git a/src/client/state/chunks.hh b/src/client/state/chunks.hh
new file mode 100644
index 0000000..e0d4d82
--- /dev/null
+++ b/src/client/state/chunks.hh
@@ -0,0 +1,18 @@
+#ifndef CLIENT_STATE_CHUNKS_HH_
+#define CLIENT_STATE_CHUNKS_HH_
+
+#include <memory>
+#include <unordered_map>
+
+#include "client/world/chunk.hh"
+
+namespace client {
+namespace state {
+
+inline client::world::chunks_t chunks{4096, shared::world::chunk::hash,
+ shared::world::chunk::equal};
+
+} // namespace state
+} // namespace client
+
+#endif
diff --git a/src/client/state/entities.cc b/src/client/state/entities.cc
new file mode 100644
index 0000000..efb3533
--- /dev/null
+++ b/src/client/state/entities.cc
@@ -0,0 +1 @@
+#include "client/state/entities.hh"
diff --git a/src/client/state/entities.hh b/src/client/state/entities.hh
new file mode 100644
index 0000000..3c807b9
--- /dev/null
+++ b/src/client/state/entities.hh
@@ -0,0 +1,21 @@
+#ifndef CLIENT_STATE_ENTITIES_HH_
+#define CLIENT_STATE_ENTITIES_HH_
+
+#include <memory>
+#include <unordered_map>
+
+#include "client/entity/entity.hh"
+
+namespace client {
+
+using entities_t = std::unordered_map<shared::entity::index_t,
+ std::unique_ptr<client::entity>>;
+
+namespace state {
+
+inline entities_t entities;
+
+} // namespace state
+} // namespace client
+
+#endif
diff --git a/src/client/state/state.cc b/src/client/state/state.cc
new file mode 100644
index 0000000..2423c9a
--- /dev/null
+++ b/src/client/state/state.cc
@@ -0,0 +1,15 @@
+#include "client/state/state.hh"
+
+namespace client {
+namespace state {
+
+shared::time_duration_t get_time_per_tick() noexcept {
+ return std::chrono::microseconds(1'000'000) / tickrate;
+}
+
+bool has_initialised() noexcept {
+ return state::localplayer_index.has_value();
+}
+
+} // namespace state
+} // namespace client
diff --git a/src/client/state/state.hh b/src/client/state/state.hh
new file mode 100644
index 0000000..cfbb440
--- /dev/null
+++ b/src/client/state/state.hh
@@ -0,0 +1,41 @@
+#ifndef CLIENT_STATE_STATE_HH_
+#define CLIENT_STATE_STATE_HH_
+
+#include <optional>
+
+#include "shared/entity/entity.hh"
+#include "shared/net/connection.hh"
+#include "shared/shared.hh"
+
+namespace client {
+namespace state {
+
+inline std::string_view address;
+inline std::string_view port;
+inline std::uint64_t seed;
+inline std::size_t player_count;
+inline std::size_t requested_chunk_count;
+inline std::size_t networked_chunk_count;
+inline std::int32_t draw_distance;
+inline std::int32_t chunks_per_frame;
+inline std::uint32_t tickrate;
+
+inline shared::tick_t tick;
+inline float delta_ticks; // num ticks since the last tick, [0, inf)
+shared::time_duration_t get_time_per_tick() noexcept;
+
+// Speed of the client's move ticks should be 1.0f but if we're too slow (or the
+// server lags and we're too fast) this value should move to speed up the client
+// somewhat gracefully.
+inline float time_factor = 1.0f;
+
+inline shared::net::connection* connection = nullptr;
+
+// non-opt after init
+inline std::optional<shared::entity::index_t> localplayer_index;
+bool has_initialised() noexcept;
+
+} // namespace state
+} // namespace client
+
+#endif
diff --git a/src/client/window.cc b/src/client/window.cc
deleted file mode 100644
index 6a11252..0000000
--- a/src/client/window.cc
+++ /dev/null
@@ -1,449 +0,0 @@
-#include "window.hh"
-
-namespace client {
-namespace window {
-
-// All window objects should derive from basic_window, and make use of multiple
-// inheritance for specific behaviours.
-class basic_window {
-protected:
- // Colours borrowed from arc-dark(er).
- static constexpr glm::vec3 primary_clr{0.21f, 0.23f, 0.29f}; // dark
- static constexpr glm::vec3 secondary_clr{0.29f, 0.32f, 0.38f}; // less dark
- static constexpr glm::vec3 tertiary_clr{0.48, 0.50, 0.54}; // less dark ^2
- static constexpr glm::vec3 highlight_clr{0.32, 0.58, 0.88}; // light blue
- static constexpr glm::vec3 font_colour{0.88, 0.88, 0.88}; // light grey
-
-protected:
- glm::vec2 pos;
- glm::vec2 size;
-
-public:
- bool is_inside(const glm::vec2& v) const noexcept {
- if (v.x < this->pos.x || v.x > this->pos.x + this->size.x) {
- return false;
- }
- if (v.y < this->pos.y || v.y > this->pos.y + this->size.y) {
- return false;
- }
- return true;
- }
-
-public:
- basic_window(const client::draw::relative_arg& p,
- const client::draw::relative_arg& s) noexcept
- : pos(p.to_vec2()), size(s.to_vec2()) {}
- virtual ~basic_window() noexcept {}
-
- virtual bool maybe_handle_event(const SDL_Event&) noexcept { return false; }
- virtual void draw() noexcept {
- client::draw::draw_rectangle({.pos = {.offset = this->pos + 6.0f},
- .size = {.offset = this->size},
- .colour = {this->tertiary_clr, 0.9f}});
- client::draw::draw_rectangle({.pos = {.offset = this->pos},
- .size = {.offset = this->size},
- .colour = {this->primary_clr, 1.0f}});
- }
-};
-
-static void remove_top_layer() noexcept;
-class text_input_window : public basic_window {
-private:
- // text restricted to a size of 32.
- static const std::string& get_send_text() noexcept {
- auto& text = client::input::state.text_input;
- text = std::string{std::begin(text),
- std::begin(text) +
- static_cast<long>(std::min(
- std::size(text), shared::MAX_SAY_LENGTH))};
- return text;
- }
- static bool maybe_handle_keydown(const SDL_Event& event) noexcept {
- if (event.key.keysym.sym == SDLK_BACKSPACE) {
- if (!client::input::state.text_input.empty()) {
- client::input::state.text_input.pop_back();
- }
- return true;
- }
-
- if (event.key.keysym.sym != SDLK_RETURN || event.key.repeat) {
- return false;
- }
-
- if (!client::input::state.text_input.empty()) {
- client::send_say_packet(get_send_text());
- }
-
- remove_top_layer(); // DELETE ME
- return true;
- }
- static const glm::vec3& get_draw_colour() noexcept {
- if (client::input::state.text_input.length() >=
- shared::MAX_SAY_LENGTH) {
- return basic_window::highlight_clr;
- }
- return basic_window::primary_clr;
- }
-
-public:
- template <typename... Args>
- text_input_window(Args&&... args) noexcept
- : basic_window(std::forward<Args>(args)...) {
- client::input::set_text_input(true);
- }
- virtual ~text_input_window() noexcept {
- client::input::set_text_input(false);
- }
- virtual bool maybe_handle_event(const SDL_Event& event) noexcept override {
- switch (event.type) {
- case SDL_KEYDOWN:
- return this->maybe_handle_keydown(event);
- }
-
- return basic_window::maybe_handle_event(event);
- }
-
- virtual void draw() noexcept override {
- basic_window::draw();
-
- client::draw::draw_rectangle(
- {.pos = {.offset = this->pos},
- .size = {.offset = this->size},
- .colour = {this->get_draw_colour(), 1.0f}});
-
- client::draw::draw_text(
- this->get_send_text(),
- {.pos = {.extent = {0.0f, 0.0f},
- .offset = this->pos + (this->size / 2.0f)},
- .offset_height = this->size.y / 2.0f,
- .colour = {this->font_colour, 1.0f},
- .has_backing = false,
- .is_centered = true,
- .is_vcentered = true});
- };
-};
-
-class button_window : public basic_window {
-protected:
- std::string name;
- std::function<void()> callback;
- bool is_pressed;
-
-private:
- void handle_mousebuttondown(const SDL_Event& event) noexcept {
- if (event.button.button != SDL_BUTTON_LEFT) {
- return;
- }
- this->is_pressed = true;
- }
- void handle_mousebuttonup(const SDL_Event& event) noexcept {
- if (event.button.button != SDL_BUTTON_LEFT) {
- return;
- }
- if (!this->is_pressed) {
- return;
- }
- this->is_pressed = false;
- std::invoke(this->callback); // edge
- }
- const glm::vec3& get_draw_colour() noexcept {
- if (!this->is_inside(client::input::state.mouse_pos)) {
- this->is_pressed = false;
- return this->primary_clr;
- }
-
- if (!this->is_pressed) {
- return this->secondary_clr;
- }
-
- return this->highlight_clr;
- }
-
-public:
- template <typename... Args>
- button_window(const std::string_view n, const decltype(callback)& c,
- Args&&... args) noexcept
- : basic_window(std::forward<Args>(args)...), name(n), callback(c) {}
-
- virtual bool maybe_handle_event(const SDL_Event& event) noexcept override {
- if (this->is_inside(client::input::state.mouse_pos)) {
- switch (event.type) {
- case SDL_MOUSEBUTTONDOWN:
- this->handle_mousebuttondown(event);
- return true;
- case SDL_MOUSEBUTTONUP:
- this->handle_mousebuttonup(event);
- return true;
- }
- }
-
- return basic_window::maybe_handle_event(event);
- }
- virtual void draw() noexcept override {
- basic_window::draw();
-
- client::draw::draw_rectangle(
- {.pos = {.offset = this->pos},
- .size = {.offset = this->size},
- .colour = {this->get_draw_colour(), 1.0f}});
- client::draw::draw_text(
- this->name, {.pos = {.extent = {0.0f, 0.0f},
- .offset = this->pos + (this->size / 2.0f)},
- .offset_height = this->size.y / 2.0f,
- .colour = {this->font_colour, 1.0f},
- .has_backing = false,
- .is_centered = true,
- .is_vcentered = true});
- };
-};
-
-// Sliders are for numerical values of some type T.
-// TODO
-/*
-template <typename T>
-class slider_window : public basic_window {
-protected:
- std::string name;
- T min;
- T cur;
- T max;
- T& var;
-
-private:
- void handle_mousebuttondown(const SDL_Event& event) noexcept {}
- void handle_mousebuttonup(const SDL_Event& event) noexcept {}
-
-public:
- template <typename... Args>
- slider_window(const std::string_view name, const T& min, const T& cur,
- const T& max, T& var, Args&&... args) noexcept
- : basic_window(std::forward<Args>(args)...), name(name), min(min),
- cur(cur), max(max), var(var) {}
-
- // slider_window(
- virtual bool maybe_handle_event(const SDL_Event& event) noexcept
-override { switch (event.type) { case SDL_MOUSEBUTTONDOWN:
- this->handle_mousebuttondown(event);
- return true;
- case SDL_MOUSEBUTTONUP:
- this->handle_mousebuttonup(event);
- return true;
- }
- return basic_window::maybe_handle_event(event);
- }
- virtual void draw() noexcept override { basic_window::draw(); }
-};
-*/
-
-static void handle_event(const SDL_Event& event) noexcept; // ignore
-
-// All windows go in this list!
-using layer = std::forward_list<std::unique_ptr<basic_window>>;
-using layers = std::forward_list<layer>;
-static layers& get_layers() noexcept {
- // We callbacks for our window manager are initialised here too.
- static layers ret = []() -> layers {
- client::input::register_event_handler(&handle_event);
- client::input::set_text_input(false);
- client::input::set_mouse_relative(true);
- return {};
- }();
- return ret;
-}
-
-static void remove_top_layer() noexcept {
- if (!get_layers().empty()) {
- get_layers().pop_front();
- }
- // Our windows might be empty here, so set our mouse mode accordingly.
- if (!client::window::is_open()) {
- client::input::set_mouse_relative(true);
- }
-}
-
-// Constants used for uniform ui sizes.
-constexpr glm::vec2 lsize_extent{0.4, 0.075};
-constexpr glm::vec2 ssize_extent{0.15, 0.075};
-
-static void center_mouse_position() noexcept {
- const glm::vec2& window = client::render::get_window_size();
- client::input::set_mouse_position({window.x / 2.0f, window.y / 2.0f});
-}
-
-template <typename T, typename... Args>
-void push_window(Args&&... args) noexcept {
- get_layers().front().push_front(
- std::make_unique<T>(std::forward<Args>(args)...));
-}
-
-constexpr glm::vec2 center_extent(const glm::vec2 pos,
- const glm::vec2 size) noexcept {
- return {pos.x, pos.y - size.y / 2.0f};
-}
-
-static void make_options_menu() noexcept {
- get_layers().push_front({});
-
- /*
- push_window<::slider_window<float>>(
- "Field of Vision", 0.0f,
- settings::get(std::make_pair("gameplay", "fov"), 100.0f), 145.0f,
- remove_top_layer,
- client::draw::relative_arg{.extent =
- center_extent({0.3, 0.7},
- lsize_extent)}, client::draw::relative_arg{.extent = lsize_extent});
- */
-
- push_window<button_window>(
- "Back", remove_top_layer,
- client::draw::relative_arg{.extent =
- center_extent({0.3, 0.3}, ssize_extent)},
- client::draw::relative_arg{.extent = ssize_extent});
-}
-
-static void make_main_menu() noexcept {
- get_layers().push_front({});
-
- push_window<button_window>(
- "Return to Game", remove_top_layer,
- client::draw::relative_arg{.extent =
- center_extent({0.3, 0.7}, lsize_extent)},
- client::draw::relative_arg{.extent = lsize_extent});
-
- push_window<button_window>(
- "Options", make_options_menu,
- client::draw::relative_arg{
- .extent = center_extent({0.55, 0.6}, ssize_extent)},
- client::draw::relative_arg{.extent = ssize_extent});
-
- push_window<button_window>(
- "?", remove_top_layer,
- client::draw::relative_arg{
- .extent = center_extent({0.55, 0.5}, ssize_extent)},
- client::draw::relative_arg{.extent = ssize_extent});
-
- push_window<button_window>(
- "?", remove_top_layer,
- client::draw::relative_arg{
- .extent = center_extent({0.55, 0.4}, ssize_extent)},
- client::draw::relative_arg{.extent = ssize_extent});
-
- push_window<button_window>(
- "Exit Game", [] { shared::should_exit = true; },
- client::draw::relative_arg{.extent =
- center_extent({0.3, 0.3}, lsize_extent)},
- client::draw::relative_arg{.extent = lsize_extent});
-
- client::input::set_mouse_relative(false);
- center_mouse_position();
-}
-
-static void make_chat_window() noexcept {
- get_layers().push_front({});
-
- push_window<text_input_window>(
- client::draw::relative_arg{.extent =
- center_extent({0.3, 0.3}, lsize_extent)},
- client::draw::relative_arg{.extent = lsize_extent});
-
- client::input::set_mouse_relative(false);
- center_mouse_position();
-}
-
-static void handle_meta_return() noexcept {
- if (!is_open()) {
- make_chat_window();
- return;
- }
-}
-
-static void handle_meta_escape() noexcept {
- if (!is_open()) {
- make_main_menu();
- return;
- }
-
- remove_top_layer();
-}
-
-static void handle_meta_keydown(const SDL_Event& event) noexcept {
- if (event.key.repeat) { // only handle keypresses
- return;
- }
-
- switch (event.key.keysym.sym) {
- case SDLK_ESCAPE:
- handle_meta_escape();
- break;
- case SDLK_RETURN:
- handle_meta_return();
- break;
- }
-}
-
-static void handle_meta_mousemotion(const SDL_Event& event) noexcept {
- // We convert SDL's weird coordinates into useful ones (0,0 = bottom
- // left).
- client::input::state.mouse_pos = {
- event.motion.x,
- static_cast<int>(client::render::get_window_size().y) - event.motion.y};
-}
-
-static void handle_meta_windowevent(const SDL_Event& event) noexcept {
- if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
- if (!is_open()) {
- make_main_menu();
- return;
- }
- }
-}
-
-static void handle_meta_event(const SDL_Event& event) noexcept {
- switch (event.type) {
- case SDL_KEYDOWN:
- handle_meta_keydown(event);
- break;
- case SDL_MOUSEMOTION:
- handle_meta_mousemotion(event);
- break;
- case SDL_WINDOWEVENT:
- handle_meta_windowevent(event);
- break;
- }
-}
-
-static void handle_event(const SDL_Event& event) noexcept {
- // We ALWAYS update our mouse position.
- if (event.type == SDL_MOUSEMOTION) {
- handle_meta_mousemotion(event);
- }
-
- // Either a window consumes our event, or no window does - so we send
- // the event to our "meta handler" which does things like closing
- // windows etc.
- if (is_open()) {
- for (const auto& window : get_layers().front()) {
- if (window->maybe_handle_event(event)) {
- return;
- }
- }
- }
-
- handle_meta_event(event);
-}
-
-void draw() noexcept {
- if (!is_open()) {
- return;
- }
-
- client::draw::draw_colour({0.0f, 0.0f, 0.0f, 0.10f}); // very light shade
- for (const auto& window : get_layers().front()) {
- window->draw();
- }
-}
-
-bool is_open() noexcept { return !get_layers().empty(); }
-
-} // namespace window
-} // namespace client
diff --git a/src/client/window/basic_window.cc b/src/client/window/basic_window.cc
new file mode 100644
index 0000000..c96f94a
--- /dev/null
+++ b/src/client/window/basic_window.cc
@@ -0,0 +1,33 @@
+#include "client/window/basic_window.hh"
+
+namespace client {
+namespace window {
+
+using bw = basic_window;
+
+bool bw::is_inside(const glm::vec2& v, const glm::vec2& p,
+ const glm::vec2& s) noexcept {
+ if (v.x < p.x || v.x > p.x + s.x) {
+ return false;
+ }
+ if (v.y < p.y || v.y > p.y + s.y) {
+ return false;
+ }
+ return true;
+}
+
+bool bw::is_inside(const glm::vec2& v) const noexcept {
+ return is_inside(v, this->pos, this->size);
+}
+
+void bw::draw() noexcept {
+ client::render::draw_rectangle({.pos = {.offset = this->pos + 6.0f},
+ .size = {.offset = this->size},
+ .colour = {this->tertiary_clr, 0.9f}});
+ client::render::draw_rectangle({.pos = {.offset = this->pos},
+ .size = {.offset = this->size},
+ .colour = {this->primary_clr, 1.0f}});
+}
+
+} // namespace window
+} // namespace client
diff --git a/src/client/window/basic_window.hh b/src/client/window/basic_window.hh
new file mode 100644
index 0000000..292cc64
--- /dev/null
+++ b/src/client/window/basic_window.hh
@@ -0,0 +1,52 @@
+#ifndef CLIENT_WINDOW_BASIC_WINDOW_HH_
+#define CLIENT_WINDOW_BASIC_WINDOW_HH_
+
+#include <glm/glm.hpp>
+
+#include "client/entity/player.hh"
+#include "client/render/render.hh"
+#include "client/render/struct.hh"
+
+namespace client {
+namespace window {
+
+// All window objects should derive from basic_window, and make use of multiple
+// inheritance for specific behaviours.
+class basic_window {
+protected:
+ // Colours borrowed from arc-dark(er).
+ static constexpr glm::vec3 primary_clr{0.21f, 0.23f, 0.29f}; // dark
+ static constexpr glm::vec3 secondary_clr{0.29f, 0.32f, 0.38f}; // less dark
+ static constexpr glm::vec3 tertiary_clr{0.48, 0.50, 0.54}; // less dark ^2
+ static constexpr glm::vec3 highlight_clr{0.32, 0.58, 0.88}; // light blue
+ static constexpr glm::vec3 font_colour{0.88, 0.88, 0.88}; // light grey
+
+protected:
+ static constexpr float OUTLINE_WIDTH = 2.0f;
+ float get_item_size() const noexcept { return this->size.x / 10.0f; }
+
+protected:
+ glm::vec2 pos;
+ glm::vec2 size;
+
+public:
+ // Test if a vec2 v is inside a square starting at position p with size s.
+ static bool is_inside(const glm::vec2& v, const glm::vec2& p,
+ const glm::vec2& s) noexcept;
+ // Test if v is inside this->pos, this->size
+ bool is_inside(const glm::vec2& v) const noexcept;
+
+public:
+ basic_window(const client::render::relative_arg& pos,
+ const client::render::relative_arg& size) noexcept
+ : pos(pos.to_vec2()), size(size.to_vec2()) {}
+ virtual ~basic_window() noexcept {}
+
+ virtual bool maybe_handle_event(const SDL_Event&) noexcept { return false; }
+ virtual void draw() noexcept;
+};
+
+} // namespace window
+} // namespace client
+
+#endif
diff --git a/src/client/window/button_window.cc b/src/client/window/button_window.cc
new file mode 100644
index 0000000..4743c0a
--- /dev/null
+++ b/src/client/window/button_window.cc
@@ -0,0 +1,71 @@
+#include "client/window/button_window.hh"
+
+namespace client {
+namespace window {
+
+using bw = class button_window;
+
+void bw::handle_mousebuttondown(const SDL_Event& event) noexcept {
+ if (event.button.button != SDL_BUTTON_LEFT) {
+ return;
+ }
+ this->is_pressed = true;
+}
+
+void bw::handle_mousebuttonup(const SDL_Event& event) noexcept {
+ if (event.button.button != SDL_BUTTON_LEFT) {
+ return;
+ }
+ if (!this->is_pressed) {
+ return;
+ }
+ this->is_pressed = false;
+ std::invoke(this->callback); // edge
+}
+
+const glm::vec3& bw::get_draw_colour() noexcept {
+ if (!this->is_inside(client::input::state.mouse_pos)) {
+ this->is_pressed = false;
+ return this->primary_clr;
+ }
+
+ if (!this->is_pressed) {
+ return this->secondary_clr;
+ }
+
+ return this->highlight_clr;
+}
+
+bool bw::maybe_handle_event(const SDL_Event& event) noexcept {
+ if (this->is_inside(client::input::state.mouse_pos)) {
+ switch (event.type) {
+ case SDL_MOUSEBUTTONDOWN:
+ this->handle_mousebuttondown(event);
+ return true;
+ case SDL_MOUSEBUTTONUP:
+ this->handle_mousebuttonup(event);
+ return true;
+ }
+ }
+
+ return basic_window::maybe_handle_event(event);
+}
+
+void bw::draw() noexcept {
+ basic_window::draw();
+
+ client::render::draw_rectangle({.pos = {.offset = this->pos},
+ .size = {.offset = this->size},
+ .colour = {this->get_draw_colour(), 1.0f}});
+ client::render::draw_text(
+ this->name, {.pos = {.extent = {0.0f, 0.0f},
+ .offset = this->pos + (this->size / 2.0f)},
+ .offset_height = this->size.y / 2.0f,
+ .colour = {this->font_colour, 1.0f},
+ .has_backing = false,
+ .is_centered = true,
+ .is_vcentered = true});
+};
+
+} // namespace window
+} // namespace client
diff --git a/src/client/window/button_window.hh b/src/client/window/button_window.hh
new file mode 100644
index 0000000..6847696
--- /dev/null
+++ b/src/client/window/button_window.hh
@@ -0,0 +1,40 @@
+#ifndef CLIENT_WINDOW_BUTTON_WINDOW_HH_
+#define CLIENT_WINDOW_BUTTON_WINDOW_HH_
+
+#include <functional>
+#include <string>
+
+#include "client/input.hh"
+#include "client/render/render.hh"
+#include "client/window/basic_window.hh"
+
+namespace client {
+namespace window {
+
+class button_window : public basic_window {
+protected:
+ std::string name;
+ std::function<void()> callback;
+ bool is_pressed;
+
+private:
+ void handle_mousebuttondown(const SDL_Event& event) noexcept;
+ void handle_mousebuttonup(const SDL_Event& event) noexcept;
+ const glm::vec3& get_draw_colour() noexcept;
+
+public:
+ template <typename... Args>
+ button_window(const std::string_view n, const decltype(callback)& c,
+ Args&&... args) noexcept
+ : basic_window(std::forward<Args>(args)...), name(n), callback(c),
+ is_pressed(false) {}
+ virtual ~button_window() noexcept {}
+
+ virtual bool maybe_handle_event(const SDL_Event& event) noexcept override;
+ virtual void draw() noexcept override;
+};
+
+} // namespace window
+} // namespace client
+
+#endif
diff --git a/src/client/window/hud_window.cc b/src/client/window/hud_window.cc
new file mode 100644
index 0000000..52995ca
--- /dev/null
+++ b/src/client/window/hud_window.cc
@@ -0,0 +1,88 @@
+#include "client/window/hud_window.hh"
+
+namespace client {
+namespace window {
+
+using hw = class hud_window;
+bool hw::maybe_handle_keydown(const SDL_Event& event) noexcept {
+ const auto& sym = event.key.keysym.sym;
+ const auto index = static_cast<int>(sym);
+
+ const auto min = static_cast<int>(SDLK_0);
+ const auto max = static_cast<int>(SDLK_9);
+ if (index < min || index > max) {
+ return false;
+ }
+
+ const auto active =
+ static_cast<std::uint32_t>((((sym - min) - 1) + 10) % 10);
+ client::get_localplayer().get_mutable_active_item() = active;
+ return true;
+}
+
+bool hw::maybe_handle_event(const SDL_Event& event) noexcept {
+ if (is_open()) {
+ return false;
+ }
+
+ switch (event.type) {
+ case SDL_KEYDOWN:
+ return this->maybe_handle_keydown(event);
+ default:
+ break;
+ }
+ return false;
+}
+
+void hw::draw() noexcept {
+ this->basic_window::draw();
+
+ const float item_size = this->get_item_size();
+
+ const auto& localplayer = client::get_localplayer();
+ client::render::draw_rectangle(
+ {.pos = {.offset = {this->pos.x + static_cast<float>(
+ localplayer.get_active_item()) *
+ item_size,
+ this->pos.y}},
+ .size = {.offset = {item_size, item_size}},
+ .colour = {this->secondary_clr, 1.0f}});
+
+ for (int i = 1; i < shared::player::INVENTORY_COLS; ++i) {
+ const float off = item_size * static_cast<float>(i);
+ client::render::draw_rectangle(
+ {.pos = {.offset = {this->pos.x + off,
+ this->pos.y + OUTLINE_WIDTH}},
+ .size = {.offset = {OUTLINE_WIDTH,
+ item_size - 2.0f * OUTLINE_WIDTH}},
+ .colour = {this->tertiary_clr, 1.0f}});
+ }
+
+ const auto& inventory = localplayer.inventory;
+ for (int x = 0; x < shared::player::INVENTORY_COLS; ++x) {
+ const auto& item = inventory.contents[static_cast<unsigned long>(x)];
+ if (item == nullptr) {
+ continue;
+ }
+
+ const glm::vec2 off{glm::vec2{x, 0} * item_size};
+ const auto item_ptr = dynamic_cast<client::item::item*>(&*item);
+ if (item_ptr == nullptr) {
+ continue;
+ }
+ item_ptr->draw(this->pos + off, glm::vec2{item_size, item_size});
+
+ client::render::draw_text(
+ std::to_string(item->quantity),
+ {.pos = {.offset = this->pos + off +
+ glm::vec2{item_size * 0.75f, item_size * 0.2f}},
+ .offset_height = item_size * 0.40f,
+ .colour = {this->font_colour, 1.0f},
+ .has_backing = false,
+ .is_centered = true,
+ .is_vcentered = true});
+ }
+}
+
+} // namespace window
+} // namespace client
diff --git a/src/client/window/hud_window.hh b/src/client/window/hud_window.hh
new file mode 100644
index 0000000..7c03e0e
--- /dev/null
+++ b/src/client/window/hud_window.hh
@@ -0,0 +1,32 @@
+#ifndef CLIENT_WINDOW_HUD_WINDOW_HH_
+#define CLIENT_WINDOW_HUD_WINDOW_HH_
+
+#include "client/render/struct.hh"
+#include "client/window/basic_window.hh"
+#include "client/window/window.hh"
+#include "shared/entity/player.hh"
+
+namespace client {
+namespace window {
+
+class hud_window : public basic_window {
+
+private:
+ bool maybe_handle_keydown(const SDL_Event& event) noexcept;
+
+public:
+ template <typename... Args>
+ hud_window(const float size) noexcept
+ : basic_window(
+ client::render::relative_arg{.extent = {0.5f, 0.0f},
+ .offset = {-size / 2.0f, 0.0f}},
+ client::render::relative_arg{.offset = {size, size / 10.0f}}) {}
+
+ virtual void draw() noexcept override;
+ virtual bool maybe_handle_event(const SDL_Event& event) noexcept override;
+};
+
+} // namespace window
+} // namespace client
+
+#endif
diff --git a/src/client/window/inventory_window.cc b/src/client/window/inventory_window.cc
new file mode 100644
index 0000000..5023f1d
--- /dev/null
+++ b/src/client/window/inventory_window.cc
@@ -0,0 +1,196 @@
+#include "client/window/inventory_window.hh"
+
+namespace client {
+namespace window {
+
+using iw = class inventory_window;
+
+bool iw::maybe_handle_mousebuttondown(const SDL_Event& event) noexcept {
+
+ if (event.button.button != SDL_BUTTON_LEFT) {
+ return false;
+ }
+ if (!this->is_inside(client::input::state.mouse_pos)) {
+ return false;
+ }
+
+ const auto index =
+ this->maybe_get_inventory_index(client::input::state.mouse_pos);
+ if (!index.has_value()) {
+ return false;
+ }
+
+ const auto& inventory = client::get_localplayer().inventory;
+ if (inventory.contents[*index] != nullptr) {
+ this->grabbed.emplace(*index);
+ }
+ return true;
+}
+
+static proto::packet make_swap_packet(const std::uint32_t& a,
+ const std::uint32_t& b) noexcept {
+ proto::packet ret;
+ const auto packet = ret.mutable_item_swap_packet();
+ packet->set_index_a(a);
+ packet->set_index_b(b);
+ return ret;
+}
+
+bool iw::maybe_handle_mousebuttonup(const SDL_Event& event) noexcept {
+
+ if (event.button.button != SDL_BUTTON_LEFT) {
+ return false;
+ }
+ if (!this->grabbed.has_value()) {
+ return false;
+ }
+
+ if (const auto index =
+ this->maybe_get_inventory_index(input::state.mouse_pos);
+ index.has_value() && index != *grabbed) {
+
+ auto& inventory = client::get_localplayer().inventory;
+ std::swap(inventory.contents[*grabbed], inventory.contents[*index]);
+
+ // replicate on server
+ state::connection->rsend_packet(
+ make_swap_packet(static_cast<std::uint32_t>(*grabbed),
+ static_cast<std::uint32_t>(*index)));
+ }
+
+ this->grabbed.reset();
+ return true;
+}
+
+std::optional<unsigned long>
+iw::maybe_get_inventory_index(const glm::vec2& pos) const noexcept {
+
+ const float item_size = this->get_item_size();
+ for (int x = 0; x < shared::player::INVENTORY_COLS; ++x) {
+ for (int y = 0; y < shared::player::INVENTORY_ROWS; ++y) {
+ const glm::vec2 off{this->pos +
+ glm::vec2{OUTLINE_WIDTH, OUTLINE_WIDTH} +
+ glm::vec2{x, y} * item_size};
+ if (!basic_window::is_inside(pos, off,
+ glm::vec2{item_size, item_size})) {
+ continue;
+ }
+ return x + y * shared::player::INVENTORY_COLS;
+ }
+ }
+ return std::nullopt;
+}
+
+bool iw::maybe_handle_event(const SDL_Event& event) noexcept {
+ switch (event.type) {
+ case SDL_MOUSEBUTTONDOWN:
+ return this->maybe_handle_mousebuttondown(event);
+ case SDL_MOUSEBUTTONUP:
+ return this->maybe_handle_mousebuttonup(event);
+ }
+
+ return basic_window::maybe_handle_event(event);
+}
+
+void iw::draw() noexcept {
+ basic_window::draw();
+
+ const float item_size = this->get_item_size();
+
+ const auto& localplayer = client::get_localplayer();
+ client::render::draw_rectangle(
+ {.pos = {.offset = {this->pos.x + static_cast<float>(
+ localplayer.get_active_item()) *
+ item_size,
+ this->pos.y}},
+ .size = {.offset = {item_size, item_size}},
+ .colour = {this->secondary_clr, 1.0f}});
+ for (int i = 1; i < shared::player::INVENTORY_COLS; ++i) {
+ const float off = item_size * static_cast<float>(i);
+ client::render::draw_rectangle(
+ {.pos = {.offset = {this->pos.x + off,
+ this->pos.y + OUTLINE_WIDTH}},
+ .size = {.offset = {OUTLINE_WIDTH,
+ item_size * shared::player::INVENTORY_ROWS -
+ OUTLINE_WIDTH}},
+ .colour = {this->tertiary_clr, 1.0f}});
+
+ if (i <= shared::player::INVENTORY_ROWS) {
+ client::render::draw_rectangle(
+ {.pos = {.offset = {pos.x + OUTLINE_WIDTH, pos.y + off}},
+ .size = {.offset = {this->size.x - 2.0f * OUTLINE_WIDTH,
+ OUTLINE_WIDTH}},
+ .colour = {this->tertiary_clr, 1.0f}});
+ }
+ }
+
+ const auto& inventory = localplayer.inventory;
+ if (const auto index =
+ this->maybe_get_inventory_index(client::input::state.mouse_pos);
+ index.has_value()) {
+ const glm::vec2 off{glm::vec2{*index % shared::player::INVENTORY_COLS,
+ *index / shared::player::INVENTORY_COLS} *
+ item_size};
+ client::render::draw_rectangle(
+ {.pos = {.offset = this->pos + off +
+ glm::vec2{OUTLINE_WIDTH, OUTLINE_WIDTH}},
+ .size = {.offset = glm::vec2{item_size, item_size} -
+ glm::vec2{OUTLINE_WIDTH, OUTLINE_WIDTH}},
+ .colour = {this->secondary_clr, 1.0f}});
+ }
+
+ for (int x = 0; x < shared::player::INVENTORY_COLS; ++x) {
+ for (int y = 0; y < shared::player::INVENTORY_ROWS; ++y) {
+ const auto index = static_cast<unsigned long>(
+ x + y * shared::player::INVENTORY_COLS);
+ const auto& item = inventory.contents[index];
+ if (item == nullptr) {
+ continue;
+ }
+ if (this->grabbed.has_value() && *this->grabbed == index) {
+ continue;
+ }
+
+ const glm::vec2 off{glm::vec2{x, y} * item_size};
+ const auto item_ptr = dynamic_cast<client::item::item*>(&*item);
+ if (item_ptr == nullptr) {
+ continue;
+ }
+ item_ptr->draw(this->pos + off, glm::vec2{item_size, item_size});
+
+ client::render::draw_text(
+ std::to_string(item->quantity),
+ {.pos = {.offset =
+ this->pos + off +
+ glm::vec2{item_size * 0.75f, item_size * 0.2f}},
+ .offset_height = item_size * 0.40f,
+ .colour = {this->font_colour, 1.0f},
+ .has_backing = false,
+ .is_centered = true,
+ .is_vcentered = true});
+ }
+ }
+
+ if (this->grabbed.has_value()) {
+ const auto item_ptr =
+ dynamic_cast<client::item::item*>(&*inventory.contents[*grabbed]);
+ if (item_ptr != nullptr) {
+ item_ptr->draw(client::input::state.mouse_pos -
+ glm::vec2{item_size, item_size} * 0.5f,
+ glm::vec2{item_size, item_size});
+ client::render::draw_text(
+ std::to_string(item_ptr->quantity),
+ {.pos = {.offset =
+ client::input::state.mouse_pos +
+ glm::vec2{item_size * 0.25f, item_size * -0.3f}},
+ .offset_height = item_size * 0.40f,
+ .colour = {this->font_colour, 1.0f},
+ .has_backing = false,
+ .is_centered = true,
+ .is_vcentered = true});
+ }
+ }
+}
+
+} // namespace window
+} // namespace client
diff --git a/src/client/window/inventory_window.hh b/src/client/window/inventory_window.hh
new file mode 100644
index 0000000..a77ae2f
--- /dev/null
+++ b/src/client/window/inventory_window.hh
@@ -0,0 +1,40 @@
+#ifndef CLIENT_WINDOW_INVENTORY_WINDOW_HH_
+#define CLIENT_WINDOW_INVENTORY_WINDOW_HH_
+
+#include <cstdint>
+#include <optional>
+
+#include "client/item/item.hh"
+#include "client/render/draw.hh"
+#include "client/state/state.hh"
+#include "client/window/basic_window.hh"
+
+namespace client {
+namespace window {
+
+class inventory_window : public basic_window {
+private:
+ std::optional<unsigned long> grabbed;
+
+private:
+ std::optional<unsigned long>
+ maybe_get_inventory_index(const glm::vec2& pos) const noexcept;
+
+private:
+ bool maybe_handle_mousebuttondown(const SDL_Event& event) noexcept;
+ bool maybe_handle_mousebuttonup(const SDL_Event& event) noexcept;
+
+public:
+ template <typename... Args>
+ inventory_window(Args&&... args) noexcept
+ : basic_window(std::forward<Args>(args)...) {}
+ virtual ~inventory_window() noexcept {}
+
+ virtual bool maybe_handle_event(const SDL_Event& event) noexcept override;
+ virtual void draw() noexcept override;
+};
+
+} // namespace window
+} // namespace client
+
+#endif
diff --git a/src/client/window/text_input_window.cc b/src/client/window/text_input_window.cc
new file mode 100644
index 0000000..0d556a4
--- /dev/null
+++ b/src/client/window/text_input_window.cc
@@ -0,0 +1,81 @@
+#include "client/window/text_input_window.hh"
+
+namespace client {
+namespace window {
+
+using tiw = class text_input_window;
+
+const std::string& tiw::get_send_text() noexcept {
+ auto& text = client::input::state.text_input;
+ text = std::string{std::begin(text),
+ std::begin(text) +
+ static_cast<long>(std::min(std::size(text),
+ shared::MAX_SAY_LENGTH))};
+ return text;
+}
+
+proto::packet tiw::make_say_packet() noexcept {
+ proto::packet packet;
+
+ const auto sub_say_packet = packet.mutable_say_packet();
+ sub_say_packet->set_text(get_send_text());
+
+ return packet;
+}
+
+bool tiw::maybe_handle_keydown(const SDL_Event& event) noexcept {
+ if (event.key.keysym.sym == SDLK_BACKSPACE) {
+ if (!client::input::state.text_input.empty()) {
+ client::input::state.text_input.pop_back();
+ }
+ return true;
+ }
+
+ if (event.key.keysym.sym != SDLK_RETURN || event.key.repeat) {
+ return false;
+ }
+
+ if (!client::input::state.text_input.empty()) {
+ client::state::connection->rsend_packet(make_say_packet());
+ }
+
+ pop_window();
+ return true;
+}
+
+const glm::vec3& tiw::get_draw_colour() noexcept {
+ if (client::input::state.text_input.length() >= shared::MAX_SAY_LENGTH) {
+ return basic_window::highlight_clr;
+ }
+ return basic_window::primary_clr;
+}
+
+bool tiw::maybe_handle_event(const SDL_Event& event) noexcept {
+ switch (event.type) {
+ case SDL_KEYDOWN:
+ return this->maybe_handle_keydown(event);
+ }
+
+ return basic_window::maybe_handle_event(event);
+}
+
+void tiw::draw() noexcept {
+ basic_window::draw();
+
+ client::render::draw_rectangle({.pos = {.offset = this->pos},
+ .size = {.offset = this->size},
+ .colour = {this->get_draw_colour(), 1.0f}});
+
+ client::render::draw_text(
+ this->get_send_text(),
+ {.pos = {.extent = {0.0f, 0.0f},
+ .offset = this->pos + (this->size / 2.0f)},
+ .offset_height = this->size.y / 2.0f,
+ .colour = {this->font_colour, 1.0f},
+ .has_backing = false,
+ .is_centered = true,
+ .is_vcentered = true});
+};
+
+} // namespace window
+} // namespace client
diff --git a/src/client/window/text_input_window.hh b/src/client/window/text_input_window.hh
new file mode 100644
index 0000000..edff672
--- /dev/null
+++ b/src/client/window/text_input_window.hh
@@ -0,0 +1,43 @@
+#ifndef CLIENT_WINDOW_TEXT_INPUT_WINDOW_HH_
+#define CLIENT_WINDOW_TEXT_INPUT_WINDOW_HH_
+
+#include <string>
+
+#include "client/input.hh"
+#include "client/shared.hh"
+#include "client/state/state.hh"
+#include "client/window/basic_window.hh"
+#include "shared/net/proto.hh"
+
+namespace client {
+namespace window {
+
+void pop_window() noexcept; // forward declaration
+
+class text_input_window : public basic_window {
+private:
+ // text restricted to a size of 32.
+ static const std::string& get_send_text() noexcept;
+ static proto::packet make_say_packet() noexcept;
+
+ static bool maybe_handle_keydown(const SDL_Event& event) noexcept;
+ static const glm::vec3& get_draw_colour() noexcept;
+
+public:
+ template <typename... Args>
+ text_input_window(Args&&... args) noexcept
+ : basic_window(std::forward<Args>(args)...) {
+ client::input::set_text_input(true);
+ }
+ virtual ~text_input_window() noexcept {
+ client::input::set_text_input(false);
+ }
+ virtual bool maybe_handle_event(const SDL_Event& event) noexcept override;
+
+ virtual void draw() noexcept override;
+};
+
+} // namespace window
+} // namespace client
+
+#endif
diff --git a/src/client/window/window.cc b/src/client/window/window.cc
new file mode 100644
index 0000000..648997f
--- /dev/null
+++ b/src/client/window/window.cc
@@ -0,0 +1,318 @@
+#include "window.hh"
+
+namespace client {
+namespace window {
+
+// TODO
+
+// Sliders are for numerical values of some type T.
+// TODO
+/*
+template <typename T>
+class slider_window : public basic_window {
+protected:
+ std::string name;
+ T min;
+ T cur;
+ T max;
+ T& var;
+
+private:
+ void handle_mousebuttondown(const SDL_Event& event) noexcept {}
+ void handle_mousebuttonup(const SDL_Event& event) noexcept {}
+
+public:
+ template <typename... Args>
+ slider_window(const std::string_view name, const T& min, const T& cur,
+ const T& max, T& var, Args&&... args) noexcept
+ : basic_window(std::forward<Args>(args)...), name(name), min(min),
+ cur(cur), max(max), var(var) {}
+
+ // slider_window(
+ virtual bool maybe_handle_event(const SDL_Event& event) noexcept
+override { switch (event.type) { case SDL_MOUSEBUTTONDOWN:
+ this->handle_mousebuttondown(event);
+ return true;
+ case SDL_MOUSEBUTTONUP:
+ this->handle_mousebuttonup(event);
+ return true;
+ }
+ return basic_window::maybe_handle_event(event);
+ }
+ virtual void draw() noexcept override { basic_window::draw(); }
+};
+*/
+
+static void handle_event(const SDL_Event& event) noexcept; // ignore
+
+static hud_window& get_hud_window() noexcept {
+ static hud_window ret = []() {
+ const auto& window = client::render::get_window_size();
+ const float size = std::min(static_cast<float>(window.x) / 2.0f,
+ static_cast<float>(window.y) / 2.0f);
+ client::window::hud_window ret{size};
+ return ret;
+ }();
+ return ret;
+}
+
+// All dynamic windows go in this list!
+using layer = std::forward_list<std::unique_ptr<basic_window>>;
+using layers = std::forward_list<layer>;
+static layers& get_layers() noexcept {
+ // We callbacks for our window manager are initialised here too.
+ static layers ret = []() -> layers {
+ client::input::register_event_handler(&handle_event);
+ client::input::set_text_input(false);
+ client::input::set_mouse_relative(true);
+ return {};
+ }();
+ return ret;
+}
+
+void pop_window() noexcept {
+ if (!get_layers().empty()) {
+ get_layers().pop_front();
+ }
+ // Our windows might be empty here, so set our mouse mode accordingly.
+ if (!client::window::is_open()) {
+ client::input::set_mouse_relative(true);
+ }
+}
+
+// Constants used for uniform ui sizes.
+constexpr glm::vec2 lsize_extent{0.4, 0.075};
+constexpr glm::vec2 ssize_extent{0.15, 0.075};
+
+static void center_mouse_position() noexcept {
+ const glm::vec2& window = client::render::get_window_size();
+ client::input::set_mouse_position({window.x / 2.0f, window.y / 2.0f});
+}
+
+template <typename T, typename... Args>
+void push_window(Args&&... args) noexcept {
+ get_layers().front().push_front(
+ std::make_unique<T>(std::forward<Args>(args)...));
+}
+
+constexpr glm::vec2 center_extent(const glm::vec2 pos,
+ const glm::vec2 size) noexcept {
+ return {pos.x, pos.y - size.y / 2.0f};
+}
+
+static void make_options_menu() noexcept {
+ get_layers().push_front({});
+
+ /*
+ push_window<::slider_window<float>>(
+ "Field of Vision", 0.0f,
+ settings::get(std::make_pair("gameplay", "fov"), 100.0f), 145.0f,
+ pop_window,
+ client::draw::relative_arg{.extent =
+ center_extent({0.3, 0.7},
+ lsize_extent)}, client::draw::relative_arg{.extent = lsize_extent});
+ */
+
+ push_window<button_window>(
+ "Back", pop_window,
+ client::render::relative_arg{
+ .extent = center_extent({0.3, 0.3}, ssize_extent)},
+ client::render::relative_arg{.extent = ssize_extent});
+}
+
+static void make_main_menu() noexcept {
+ get_layers().push_front({});
+
+ push_window<button_window>(
+ "Return to Game", pop_window,
+ client::render::relative_arg{
+ .extent = center_extent({0.3, 0.7}, lsize_extent)},
+ client::render::relative_arg{.extent = lsize_extent});
+
+ push_window<button_window>(
+ "Options", make_options_menu,
+ client::render::relative_arg{
+ .extent = center_extent({0.55, 0.6}, ssize_extent)},
+ client::render::relative_arg{.extent = ssize_extent});
+
+ push_window<button_window>(
+ "?", pop_window,
+ client::render::relative_arg{
+ .extent = center_extent({0.55, 0.5}, ssize_extent)},
+ client::render::relative_arg{.extent = ssize_extent});
+
+ push_window<button_window>(
+ "?", pop_window,
+ client::render::relative_arg{
+ .extent = center_extent({0.55, 0.4}, ssize_extent)},
+ client::render::relative_arg{.extent = ssize_extent});
+
+ push_window<button_window>(
+ "Exit Game", [] { shared::should_exit = true; },
+ client::render::relative_arg{
+ .extent = center_extent({0.3, 0.3}, lsize_extent)},
+ client::render::relative_arg{.extent = lsize_extent});
+
+ client::input::set_mouse_relative(false);
+ center_mouse_position();
+}
+
+static void make_chat_window() noexcept {
+ get_layers().push_front({});
+
+ push_window<text_input_window>(
+ client::render::relative_arg{
+ .extent = center_extent({0.3, 0.3}, lsize_extent)},
+ client::render::relative_arg{.extent = lsize_extent});
+
+ client::input::set_mouse_relative(false);
+ center_mouse_position();
+}
+
+static void make_inventory_window() noexcept {
+ get_layers().push_front({});
+
+ const glm::vec2& window = client::render::get_window_size();
+
+ const float size = std::min(window.x / 2.0f, window.y / 2.0f);
+ const glm::vec2 pos{(window - size) / 2.0f};
+
+ push_window<inventory_window>(
+ client::render::relative_arg{.offset = pos},
+ client::render::relative_arg{.offset = glm::vec2{size, size}});
+ client::input::set_mouse_relative(false);
+ center_mouse_position();
+}
+
+static void handle_meta_return() noexcept {
+ if (!is_open()) {
+ make_chat_window();
+ return;
+ }
+}
+
+static void handle_meta_escape() noexcept {
+ if (!is_open()) {
+ make_main_menu();
+ return;
+ }
+
+ pop_window();
+}
+
+static void handle_meta_e() noexcept {
+ if (!is_open()) {
+ make_inventory_window();
+ return;
+ }
+
+ // The inventory window must be open.
+ if (!dynamic_cast<inventory_window*>(&*get_layers().front().front())) {
+ return;
+ }
+
+ pop_window();
+}
+
+static void handle_meta_keydown(const SDL_Event& event) noexcept {
+ if (event.key.repeat) { // only handle keypresses
+ return;
+ }
+
+ switch (event.key.keysym.sym) {
+ case SDLK_ESCAPE:
+ handle_meta_escape();
+ break;
+ case SDLK_RETURN:
+ handle_meta_return();
+ break;
+ case SDLK_e:
+ handle_meta_e();
+ break;
+ case SDLK_0:
+ case SDLK_1:
+ case SDLK_2:
+ case SDLK_3:
+ case SDLK_4:
+ case SDLK_5:
+ case SDLK_6:
+ case SDLK_7:
+ case SDLK_8:
+ case SDLK_9:
+ get_hud_window().maybe_handle_event(event);
+ default:
+ break;
+ }
+}
+
+static void handle_meta_mousemotion(const SDL_Event& event) noexcept {
+ // We convert SDL's weird coordinates into useful ones (0,0 = bottom
+ // left).
+ client::input::state.mouse_pos = {
+ event.motion.x,
+ static_cast<int>(client::render::get_window_size().y) - event.motion.y};
+}
+
+static void handle_meta_windowevent(const SDL_Event& event) noexcept {
+ if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
+ if (!is_open()) {
+ make_main_menu();
+ return;
+ }
+ }
+}
+
+static void handle_meta_event(const SDL_Event& event) noexcept {
+ switch (event.type) {
+ case SDL_KEYDOWN:
+ handle_meta_keydown(event);
+ break;
+ case SDL_MOUSEMOTION:
+ handle_meta_mousemotion(event);
+ break;
+ case SDL_WINDOWEVENT:
+ handle_meta_windowevent(event);
+ break;
+ }
+}
+
+static void handle_event(const SDL_Event& event) noexcept {
+ // We ALWAYS update our mouse position.
+ if (event.type == SDL_MOUSEMOTION) {
+ handle_meta_mousemotion(event);
+ }
+
+ // Either a window consumes our event, or no window does - so we send
+ // the event to our "meta handler" which does things like closing
+ // windows etc.
+ if (is_open()) {
+ for (const auto& window : get_layers().front()) {
+ if (window->maybe_handle_event(event)) {
+ return;
+ }
+ }
+ }
+
+ handle_meta_event(event);
+}
+
+void draw() noexcept {
+ if (!is_open()) {
+
+ if (!client::input::is_key_toggled(SDLK_F1)) {
+ get_hud_window().draw();
+ }
+
+ return;
+ }
+
+ client::render::draw_colour({0.0f, 0.0f, 0.0f, 0.10f}); // very light shade
+ for (const auto& window : get_layers().front()) {
+ window->draw();
+ }
+}
+
+bool is_open() noexcept { return !get_layers().empty(); }
+
+} // namespace window
+} // namespace client
diff --git a/src/client/window.hh b/src/client/window/window.hh
index 094c4ca..d112d18 100644
--- a/src/client/window.hh
+++ b/src/client/window/window.hh
@@ -1,5 +1,5 @@
-#ifndef CLIENT_WINDOW_HH_
-#define CLIENT_WINDOW_HH_
+#ifndef CLIENT_WINDOW_WINDOW_HH_
+#define CLIENT_WINDOW_WINDOW_HH_
#include <algorithm>
#include <forward_list>
@@ -13,17 +13,25 @@
#include <glm/glm.hpp>
#include "client/client.hh"
-#include "client/draw.hh"
#include "client/input.hh"
+#include "client/render/render.hh"
#include "client/settings.hh"
+#include "client/window/basic_window.hh"
+#include "client/window/button_window.hh"
+#include "client/window/hud_window.hh"
+#include "client/window/inventory_window.hh"
+#include "client/window/text_input_window.hh"
#include "shared/shared.hh"
namespace client {
namespace window {
-void draw() noexcept;
+// removes the topmost window
+void pop_window() noexcept;
bool is_open() noexcept;
+void draw() noexcept;
+
} // namespace window
} // namespace client
diff --git a/src/client/world.cc b/src/client/world.cc
deleted file mode 100644
index 1ceb9fb..0000000
--- a/src/client/world.cc
+++ /dev/null
@@ -1,429 +0,0 @@
-#include "world.hh"
-
-namespace client {
-namespace world {
-
-// Additional sanity checks for our atlas.
-static void check_atlas(const client::render::texture& texture) {
- if (texture.width % 6) {
- throw std::runtime_error("invalid atlas; WIDTH is not divisible by 6");
- }
- if (texture.height % (texture.width / 6)) {
- throw std::runtime_error(
- "invalid atlas, HEIGHT is not divisible by (WIDTH / 6)");
- }
-}
-
-void chunk::render(const float world_x, const float world_z,
- const pass& pass) noexcept {
- const auto make_texture = []() -> GLuint {
- GLuint texture = 0;
- glActiveTexture(GL_TEXTURE1);
- glGenTextures(1, &texture);
- glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
-
- glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S,
- GL_CLAMP_TO_EDGE);
- glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T,
- GL_CLAMP_TO_EDGE);
- glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER,
- GL_LINEAR_MIPMAP_LINEAR);
- glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
- glTexParameterf(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_ANISOTROPY, 16.0f);
-
- const client::render::texture stbi{"res/textures/atlas.png"};
- check_atlas(stbi);
- const int face_size = stbi.width / 6;
-
- // 2D texture array, where our depth is our block face.
- glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, face_size, face_size,
- 6 * (stbi.height / face_size), 0,
- stbi.channels == 3 ? GL_RGB : GL_RGBA, GL_UNSIGNED_BYTE,
- nullptr);
-
- // Fill the 2D texture array.
- // Because our image has multiple images on the x-axis and opengl
- // expects a single image per axis, we must fill it in row by row.
- const auto get_pixel_xy = [&stbi](const int x, const int y) {
- return stbi.image + 4 * (y * stbi.width + x);
- };
- for (int x = 0; x < 6; ++x) {
- const int x_pixel = x * face_size;
-
- for (int y = 0; y < stbi.height / face_size; ++y) {
- const int y_pixel = y * face_size;
-
- for (auto row = 0; row < face_size; ++row) {
- glTexSubImage3D(
- GL_TEXTURE_2D_ARRAY, 0, 0, row, x + y * 6, face_size, 1,
- 1, GL_RGBA, GL_UNSIGNED_BYTE,
- get_pixel_xy(x_pixel, row + y_pixel)); // pixel
- }
- }
- }
-
- glGenerateMipmap(GL_TEXTURE_2D_ARRAY);
-
- return texture;
- };
- const auto make_matrix = [&]() -> glm::mat4 {
- const auto& proj = client::render::camera::get_proj();
- const auto& view = client::render::camera::get_view();
- return glm::translate(proj * view, glm::vec3{world_x, 0, world_z});
- };
- static client::render::program program{"res/shaders/face.vs",
- "res/shaders/face.fs"};
- static const GLuint texture [[maybe_unused]] = make_texture();
- static const GLint u_matrix = glGetUniformLocation(program, "_u_matrix");
-
- glDisable(GL_BLEND);
- glEnable(GL_DEPTH_TEST);
- glUseProgram(program);
- // Our choice of vao depends on which pass we're doing.
- const auto [vao, elements] = [&pass, this]() -> std::pair<GLuint, GLuint> {
- if (pass == pass::solid) {
- return {this->glo->solid_vao, this->glo->solid_elements};
- }
- return {this->glo->water_vao, this->glo->water_elements};
- }();
- glBindVertexArray(vao);
-
- glUniformMatrix4fv(u_matrix, 1, GL_FALSE, glm::value_ptr(make_matrix()));
-
- glDrawArrays(GL_TRIANGLES, 0, elements);
-}
-
-// This function translates and rotates a set of vertices that describes a
-// neutral face and also adds a vector to the texture coords.
-std::array<chunk::glface, 6>
-chunk::make_glfaces(const glface_args& args) noexcept {
-
- static constexpr std::array<glface, 6> glfaces = {
- glface{{-0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}},
- glface{{0.5f, -0.5f, 0.0f}, {1.0f, 1.0f, 0.0f}},
- glface{{0.5f, 0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}},
- glface{{0.5f, 0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}},
- glface{{-0.5f, 0.5f, 0.0f}, {0.0f, 0.0f, 0.0f}},
- glface{{-0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}}};
-
- // We have to be careful here not to rotate/translate a zero vector.
- const glm::mat4 mtranslate =
- args.translate == glm::vec3{}
- ? glm::mat4{1.0f}
- : glm::translate(glm::mat4{1.0f}, args.translate);
- const glm::mat4 mrotate =
- args.rotate_axis == glm::vec3{}
- ? glm::mat4{1.0f}
- : glm::rotate(glm::mat4{1.0f}, glm::radians(args.rotate_degrees),
- args.rotate_axis);
-
- std::array<glface, 6> ret;
-
- std::ranges::transform(glfaces, std::begin(ret), [&](const auto f) {
- auto face = f; // unfortunate copy
- face.vertice =
- glm::vec3(mtranslate * mrotate * glm::vec4{face.vertice, 1.0f});
- face.texture += args.texture_offset;
- return face;
- });
-
- return ret;
-}
-
-const chunk* chunk::get_neighbour(const chunk::map& chunks,
- shared::math::coords offset) const noexcept {
- const auto find_it = chunks.find(this->pos + offset);
- if (find_it == std::end(chunks) || !find_it->second.has_value()) {
- return nullptr;
- }
- return &((*find_it).second.value());
-}
-
-bool chunk::maybe_regenerate_glo(const chunk::map& chunks) noexcept {
- // We need all surrounding chunks to make our vbo, this is why it's called
- // "maybe" regenerate vbo.
- const auto chunk_forward = this->get_neighbour(chunks, {0, 1});
- const auto chunk_backward = this->get_neighbour(chunks, {0, -1});
- const auto chunk_right = this->get_neighbour(chunks, {1, 0});
- const auto chunk_left = this->get_neighbour(chunks, {-1, 0});
- if (!chunk_forward || !chunk_left || !chunk_backward || !chunk_right) {
- return false;
- }
-
- static const auto [atlas_width,
- atlas_height] = []() -> std::pair<int, int> {
- const client::render::texture texture{"res/textures/atlas.png"};
- check_atlas(texture);
- return {texture.width, texture.height};
- }();
-
- // Single-axis-outside-chunk-bounds-allowed block access.
- const auto get_outside_block = [&](const int x, const int y,
- const int z) -> shared::world::block {
- if (y < 0 || y >= shared::world::chunk::HEIGHT) {
- return shared::world::block::type::air;
- } else if (x >= shared::world::chunk::WIDTH) {
- return chunk_right->get_block({x - WIDTH, y, z});
- } else if (x < 0) {
- return chunk_left->get_block({x + WIDTH, y, z});
- } else if (z >= shared::world::chunk::WIDTH) {
- return chunk_forward->get_block({x, y, z - WIDTH});
- } else if (z < 0) {
- return chunk_backward->get_block({x, y, z + WIDTH});
- }
- return this->get_block({x, y, z});
- };
-
- // We fill up two vbos, one for each possible rendering pass.
- std::vector<glface> solid_data;
- std::vector<glface> water_data;
-
- // For all blocks in the chunk, check if its neighbours are air. If they
- // are, it's possible that we can see the block, so add it to vertices.
- // We need to read into the neighbours chunk occasionally.
- for (auto x = 0; x < WIDTH; ++x) {
- for (auto y = 0; y < HEIGHT; ++y) {
- for (auto z = 0; z < WIDTH; ++z) {
- const auto& block = this->get_block({x, y, z});
- const auto bv = shared::world::block::get_visibility(block);
-
- if (bv == shared::world::block::visibility::invisible) {
- continue;
- }
-
- const auto& front{get_outside_block(x, y, z + 1)};
- const auto& back{get_outside_block(x, y, z - 1)};
- const auto& right{get_outside_block(x + 1, y, z)};
- const auto& left{get_outside_block(x - 1, y, z)};
- const auto& up{get_outside_block(x, y + 1, z)};
- const auto& down{get_outside_block(x, y - 1, z)};
-
- std::vector<glface> glfaces;
- glfaces.reserve(6 * 6);
-
- const auto should_draw_face = [&bv](const auto& other) -> bool {
- const auto ov = shared::world::block::get_visibility(other);
- if (bv == shared::world::block::visibility::translucent &&
- ov == shared::world::block::visibility::translucent) {
- return false;
- }
- return ov != shared::world::block::visibility::solid;
- };
- // Special shrub block case, ugly I know.
- if (block.type == shared::world::block::type::shrub ||
- block.type == shared::world::block::type::dead_shrub ||
- block.type == shared::world::block::type::snowy_shrub) {
- static const auto front_shrub =
- make_glfaces({.translate = {0.0f, 0.0f, 0.0f},
- .rotate_degrees = 45.0f,
- .rotate_axis = {0.0f, 1.0f, 0.0f},
- .texture_offset = {0.0f, 0.0f, 0.0f}});
- static const auto right_shrub =
- make_glfaces({.translate = {0.0f, 0.0f, 0.0f},
- .rotate_degrees = 135.0f,
- .rotate_axis = {0.0f, 1.0f, 0.0f},
- .texture_offset = {0.0f, 0.0f, 0.0f}});
- static const auto back_shrub =
- make_glfaces({.translate = {0.0f, 0.0f, 0.0f},
- .rotate_degrees = 225.0f,
- .rotate_axis = {0.0f, 1.0f, 0.0f},
- .texture_offset = {0.0f, 0.0f, 0.0f}});
- static const auto left_shrub =
- make_glfaces({.translate = {0.0f, 0.0f, 0.0f},
- .rotate_degrees = 315.0f,
- .rotate_axis = {0.0f, 1.0f, 0.0f},
- .texture_offset = {0.0f, 0.0f, 0.0f}});
-
- std::ranges::copy(front_shrub, std::back_inserter(glfaces));
- std::ranges::copy(right_shrub, std::back_inserter(glfaces));
- std::ranges::copy(back_shrub, std::back_inserter(glfaces));
- std::ranges::copy(left_shrub, std::back_inserter(glfaces));
-
- } else {
- if (should_draw_face(front)) {
- static const auto front_faces = make_glfaces(
- {.translate = {0.0f, 0.0f, 0.5f},
- .rotate_degrees = 0.0f,
- .rotate_axis = {0.0f, 0.0f, 0.0f},
- .texture_offset = {0.0f, 0.0f, 0.0f}});
- std::ranges::copy(front_faces,
- std::back_inserter(glfaces));
- }
- if (should_draw_face(right)) {
- static const auto right_faces = make_glfaces(
- {.translate = {0.5f, 0.0f, 0.0f},
- .rotate_degrees = 90.0f,
- .rotate_axis = {0.0f, 1.0f, 0.0f},
- .texture_offset = {0.0f, 0.0f, 1.0f}});
- std::ranges::copy(right_faces,
- std::back_inserter(glfaces));
- }
- if (should_draw_face(back)) {
- static const auto back_faces = make_glfaces(
- {.translate = {0.0f, 0.0f, -0.5f},
- .rotate_degrees = 180.0f,
- .rotate_axis = {0.0f, 1.0f, 0.0f},
- .texture_offset = {0.0f, 0.0f, 2.0f}});
- std::ranges::copy(back_faces,
- std::back_inserter(glfaces));
- }
- if (should_draw_face(left)) {
- static const auto left_faces = make_glfaces(
- {.translate = {-0.5f, 0.0f, 0.0f},
- .rotate_degrees = 270.0f,
- .rotate_axis = {0.0f, 1.0f, 0.0f},
- .texture_offset = {0.0f, 0.0f, 3.0f}});
- std::ranges::copy(left_faces,
- std::back_inserter(glfaces));
- }
- if (should_draw_face(up)) {
- static const auto up_faces = make_glfaces(
- {.translate = {0.0f, 0.5f, 0.0f},
- .rotate_degrees = -90.0f,
- .rotate_axis = {1.0f, 0.0f, 0.0f},
- .texture_offset = {0.0f, 0.0f, 4.0f}});
- std::ranges::copy(up_faces,
- std::back_inserter(glfaces));
- }
- if (should_draw_face(down)) {
- static const auto down_faces = make_glfaces(
- {.translate = {0.0f, -0.5f, 0.0f},
- .rotate_degrees = 90.0f,
- .rotate_axis = {1.0f, 0.0f, 0.0f},
- .texture_offset = {0.0f, 0.0f, 5.0f}});
- std::ranges::copy(down_faces,
- std::back_inserter(glfaces));
- }
- }
-
- // Move the block pos verts to its intended position.
- // Move the block texture verts to fit in the atlas.
- const glm::vec3 offset_vec3{x, y, z};
- const float tex_yoff = static_cast<float>(block.type) - 1.0f;
-
- const auto fix_face = [&, atlas_width = std::ref(atlas_width),
- atlas_height =
- std::ref(atlas_height)](auto& face) {
- face.vertice += offset_vec3 + 0.5f; // move to origin too
- face.texture.z += tex_yoff * 6.0f;
- return face;
- };
-
- auto& vbo_dest = block.type == shared::world::block::type::water
- ? water_data
- : solid_data;
- std::ranges::transform(glfaces, std::back_inserter(vbo_dest),
- fix_face);
- }
- }
- }
-
- const auto generate_vbo = [](const auto& data) -> GLuint {
- GLuint vbo = 0;
- glGenBuffers(1, &vbo);
- glBindBuffer(GL_ARRAY_BUFFER, vbo);
- glBufferData(GL_ARRAY_BUFFER, std::size(data) * sizeof(glface),
- std::data(data), GL_STATIC_DRAW);
- return vbo;
- };
- const auto generate_vao = []() -> GLuint {
- GLuint vao = 0;
- glGenVertexArrays(1, &vao);
- glBindVertexArray(vao);
- // position
- glEnableVertexAttribArray(0);
- glVertexAttribPointer(0, sizeof(glm::vec3) / sizeof(float), GL_FLOAT,
- GL_FALSE, sizeof(glface), nullptr);
- // texture
- glEnableVertexAttribArray(1);
- glVertexAttribPointer(1, sizeof(glm::vec3) / sizeof(float), GL_FLOAT,
- GL_FALSE, sizeof(glface),
- reinterpret_cast<void*>(sizeof(glm::vec3)));
- return vao;
- };
- // If we were to emplace glo with these there is no guarantee that each
- // function will be called in order (at least, for g++ it isn't). Therefore
- // we need to call them in order first.
- const auto solid_vbo = generate_vbo(solid_data);
- const auto solid_vao = generate_vao();
- const auto water_vbo = generate_vbo(water_data);
- const auto water_vao = generate_vao();
- this->glo.emplace(std::size(solid_data), solid_vbo, solid_vao,
- std::size(water_data), water_vbo, water_vao);
- return true;
-}
-
-// http://www.lighthouse3d.com/tutorials/view-frustum-culling/geometric-approach-testing-boxes/
-static bool box_in_frustum(const std::array<glm::vec3, 8>& points) noexcept {
- const auto& frustum = client::render::camera::get_frustum();
-
- for (const auto& plane : frustum) {
- bool inside = false;
- bool outside = false;
-
- for (const auto& point : points) {
- const float distance = plane.x * point.x + plane.y * point.y +
- plane.z * point.z + plane.w;
- if (distance < 0.0f) {
- outside = true;
- } else {
- inside = true;
- }
-
- if (inside && outside) {
- break;
- }
- }
-
- if (!inside) {
- return false;
- }
- }
-
- return true;
-};
-
-static bool is_chunk_visible(const float world_x,
- const float world_z) noexcept {
- const std::array<glm::vec3, 8> box_vertices =
- [&world_x, &world_z]() -> std::array<glm::vec3, 8> {
- const float max_world_x = world_x + shared::world::chunk::WIDTH;
- const float max_world_z = world_z + shared::world::chunk::WIDTH;
-
- return {glm::vec3{world_x, 0.0f, world_z},
- {max_world_x, 0.0f, world_z},
- {world_x, 0.0f, max_world_z},
- {max_world_x, 0.0f, max_world_z},
- {world_x, shared::world::chunk::HEIGHT, world_z},
- {max_world_x, shared::world::chunk::HEIGHT, world_z},
- {world_x, shared::world::chunk::HEIGHT, max_world_z},
- {max_world_x, shared::world::chunk::HEIGHT, max_world_z}};
- }();
-
- return box_in_frustum(box_vertices);
-}
-
-void chunk::draw(const chunk::map& chunks, const shared::player& lp,
- const pass& pass) noexcept {
- if (!this->glo.has_value() || this->should_regenerate_vbo) {
- if (!maybe_regenerate_glo(chunks)) {
- return;
- }
- this->should_regenerate_vbo = false;
- }
-
- const auto [world_x, world_z] = [&lp, this]() -> std::pair<float, float> {
- const float offset_x = static_cast<float>(this->pos.x - lp.chunk_pos.x);
- const float offset_z = static_cast<float>(this->pos.z - lp.chunk_pos.z);
- return {offset_x * chunk::WIDTH, offset_z * chunk::WIDTH};
- }();
-
- if (!is_chunk_visible(world_x, world_z)) {
- return;
- }
-
- render(world_x, world_z, pass);
-}
-
-} // namespace world
-} // namespace client
diff --git a/src/client/world/block.cc b/src/client/world/block.cc
new file mode 100644
index 0000000..df3ddcd
--- /dev/null
+++ b/src/client/world/block.cc
@@ -0,0 +1,117 @@
+#include "client/world/block.hh"
+
+namespace client {
+namespace world {
+
+struct glvert_args {
+ glm::vec3 translate;
+ float rotate_degrees;
+ glm::vec3 rotate_axis;
+ glm::vec3 texture_offset;
+};
+static block::glface_t make_glface(const glvert_args& args) noexcept {
+ static constexpr block::glface_t glverts = {
+ block::glvert{{-0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}},
+ block::glvert{{0.5f, -0.5f, 0.0f}, {1.0f, 1.0f, 0.0f}},
+ block::glvert{{0.5f, 0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}},
+ block::glvert{{0.5f, 0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}},
+ block::glvert{{-0.5f, 0.5f, 0.0f}, {0.0f, 0.0f, 0.0f}},
+ block::glvert{{-0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}}};
+
+ // We have to be careful here not to rotate/translate a zero vector.
+ constexpr auto zero = glm::vec3{};
+ const glm::mat4 mtranslate =
+ args.translate == zero
+ ? glm::mat4{1.0f}
+ : glm::translate(glm::mat4{1.0f}, args.translate);
+ const glm::mat4 mrotate =
+ args.rotate_axis == zero
+ ? glm::mat4{1.0f}
+ : glm::rotate(glm::mat4{1.0f}, glm::radians(args.rotate_degrees),
+ args.rotate_axis);
+
+ block::glface_t ret;
+ std::ranges::transform(glverts, std::begin(ret), [&](auto f) {
+ f.vertice =
+ glm::vec3(mtranslate * mrotate * glm::vec4{f.vertice, 1.0f});
+ f.texture += args.texture_offset;
+ return f;
+ });
+ return ret;
+}
+
+static const block::glfaces_t& get_shrub_faces() noexcept {
+ static block::glfaces_t faces{
+ make_glface({.translate = {0.0f, 0.0f, 0.0f},
+ .rotate_degrees = 45.0f,
+ .rotate_axis = {0.0f, 1.0f, 0.0f},
+ .texture_offset = {0.0f, 0.0f, 0.0f}}),
+ make_glface({.translate = {0.0f, 0.0f, 0.0f},
+ .rotate_degrees = 135.0f,
+ .rotate_axis = {0.0f, 1.0f, 0.0f},
+ .texture_offset = {0.0f, 0.0f, 0.0f}}),
+ make_glface({.translate = {0.0f, 0.0f, 0.0f},
+ .rotate_degrees = 225.0f,
+ .rotate_axis = {0.0f, 1.0f, 0.0f},
+ .texture_offset = {0.0f, 0.0f, 0.0f}}),
+ make_glface({.translate = {0.0f, 0.0f, 0.0f},
+ .rotate_degrees = 315.0f,
+ .rotate_axis = {0.0f, 1.0f, 0.0f},
+ .texture_offset = {0.0f, 0.0f, 0.0f}})};
+ return faces;
+}
+
+static const block::glfaces_t& get_cube_faces() noexcept {
+ static block::glfaces_t faces{
+ make_glface({.translate = {0.0f, 0.0f, 0.5f},
+ .rotate_degrees = 0.0f,
+ .rotate_axis = {0.0f, 0.0f, 0.0f},
+ .texture_offset = {0.0f, 0.0f, 0.0f}}),
+ make_glface({.translate = {0.5f, 0.0f, 0.0f},
+ .rotate_degrees = 90.0f,
+ .rotate_axis = {0.0f, 1.0f, 0.0f},
+ .texture_offset = {0.0f, 0.0f, 1.0f}}),
+ make_glface({.translate = {0.0f, 0.0f, -0.5f},
+ .rotate_degrees = 180.0f,
+ .rotate_axis = {0.0f, 1.0f, 0.0f},
+ .texture_offset = {0.0f, 0.0f, 2.0f}}),
+ make_glface({.translate = {-0.5f, 0.0f, 0.0f},
+ .rotate_degrees = 270.0f,
+ .rotate_axis = {0.0f, 1.0f, 0.0f},
+ .texture_offset = {0.0f, 0.0f, 3.0f}}),
+ make_glface({.translate = {0.0f, 0.5f, 0.0f},
+ .rotate_degrees = -90.0f,
+ .rotate_axis = {1.0f, 0.0f, 0.0f},
+ .texture_offset = {0.0f, 0.0f, 4.0f}}),
+ make_glface({.translate = {0.0f, -0.5f, 0.0f},
+ .rotate_degrees = 90.0f,
+ .rotate_axis = {1.0f, 0.0f, 0.0f},
+ .texture_offset = {0.0f, 0.0f, 5.0f}}),
+ };
+ return faces;
+}
+
+enum block::draw_type
+block::get_draw_type(const enum block::type& type) noexcept {
+ using t = enum shared::world::block::type;
+ switch (type) {
+ case t::dead_shrub:
+ case t::shrub:
+ case t::snowy_shrub:
+ return draw_type::custom;
+ default:
+ return draw_type::block;
+ }
+}
+
+const block::glfaces_t&
+block::get_glfaces(const enum block::type& type) noexcept {
+ const auto draw_type = get_draw_type(type);
+ if (draw_type == block::draw_type::custom) {
+ return get_shrub_faces();
+ }
+ return get_cube_faces();
+}
+
+} // namespace world
+} // namespace client
diff --git a/src/client/world/block.hh b/src/client/world/block.hh
new file mode 100644
index 0000000..9dfe303
--- /dev/null
+++ b/src/client/world/block.hh
@@ -0,0 +1,53 @@
+#ifndef CLIENT_WORLD_BLOCK_HH_
+#define CLIENT_WORLD_BLOCK_HH_
+
+#include <algorithm>
+#include <array>
+#include <vector>
+
+#include <glm/glm.hpp>
+#include <glm/gtc/matrix_access.hpp>
+#include <glm/gtc/matrix_transform.hpp>
+
+#include "shared/world/block.hh"
+
+namespace client {
+namespace world {
+
+// Doesn't add any data, just information for rendering.
+class block : public shared::world::block {
+public:
+ struct glvert {
+ glm::vec3 vertice;
+ glm::vec3 texture;
+ };
+ using glface_t = std::array<glvert, 6>; // array of verts (a face)
+ using glfaces_t = std::vector<glface_t>; // vector of faces (a block)
+
+ // Render types refer to how the block should be culled when making the vbo.
+ enum class draw_type {
+ block, // face testing
+ custom, // no testing
+ };
+
+public:
+ static enum draw_type get_draw_type(const enum block::type& type) noexcept;
+ static const glfaces_t& get_glfaces(const enum block::type& type) noexcept;
+
+public:
+ template <typename... Args>
+ block(Args&&... args) noexcept : block(std::forward<Args>(args)...) {}
+
+public:
+ enum draw_type get_draw_type() const noexcept {
+ return get_draw_type(this->type);
+ }
+ const glfaces_t& get_glfaces() const noexcept {
+ return get_glfaces(this->type);
+ }
+};
+
+} // namespace world
+} // namespace client
+
+#endif
diff --git a/src/client/world/chunk.cc b/src/client/world/chunk.cc
new file mode 100644
index 0000000..99720cb
--- /dev/null
+++ b/src/client/world/chunk.cc
@@ -0,0 +1,255 @@
+#include "client/world/chunk.hh"
+
+namespace client {
+namespace world {
+
+void chunk::render(const float world_x, const float world_z,
+ const pass& pass) noexcept {
+ const auto make_matrix = [&]() -> glm::mat4 {
+ const auto& proj = client::render::camera::get_proj();
+ const auto& view = client::render::camera::get_view();
+ return glm::translate(proj * view, glm::vec3{world_x, 0, world_z});
+ };
+ static client::render::program program{"res/shaders/face.vs",
+ "res/shaders/face.fs"};
+ static const GLint u_matrix = glGetUniformLocation(program, "_u_matrix");
+
+ const GLuint texture [[maybe_unused]] = client::render::get_texture_atlas();
+ glDisable(GL_BLEND);
+ glEnable(GL_DEPTH_TEST);
+ glUseProgram(program);
+ // Our choice of vao depends on which pass we're doing.
+ const auto [vao, elements] = [&pass, this]() -> std::pair<GLuint, GLuint> {
+ if (pass == pass::solid) {
+ return {this->glo->solid_vao, this->glo->solid_elements};
+ }
+ return {this->glo->water_vao, this->glo->water_elements};
+ }();
+ glBindVertexArray(vao);
+
+ glUniformMatrix4fv(u_matrix, 1, GL_FALSE, glm::value_ptr(make_matrix()));
+
+ glDrawArrays(GL_TRIANGLES, 0, elements);
+}
+
+const chunk* chunk::get_neighbour(const chunks_t& chunks,
+ shared::math::coords offset) const noexcept {
+ const auto find_it = chunks.find(this->pos + offset);
+ if (find_it == std::end(chunks) || !find_it->second.has_value()) {
+ return nullptr;
+ }
+ return &((*find_it).second.value());
+}
+
+bool chunk::maybe_regenerate_glo(const chunks_t& chunks) noexcept {
+ // We need all surrounding chunks to make our vbo, so early out with false
+ // if we can't do that yet.
+ const auto chunk_forward = this->get_neighbour(chunks, {0, 1});
+ const auto chunk_backward = this->get_neighbour(chunks, {0, -1});
+ const auto chunk_right = this->get_neighbour(chunks, {1, 0});
+ const auto chunk_left = this->get_neighbour(chunks, {-1, 0});
+ if (!chunk_forward || !chunk_left || !chunk_backward || !chunk_right) {
+ return false;
+ }
+
+ // Single-axis-outside-chunk-bounds-allowed block access.
+ const auto get_outside_block = [&](const int x, const int y,
+ const int z) -> shared::world::block {
+ if (y < 0 || y >= shared::world::chunk::HEIGHT) {
+ return shared::world::block::type::air;
+ } else if (x >= shared::world::chunk::WIDTH) {
+ return chunk_right->get_block({x - WIDTH, y, z});
+ } else if (x < 0) {
+ return chunk_left->get_block({x + WIDTH, y, z});
+ } else if (z >= shared::world::chunk::WIDTH) {
+ return chunk_forward->get_block({x, y, z - WIDTH});
+ } else if (z < 0) {
+ return chunk_backward->get_block({x, y, z + WIDTH});
+ }
+ return this->get_block({x, y, z});
+ };
+
+ // We fill up two vbos, one for each possible rendering pass.
+ std::vector<block::glvert> solid_data;
+ std::vector<block::glvert> water_data;
+
+ // For all blocks in the chunk, check if its neighbours are air. If they
+ // are, it's possible that we can see the block, so add it to vertices.
+ // We need to read into the neighbours chunk occasionally.
+ for (auto x = 0; x < WIDTH; ++x) {
+ for (auto y = 0; y < HEIGHT; ++y) {
+ for (auto z = 0; z < WIDTH; ++z) {
+ const auto& block = this->get_block({x, y, z});
+ const auto bv = shared::world::block::get_visibility(block);
+
+ if (bv == shared::world::block::visibility::invisible) {
+ continue;
+ }
+
+ std::vector<block::glvert> glverts;
+ glverts.reserve(6 * 6);
+
+ const auto draw_type = block::get_draw_type(block.type);
+
+ const block::glfaces_t& faces = block::get_glfaces(block.type);
+ if (draw_type == block::draw_type::block) {
+ const std::array<shared::world::block, 6> around{
+ get_outside_block(x, y, z + 1),
+ get_outside_block(x + 1, y, z),
+ get_outside_block(x, y, z - 1),
+ get_outside_block(x - 1, y, z),
+ get_outside_block(x, y + 1, z),
+ get_outside_block(x, y - 1, z),
+ };
+
+ for (auto i = 0ul; i < std::size(faces); ++i) {
+ const auto ov = block::get_visibility(around[i]);
+ if (bv == block::visibility::translucent &&
+ ov == block::visibility::translucent) {
+ continue;
+ }
+ if (ov == block::visibility::solid) {
+ continue;
+ }
+
+ std::ranges::copy(faces[i],
+ std::back_inserter(glverts));
+ }
+ } else if (draw_type == block::draw_type::custom) {
+ for (const auto& face : faces) {
+ std::ranges::copy(face, std::back_inserter(glverts));
+ }
+ }
+
+ // Move the block pos verts to its intended position.
+ // Move the block texture verts to fit in the atlas.
+ const glm::vec3 offset_vec3{x, y, z};
+ const float tex_yoff = static_cast<float>(block.type) - 1.0f;
+ const auto fix_face = [&](auto& face) {
+ face.vertice += offset_vec3 + 0.5f; // move to origin too
+ face.texture.z += tex_yoff * 6.0f;
+ return face;
+ };
+
+ auto& vbo_dest = block.type == shared::world::block::type::water
+ ? water_data
+ : solid_data;
+ std::ranges::transform(glverts, std::back_inserter(vbo_dest),
+ fix_face);
+ }
+ }
+ }
+
+ const auto generate_vbo = [](const auto& data) -> GLuint {
+ GLuint vbo = 0;
+ glGenBuffers(1, &vbo);
+ glBindBuffer(GL_ARRAY_BUFFER, vbo);
+ glBufferData(GL_ARRAY_BUFFER, std::size(data) * sizeof(block::glvert),
+ std::data(data), GL_STATIC_DRAW);
+ return vbo;
+ };
+ const auto generate_vao = []() -> GLuint {
+ GLuint vao = 0;
+ glGenVertexArrays(1, &vao);
+ glBindVertexArray(vao);
+ // position
+ glEnableVertexAttribArray(0);
+ glVertexAttribPointer(0, sizeof(glm::vec3) / sizeof(float), GL_FLOAT,
+ GL_FALSE, sizeof(block::glvert), nullptr);
+ // texture
+ glEnableVertexAttribArray(1);
+ glVertexAttribPointer(1, sizeof(glm::vec3) / sizeof(float), GL_FLOAT,
+ GL_FALSE, sizeof(block::glvert),
+ reinterpret_cast<void*>(sizeof(glm::vec3)));
+ return vao;
+ };
+ // If we were to emplace glo with these there is no guarantee that each
+ // function will be called in order (at least, for g++ it isn't). Therefore
+ // we need to call them in order first.
+ const auto solid_vbo = generate_vbo(solid_data);
+ const auto solid_vao = generate_vao();
+ const auto water_vbo = generate_vbo(water_data);
+ const auto water_vao = generate_vao();
+ this->glo.emplace(std::size(solid_data), solid_vbo, solid_vao,
+ std::size(water_data), water_vbo, water_vao);
+ return true;
+}
+
+// http://www.lighthouse3d.com/tutorials/view-frustum-culling/geometric-approach-testing-boxes/
+static bool box_in_frustum(const std::array<glm::vec3, 8>& points) noexcept {
+ const auto& frustum = client::render::camera::get_frustum();
+
+ for (const auto& plane : frustum) {
+ bool inside = false;
+ bool outside = false;
+
+ for (const auto& point : points) {
+ const float distance = plane.x * point.x + plane.y * point.y +
+ plane.z * point.z + plane.w;
+ if (distance < 0.0f) {
+ outside = true;
+ } else {
+ inside = true;
+ }
+
+ if (inside && outside) {
+ break;
+ }
+ }
+
+ if (!inside) {
+ return false;
+ }
+ }
+
+ return true;
+};
+
+static bool is_chunk_visible(const float world_x,
+ const float world_z) noexcept {
+ const std::array<glm::vec3, 8> box_vertices =
+ [&world_x, &world_z]() -> std::array<glm::vec3, 8> {
+ const float max_world_x = world_x + shared::world::chunk::WIDTH;
+ const float max_world_z = world_z + shared::world::chunk::WIDTH;
+
+ return {glm::vec3{world_x, 0.0f, world_z},
+ {max_world_x, 0.0f, world_z},
+ {world_x, 0.0f, max_world_z},
+ {max_world_x, 0.0f, max_world_z},
+ {world_x, shared::world::chunk::HEIGHT, world_z},
+ {max_world_x, shared::world::chunk::HEIGHT, world_z},
+ {world_x, shared::world::chunk::HEIGHT, max_world_z},
+ {max_world_x, shared::world::chunk::HEIGHT, max_world_z}};
+ }();
+
+ return box_in_frustum(box_vertices);
+}
+
+bool chunk::draw(const chunks_t& chunks, const shared::player& lp,
+ const pass& pass, const bool skip_regen) noexcept {
+ bool did_regen = false;
+ if (!this->glo.has_value() || this->should_regenerate_vbo) {
+ if (skip_regen || !maybe_regenerate_glo(chunks)) {
+ return false;
+ }
+ this->should_regenerate_vbo = false;
+ did_regen = true;
+ }
+
+ const auto [world_x, world_z] = [&lp, this]() -> std::pair<float, float> {
+ const float offset_x =
+ static_cast<float>(this->pos.x - lp.get_chunk_pos().x);
+ const float offset_z =
+ static_cast<float>(this->pos.z - lp.get_chunk_pos().z);
+ return {offset_x * chunk::WIDTH, offset_z * chunk::WIDTH};
+ }();
+
+ if (is_chunk_visible(world_x, world_z)) {
+ render(world_x, world_z, pass);
+ }
+
+ return did_regen;
+}
+
+} // namespace world
+} // namespace client
diff --git a/src/client/world.hh b/src/client/world/chunk.hh
index 067f74c..5436525 100644
--- a/src/client/world.hh
+++ b/src/client/world/chunk.hh
@@ -1,5 +1,5 @@
-#ifndef CLIENT_WORLD_HH_
-#define CLIENT_WORLD_HH_
+#ifndef CLIENT_WORLD_CHUNK_HH_
+#define CLIENT_WORLD_CHUNK_HH_
#include <algorithm>
#include <optional>
@@ -9,19 +9,22 @@
#include "client/render/render.hh"
#include "client/render/texture.hh"
-#include "shared/player.hh"
-#include "shared/world.hh"
+#include "client/world/block.hh"
+#include "shared/entity/player.hh"
+#include "shared/world/chunk.hh"
namespace client {
namespace world {
+class chunk;
+using chunks_t = std::unordered_map<shared::math::coords,
+ std::optional<client::world::chunk>,
+ decltype(&shared::world::chunk::hash),
+ decltype(&shared::world::chunk::equal)>;
+
// client::world::chunk is a renderable shared::world::chunk.
class chunk : public shared::world::chunk {
public:
- using map = std::unordered_map<shared::math::coords,
- std::optional<client::world::chunk>,
- decltype(&shared::world::chunk::hash),
- decltype(&shared::world::chunk::equal)>;
// Which part to draw when we call draw.
enum class pass { solid, water };
@@ -53,35 +56,24 @@ private:
std::optional<gl_objects> glo;
private:
- const chunk* get_neighbour(const chunk::map& chunks,
+ const chunk* get_neighbour(const chunks_t& chunks,
shared::math::coords offset) const noexcept;
- struct glface {
- glm::vec3 vertice;
- glm::vec3 texture;
- };
- struct glface_args {
- glm::vec3 translate;
- float rotate_degrees;
- glm::vec3 rotate_axis;
- glm::vec3 texture_offset;
- };
- static std::array<glface, 6> make_glfaces(const glface_args& args) noexcept;
void render(const float x_offset, const float z_offset,
const pass& pass) noexcept;
- bool maybe_regenerate_glo(const chunk::map& chunks) noexcept;
+ bool maybe_regenerate_glo(const chunks_t& chunks) noexcept;
public:
template <typename... Args>
chunk(Args&&... args) noexcept
: shared::world::chunk(std::forward<Args>(args)...) {}
+ virtual ~chunk() noexcept = default;
- void draw(const map& chunks, const shared::player& lp,
- const pass& pass) noexcept;
+ // true if we regen'd, false otherwise
+ bool draw(const chunks_t& chunks, const shared::player& lp,
+ const pass& pass, const bool skip_regen = false) noexcept;
- bool can_draw() const noexcept {
- return this->glo.has_value();
- }
+ bool can_draw() const noexcept { return this->glo.has_value(); }
};
} // namespace world
diff --git a/src/client/world/world.dat b/src/client/world/world.dat
new file mode 100644
index 0000000..e210d11
--- /dev/null
+++ b/src/client/world/world.dat
Binary files differ