From 2861497351accf4728e8b7f5d97f2c388a84f9bf Mon Sep 17 00:00:00 2001 From: Matthew Hopson Date: Mon, 6 Apr 2020 22:35:47 +0100 Subject: [PATCH] The Big Client/ Server refactor (#195) * Start the server netcode re-write * Begin work on new client for network stuff * add connection state enum for network client * Fix the cmakelists * Write tests for network client class * Add function to disconnect the client from the server * Ensure packets are destroyed * Add test for disconnecting from server * Move the enet client connect/disconnect functions to be free functions git push * [ENet] Add broadcast method to enet helpers * Create packet to hold data about packets * Make the client/server classes use the new packet struct in the handle packet function * Handle the handshake between the client and server * move old network code into a temporary folder * move more names into temp folder * Create packet type for acceptig or rejecting a connection request * Prevent disconneting clients with emptiness * Write tests for if the server is full, reject the client * Fix crash to do with the client not diconnecting orrectly * Add extra test * Created NetHost struct to be a wrapper around the basics of ENetHost * [Server] Use the new enet host wrapper struct in the Server class * [Client] Use new host struct in the client * Update the packet class to be less error prone * Move pending client session to new file * [Server] Broadcast a player join event * Add player leave events * make server engine a non global variable * Wrap client code into a nice class * Create a state manager class to handle different game states * allow gui events to be created via the c++ side * Make the main menu switch to the in game tate * vs what are you doing * Revert back to more simple times * Update game defintions to have specific game types, organising the code and creating a beter interface as a result * Have the entity draw * Move camera stuff to camera class * Add fps controller * Send player position over the network * Handle players joining and leaving game correctly * Add network event to update enttity positions * Server sends voxel data to client * send chunks from server to client * Fix linux unit tests * simplify the server code * Make the selected box show around the voxel the player is currently looking at * add packets for sending play spawn etc * Hacky fix for the game now shutting down correctly * Reimplement digging * Reimplement block placing * remove temp things * Clang format and fix unit tests --- CMakeLists.txt | 12 +- ...ng-C++-Network-Events(Server to Client).md | 144 +++++++ game/client/menus/join_world_menu.lua | 16 +- src/client/CMakeLists.txt | 16 +- src/client/client.cpp | 377 ------------------ src/client/client.h | 126 ------ src/client/client_engine.cpp | 233 ++++------- src/client/client_engine.h | 69 +++- src/client/client_state_controller.cpp | 16 +- src/client/game.cpp | 68 ---- src/client/game.h | 27 -- src/client/{world => game}/chunk_mesh.cpp | 0 src/client/{world => game}/chunk_mesh.h | 0 .../{world => game}/chunk_mesh_generation.cpp | 1 - .../{world => game}/chunk_mesh_generation.h | 0 src/client/game/client_world.cpp | 222 +++++++++++ src/client/game/client_world.h | 70 ++++ src/client/game/game.cpp | 53 +++ src/client/game/game.h | 27 ++ src/client/game/game_def.cpp | 184 +++++++++ src/client/game/game_def.h | 57 +++ src/client/game/game_type.cpp | 36 ++ src/client/game/game_type.h | 28 ++ src/client/gl/gl_errors.cpp | 3 +- src/client/gui/component/text_component.cpp | 2 +- src/client/gui/overlay.cpp | 2 +- src/client/gui/overlay.h | 2 +- src/client/gui/widget/checkbox_widget.cpp | 5 +- src/client/gui/widget/checkbox_widget.h | 32 +- src/client/lua/client_lua_callback.h | 4 +- src/client/lua/gui_widget_api.cpp | 6 +- src/client/network/client.cpp | 119 ++++++ src/client/network/client.h | 64 +++ src/client/network/client_commands.cpp | 209 ---------- src/client/network/client_packet_handling.cpp | 144 +++++++ src/client/renderer/camera.cpp | 40 ++ src/client/renderer/camera.h | 23 ++ src/client/renderer/chunk_renderer.cpp | 21 +- src/client/renderer/chunk_renderer.h | 15 +- src/client/window.cpp | 7 +- src/client/window.h | 7 +- src/common/common/CMakeLists.txt | 11 +- src/{client => common/common}/maths.cpp | 2 +- src/{client => common/common}/maths.h | 2 +- .../common/network/command_dispatcher.h | 29 -- src/common/common/network/enet.cpp | 152 +++++++ src/common/common/network/enet.h | 62 ++- src/common/common/network/net_command.h | 146 ++++--- src/common/common/network/net_host.cpp | 224 ----------- src/common/common/network/net_host.h | 107 ----- src/common/common/network/packet.cpp | 52 +++ src/common/common/network/packet.h | 88 ++++ src/common/common/world/chunk.h | 5 + src/common/common/world/chunk_manager.h | 1 + src/common/common/world/entity_state.h | 15 + src/common/common/world/voxel_data.cpp | 7 +- src/main.cpp | 19 +- src/server/CMakeLists.txt | 7 +- src/server/network/client_session.cpp | 92 +++++ src/server/network/client_session.h | 51 +++ src/server/network/pending_client_session.cpp | 51 +++ src/server/network/server.cpp | 333 ---------------- src/server/network/server.h | 79 ---- src/server/network/server_packet_handling.cpp | 149 +++++++ src/server/server_engine.cpp | 191 +++++++-- src/server/server_engine.h | 57 ++- src/server/world/server_world.cpp | 175 ++++++++ src/server/world/server_world.h | 53 +++ src/server/world/terrain_generation.cpp | 10 +- src/server/world/terrain_generation.h | 8 +- tests/client/lua/client_control_api_test.cpp | 2 +- tests/client_server_tests.cpp | 96 +++++ .../network/command_dispatcher_test.cpp | 88 ---- 73 files changed, 2819 insertions(+), 2032 deletions(-) create mode 100644 docs/Adding-C++-Network-Events(Server to Client).md delete mode 100644 src/client/client.cpp delete mode 100644 src/client/client.h delete mode 100644 src/client/game.cpp delete mode 100644 src/client/game.h rename src/client/{world => game}/chunk_mesh.cpp (100%) rename src/client/{world => game}/chunk_mesh.h (100%) rename src/client/{world => game}/chunk_mesh_generation.cpp (98%) rename src/client/{world => game}/chunk_mesh_generation.h (100%) create mode 100644 src/client/game/client_world.cpp create mode 100644 src/client/game/client_world.h create mode 100644 src/client/game/game.cpp create mode 100644 src/client/game/game.h create mode 100644 src/client/game/game_def.cpp create mode 100644 src/client/game/game_def.h create mode 100644 src/client/game/game_type.cpp create mode 100644 src/client/game/game_type.h create mode 100644 src/client/network/client.cpp create mode 100644 src/client/network/client.h delete mode 100644 src/client/network/client_commands.cpp create mode 100644 src/client/network/client_packet_handling.cpp create mode 100644 src/client/renderer/camera.cpp create mode 100644 src/client/renderer/camera.h rename src/{client => common/common}/maths.cpp (99%) rename src/{client => common/common}/maths.h (98%) delete mode 100644 src/common/common/network/command_dispatcher.h create mode 100644 src/common/common/network/enet.cpp delete mode 100644 src/common/common/network/net_host.cpp delete mode 100644 src/common/common/network/net_host.h create mode 100644 src/common/common/network/packet.cpp create mode 100644 src/common/common/network/packet.h create mode 100644 src/common/common/world/entity_state.h create mode 100644 src/server/network/client_session.cpp create mode 100644 src/server/network/client_session.h create mode 100644 src/server/network/pending_client_session.cpp delete mode 100644 src/server/network/server.cpp delete mode 100644 src/server/network/server.h create mode 100644 src/server/network/server_packet_handling.cpp create mode 100644 src/server/world/server_world.cpp create mode 100644 src/server/world/server_world.h create mode 100644 tests/client_server_tests.cpp delete mode 100644 tests/common/network/command_dispatcher_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 93d99113..9ddd990f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,6 +87,13 @@ add_executable(${PROJECT_NAME} src/main.cpp ) +target_include_directories(${PROJECT_NAME} + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src/server/ +) + + + target_include_directories(ob-common PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/deps/ @@ -111,13 +118,14 @@ add_executable(tests tests/common/world/chunk_manager_test.cpp tests/common/world/coordinate_test.cpp tests/common/world/chunk_test.cpp - tests/common/network/net_host_test.cpp - tests/common/network/command_dispatcher_test.cpp + tests/common/util/obd_parser_test.cpp tests/common/lua/script_engine_test.cpp tests/client/lua/client_control_api_test.cpp tests/client/lua/gui_widget_api_test.cpp + + tests/client_server_tests.cpp ) include_dirs(tests) diff --git a/docs/Adding-C++-Network-Events(Server to Client).md b/docs/Adding-C++-Network-Events(Server to Client).md new file mode 100644 index 00000000..956f4204 --- /dev/null +++ b/docs/Adding-C++-Network-Events(Server to Client).md @@ -0,0 +1,144 @@ +# Adding new network events + +This is a step-by-step guide on adding new network events that are sent from the server side, to be handled by the client side. + +For example, updating the positions of entities on the client, by sending a packet from the server + +## Step 1 - Add the event to the enum class + +1. Go to (TODO: Add link) `src/common/network/net_command.h` + +2. Add to either `ClientCommand` or `ServerCommand` + + a. `ClientCommand` is for sending a message FROM the server TO the client + + b. `ServerCommand` is for sending a message FROM the client TO the server + +3. In this case, it is a `ClientCommand`. Add the enum, and explain what data will be sent in a comment + +Example: +```cpp +enum class ClientCommand : command_t { + ... + // Update entities states (position and rotation) + // u32: The number of entities to update + // [Loop this] + // u32: The ID of the entity + // float[3]: The position of the entity + // float[3]: The rotation of the entity + UpdateEntityStates, +``` + +## Step 2 - Add a function to the client class for handling the event + +1. Go to (TODO Add link) `src/client/network/client_packet_handler.cpp` and (TODO Add link) `src/client/network/client.h` + +2. In the header file, find the `void on(ClientPacket& packet);` function declarations and add your one here, keeping it in alphabetical order + +3. In this case, it would be `void onUpdateEntityStates(ClientPacket& packet);` + +4. Define your function in the source file, in this case it would be + +```cpp +void Client::onUpdateEntityStates(ClientPacket& packet) +{ + //... +} +``` + +## Step 3 - Read the packet + +1. Read the data from the packet, you can see the other functions to understand how the interface of `ClientPacket` works (`auto data = packet.read()`) + +2. For example, for this you would read a u32 for the entity count, loop through it "count" times, updating the entities as you go: + +```cpp + u32 count = packet.read(); + for (u32 i = 0; i < count; i++) { + auto entId = packet.read(); + auto entPosition = packet.read(); + auto entRotation = packet.read(); + mp_world->updateEntity(entId, entPosition, entRotation); + } +``` + +3. If needed, you would probably have to add the functionality to do whatever your event is handling (eg I had to add the update entity function to the ClientWorld class for this one) + +## Step 4 - Listen for the packet + +1. In the `client.cpp` file, find the `handlePacket` function + +2. You'll see a switch case, simply add a `case` for the new packet type, calling the new function; keeping things in alphabetical order and alligned eg + +```cpp +void Client::handlePacket(ClientPacket& packet) +{ +using Cmd = ClientCommand; +// .... +case Cmd::UpdateEntityStates: onUpdateEntityStates (packet); break; +// .... + +``` + +## Step 5 - Write functions to create a packet on the server + +1. Go to (TODO Add link) `src/server/network/` folder. + +2. Depending on the packet type depends where the function to create the packet is created. + +3. If it is client-specific, then it would go into the `ClientSession` class (`client_session.h` and `client_session.cpp`) + +3. In this case, it is a broadcast to all clients, so it goes in the `Server` class (`server.h` and `server.cpp`) + +4. For a broadcast, name the function `broadcast`, eg this case would be `void broadcastEntityStates();` + +5. Again, define it in the source file: + +```cpp +void Server::broadcastEntityStates() +{ + +} +``` + +## Step 6 - Send the packet + +1. In the function body, create a ServerPacket, where the first parameter is the command name and the second parameter is `m_salt` + +2. Eg, in this case it would be like ` ServerPacket packet(ClientCommand::UpdateEntityStates, m_salt); +` + +3. Write some data to the packet. Usually, this would be passed in via the function parameters + +4. In this case, I already have a function to serialise packets, so I will just call that + +```cpp +mp_world->serialiseEntities(packet, 0); +``` + +5. Send the packet. For a broadcast, this is like: + + ``` + broadcastToPeers(m_host.handle, packet.get()); + ``` + +## Step 7 - Call your new function from somewhere on the server + +1. The server is owned by the ServerLauncher class, and maybe you could call it from the server loop, for example: + +```cpp + +void ServerLauncher::launch() +{ + // ... + while (m_isServerRunning) { + // ... + m_server.broadcastEntityStates(); + // ... + } +} +``` + +2. Done + +# END diff --git a/game/client/menus/join_world_menu.lua b/game/client/menus/join_world_menu.lua index 9fa866cb..19130e1d 100644 --- a/game/client/menus/join_world_menu.lua +++ b/game/client/menus/join_world_menu.lua @@ -9,7 +9,8 @@ local function onCreate(overlay) menu:setBackground(backgroundTexture) local serverIpBox = menu:addTextBox("Server IP", "Enter server IP...") - serverIpBox.text = "178.62.64.146" + --serverIpBox.text = "178.62.64.146" + serverIpBox.text = "127.0.0.1" local label = menu:addLabel() label.text = "This will either automatically register \nyou to the server, or log you in." @@ -17,10 +18,10 @@ local function onCreate(overlay) menu:pad(45) - local usernameBox = menu:addTextBox("Username", "Enter username...") - local passwordBox = menu:addTextBox("Password", "Enter password...") + --local usernameBox = menu:addTextBox("Username", "Enter username...") - passwordBox:hideInput() + --local passwordBox = menu:addTextBox("Password", "Enter password...") + --passwordBox:hideInput() local joinButton = menu:addButton("Join World") @@ -29,11 +30,12 @@ local function onCreate(overlay) joinButton.onClick = function() local serverIp = serverIpBox:getText() - local username = usernameBox:getText() - local password = passwordBox:getText() + -- local username = usernameBox:getText() + -- local password = passwordBox:getText() if string.len(serverIp) > 0 then game.gui():change("transition", { message = "Joining World" } ) - game.control():joinGame(serverIp, username, password) + --game.control():joinGame(serverIp, username, password) + game.control():joinGame(serverIp, "", "") end end end diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index 0da4953a..2a3901a1 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -1,9 +1,6 @@ add_library(ob-client input/keyboard.cpp - world/chunk_mesh.cpp - world/chunk_mesh_generation.cpp - maths.cpp gl/textures.cpp gl/shader.cpp @@ -15,8 +12,13 @@ add_library(ob-client client_state_controller.cpp client_engine.cpp - client.cpp - game.cpp + + game/game.cpp + game/game_def.cpp + game/game_type.cpp + game/chunk_mesh.cpp + game/chunk_mesh_generation.cpp + game/client_world.cpp lua/client_lua_callback.cpp lua/gui_api.cpp @@ -41,8 +43,10 @@ add_library(ob-client window.cpp - network/client_commands.cpp + network/client.cpp + network/client_packet_handling.cpp + renderer/camera.cpp renderer/gui_renderer.cpp renderer/chunk_renderer.cpp ) diff --git a/src/client/client.cpp b/src/client/client.cpp deleted file mode 100644 index 5b946f94..00000000 --- a/src/client/client.cpp +++ /dev/null @@ -1,377 +0,0 @@ -#include "client.h" - -#include "client_config.h" -#include "gl/gl_errors.h" -#include "gl/primitive.h" -#include "gui/widget/label_widget.h" -#include "input/input_state.h" -#include "input/keyboard.h" -#include "world/chunk_mesh_generation.h" -#include -#include -#include -#include -#include -#include - -namespace { - bool isVoxelSelectable(VoxelType voxelType) - { - return voxelType == VoxelType::Solid || voxelType == VoxelType::Flora; - } -} // namespace - -Client::Client() - : NetworkHost("Client") -{ - // clang-format off - m_commandDispatcher.addCommand(ClientCommand::VoxelUpdate, &Client::onVoxelUpdate); - m_commandDispatcher.addCommand(ClientCommand::ChunkData, &Client::onChunkData); - m_commandDispatcher.addCommand(ClientCommand::GameRegistryData, &Client::onGameRegistryData); - m_commandDispatcher.addCommand(ClientCommand::PlayerJoin, &Client::onPlayerJoin); - m_commandDispatcher.addCommand(ClientCommand::PlayerLeave, &Client::onPlayerLeave); - m_commandDispatcher.addCommand(ClientCommand::Snapshot, &Client::onSnapshot); - m_commandDispatcher.addCommand(ClientCommand::SpawnPoint, &Client::onSpawnPoint); - m_commandDispatcher.addCommand(ClientCommand::NewPlayerSkin, &Client::onPlayerSkinReceive); - // clang-format on -} - -bool Client::init(const std::string& ipAddress) -{ - // OpenGL stuff - m_cube = makeCubeVertexArray(1, 2, 1); - - m_selectionBox = makeWireCubeVertexArray(1, 1, 1); - - m_chunkRenderer.init(); - - // Selection box shader - m_selectionShader.program.create("selection", "selection"); - m_selectionShader.program.bind(); - m_selectionShader.modelLocation = - m_selectionShader.program.getUniformLocation("modelMatrix"); - m_selectionShader.projectionViewLocation = - m_selectionShader.program.getUniformLocation("projectionViewMatrix"); - - // Basic shader - m_basicShader.program.create("static", "static"); - m_basicShader.program.bind(); - m_basicShader.modelLocation = m_basicShader.program.getUniformLocation("modelMatrix"); - m_basicShader.projectionViewLocation = - m_basicShader.program.getUniformLocation("projectionViewMatrix"); - - // Texture for the player model - m_errorSkinTexture.create("res/skins/error.png", false); - m_errorSkinTexture.bind(); - - // Set up the server connection - auto peer = NetworkHost::createAsClient(ipAddress); - if (!peer) { - return false; - } - mp_serverPeer = *peer; - - // Set player stuff - mp_player = &m_entities[NetworkHost::getPeerId()]; - mp_player->position = {CHUNK_SIZE * 2, CHUNK_SIZE * 2 + 1, CHUNK_SIZE * 2}; - - m_rawPlayerSkin = gl::loadRawImageFile("skins/" + ClientConfig::get().skinName); - sendPlayerSkin(m_rawPlayerSkin); - - float aspect = static_cast(ClientConfig::get().windowWidth) / - static_cast(ClientConfig::get().windowHeight); - m_projectionMatrix = glm::perspective(3.14f / 2.0f, aspect, 0.01f, 2000.0f); - return true; -} - -void Client::handleInput(const sf::Window& window, const Keyboard& keyboard, - const InputState& inputState) -{ - if (!m_hasReceivedGameData) { - return; - } - static auto lastMousePosition = sf::Mouse::getPosition(window); - - if (inputState.isMouseLocked && window.hasFocus() && - sf::Mouse::getPosition(window).y >= 0) { - float verticalSensitivity = ClientConfig::get().verticalSensitivity; - float horizontalSensitivity = ClientConfig::get().horizontalSensitivity; - auto change = sf::Mouse::getPosition(window) - lastMousePosition; - - mp_player->rotation.x += - static_cast(change.y / 8.0f * verticalSensitivity); - mp_player->rotation.y += - static_cast(change.x / 8.0f * horizontalSensitivity); - sf::Mouse::setPosition({(int)window.getSize().x / 2, (int)window.getSize().y / 2}, - window); - -// This fixes mouse jittering on mac -#ifndef __APPLE__ - lastMousePosition = sf::Mouse::getPosition(window); -#else - lastMousePosition.x = (int)window.getSize().x / 2; - lastMousePosition.y = (int)window.getSize().y / 2; -#endif - } - - // Handle keyboard input - float PLAYER_SPEED = 5.0f; - if (keyboard.isKeyDown(sf::Keyboard::LControl)) { - PLAYER_SPEED *= 10; - } - - // Handle mouse input - auto& rotation = mp_player->rotation; - auto& velocity = mp_player->velocity; - if (keyboard.isKeyDown(sf::Keyboard::W)) { - velocity += forwardsVector(rotation) * PLAYER_SPEED; - } - else if (keyboard.isKeyDown(sf::Keyboard::S)) { - velocity += backwardsVector(rotation) * PLAYER_SPEED; - } - if (keyboard.isKeyDown(sf::Keyboard::A)) { - velocity += leftVector(rotation) * PLAYER_SPEED; - } - else if (keyboard.isKeyDown(sf::Keyboard::D)) { - velocity += rightVector(rotation) * PLAYER_SPEED; - } - if (keyboard.isKeyDown(sf::Keyboard::Space)) { - velocity.y += PLAYER_SPEED * 2; - } - else if (keyboard.isKeyDown(sf::Keyboard::LShift)) { - velocity.y -= PLAYER_SPEED * 2; - } - if (rotation.x < -80.0f) { - rotation.x = -79.9f; - } - else if (rotation.x > 85.0f) { - rotation.x = 84.9f; - } -} - -void Client::onMouseRelease(sf::Mouse::Button button) -{ - // Handle voxel removal/ voxel placing events - - auto voxels = - getIntersectedVoxels(mp_player->position, forwardsVector(mp_player->rotation), 8); - - VoxelPosition& previous = voxels.at(0); - for (auto& position : voxels) { - auto& voxel = m_voxelData.getVoxelData(m_chunks.manager.getVoxel(position)); - - if (isVoxelSelectable(voxel.type)) { - VoxelUpdate voxelUpdate; - voxelUpdate.voxel = button == sf::Mouse::Left ? 0 : 1; - if (button == sf::Mouse::Left) { - voxelUpdate.position = position; - } - else if (previous == toVoxelPosition(mp_player->position)) { - // prevents players from replacing voxels they're inside of - break; - } - else { - voxelUpdate.position = previous; - } - voxelUpdate.position = button == sf::Mouse::Left ? position : previous; - m_chunks.voxelUpdates.push_back(voxelUpdate); - sendVoxelUpdate(voxelUpdate); - break; - } - previous = position; - } -} - -void Client::update(float dt) -{ - NetworkHost::tick(); - if (!m_hasReceivedGameData) { - return; - } - - mp_player->position += mp_player->velocity * dt; - mp_player->velocity *= 0.99 * dt; - - sendPlayerPosition(mp_player->position); - - // Update voxels - for (auto& voxelUpdate : m_chunks.voxelUpdates) { - auto chunkPosition = toChunkPosition(voxelUpdate.position); - m_chunks.manager.ensureNeighbours(chunkPosition); - m_chunks.manager.setVoxel(voxelUpdate.position, voxelUpdate.voxel); - m_chunks.updates.push_back(chunkPosition); - - auto p = chunkPosition; - auto localVoxelPostion = toLocalVoxelPosition(voxelUpdate.position); - if (localVoxelPostion.x == 0) { - m_chunks.updates.push_back({p.x - 1, p.y, p.z}); - } - else if (localVoxelPostion.x == CHUNK_SIZE - 1) { - m_chunks.updates.push_back({p.x + 1, p.y, p.z}); - } - - if (localVoxelPostion.y == 0) { - m_chunks.updates.push_back({p.x, p.y - 1, p.z}); - } - else if (localVoxelPostion.y == CHUNK_SIZE - 1) { - m_chunks.updates.push_back({p.x, p.y + 1, p.z}); - } - - if (localVoxelPostion.z == 0) { - m_chunks.updates.push_back({p.x, p.y, p.z - 1}); - } - else if (localVoxelPostion.z == CHUNK_SIZE - 1) { - m_chunks.updates.push_back({p.x, p.y, p.z + 1}); - } - } - m_chunks.voxelUpdates.clear(); - - auto playerChunk = worldToChunkPosition(mp_player->position); - auto distanceToPlayer = [&playerChunk](const ChunkPosition& chunkPosition) { - return glm::abs(playerChunk.x - chunkPosition.x) + - glm::abs(playerChunk.y - chunkPosition.y) + - glm::abs(playerChunk.z - chunkPosition.z); - }; - - if (!m_chunks.updates.empty()) { - // Sort chunk updates by distance if the update vector is not - // sorted already - if (!std::is_sorted(m_chunks.updates.begin(), m_chunks.updates.end(), - [&](const auto& a, const auto& b) { - return distanceToPlayer(a) < distanceToPlayer(b); - })) { - // Remove non-unique elements - std::unordered_set updates; - for (auto& update : m_chunks.updates) { - updates.insert(update); - } - - m_chunks.updates.assign(updates.cbegin(), updates.cend()); - - // Sort it to find chunk mesh cloest to the player - std::sort(m_chunks.updates.begin(), m_chunks.updates.end(), - [&](const auto& a, const auto& b) { - return distanceToPlayer(a) < distanceToPlayer(b); - }); - } - - if (m_noMeshingCount != m_chunks.updates.size()) { - m_voxelMeshing = false; - } - - // Find first "meshable" chunk - int count = 0; - if (!m_voxelMeshing) { - m_noMeshingCount = 0; - for (auto itr = m_chunks.updates.cbegin(); itr != m_chunks.updates.cend();) { - if (m_chunks.manager.hasNeighbours(*itr)) { - auto& chunk = m_chunks.manager.getChunk(*itr); - - auto buffer = makeChunkMesh(chunk, m_voxelData); - m_chunkRenderer.updateMesh(*itr, std::move(buffer)); - itr = m_chunks.updates.erase(itr); - - // Break so that the game still runs while world is - // being built - // @TODO: Work out a way to make this concurrent (aka - // run seperate from rest of application) - if (count++ > 3) { - break; - } - } - else { - m_noMeshingCount++; - itr++; - } - } - if (m_noMeshingCount == m_chunks.updates.size()) { - m_voxelMeshing = true; - } - } - } - - // Determine if a player is selecting a voxel & if so, which - m_voxelSelected = false; - auto voxels = - getIntersectedVoxels(mp_player->position, forwardsVector(mp_player->rotation), 8); - for (auto& position : voxels) { - auto& voxel = m_voxelData.getVoxelData(m_chunks.manager.getVoxel(position)); - if (isVoxelSelectable(voxel.type)) { - m_currentSelectedVoxelPos = position; - m_voxelSelected = true; - break; - } - } -} - -void Client::render() -{ - // @TODO [Hopson] Clean this up - if (!m_hasReceivedGameData) { - return; - } - // Setup matrices - m_basicShader.program.bind(); - glm::mat4 playerProjectionView = createProjectionViewMatrix( - mp_player->position, mp_player->rotation, m_projectionMatrix); - - gl::loadUniform(m_basicShader.projectionViewLocation, playerProjectionView); - - // Update the viewing frustum for frustum culling - m_frustum.update(playerProjectionView); - - // Render all the entities - auto drawable = m_cube.getDrawable(); - drawable.bind(); - - for (auto& ent : m_entities) { - - if (ent.active && &ent != mp_player) { - if (ent.playerSkin.textureExists()) { - ent.playerSkin.bind(); - } - else { - m_errorSkinTexture.bind(); - } - - glm::mat4 modelMatrix{1.0f}; - translateMatrix(modelMatrix, - {ent.position.x, ent.position.y, ent.position.z}); - gl::loadUniform(m_basicShader.modelLocation, modelMatrix); - drawable.draw(); - } - }; - // Render chunks - m_voxelTextures.bind(); - - bool isPlayerInWater = - m_chunks.manager.getVoxel(toVoxelPosition(mp_player->position)) == - m_voxelData.getVoxelId(CommonVoxel::Water); - auto result = m_chunkRenderer.renderChunks(mp_player->position, m_frustum, - playerProjectionView, isPlayerInWater); - - // Render selection box - if (m_voxelSelected) { - glCheck(glEnable(GL_BLEND)); - glCheck(glEnable(GL_LINE_SMOOTH)); - glCheck(glLineWidth(2.0)); - m_selectionShader.program.bind(); - glm::mat4 modelMatrix{1.0}; - float size = 1.005f; - translateMatrix(modelMatrix, {m_currentSelectedVoxelPos.x - (size - 1) / 2, - m_currentSelectedVoxelPos.y - (size - 1) / 2, - m_currentSelectedVoxelPos.z - (size - 1) / 2}); - scaleMatrix(modelMatrix, size); - gl::loadUniform(m_selectionShader.modelLocation, modelMatrix); - gl::loadUniform(m_selectionShader.projectionViewLocation, playerProjectionView); - m_selectionBox.getDrawable().bindAndDraw(GL_LINES); - glCheck(glDisable(GL_BLEND)); - } -} - -void Client::endGame() -{ - if (mp_serverPeer) { - NetworkHost::disconnectFromPeer(mp_serverPeer); - } -} diff --git a/src/client/client.h b/src/client/client.h deleted file mode 100644 index 989c9e27..00000000 --- a/src/client/client.h +++ /dev/null @@ -1,126 +0,0 @@ -#pragma once - -#include "gl/shader.h" -#include "gl/textures.h" -#include "gl/vertex_array.h" -#include "input/keyboard.h" -#include "maths.h" -#include "renderer/chunk_renderer.h" -#include -#include -#include -#include -#include -#include -#include - -class Keyboard; -struct InputState; - -namespace gui { - class LabelWidget; -} - -struct VoxelUpdate { - VoxelPosition position; - voxel_t voxel = 0; -}; - -struct Entity final { - glm::vec3 position{0.0f}; - glm::vec3 rotation{0.0f}; - glm::vec3 velocity{0.0f}; - bool active = false; - - gl::Texture2d playerSkin; // May need to be relocated to its own Player Entity -}; - -class Client final : public NetworkHost { - public: - Client(); - - bool init(const std::string& ipAddress); - void handleInput(const sf::Window& window, const Keyboard& keyboard, - const InputState& inputState); - void onMouseRelease(sf::Mouse::Button button); - - void update(float dt); - void render(); - void endGame(); - - private: - // Network functions; defined in the src/client/network/client_command.cpp - // directory - void sendPlayerPosition(const glm::vec3& position); - void sendVoxelUpdate(const VoxelUpdate& update); - void sendPlayerSkin(const sf::Image& playerSkin); - - void onPeerConnect(ENetPeer* peer) override; - void onPeerDisconnect(ENetPeer* peer) override; - void onPeerTimeout(ENetPeer* peer) override; - void onCommandRecieve(ENetPeer* peer, sf::Packet& packet, command_t command) override; - - void onPlayerJoin(sf::Packet& packet); - void onPlayerLeave(sf::Packet& packet); - void onSnapshot(sf::Packet& packet); - void onChunkData(sf::Packet& packet); - void onSpawnPoint(sf::Packet& packet); - void onVoxelUpdate(sf::Packet& packet); - void onPlayerSkinReceive(sf::Packet& packet); - - void onGameRegistryData(sf::Packet& packet); - // End of network functions - - // Network - ENetPeer* mp_serverPeer = nullptr; - CommandDispatcher m_commandDispatcher; - bool m_hasReceivedGameData = false; - - // Rendering/ OpenGL stuff - ViewFrustum m_frustum{}; - glm::mat4 m_projectionMatrix{1.0f}; - - gl::VertexArray m_cube; - - gl::VertexArray m_selectionBox; - - gl::Texture2d m_errorSkinTexture; - sf::Image m_rawPlayerSkin; - - gl::TextureArray m_voxelTextures; - - ChunkRenderer m_chunkRenderer; - - struct { - gl::Shader program; - gl::UniformLocation modelLocation; - gl::UniformLocation projectionViewLocation; - } m_basicShader; - - struct { - gl::Shader program; - gl::UniformLocation modelLocation; - gl::UniformLocation projectionViewLocation; - } m_selectionShader; - - // For time-based render stuff eg waves in the water - sf::Clock m_clock; - - // Gameplay/ World - std::array m_entities; - - VoxelPosition m_currentSelectedVoxelPos; - bool m_voxelSelected = false; - Entity* mp_player = nullptr; - - struct { - ChunkManager manager; - std::vector updates; - std::vector voxelUpdates; - } m_chunks; - - VoxelDataManager m_voxelData; - - unsigned m_noMeshingCount = 0; - bool m_voxelMeshing = false; -}; diff --git a/src/client/client_engine.cpp b/src/client/client_engine.cpp index d652c300..6947c781 100644 --- a/src/client/client_engine.cpp +++ b/src/client/client_engine.cpp @@ -1,175 +1,112 @@ #include "client_engine.h" #include "client_config.h" -#include "client_state_controller.h" -#include "game.h" -#include "gl/framebuffer.h" -#include "gl/gl_errors.h" #include "gl/primitive.h" -#include "gl/vertex_array.h" -#include "gui/gui_system.h" -#include "gui/widget/label_widget.h" -#include "input/input_state.h" #include "lua/client_lua_api.h" -#include "lua/client_lua_callback.h" -#include "renderer/chunk_renderer.h" -#include "renderer/gui_renderer.h" #include "window.h" -#include -#include -#include - -namespace { - struct FPSCounter final { - sf::Clock timer; - float frameTime = 0; - float frameCount = 0; - - void update() - { - frameCount++; - if (timer.getElapsedTime() > sf::seconds(0.25)) { - auto time = timer.getElapsedTime(); - frameTime = time.asMilliseconds() / frameCount; - timer.restart(); - frameCount = 0; - } - } - }; - - // -} // namespace -void runClientEngine() +bool ClientEngine::init(sf::Window& window) { - // Window/ OpenGL context setup - sf::Window window; - if (!createWindowInitOpengl(window)) { - return; // EngineStatus::GLInitError; - } + mp_window = &window; - // Client engine stuff - ClientStateController control; - FPSCounter fps; - sf::Clock gameTimer; + // Init all the lua api stuff + m_luaCallbacks.initCallbacks(m_lua); + luaInitGuiWidgetApi(m_lua); + luaInitInputApi(m_lua, window, m_inputState); + luaInitClientControlApi(m_lua, m_controller); + luaInitGuiApi(m_lua, m_gui, &m_guiRenderer); - // Input - Keyboard keyboard; - InputState inputState; + m_lua.runLuaFile("game/client/main.lua"); + m_luaCallbacks.onClientStartup(); - // Init Lua lua - ScriptEngine scriptEngine; - ClientLuaCallbacks callbacks; + m_guiRenderTarget.create(GUI_WIDTH, GUI_HEIGHT); + m_worldRenderTarget.create(ClientConfig::get().windowHeight, + ClientConfig::get().windowWidth); - // Gui - gui::GuiSystem gui; - GuiRenderer guiRenderer; + m_screenShader.create("minimal", "minimal"); + m_screenBuffer = makeScreenQuadVertexArray(); + return true; +} - // Init all the lua api stuff - callbacks.initCallbacks(scriptEngine); - luaInitGuiWidgetApi(scriptEngine); - luaInitInputApi(scriptEngine, window, inputState); - luaInitClientControlApi(scriptEngine, control); - luaInitGuiApi(scriptEngine, gui, &guiRenderer); - - // Run the lua file to init the client engine - scriptEngine.runLuaFile("game/client/main.lua"); - callbacks.onClientStartup(); - - Game game; - - //============================================================= - - // Temp render stuff for testing - gl::Framebuffer guiRenderTarget; - gl::Framebuffer worldRenderTarget; - gl::Shader screenShader; - gl::VertexArray screenVAO = makeScreenQuadVertexArray(); - - guiRenderTarget.create(static_cast(GUI_WIDTH), - static_cast(GUI_HEIGHT)); - worldRenderTarget.create(ClientConfig::get().windowWidth, - ClientConfig::get().windowHeight); - screenShader.create("minimal", "minimal"); - - bool isRunning = true; - - // Main loop of the client code - while (isRunning) { - sf::Event event; - while (window.pollEvent(event)) { - if (window.hasFocus()) { - keyboard.update(event); - gui.handleEvent(event); - } - switch (event.type) { - case sf::Event::Closed: - isRunning = false; - break; - - case sf::Event::MouseWheelScrolled: - callbacks.onMouseWheelScroll(event.mouseWheelScroll); - break; - - case sf::Event::KeyReleased: - callbacks.onKeyboardKeyReleased(event.key.code); - break; - - case sf::Event::MouseButtonReleased: - game.onMouseRelease(event.mouseButton.button); - break; - - default: - break; - } +void ClientEngine::runClient() +{ + while (mp_window->isOpen()) { + pollWindowEvents(); + m_game.handleInput(m_keyboard, m_inputState); + update(); + render(); + + if (!m_controller.executeAction(m_game, m_luaCallbacks)) { + mp_window->close(); } + } +} - // Input - game.input(window, keyboard, inputState); +void ClientEngine::update() +{ + m_fpsCounter.update(); + m_game.tick(0.16f); + m_gui.update(); + if (((int)m_fpsCounter.frameCount % 256) == 0) { + // std::cout << m_fpsCounter.frameTime << '\n'; + } +} - // Update - game.update(gameTimer.restart().asSeconds()); - gui.update(); +void ClientEngine::render() +{ + glEnable(GL_DEPTH_TEST); - //============================================================= - // Render - // - glEnable(GL_DEPTH_TEST); + // World + m_worldRenderTarget.bind(); + glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); + m_game.render(); - // World + // GUI + m_guiRenderTarget.bind(); + glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); + m_gui.render(m_guiRenderer); - worldRenderTarget.bind(); - glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); - game.render(); + // Buffer to window + gl::unbindFramebuffers(ClientConfig::get().windowWidth, + ClientConfig::get().windowHeight); + glDisable(GL_DEPTH_TEST); + glClear(GL_COLOR_BUFFER_BIT); + auto drawable = m_screenBuffer.getDrawable(); + drawable.bind(); + m_screenShader.bind(); - // GUI - guiRenderTarget.bind(); - glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); - gui.render(guiRenderer); + m_worldRenderTarget.bindTexture(); + drawable.draw(); - // Buffer to window - gl::unbindFramebuffers(ClientConfig::get().windowWidth, - ClientConfig::get().windowHeight); - glDisable(GL_DEPTH_TEST); - glClear(GL_COLOR_BUFFER_BIT); - auto drawable = screenVAO.getDrawable(); - drawable.bind(); - screenShader.bind(); + glEnable(GL_BLEND); - worldRenderTarget.bindTexture(); - drawable.draw(); + m_guiRenderTarget.bindTexture(); + drawable.draw(); - glEnable(GL_BLEND); + mp_window->display(); + glDisable(GL_BLEND); +} - guiRenderTarget.bindTexture(); - drawable.draw(); +void ClientEngine::pollWindowEvents() +{ + sf::Event event; + while (mp_window->pollEvent(event)) { + if (mp_window->hasFocus()) { + m_keyboard.update(event); + m_gui.handleEvent(event); + m_game.handleEvent(event); + } + switch (event.type) { + case sf::Event::MouseWheelScrolled: + m_luaCallbacks.onMouseWheelScroll(event.mouseWheelScroll); + break; - window.display(); - glDisable(GL_BLEND); - //======================================================================= + case sf::Event::KeyReleased: + m_luaCallbacks.onKeyboardKeyReleased(event.key.code); + break; - fps.update(); - isRunning = control.executeAction(game, callbacks); + default: + break; + } } - window.close(); -} \ No newline at end of file +} diff --git a/src/client/client_engine.h b/src/client/client_engine.h index eb747959..706a45f9 100644 --- a/src/client/client_engine.h +++ b/src/client/client_engine.h @@ -1,7 +1,66 @@ #pragma once -/** - * @brief Run the client - * @param config - */ -void runClientEngine(); \ No newline at end of file +#include "client_state_controller.h" +#include "game/game.h" +#include "gui/gui_system.h" +#include "input/input_state.h" +#include "input/keyboard.h" +#include "lua/client_lua_callback.h" +#include "renderer/gui_renderer.h" +#include +#include +#include + +struct FPSCounter final { + sf::Clock timer; + float frameTime = 0; + float frameCount = 0; + + void update() + { + frameCount++; + if (timer.getElapsedTime() > sf::seconds(0.25)) { + auto time = timer.getElapsedTime(); + frameTime = time.asMilliseconds() / frameCount; + timer.restart(); + frameCount = 0; + } + } +}; + +class ClientEngine { + public: + // Window is pass in to init opengl + // before running code here + bool init(sf::Window& window); + void runClient(); + + private: + void update(); + void render(); + + void pollWindowEvents(); + + ClientStateController m_controller; + + sf::Window* mp_window; + Keyboard m_keyboard; + InputState m_inputState; + + ScriptEngine m_lua; + ClientLuaCallbacks m_luaCallbacks; + + gui::GuiSystem m_gui; + GuiRenderer m_guiRenderer; + + FPSCounter m_fpsCounter; + + // Rendering??? + gl::Framebuffer m_guiRenderTarget; + gl::Framebuffer m_worldRenderTarget; + + gl::Shader m_screenShader; + gl::VertexArray m_screenBuffer; + + Game m_game; +}; \ No newline at end of file diff --git a/src/client/client_state_controller.cpp b/src/client/client_state_controller.cpp index f44ce438..12d2c5ec 100644 --- a/src/client/client_state_controller.cpp +++ b/src/client/client_state_controller.cpp @@ -1,5 +1,6 @@ #include "client_state_controller.h" -#include "game.h" +#include "game/game.h" +#include "game/game_type.h" #include "lua/client_lua_callback.h" namespace { @@ -16,7 +17,8 @@ namespace { bool executeAction(Game& game, State& m_currentState, ClientLuaCallbacks& callbacks) final override { - if (game.initGame()) { + game.setGameDefintion("Test"); + if (game.start()) { callbacks.onEnterGame(); m_currentState = State::InGame; } @@ -42,7 +44,8 @@ namespace { bool executeAction(Game& game, State& m_currentState, ClientLuaCallbacks& callbacks) final override { - if (game.initGame()) { + game.setGameDefintion("Test"); + if (game.start()) { callbacks.onEnterGame(); m_currentState = State::InGame; } @@ -67,7 +70,8 @@ namespace { bool executeAction(Game& game, State& m_currentState, ClientLuaCallbacks& callbacks) final override { - if (game.initGame(m_serverIp)) { + game.setGameDefintion(m_serverIp); + if (game.start()) { callbacks.onEnterGame(); m_currentState = State::InGame; } @@ -87,7 +91,7 @@ namespace { bool executeAction(Game& game, State& m_currentState, ClientLuaCallbacks& callbacks) final override { - game.stopGame(); + game.shutdown(); callbacks.onExitGame(); m_currentState = State::InMenu; return true; @@ -154,7 +158,7 @@ bool ClientStateController::executeAction(Game& game, ClientLuaCallbacks& callba { if (m_nextAction) { bool result = m_nextAction->executeAction(game, m_currentState, callbacks); - m_nextAction.release(); + m_nextAction.reset(); return result; } return true; diff --git a/src/client/game.cpp b/src/client/game.cpp deleted file mode 100644 index acbf1d97..00000000 --- a/src/client/game.cpp +++ /dev/null @@ -1,68 +0,0 @@ -#include "game.h" - -bool Game::initGame() -{ - // Client.serverIp = LOCAL_HOST; - - m_serverLauncher = std::make_unique(sf::milliseconds(1000)); - m_serverLauncher->runAsThread(); - - return init(LOCAL_HOST); -} - -bool Game::initGame(const std::string& ipAddress) -{ - return init(ipAddress); -} - -bool Game::init(const std::string& ip) -{ - m_client = std::make_unique(); - if (!m_client->init(ip)) { - stopGame(); - return false; - } - return true; -} - -void Game::stopGame() -{ - if (m_serverLauncher) { - m_serverLauncher->stop(); - m_serverLauncher.release(); - } - if (m_client) { - m_client->endGame(); - m_client->destroy(); - m_client.release(); - } -} - -void Game::onMouseRelease(sf::Mouse::Button button) -{ - if (m_client) { - m_client->onMouseRelease(button); - } -} - -void Game::input(sf::Window& window, const Keyboard& keyboard, - const InputState& inputState) -{ - if (m_client) { - m_client->handleInput(window, keyboard, inputState); - } -} - -void Game::update(float dt) -{ - if (m_client) { - m_client->update(dt); - } -} - -void Game::render() -{ - if (m_client) { - m_client->render(); - } -} \ No newline at end of file diff --git a/src/client/game.h b/src/client/game.h deleted file mode 100644 index d8e6c48c..00000000 --- a/src/client/game.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include "client.h" -#include "client_config.h" -#include -#include - -class Game { - public: - bool initGame(const std::string& ipAddress); - bool initGame(); - - void stopGame(); - - // Ran during game loop - void onMouseRelease(sf::Mouse::Button button); - void input(sf::Window& window, const Keyboard& keyboard, - const InputState& inputState); - void update(float dt); - void render(); - - private: - bool init(const std::string& ip); - - std::unique_ptr m_client; - std::unique_ptr m_serverLauncher; -}; diff --git a/src/client/world/chunk_mesh.cpp b/src/client/game/chunk_mesh.cpp similarity index 100% rename from src/client/world/chunk_mesh.cpp rename to src/client/game/chunk_mesh.cpp diff --git a/src/client/world/chunk_mesh.h b/src/client/game/chunk_mesh.h similarity index 100% rename from src/client/world/chunk_mesh.h rename to src/client/game/chunk_mesh.h diff --git a/src/client/world/chunk_mesh_generation.cpp b/src/client/game/chunk_mesh_generation.cpp similarity index 98% rename from src/client/world/chunk_mesh_generation.cpp rename to src/client/game/chunk_mesh_generation.cpp index 30453e73..8c927f3e 100644 --- a/src/client/world/chunk_mesh_generation.cpp +++ b/src/client/game/chunk_mesh_generation.cpp @@ -22,7 +22,6 @@ namespace { { voxel_t air = voxelData.getVoxelId(CommonVoxel::Air); - auto& thisVoxel = voxelData.getVoxelData(thisId); auto& compareVoxel = voxelData.getVoxelData(compareId); if (compareId == air) { return true; diff --git a/src/client/world/chunk_mesh_generation.h b/src/client/game/chunk_mesh_generation.h similarity index 100% rename from src/client/world/chunk_mesh_generation.h rename to src/client/game/chunk_mesh_generation.h diff --git a/src/client/game/client_world.cpp b/src/client/game/client_world.cpp new file mode 100644 index 00000000..e9b825e3 --- /dev/null +++ b/src/client/game/client_world.cpp @@ -0,0 +1,222 @@ +#include "client_world.h" +#include "../client_config.h" +#include "../gl/primitive.h" +#include "../renderer/camera.h" +#include "chunk_mesh_generation.h" +#include +#include + +GLuint VoxelTextureMap::getTextureId(const std::string& name) +{ + auto itr = textureMap.find(name); + if (itr == textureMap.end()) { + GLuint textureId = textures.addTexture(name); + textureMap.emplace(name, textureId); + return textureId; + } + return itr->second; +} + +ClientWorld::ClientWorld() +{ + m_entities.resize(1024); + m_chunkRenderer.init(); + + m_vao = makeCubeVertexArray(1, 2, 1); + m_entityShader.create("static", "static"); + m_entityShader.bind(); + m_playerTexture.create("res/skins/player.png", false); + m_entityProj = m_entityShader.getUniformLocation("projectionViewMatrix"); + m_entityModel = m_entityShader.getUniformLocation("modelMatrix"); +} + +void ClientWorld::setPlayerId(u32 id) +{ + std::cout << "Got player ID: " << id << std::endl; + m_playerId = id; +} + +void ClientWorld::setupData(int maxEntities) +{ + m_issetup = true; +} + +void ClientWorld::tick(float dt) +{ + if (!m_issetup) { + return; + } + auto& player = getPlayer(); + player.position += player.velocity * dt; + player.velocity *= 0.90 * dt; + + // Update chunks + i32 count = 0; + for (auto itr = m_chunkUpdates.begin(); itr != m_chunkUpdates.end();) { + if (m_chunks.hasNeighbours(*itr)) { + const Chunk& chunk = m_chunks.getChunk(*itr); + ChunkMeshCollection meshes = makeChunkMesh(chunk, m_voxelData); + m_chunkRenderer.updateMesh(*itr, std::move(meshes)); + itr = m_chunkUpdates.erase(itr); + if (count++ > 3) { + break; + } + } + else { + itr++; + } + } + + // Update voxels + for (auto& update : m_voxelUpdates) { + auto chunkPosition = toChunkPosition(update.voxelPosition); + m_chunks.ensureNeighbours(chunkPosition); + m_chunks.setVoxel(update.voxelPosition, update.voxel); + m_chunkUpdates.push_back(chunkPosition); + + auto p = chunkPosition; + auto localVoxelPostion = toLocalVoxelPosition(update.voxelPosition); + if (localVoxelPostion.x == 0) { + m_chunkUpdates.push_back({p.x - 1, p.y, p.z}); + } + else if (localVoxelPostion.x == CHUNK_SIZE - 1) { + m_chunkUpdates.push_back({p.x + 1, p.y, p.z}); + } + + if (localVoxelPostion.y == 0) { + m_chunkUpdates.push_back({p.x, p.y - 1, p.z}); + } + else if (localVoxelPostion.y == CHUNK_SIZE - 1) { + m_chunkUpdates.push_back({p.x, p.y + 1, p.z}); + } + + if (localVoxelPostion.z == 0) { + m_chunkUpdates.push_back({p.x, p.y, p.z - 1}); + } + else if (localVoxelPostion.z == CHUNK_SIZE - 1) { + m_chunkUpdates.push_back({p.x, p.y, p.z + 1}); + } + } + m_voxelUpdates.clear(); +} + +void ClientWorld::render(const Camera& camera) +{ + if (!m_issetup) { + return; + } + m_voxelTextures.textures.bind(); + + // Render chunks, getting the block at the player position for """effects""" + auto playerVoxel = m_chunks.getVoxel(toVoxelPosition(getPlayer().position)); + auto waterId = m_voxelData.getVoxelId(CommonVoxel::Water); + m_chunkRenderer.renderChunks(camera, playerVoxel == waterId); + + // Render entities + m_entityShader.bind(); + m_playerTexture.bind(); + gl::loadUniform(m_entityProj, camera.getProjectionView()); + auto d = m_vao.getDrawable(); + d.bind(); + for (u32 i = 1; i < m_entities.size(); i++) { + auto& entity = m_entities[i]; + if (entity.active && i != m_playerId) { + glm::mat4 model{1.0f}; + translateMatrix(model, {entity.position.x - 0.5, entity.position.y - 3, + entity.position.z - 0.5}); + gl::loadUniform(m_entityModel, model); + d.draw(); + } + } +} + +void ClientWorld::addEntity(u32 id, const glm::vec3& position, const glm::vec3& rotation) +{ + std::cout << "Got entity ID: " << id << std::endl; + assert(id <= m_entities.size()); + m_entities[id].active = true; + m_entities[id].position = position; + m_entities[id].rotation = rotation; +} + +void ClientWorld::updateEntity(u32 id, const glm::vec3& position, + const glm::vec3& rotation) +{ + assert(id <= m_entities.size()); + if (!m_entities[id].active) { + return; + } + m_entities[id].position = position; + m_entities[id].rotation = rotation; +} + +void ClientWorld::removeEntity(u32 id) +{ + assert(id <= m_entities.size()); + m_entities[id].active = false; +} + +void ClientWorld::setVoxelTextureCount(int count) +{ + // 1. Need to somehow work out the exact amount of textures needed + // 2. Need to pass in the actual texture pack resolution (right now it is hardcoded + // 16) + m_voxelTextures.textures.create(count * 3, 16); +} + +void ClientWorld::addVoxelType(VoxelData&& voxel) +{ + const std::string texturePath = + "texture_packs/" + ClientConfig::get().texturePack + "/voxels/"; + + std::string& top = voxel.topTexture; + std::string& side = voxel.sideTexture; + std::string& bottom = voxel.topTexture; + + voxel.topTextureId = m_voxelTextures.getTextureId(texturePath + top); + voxel.sideTextureId = m_voxelTextures.getTextureId(texturePath + side); + voxel.bottomTextureId = m_voxelTextures.getTextureId(texturePath + bottom); + + m_voxelData.addVoxelData(std::move(voxel)); +} + +void ClientWorld::initialiseCommonVoxels() +{ + m_voxelData.initCommonVoxelTypes(); +} + +bool ClientWorld::isVoxelInteractable(const VoxelPosition& position) +{ + auto voxelId = m_chunks.getVoxel(position); + auto type = m_voxelData.getVoxelData(voxelId).type; + + return type == VoxelType::Solid || type == VoxelType::Flora; +} + +bool ClientWorld::hasChunk(const ChunkPosition& position) const +{ + return m_chunks.hasChunk(position); +} + +void ClientWorld::createChunkFromCompressed(const ChunkPosition& position, + const CompressedVoxels& voxels) +{ + Chunk& chunk = m_chunks.addChunk(position); + chunk.voxels = decompressVoxelData(voxels); + m_chunkUpdates.push_back(position); +} + +EntityState& ClientWorld::getPlayer() +{ + return m_entities[m_playerId]; +} + +u32 ClientWorld::getPlayerId() const +{ + return m_playerId; +} + +void ClientWorld::updateVoxel(const VoxelUpdate& update) +{ + m_voxelUpdates.push_back(update); +} diff --git a/src/client/game/client_world.h b/src/client/game/client_world.h new file mode 100644 index 00000000..8f52be8b --- /dev/null +++ b/src/client/game/client_world.h @@ -0,0 +1,70 @@ +#pragma once + +#include "../gl/textures.h" +#include "../renderer/chunk_renderer.h" +#include +#include +#include +#include + +class Camera; + +struct VoxelTextureMap { + std::unordered_map textureMap; + gl::TextureArray textures; + + GLuint getTextureId(const std::string& name); +}; + +class ClientWorld { + public: + ClientWorld(); + + void setPlayerId(u32 id); + void setupData(int maxEntities); + + void tick(float dt); + void render(const Camera& camera); + + void addEntity(u32 id, const glm::vec3& position, const glm::vec3& rotation); + void updateEntity(u32 id, const glm::vec3& position, const glm::vec3& rotation); + void removeEntity(u32 id); + + void setVoxelTextureCount(int count); + void addVoxelType(VoxelData&& voxel); + void initialiseCommonVoxels(); + + bool isVoxelInteractable(const VoxelPosition& position); + + bool hasChunk(const ChunkPosition& position) const; + void createChunkFromCompressed(const ChunkPosition& position, + const CompressedVoxels& voxels); + + EntityState& getPlayer(); + u32 getPlayerId() const; + + void updateVoxel(const VoxelUpdate& update); + + private: + std::vector m_entities; + + ChunkManager m_chunks; + ChunkRenderer m_chunkRenderer; + std::vector m_chunkUpdates; + + std::vector m_voxelUpdates; + + VoxelDataManager m_voxelData; + VoxelTextureMap m_voxelTextures; + + u32 m_playerId = 0; + + bool m_issetup = false; + + // temp + gl::VertexArray m_vao; + gl::Shader m_entityShader; + gl::UniformLocation m_entityProj; + gl::UniformLocation m_entityModel; + gl::Texture2d m_playerTexture; +}; \ No newline at end of file diff --git a/src/client/game/game.cpp b/src/client/game/game.cpp new file mode 100644 index 00000000..6c3ad69d --- /dev/null +++ b/src/client/game/game.cpp @@ -0,0 +1,53 @@ +#include "game.h" + +#include "game_type.h" + +bool Game::start() +{ + if (isInGame()) { + return m_gameDef->start(); + } + return false; +} + +void Game::shutdown() +{ + if (isInGame()) { + std::cout << "Shutting down\n"; + m_gameDef->shutdown(); + m_gameDef.reset(); + } +} + +void Game::handleEvent(const sf::Event& event) +{ + if (isInGame()) { + m_gameDef->handleEvent(event); + } +} + +void Game::handleInput(const Keyboard& keyboard, const InputState& inputState) +{ + if (isInGame()) { + m_gameDef->handleInput(keyboard, inputState); + } +} + +void Game::tick(float dt) +{ + if (isInGame()) { + m_gameDef->tick(dt); + } +} + +void Game::render() +{ + if (isInGame()) { + m_gameDef->render(); + } +} + +bool Game::isInGame() const +{ + return m_gameDef != nullptr; +} diff --git a/src/client/game/game.h b/src/client/game/game.h new file mode 100644 index 00000000..729103ad --- /dev/null +++ b/src/client/game/game.h @@ -0,0 +1,27 @@ +#pragma once + +#include "game_def.h" +#include +#include +#include + +class Game { + public: + template + void setGameDefintion(Args&&... args) + { + m_gameDef = std::make_unique(std::forward(args)...); + } + + bool start(); + void shutdown(); + + void handleEvent(const sf::Event& event); + void handleInput(const Keyboard& keyboard, const InputState& inputState); + void tick(float dt); + void render(); + + private: + bool isInGame() const; + std::unique_ptr m_gameDef; +}; diff --git a/src/client/game/game_def.cpp b/src/client/game/game_def.cpp new file mode 100644 index 00000000..0ca09b7e --- /dev/null +++ b/src/client/game/game_def.cpp @@ -0,0 +1,184 @@ +#include "game_def.h" + +#include "../client_config.h" +#include "../gl/gl_errors.h" +#include "../gl/primitive.h" +#include "../input/input_state.h" +#include "../input/keyboard.h" +#include "../window.h" + +void SelectedBoxRenderer::create() +{ + program.create("selection", "selection"); + program.bind(); + modelLocation = program.getUniformLocation("modelMatrix"); + projectionViewLocation = program.getUniformLocation("projectionViewMatrix"); + m_selectionBox = makeWireCubeVertexArray(1.02f, 1.02f, 1.02f); +} + +void SelectedBoxRenderer::render(const Camera& camera, const VoxelPosition& position) +{ + glCheck(glEnable(GL_BLEND)); + glCheck(glEnable(GL_LINE_SMOOTH)); + glCheck(glLineWidth(2.0)); + program.bind(); + glm::mat4 modelMatrix{1.0}; + float size = 1.005f; + translateMatrix(modelMatrix, + {position.x - (size - 1) / 2, position.y - (size - 1) / 2, + position.z - (size - 1) / 2}); + scaleMatrix(modelMatrix, size); + gl::loadUniform(modelLocation, modelMatrix); + gl::loadUniform(projectionViewLocation, camera.getProjectionView()); + m_selectionBox.getDrawable().bindAndDraw(GL_LINES); + glCheck(glDisable(GL_BLEND)); +} + +bool ClientGameDef::start(const std::string ipAddress) +{ + auto connection = m_client.connectTo(ipAddress); + if (!connection.success) { + std::cout << "ERROR: " << connection.message << "\n"; + shutdown(); + return false; + } + m_client.setWorld(m_world); + + m_camera = Camera::createCamera(); + m_selectionBoxRenderer.create(); + + // TODO Move to the client/server handling + m_world.setupData(1024); + return true; +} + +void ClientGameDef::shutdown() +{ + m_client.disconnect(); + onShutdown(); +} + +void ClientGameDef::handleEvent(const sf::Event& event) +{ + if (event.type == sf::Event::MouseButtonPressed) { + if (event.mouseButton.button == sf::Mouse::Left) { + m_client.sendMouseEvent(MouseEventState::Click); + } + else { + m_client.sendInteraction(); + } + } + else if (event.type == sf::Event::MouseButtonReleased) { + if (event.mouseButton.button == sf::Mouse::Left) { + m_client.sendMouseEvent(MouseEventState::Release); + } + } +} + +void ClientGameDef::handleInput(const Keyboard& keyboard, const InputState& inputState) +{ + if (m_client.getConnnectionState() != ConnectionState::Connected) { + return; + } + + if (inputState.isMouseLocked) { + handlePlayerInput(keyboard); + } + + // Test whether the voxel the player is looking at is interactable + auto& player = m_world.getPlayer(); + auto& position = player.position; + auto& rotation = player.rotation; + + m_isVoxelSelected = false; + auto voxels = getIntersectedVoxels(position, forwardsVector(rotation), 8); + for (auto& position : voxels) { + if (m_world.isVoxelInteractable(position)) { + m_currentSelectedVoxelPos = position; + m_isVoxelSelected = true; + break; + } + } +} + +void ClientGameDef::tick(float dt) +{ + m_client.tick(); + if (m_client.getConnnectionState() != ConnectionState::Connected) { + return; + } + + m_camera.update(m_world.getPlayer()); + m_world.tick(dt); + if (m_client.getConnnectionState() == ConnectionState::Disconnected) { + shutdown(); + } + + auto thisTime = m_timer.getElapsedTime(); + if (thisTime - m_lastTime > sf::milliseconds(50)) { + m_client.sendPlayerState(m_world.getPlayer()); + } +} + +void ClientGameDef::render() +{ + m_world.render(m_camera); + if (m_isVoxelSelected) { + m_selectionBoxRenderer.render(m_camera, m_currentSelectedVoxelPos); + } +} + +void ClientGameDef::handlePlayerInput(const Keyboard& keyboard) +{ + auto& ctx = *Window::context; + static auto lastMousePosition = sf::Mouse::getPosition(ctx); + + auto& player = m_world.getPlayer(); + glm::vec3& rotation = player.rotation; + glm::vec3& velocity = player.velocity; + + float verticalSensitivity = ClientConfig::get().verticalSensitivity; + float horizontalSensitivity = ClientConfig::get().horizontalSensitivity; + auto change = sf::Mouse::getPosition(ctx) - lastMousePosition; + rotation.x += static_cast(change.y / 8.0f * verticalSensitivity); + rotation.y += static_cast(change.x / 8.0f * horizontalSensitivity); + sf::Mouse::setPosition({(int)ctx.getSize().x / 2, (int)ctx.getSize().y / 2}, ctx); + +// This fixes mouse jittering on mac +#ifndef __APPLE__ + lastMousePosition = sf::Mouse::getPosition(ctx); +#else + lastMousePosition.x = (int)window.getSize().x / 2; + lastMousePosition.y = (int)window.getSize().y / 2; +#endif + + float PLAYER_SPEED = 3.0f; + if (keyboard.isKeyDown(sf::Keyboard::LControl)) { + PLAYER_SPEED *= 10; + } + + if (keyboard.isKeyDown(sf::Keyboard::W)) { + velocity += forwardsVector(rotation) * PLAYER_SPEED; + } + else if (keyboard.isKeyDown(sf::Keyboard::S)) { + velocity += backwardsVector(rotation) * PLAYER_SPEED; + } + if (keyboard.isKeyDown(sf::Keyboard::A)) { + velocity += leftVector(rotation) * PLAYER_SPEED; + } + else if (keyboard.isKeyDown(sf::Keyboard::D)) { + velocity += rightVector(rotation) * PLAYER_SPEED; + } + if (keyboard.isKeyDown(sf::Keyboard::Space)) { + velocity.y += PLAYER_SPEED * 2; + } + else if (keyboard.isKeyDown(sf::Keyboard::LShift)) { + velocity.y -= PLAYER_SPEED * 2; + } + if (rotation.x < -80.0f) { + rotation.x = -79.9f; + } + else if (rotation.x > 85.0f) { + rotation.x = 84.9f; + } +} diff --git a/src/client/game/game_def.h b/src/client/game/game_def.h new file mode 100644 index 00000000..a7b72ede --- /dev/null +++ b/src/client/game/game_def.h @@ -0,0 +1,57 @@ +#pragma once + +#include "../network/client.h" +#include +#include + +#include "../gl/shader.h" +#include "../gl/textures.h" +#include "../gl/vertex_array.h" +#include "../renderer/camera.h" +#include "client_world.h" + +class Keyboard; +struct InputState; + +struct SelectedBoxRenderer { + gl::Shader program; + gl::UniformLocation modelLocation; + gl::UniformLocation projectionViewLocation; + gl::VertexArray m_selectionBox; + + void create(); + void render(const Camera& camera, const VoxelPosition& position); +}; + +class ClientGameDef { + public: + virtual ~ClientGameDef() = default; + + void handleEvent(const sf::Event& event); + void handleInput(const Keyboard& keyboard, const InputState& inputState); + void tick(float dt); + void render(); + + virtual bool start() = 0; + + void shutdown(); + + protected: + bool start(const std::string ipAddress); + + private: + void handlePlayerInput(const Keyboard& keyboard); + + virtual void onShutdown() = 0; + + ClientWorld m_world; + Camera m_camera; + Client m_client; + + SelectedBoxRenderer m_selectionBoxRenderer; + VoxelPosition m_currentSelectedVoxelPos; + bool m_isVoxelSelected = false; + + sf::Clock m_timer; + sf::Time m_lastTime; +}; \ No newline at end of file diff --git a/src/client/game/game_type.cpp b/src/client/game/game_type.cpp new file mode 100644 index 00000000..cd8faba9 --- /dev/null +++ b/src/client/game/game_type.cpp @@ -0,0 +1,36 @@ +#include "game_type.h" + +#include + +/// Local game +LocalGame::LocalGame(const std::string& worldName) + : m_worldName(worldName) +{ +} + +bool LocalGame::start() +{ + m_serverLauncher.runAsThread(); + return ClientGameDef::start(LOCAL_HOST); +} + +void LocalGame::onShutdown() +{ + m_serverLauncher.stop(); +} + +/// Remote game +RemoteGame::RemoteGame(const std::string& ipAddress) + : m_serverIp(ipAddress) +{ +} + +bool RemoteGame::start() +{ + return ClientGameDef::start(m_serverIp); +} + +void RemoteGame::onShutdown() +{ + // empty +} \ No newline at end of file diff --git a/src/client/game/game_type.h b/src/client/game/game_type.h new file mode 100644 index 00000000..b11e55a0 --- /dev/null +++ b/src/client/game/game_type.h @@ -0,0 +1,28 @@ +#pragma once + +#include "game_def.h" +#include + +class LocalGame final : public ClientGameDef { + public: + LocalGame(const std::string& worldName); + + bool start() final override; + + private: + void onShutdown() final override; + + const std::string m_worldName; + ServerEngine m_serverLauncher; +}; + +class RemoteGame final : public ClientGameDef { + public: + RemoteGame(const std::string& ipAddress); + + bool start() final override; + + private: + void onShutdown() final override; + const std::string m_serverIp; +}; \ No newline at end of file diff --git a/src/client/gl/gl_errors.cpp b/src/client/gl/gl_errors.cpp index 2a548948..304fb415 100644 --- a/src/client/gl/gl_errors.cpp +++ b/src/client/gl/gl_errors.cpp @@ -21,8 +21,7 @@ void GLAPIENTRY glDebugCallback(GLenum source, GLenum type, GLuint, GLenum sever break; case GL_DEBUG_SEVERITY_NOTIFICATION: - severity_str = "notification"; - break; + return; } const char* src = "?"; diff --git a/src/client/gui/component/text_component.cpp b/src/client/gui/component/text_component.cpp index 267713f6..14b0b7ea 100644 --- a/src/client/gui/component/text_component.cpp +++ b/src/client/gui/component/text_component.cpp @@ -1,9 +1,9 @@ #include "text_component.h" #include "../../gl/font.h" -#include "../../maths.h" #include "../../renderer/gui_shader.h" #include "../gui_constants.h" +#include namespace { struct Mesh { diff --git a/src/client/gui/overlay.cpp b/src/client/gui/overlay.cpp index a27767c7..578c63f0 100644 --- a/src/client/gui/overlay.cpp +++ b/src/client/gui/overlay.cpp @@ -1,10 +1,10 @@ #include "overlay.h" #include "widget/button_widget.h" +#include "widget/checkbox_widget.h" #include "widget/image_widget.h" #include "widget/label_widget.h" #include "widget/text_box_widget.h" -#include "widget/checkbox_widget.h" #include namespace gui { diff --git a/src/client/gui/overlay.h b/src/client/gui/overlay.h index 05644e19..42caeb06 100644 --- a/src/client/gui/overlay.h +++ b/src/client/gui/overlay.h @@ -21,7 +21,7 @@ namespace gui { */ struct OverlayDefinition final { std::string id; - + sol::function create; }; diff --git a/src/client/gui/widget/checkbox_widget.cpp b/src/client/gui/widget/checkbox_widget.cpp index 105c9c66..36c0e0f7 100644 --- a/src/client/gui/widget/checkbox_widget.cpp +++ b/src/client/gui/widget/checkbox_widget.cpp @@ -6,10 +6,10 @@ namespace gui { - CheckBoxWidget::CheckBoxWidget(RectangleComponent* rectComponent, TextComponent* label) + CheckBoxWidget::CheckBoxWidget(RectangleComponent* rectComponent, + TextComponent* label) : mp_rectangle(rectComponent) , mp_label(label) - , m_checked(false) { componentList.push_back(mp_rectangle); componentList.push_back(mp_label); @@ -115,5 +115,4 @@ namespace gui { return m_checked; } - } // namespace gui diff --git a/src/client/gui/widget/checkbox_widget.h b/src/client/gui/widget/checkbox_widget.h index 5d79dfe3..e99d5e61 100644 --- a/src/client/gui/widget/checkbox_widget.h +++ b/src/client/gui/widget/checkbox_widget.h @@ -6,9 +6,9 @@ namespace gui { - class CheckBoxWidget final : public Widget { + class CheckBoxWidget final : public Widget { public: - CheckBoxWidget(RectangleComponent* mp_rectangle, TextComponent* label); + CheckBoxWidget(RectangleComponent* mp_rectangle, TextComponent* label); void setPosition(const GuiDimension& position) final override; void setSize(const GuiDimension& size) final override; @@ -23,28 +23,26 @@ namespace gui { void setChecked(bool check); void handleClick(sf::Mouse::Button, float, float) final override; - void handleMouseMove(float, float) final override; + void handleMouseMove(float, float) final override; - - void onClick(); - void setOnMouseOver(sol::function function); - void setOnMouseOff(sol::function function); + void onClick(); + void setOnMouseOver(sol::function function); + void setOnMouseOff(sol::function function); void prepareRender() final override; bool getChecked(); - private: - - int checkedTexture; - int uncheckedTexture; + private: + int checkedTexture; + int uncheckedTexture; - bool m_checked; - RectangleComponent* mp_rectangle = nullptr; - TextComponent* mp_label = nullptr; + RectangleComponent* mp_rectangle = nullptr; + TextComponent* mp_label = nullptr; + bool m_checked = false; - sol::function m_onMoveOver; - sol::function m_onMouseOff; - }; + sol::function m_onMoveOver; + sol::function m_onMouseOff; + }; } // namespace gui diff --git a/src/client/lua/client_lua_callback.h b/src/client/lua/client_lua_callback.h index 0bcf58d9..620d9a74 100644 --- a/src/client/lua/client_lua_callback.h +++ b/src/client/lua/client_lua_callback.h @@ -1,7 +1,7 @@ #pragma once -#include #include +#include #include #include #include @@ -25,7 +25,7 @@ class ClientLuaCallbacks { std::vector m_onExitGameCallbacks; std::vector m_onErrorCallbacks; std::vector m_onMouseWheelScroll; - + std::array, sf::Keyboard::KeyCount> m_onKeyReleaseCallbacks; }; \ No newline at end of file diff --git a/src/client/lua/gui_widget_api.cpp b/src/client/lua/gui_widget_api.cpp index ab686851..07d29051 100644 --- a/src/client/lua/gui_widget_api.cpp +++ b/src/client/lua/gui_widget_api.cpp @@ -2,10 +2,10 @@ #include "../gui/overlay.h" #include "../gui/widget/button_widget.h" +#include "../gui/widget/checkbox_widget.h" #include "../gui/widget/image_widget.h" #include "../gui/widget/label_widget.h" #include "../gui/widget/text_box_widget.h" -#include "../gui/widget/checkbox_widget.h" #include namespace { @@ -78,7 +78,8 @@ namespace { checkBoxApi["uncheckedImage"] = sol::property(&gui::CheckBoxWidget::setUncheckedImage); checkBoxApi["image"] = sol::property(&gui::CheckBoxWidget::setImage); - checkBoxApi["checked"] = sol::property(&gui::CheckBoxWidget::getChecked, &gui::CheckBoxWidget::setChecked); + checkBoxApi["checked"] = sol::property(&gui::CheckBoxWidget::getChecked, + &gui::CheckBoxWidget::setChecked); checkBoxApi["onMouseOver"] = sol::property(&gui::CheckBoxWidget::setOnMouseOver); checkBoxApi["onMouseOff"] = sol::property(&gui::CheckBoxWidget::setOnMouseOff); @@ -134,5 +135,4 @@ void luaInitGuiWidgetApi(ScriptEngine& scriptEngine) addGuiTextboxApi(scriptEngine); addGuiCenteredLabelApi(scriptEngine); addGuiCheckBoxApi(scriptEngine); - } diff --git a/src/client/network/client.cpp b/src/client/network/client.cpp new file mode 100644 index 00000000..e295e4a1 --- /dev/null +++ b/src/client/network/client.cpp @@ -0,0 +1,119 @@ +#include "client.h" + +#include +#include +#include +#include +#include +#include + +Client::Client() + : m_salt(createHandshakeRandom()) +{ +} + +Client::~Client() +{ + if (getConnnectionState() == ConnectionState::Connected) { + disconnect(); + } +} + +void Client::setWorld(ClientWorld& world) +{ + mp_world = &world; +} + +ClientConnectionResult Client::connectTo(const std::string& ipaddress) +{ + auto result = + connectEnetClientTo(m_host.handle, m_serverConnection, ipaddress.c_str()); + if (result.success) { + m_connectionState = ConnectionState::Pending; + + sf::Packet handshake; + handshake << ServerCommand::HandshakePartOne << m_salt; + m_serverConnection.send(handshake, 0, ENET_PACKET_FLAG_RELIABLE); + } + return result; +} + +void Client::disconnect() +{ + if (m_connectionState != ConnectionState::Disconnected) { + assert(m_host.handle); + assert(m_serverConnection.peer); + if (disconnectEnetClient(m_host.handle, m_serverConnection)) { + m_connectionState = ConnectionState::Disconnected; + } + } +} + +void Client::tick() +{ + assert(m_serverConnection.peer); + assert(m_host.handle); + NetEvent event; + while (m_host.pumpEvent(event)) { + if (event.type == NetEventType::Data) { + ClientPacket packet(event.packet); + handlePacket(packet); + enet_packet_destroy(event.packet); + } + } +} + +void Client::handlePacket(ClientPacket& packet) +{ + std::cout << "Client got packet: " << (int)packet.command() << std::endl; + using Cmd = ClientCommand; + // clang-format off + switch (packet.command()) { + case Cmd::HandshakeChallenge: onHandshakeChallenge (packet); break; + case Cmd::ConnectionAcceptance: onConnectionAcceptance (packet); break; + case Cmd::ForceExitGame: onForceExit (packet); break; + case Cmd::GameData: onGameData (packet); break; + + case Cmd::AddEntity: onAddEntity (packet); break; + case Cmd::AddChunk: onAddChunk (packet); break; + case Cmd::PlayerSpawnPoint: onPlayerSpawnPoint (packet); break; + case Cmd::RemoveEntity: onRemoveEntity (packet); break; + case Cmd::UpdateEntityStates: onUpdateEntityStates (packet); break; + case Cmd::VoxelUpdate: onVoxelUpdate (packet); break; + + default: std::cout << "Unhandled packet! Command ID: " << (int)packet.command() << '\n'; break; + } + // clang-format on +} + +ConnectionState Client::getConnnectionState() const +{ + return m_connectionState; +} + +void Client::sendInteraction() +{ + ClientPacket packet(ServerCommand::Interaction, m_salt); + m_serverConnection.send(packet.get()); +} + +void Client::sendMouseEvent(MouseEventState state) +{ + ClientPacket packet(ServerCommand::MouseState, m_salt); + packet.write(static_cast(state == MouseEventState::Click ? 0 : 1)); + m_serverConnection.send(packet.get()); +} + +void Client::sendPlayerState(const EntityState& state) +{ + ClientPacket packet(ServerCommand::PlayerState, m_salt); + packet.write(state.position); + packet.write(state.rotation); + m_serverConnection.send(packet.get()); +} + +void Client::sendSpawnRequest() +{ + ClientPacket packet(ServerCommand::SpawnRequest, m_salt); + m_serverConnection.send(packet.get()); +} diff --git a/src/client/network/client.h b/src/client/network/client.h new file mode 100644 index 00000000..0ce4330e --- /dev/null +++ b/src/client/network/client.h @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include +#include + +struct EntityState; +class ClientWorld; + +enum class ConnectionState { + Pending, + Connected, + Disconnected, +}; + +enum class MouseEventState { Click, Release }; + +class Client final { + public: + Client(); + ~Client(); + + void setWorld(ClientWorld& world); + + ClientConnectionResult connectTo(const std::string& ipaddress); + + void tick(); + void disconnect(); + + ConnectionState getConnnectionState() const; + + void sendInteraction(); + void sendMouseEvent(MouseEventState state); + void sendPlayerState(const EntityState& state); + void sendSpawnRequest(); + + private: + void handlePacket(ClientPacket& packet); + + void onHandshakeChallenge(ClientPacket& packet); + void onConnectionAcceptance(ClientPacket& packet); + void onGameData(ClientPacket& packet); + + void onAddEntity(ClientPacket& packet); + void onAddChunk(ClientPacket& packet); + void onForceExit(ClientPacket& packet); + void onPlayerSpawnPoint(ClientPacket& packet); + void onRemoveEntity(ClientPacket& packet); + void onUpdateEntityStates(ClientPacket& packet); + void onVoxelUpdate(ClientPacket& packet); + + ConnectionState m_connectionState = ConnectionState::Disconnected; + Connection m_serverConnection; + NetHost m_host; + + ClientWorld* mp_world = nullptr; + + u32 m_salt; + + public: + NON_COPYABLE(Client) + NON_MOVEABLE(Client) +}; \ No newline at end of file diff --git a/src/client/network/client_commands.cpp b/src/client/network/client_commands.cpp deleted file mode 100644 index 6d4ee02a..00000000 --- a/src/client/network/client_commands.cpp +++ /dev/null @@ -1,209 +0,0 @@ -#include "../client.h" - -#include "../client_config.h" -#include -#include -#include - -void Client::sendPlayerPosition(const glm::vec3& position) -{ - sf::Packet packet; - packet << ServerCommand::PlayerPosition << NetworkHost::getPeerId() << position.x - << position.y << position.z; - NetworkHost::sendToPeer(mp_serverPeer, packet, 0, 0); -} - -void Client::sendVoxelUpdate(const VoxelUpdate& update) -{ - sf::Packet packet; - packet << ServerCommand::VoxelEdit << update.position.x << update.position.y - << update.position.z << update.voxel; - NetworkHost::sendToPeer(mp_serverPeer, packet, 0, 0); -} - -void Client::sendPlayerSkin(const sf::Image& playerSkin) -{ - // Check the image is the right size - if (playerSkin.getSize() != sf::Vector2u(32, 64)) { - LOG("Client", "Player's skin has the wrong dimensions"); - return; - } - - sf::Packet packet; - packet << ServerCommand::PlayerSkin << NetworkHost::getPeerId(); - packet.append(playerSkin.getPixelsPtr(), 8192); - NetworkHost::sendToPeer(mp_serverPeer, packet, 0, ENET_PACKET_FLAG_RELIABLE); -} - -void Client::onPeerConnect([[maybe_unused]] ENetPeer* peer) -{ -} - -void Client::onPeerDisconnect([[maybe_unused]] ENetPeer* peer) -{ -} - -void Client::onPeerTimeout([[maybe_unused]] ENetPeer* peer) -{ -} - -void Client::onCommandRecieve([[maybe_unused]] ENetPeer* peer, sf::Packet& packet, - command_t command) -{ - m_commandDispatcher.execute(*this, command, packet); -} - -void Client::onPlayerJoin(sf::Packet& packet) -{ - peer_id_t id = 0; - packet >> id; - m_entities[id].active = true; - - LOGVAR("Client", "Player joined, client id: ", (int)id); -} - -void Client::onPlayerLeave(sf::Packet& packet) -{ - peer_id_t id = 0; - packet >> id; - m_entities[id].active = false; - - LOGVAR("Client", "Player left, client id: ", (int)id); -} - -void Client::onSnapshot(sf::Packet& packet) -{ - u16 updateEntityCount = 0; - packet >> updateEntityCount; - for (u16 i = 0; i < updateEntityCount; i++) { - peer_id_t id = 0; - float x, y, z; - packet >> id >> x >> y >> z; - if (id != NetworkHost::getPeerId()) { - auto* p = &m_entities[id]; - p->position = {x, y, z}; - p->active = true; - } - } -} - -void Client::onChunkData(sf::Packet& packet) -{ - // Get position of the recieved chunk, and create a chunk - ChunkPosition position; - packet >> position.x >> position.y >> position.z; - - if (!m_chunks.manager.hasChunk(position)) { - - Chunk& chunk = m_chunks.manager.addChunk(position); - - u32 size; - CompressedVoxels compressed; - packet >> size; - for (u32 i = 0; i < size; i++) { - voxel_t type; - u16 count; - packet >> type >> count; - compressed.emplace_back(type, count); - } - - // Uncompress the voxel data - chunk.voxels = decompressVoxelData(compressed); - - // Add to chunk updates - m_chunks.updates.push_back(position); - } -} - -void Client::onSpawnPoint(sf::Packet& packet) -{ - assert(mp_player); - if (mp_player) { - packet >> mp_player->position.x >> mp_player->position.y >> mp_player->position.z; - } -} - -void Client::onVoxelUpdate(sf::Packet& packet) -{ - u16 count = 0; - packet >> count; - for (u32 i = 0; i < count; i++) { - VoxelUpdate voxelUpdate; - packet >> voxelUpdate.position.x >> voxelUpdate.position.y >> - voxelUpdate.position.z >> voxelUpdate.voxel; - m_chunks.voxelUpdates.push_back(voxelUpdate); - } -} - -void Client::onPlayerSkinReceive(sf::Packet& packet) -{ - peer_id_t id = 0; - packet >> id; - - sf::Uint8* skinPixels = - (sf::Uint8*)packet.getData() + sizeof(command_t) + sizeof(peer_id_t); - m_entities[id].playerSkin.create(32, 64, skinPixels); -} - -void Client::onGameRegistryData(sf::Packet& packet) -{ - // ==== - // Get all voxels from the server - // - // Maps tewxture names to their respective IDs in the - // OpenGL texture array - std::unordered_map textureMap; - auto getTexture = [&textureMap, this](const std::string& name) { - auto itr = textureMap.find(name); - if (itr == textureMap.end()) { - auto id = m_voxelTextures.addTexture(name); - textureMap.emplace(name, id); - return id; - } - return itr->second; - }; - - u16 numVoxels; - packet >> numVoxels; - // @TODO - // 1. Need to somehow work out the exact amount of textures needed - // 2. Need to pass in the actual texture pack resolution - m_voxelTextures.create(numVoxels * 3, 16); - - const std::string texturePath = - "texture_packs/" + ClientConfig::get().texturePack + "/voxels/"; - for (u16 i = 0; i < numVoxels; i++) { - std::string name; - std::string textureTop; - std::string textureSide; - std::string textureBottom; - - u8 meshStyle = 0; - u8 type = 0; - u8 isCollidable = 0; - - packet >> name; - packet >> textureTop; - packet >> textureSide; - packet >> textureBottom; - packet >> meshStyle; - packet >> type; - packet >> isCollidable; - - VoxelData voxelData; - voxelData.name = name; - voxelData.topTextureId = getTexture(texturePath + textureTop); - voxelData.sideTextureId = getTexture(texturePath + textureSide); - voxelData.bottomTextureId = getTexture(texturePath + textureBottom); - - voxelData.meshStyle = static_cast(meshStyle); - voxelData.type = static_cast(type); - voxelData.isCollidable = isCollidable; - - m_voxelData.addVoxelData(voxelData); - } - - m_voxelData.initCommonVoxelTypes(); - - m_hasReceivedGameData = true; -} \ No newline at end of file diff --git a/src/client/network/client_packet_handling.cpp b/src/client/network/client_packet_handling.cpp new file mode 100644 index 00000000..0b9bf40f --- /dev/null +++ b/src/client/network/client_packet_handling.cpp @@ -0,0 +1,144 @@ +#include "client.h" + +#include "../game/client_world.h" +#include + +void Client::onHandshakeChallenge(ClientPacket& packet) +{ + u32 salt = packet.read(); + u32 newSalt = m_salt ^ salt; + m_salt = newSalt; + ClientPacket response(ServerCommand::HandshakeResponse, m_salt); + m_serverConnection.send(response.get()); + std::cout << "Challenging\n"; +} + +void Client::onConnectionAcceptance(ClientPacket& packet) +{ + u8 isAccepted = packet.read(); + if (isAccepted) { + std::cout << "Connected!\n"; + m_connectionState = ConnectionState::Connected; + + // For certain unit tests, the world doesn't exist + if (!mp_world) { + return; + } + + u32 playerId = packet.read(); + mp_world->setPlayerId(playerId); + + sendSpawnRequest(); + } + else { + std::string reason = packet.read(); + std::cout << "Rejected!\n" << reason << std::endl; + m_connectionState = ConnectionState::Disconnected; + } +} + +void Client::onGameData(ClientPacket& packet) +{ + u16 voxels = packet.read(); + + mp_world->setVoxelTextureCount(voxels); + + for (u16 i = 0; i < voxels; i++) { + auto voxel = packet.read(); + mp_world->addVoxelType(std::move(voxel)); + } + mp_world->initialiseCommonVoxels(); + + u32 count = packet.read(); + for (u32 i = 0; i < count; i++) { + u32 id = packet.read(); + auto position = packet.read(); + auto rotation = packet.read(); + mp_world->addEntity(id, position, rotation); + } +} + +void Client::onAddEntity(ClientPacket& packet) +{ + u32 count = packet.read(); + for (u32 i = 0; i < count; i++) { + u32 entityId = 0; + glm::vec3 position; + glm::vec3 rotation; + + packet.read(entityId); + packet.read(position); + packet.read(rotation); + + if (!mp_world) { + return; + } + mp_world->addEntity(entityId, position, rotation); + } +} + +void Client::onAddChunk(ClientPacket& packet) +{ + ChunkPosition position; + position.x = packet.read(); + position.y = packet.read(); + position.z = packet.read(); + + if (!mp_world->hasChunk(position)) { + CompressedVoxels voxels; + u32 blockCount = packet.read(); + for (u32 i = 0; i < blockCount; i++) { + voxel_t type = packet.read(); + u16 count = packet.read(); + voxels.emplace_back(type, count); + } + // For certain unit tests, the world doesn't exist + if (!mp_world) { + return; + } + mp_world->createChunkFromCompressed(position, voxels); + } +} + +void Client::onRemoveEntity(ClientPacket& packet) +{ + u32 entityId = packet.read(); + mp_world->removeEntity(entityId); +} + +void Client::onForceExit(ClientPacket& packet) +{ + m_connectionState = ConnectionState::Disconnected; + auto reason = packet.read(); +} + +void Client::onPlayerSpawnPoint(ClientPacket& packet) +{ + glm::vec3 position = packet.read(); + mp_world->getPlayer().position = position; +} + +void Client::onUpdateEntityStates(ClientPacket& packet) +{ + u32 count = packet.read(); + for (u32 i = 0; i < count; i++) { + auto entId = packet.read(); + auto entPosition = packet.read(); + auto entRotation = packet.read(); + if (entId == mp_world->getPlayerId()) { + continue; + } + mp_world->updateEntity(entId, entPosition, entRotation); + } +} + +void Client::onVoxelUpdate(ClientPacket& packet) +{ + VoxelUpdate update; + packet.read(update.voxelPosition.x); + packet.read(update.voxelPosition.y); + packet.read(update.voxelPosition.z); + packet.read(update.voxel); + + mp_world->updateVoxel(update); +} diff --git a/src/client/renderer/camera.cpp b/src/client/renderer/camera.cpp new file mode 100644 index 00000000..bc1a354e --- /dev/null +++ b/src/client/renderer/camera.cpp @@ -0,0 +1,40 @@ +#include "camera.h" + +#include "../client_config.h" +#include + +Camera Camera::createCamera() +{ + Camera camera; + float width = ClientConfig::get().windowWidth; + float height = ClientConfig::get().windowHeight; + camera.m_projectionMatrix = + glm::perspective(3.14f / 2.0f, width / height, 0.01f, 2000.0f); + return camera; +} + +void Camera::update(const EntityState& entity) +{ + m_frustum.update(m_projectionViewMatrix); + + m_position = entity.position; + m_rotation = entity.rotation; + + m_projectionViewMatrix = + createProjectionViewMatrix(m_position, m_rotation, m_projectionMatrix); +} + +const ViewFrustum& Camera::getFrustum() const +{ + return m_frustum; +} + +const glm::mat4& Camera::getProjectionView() const +{ + return m_projectionViewMatrix; +} + +const glm::vec3& Camera::getPosition() const +{ + return m_position; +} diff --git a/src/client/renderer/camera.h b/src/client/renderer/camera.h new file mode 100644 index 00000000..1567d297 --- /dev/null +++ b/src/client/renderer/camera.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include + +class Camera { + public: + static Camera createCamera(); + void update(const EntityState& entity); + + const ViewFrustum& getFrustum() const; + const glm::mat4& getProjectionView() const; + const glm::vec3& getPosition() const; + + private: + glm::mat4 m_projectionMatrix{1.0f}; + glm::mat4 m_projectionViewMatrix{1.0f}; + glm::vec3 m_position{1.0f}; + glm::vec3 m_rotation{1.0f}; + + ViewFrustum m_frustum; +}; \ No newline at end of file diff --git a/src/client/renderer/chunk_renderer.cpp b/src/client/renderer/chunk_renderer.cpp index 816530a1..c7a56312 100644 --- a/src/client/renderer/chunk_renderer.cpp +++ b/src/client/renderer/chunk_renderer.cpp @@ -2,6 +2,7 @@ #include "../client_config.h" #include "../gl/gl_errors.h" +#include "camera.h" #include #include @@ -116,10 +117,7 @@ void ChunkRenderer::updateMesh(const ChunkPosition& position, deleteChunkRenderables(position); } -ChunkRenderResult ChunkRenderer::renderChunks(const glm::vec3& cameraPosition, - const ViewFrustum& frustum, - const glm::mat4& projectionViewMatrix, - bool cameraInWater) +ChunkRenderResult ChunkRenderer::renderChunks(const Camera& camera, bool cameraInWater) { auto& solidDrawables = m_chunkRenderables[static_cast(ChunkRenderType::Solid)]; auto& fluidDrawables = m_chunkRenderables[static_cast(ChunkRenderType::Fluid)]; @@ -132,17 +130,19 @@ ChunkRenderResult ChunkRenderer::renderChunks(const glm::vec3& cameraPosition, } m_chunkMeshes.clear(); + const glm::mat4& pv = camera.getProjectionView(); + const glm::vec3& pos = camera.getPosition(); + const ViewFrustum& frustum = camera.getFrustum(); + ChunkRenderResult result; // Solid voxels m_shader.program.bind(); - gl::loadUniform(m_shader.projectionViewLocation, projectionViewMatrix); - ::renderChunks(solidDrawables, frustum, m_shader.chunkPositionLocation, result, - cameraPosition); + gl::loadUniform(m_shader.projectionViewLocation, pv); + ::renderChunks(solidDrawables, frustum, m_shader.chunkPositionLocation, result, pos); // Flora voxels glDisable(GL_CULL_FACE); - ::renderChunks(floraDrawables, frustum, m_shader.chunkPositionLocation, result, - cameraPosition); + ::renderChunks(floraDrawables, frustum, m_shader.chunkPositionLocation, result, pos); glEnable(GL_CULL_FACE); // Fluid voxels @@ -150,8 +150,7 @@ ChunkRenderResult ChunkRenderer::renderChunks(const glm::vec3& cameraPosition, if (cameraInWater) { glCheck(glCullFace(GL_FRONT)); } - ::renderChunks(fluidDrawables, frustum, m_shader.chunkPositionLocation, result, - cameraPosition); + ::renderChunks(fluidDrawables, frustum, m_shader.chunkPositionLocation, result, pos); glCheck(glCullFace(GL_BACK)); glCheck(glDisable(GL_BLEND)); diff --git a/src/client/renderer/chunk_renderer.h b/src/client/renderer/chunk_renderer.h index 6568e4bf..ca51621e 100644 --- a/src/client/renderer/chunk_renderer.h +++ b/src/client/renderer/chunk_renderer.h @@ -1,14 +1,16 @@ #pragma once +#include "../game/chunk_mesh.h" #include "../gl/shader.h" #include "../gl/vertex_array.h" -#include "../maths.h" -#include "../world/chunk_mesh.h" +#include #include #include #include +class Camera; + /** * @brief A mesh that makes up the verticies and such of a chunk in the world */ @@ -73,18 +75,13 @@ class ChunkRenderer final { /** * @brief Render all chunks (that are in view) * - * @param cameraPosition The position of the camera - * @param frustum The viewing frustum, for frustum culling - * @param projectionViewMatrix Projection view matrix for the shaders + * @param camera The camera to render the chunks from * @param cameraInWater Is the camera in water, for rendering special effects if the * case * @return ChunkRenderResult The count of chunks rendered and their total buffer size * this frame */ - ChunkRenderResult renderChunks(const glm::vec3& cameraPosition, - const ViewFrustum& frustum, - const glm::mat4& projectionViewMatrix, - bool cameraInWater); + ChunkRenderResult renderChunks(const Camera& camera, bool cameraInWater); // Used for the debug stat view int getTotalChunks() const; diff --git a/src/client/window.cpp b/src/client/window.cpp index 41da4cca..fa0e0ba4 100644 --- a/src/client/window.cpp +++ b/src/client/window.cpp @@ -6,6 +6,8 @@ #include #include +const sf::Window* Window::context = nullptr; + namespace { bool initOpenGL(const sf::Window& window) @@ -44,8 +46,9 @@ namespace { } // namespace -bool createWindowInitOpengl(sf::Window& window) +bool Window::createWindowInitOpengl(sf::Window& window) { + context = &window; window.setKeyRepeatEnabled(false); if (ClientConfig::get().fullScreen) { createWindow(window, sf::VideoMode::getDesktopMode(), sf::Style::Fullscreen); @@ -62,7 +65,7 @@ bool createWindowInitOpengl(sf::Window& window) return initOpenGL(window); } -float getWindowAspect(const sf::Window& window) +float Window::getWindowAspect(const sf::Window& window) { return static_cast(window.getSize().x) / static_cast(window.getSize().y); diff --git a/src/client/window.h b/src/client/window.h index 47a320f0..9b577609 100644 --- a/src/client/window.h +++ b/src/client/window.h @@ -5,5 +5,8 @@ #include #include -bool createWindowInitOpengl(sf::Window& window); -float getWindowAspect(const sf::Window& window); +struct Window { + static const sf::Window* context; + static bool createWindowInitOpengl(sf::Window& window); + static float getWindowAspect(const sf::Window& window); +}; diff --git a/src/common/common/CMakeLists.txt b/src/common/common/CMakeLists.txt index f47fd0cd..fc7857f2 100644 --- a/src/common/common/CMakeLists.txt +++ b/src/common/common/CMakeLists.txt @@ -2,11 +2,18 @@ add_library(ob-common util/file_io.cpp util/obd_parser.cpp + util/random_number_generator.cpp + + maths.cpp debug.cpp - network/net_host.cpp + + network/enet.cpp + network/packet.cpp + lua/script_engine.cpp lua/util_api.cpp - util/random_number_generator.cpp + + world/chunk.cpp world/chunk_manager.cpp world/coordinate.cpp diff --git a/src/client/maths.cpp b/src/common/common/maths.cpp similarity index 99% rename from src/client/maths.cpp rename to src/common/common/maths.cpp index 1aefa669..36f0fc63 100644 --- a/src/client/maths.cpp +++ b/src/common/common/maths.cpp @@ -1,6 +1,6 @@ #include "maths.h" -#include +#include "world/world_constants.h" // =============================================== // diff --git a/src/client/maths.h b/src/common/common/maths.h similarity index 98% rename from src/client/maths.h rename to src/common/common/maths.h index fed62620..6099feb5 100644 --- a/src/client/maths.h +++ b/src/common/common/maths.h @@ -2,9 +2,9 @@ #include +#include "world/coordinate.h" #include #include -#include #include glm::mat4 createProjectionViewMatrix(const glm::vec3& position, const glm::vec3& rotation, diff --git a/src/common/common/network/command_dispatcher.h b/src/common/common/network/command_dispatcher.h deleted file mode 100644 index 9b67befa..00000000 --- a/src/common/common/network/command_dispatcher.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include "net_types.h" -#include -#include - -template -class CommandDispatcher { - public: - using CommandHandler = void (Handler::*)(sf::Packet& packet); - - CommandDispatcher() - : m_commands(static_cast(CommandType::COUNT)) - { - } - - void addCommand(CommandType command, CommandHandler handler) - { - m_commands[static_cast(command)] = handler; - } - - void execute(Handler& handler, command_t command, sf::Packet& packet) - { - (handler.*(m_commands[command]))(packet); - } - - private: - std::vector m_commands; -}; \ No newline at end of file diff --git a/src/common/common/network/enet.cpp b/src/common/common/network/enet.cpp new file mode 100644 index 00000000..554aaefd --- /dev/null +++ b/src/common/common/network/enet.cpp @@ -0,0 +1,152 @@ +#include "enet.h" + +#include +#include +#include +#include + +#include "net_constants.h" + +const ClientConnectionResult ClientConnectionResult::SUCCESS; + +namespace { + ENetPacket* createPacket(const sf::Packet& packet, u32 flags) + { + return enet_packet_create(packet.getData(), packet.getDataSize(), flags); + } +} // namespace + +ClientConnectionResult::ClientConnectionResult(const char* msg) + : message(msg) + , success(false) +{ +} + +/** + Connection functions +*/ +void Connection::send(const sf::Packet& packet, int channel, u32 flags) +{ + assert(peer); + auto enetPacket = createPacket(packet, flags); + enet_peer_send(peer, channel, enetPacket); +} + +NetHost::NetHost() +{ + handle = enet_host_create(nullptr, 1, 2, 0, 0); +} + +/** + Net Host functions +*/ +NetHost::NetHost(const ENetAddress& address, int maximumConnections) noexcept +{ + handle = enet_host_create(&address, maximumConnections, 2, 0, 0); + if (!handle) { + std::cout << "Failed to create server\n"; + } +} + +NetHost::~NetHost() noexcept +{ + if (handle) { + enet_host_destroy(handle); + } +} + +NetHost::NetHost(NetHost&& other) noexcept + : handle(other.handle) +{ + enet_host_destroy(other.handle); + other.handle = nullptr; +} + +NetHost& NetHost::operator=(NetHost&& other) noexcept +{ + enet_host_destroy(other.handle); + other.handle = nullptr; + return *this; +} + +bool NetHost::pumpEvent(NetEvent& event) +{ + if (enet_host_service(handle, &event.data, 0)) { + event.type = static_cast(event.data.type); + event.packet = event.data.packet; + event.peer = event.data.peer; + return true; + } + return false; +} + +/** + Network helper functions +*/ +ClientConnectionResult connectEnetClientTo(ENetHost* host, Connection& serverConnection, + const char* ipAddress) +{ + if (!host) { + return "Failed to create the host."; + } + // Create address for the client to connect to + ENetAddress address{}; + address.port = DEFAULT_PORT; + if (enet_address_set_host(&address, ipAddress) != 0) { + return "Failed to create address."; + } + + // Connect to the server + serverConnection.peer = enet_host_connect(host, &address, 2, 0); + if (!serverConnection.peer) { + return "Failed to connect to the server."; + } + + // Wait for a connection establishment + bool connected = [&host] { + ENetEvent event; + while (enet_host_service(host, &event, 2000) > 0) { + if (event.type == ENET_EVENT_TYPE_RECEIVE) { + enet_packet_destroy(event.packet); + } + else if (event.type == ENET_EVENT_TYPE_CONNECT) { + return true; + } + } + return false; + }(); + if (!connected) { + return "Failed to establish connection with the server."; + } + return ClientConnectionResult::SUCCESS; +} + +bool disconnectEnetClient(ENetHost* host, Connection& serverConnection) +{ + enet_peer_disconnect(serverConnection.peer, 0); + ENetEvent event; + while (enet_host_service(host, &event, 2000) > 0) { + if (event.type == ENET_EVENT_TYPE_RECEIVE) { + enet_packet_destroy(event.packet); + } + else if (event.type == ENET_EVENT_TYPE_DISCONNECT) { + enet_host_flush(host); + return true; + } + } + enet_peer_reset(serverConnection.peer); + return false; +} + +void broadcastToPeers(ENetHost* host, const sf::Packet& packet, u8 channel, u32 flags) +{ + auto enetpacket = createPacket(packet, flags); + enet_host_broadcast(host, channel, enetpacket); +} + +u32 createHandshakeRandom() +{ + std::mt19937 rng(static_cast(std::time(nullptr))); + std::uniform_int_distribution dist(0, 4294967290); + return dist(rng); +} \ No newline at end of file diff --git a/src/common/common/network/enet.h b/src/common/common/network/enet.h index 8df23fcd..aec7dc18 100644 --- a/src/common/common/network/enet.h +++ b/src/common/common/network/enet.h @@ -1,4 +1,64 @@ #pragma once #define _WINSOCK_DEPRECATED_NO_WARNINGS -#include \ No newline at end of file +#include + +#include "../macros.h" +#include "../types.h" +#include +#include + +struct ClientConnectionResult { + ClientConnectionResult() = default; + ClientConnectionResult(const char* message); + + const char* message = nullptr; + bool success = true; + + const static ClientConnectionResult SUCCESS; +}; + +struct Connection { + ENetPeer* peer = nullptr; + u32 salt = 0; + + void send(const sf::Packet& packet, int channel = 0, u32 flags = 0); +}; + +enum class NetEventType { + None = ENET_EVENT_TYPE_NONE, + Data = ENET_EVENT_TYPE_RECEIVE, + Connection = ENET_EVENT_TYPE_CONNECT, + Disconnect = ENET_EVENT_TYPE_DISCONNECT, + Timeout = ENET_EVENT_TYPE_DISCONNECT_TIMEOUT, +}; + +struct NetEvent { + ENetEvent data; + NetEventType type; + + ENetPacket* packet = nullptr; + ENetPeer* peer = nullptr; +}; + +struct NetHost { + ENetHost* handle = nullptr; + + NetHost(); + NetHost(const ENetAddress& address, int maximumConnections) noexcept; + ~NetHost() noexcept; + + NetHost(NetHost&& other) noexcept; + NetHost& operator=(NetHost&& other) noexcept; + + bool pumpEvent(NetEvent& event); + + NON_COPYABLE(NetHost) +}; + +ClientConnectionResult connectEnetClientTo(ENetHost* host, Connection& serverConnection, + const char* ipAddress); +bool disconnectEnetClient(ENetHost* host, Connection& serverConnection); +void broadcastToPeers(ENetHost* host, const sf::Packet& packet, u8 channel = 0, + u32 flags = 0); +u32 createHandshakeRandom(); diff --git a/src/common/common/network/net_command.h b/src/common/common/network/net_command.h index 32394468..00ee4844 100644 --- a/src/common/common/network/net_command.h +++ b/src/common/common/network/net_command.h @@ -7,96 +7,120 @@ Commands to be sent to the client */ enum class ClientCommand : command_t { - // Send peer ID to a new connection + // Send handshake response to the client // Data: - // peer_id_t: The ID of this client - PeerId, - - // Notify client that a player has joined the server + // u32: The random number sent from the client + // u32: The server's random number + HandshakeChallenge, + + /* + Sends either a connection rejection or accept to a joining client + about the game Data: u8: 0 for reject, 1 for accept [If connection rejected, then + it sends over a reason] string: Reason for connection rejection + + [Else If connection accepted, + u32: The player ID of this player + */ + ConnectionAcceptance, + + // Data about the game itself + // - Voxel Data - + // u16: The number of different voxel types (VoxelCount) + // [Loop this VoxelCount times] + // String: name + // String: the voxel's top texture + // String: the voxel's side texture + // String: the voxel's bottom texture + // u8: The voxels mesh style aka VoxelMeshStyle + // u8: The voxels state/type aka VoxelType + // u8: Whether the voxel is collidable or not // + // - Active Entity Data - + // u32: The active entity count (EntityCount) + // [Loop this EntityCount times] + // u32: The ID of the entity + // float[3]: The position of the entity + // float[3]: The rotation of the entity + GameData, + + // Forces the user to exit the game // Data: - // peer_id_t: The ID of the client that has joined - PlayerJoin, + // string: reason + ForceExitGame, - // Notify client that a player has left the server + // Sends data about a player joining the game // Data: - // peer_id_t: The ID of the client that has joined - PlayerLeave, - - // Snapshot of the current "world state" of the entities + // u32: The number of entities to add + // [Loop this] + // u32: The ID of the entity + // float[3]: The position of the entity + // float[3]: The rotation of the entity + AddEntity, + + // Sends info that a player has left the game // Data: - // u16: The number of entitites - // [For each entity...] - // peer_id_t: The ID of this entity - // float[3]: The X, Y, Z position of the entity - Snapshot, + // u32: The ID of the entity + RemoveEntity, + + // Update entities states (position and rotation) + // u32: The number of entities to update + // [Loop this] + // u32: The ID of the entity + // float[3]: The position of the entity + // float[3]: The rotation of the entity + UpdateEntityStates, // The voxel data of a chunk // Data: // i32[3] The position of the chunk // voxel[CHUNK_VOLUME] The voxel data - ChunkData, + AddChunk, // Position for player when they spawn // Data: // float[3]: The X, Y, Z position of the entity - SpawnPoint, + PlayerSpawnPoint, - // Command to say that a voxel was updated + // Notify the client that a block has changed, and a chunk needs to be rebuilt // Data: - // u32: Number of voxel updates - // [For each voxel update...] - // i32[3]: The X, Y, Z position of the voxel edit - // voxel_t: The voxel it has been changed to + // i32[3]: The X, Y, Z of the global voxel position + // voxel_t: The voxel to update it to VoxelUpdate, - - // Command to update the skin of a player in-game - // Data: - // peer_id_t: The ID of the player - // u8[8192]: Skin data (RGBA8 format) - NewPlayerSkin, - - // The data needed for the voxels, entities etc - // The client will not process data/render anything until this has been - // recieved - // Data: - // u16: Number of voxel types - // [For each voxel type (Sent in order of Voxel ID)...] - // String: name - // String: the voxel's top texture - // String: the voxel's side texture - // String: the voxel's bottom texture - // u8: The voxels mesh style aka VoxelMeshStyle - // u8: The voxels state/type aka VoxelType - // u8: Whether the voxel is collidable or not - GameRegistryData, - - // For getting the number of commands, used by CommandDispatcher - COUNT, }; /** Commands to be sent to server */ enum class ServerCommand : command_t { - // Command to tell server the position of a player + // Sends a random number to the server + // Data: + // u32: The random number + HandshakePartOne, + + // Sends a random number to the server // Data: - // peer_id_t: The player which position is being sent - // float[3]: The x, y, z position of the player - PlayerPosition, + // u32: The random number, combined with the server random using ^ + HandshakeResponse, - // Command to say that a voxel was edited + // Current position of the player, // Data: - // i32[3]: The X, Y, Z position of the voxel edit - // voxel_t: The voxel it has been changed to - VoxelEdit, + // float[3]: The current position of the player + // float[3]: The current rotation of the player + PlayerState, - // Command that sends the player's skin + // A request to find a spawn point for the player // Data: - // u8[8192]: Imaga Data in RGBA format (Should be 8kb) - PlayerSkin, + // None, the player is worked out from the enet peer send + SpawnRequest, - // For getting the number of commands, used by CommandDispatcher - COUNT, + // Sends a player's mouse state for left clicking. (Eg for time based click events) + // Data: + // u8: 0 for click, 1 for release + MouseState, + + // Sends a player interaction. This is when they right-click + // Data: + // ??? + Interaction, }; template diff --git a/src/common/common/network/net_host.cpp b/src/common/common/network/net_host.cpp deleted file mode 100644 index ffed3fa8..00000000 --- a/src/common/common/network/net_host.cpp +++ /dev/null @@ -1,224 +0,0 @@ -#include "net_host.h" - -#include "../debug.h" -#include "net_constants.h" - -#include -#include - -namespace { - ENetHost* createHost(const ENetAddress* address, int connections) - { - return enet_host_create(address, connections, 2, 0, 0); - } - - ENetPeer* connectHostTo(ENetHost* host, const std::string& ip) - { - ENetAddress address{}; - address.port = DEFAULT_PORT; - if (enet_address_set_host(&address, ip.c_str()) != 0) { - LOG("Connection", "Failed to create address."); - return nullptr; - } - - ENetPeer* peer = enet_host_connect(host, &address, 2, 0); - if (!peer) { - LOG("Connection", "Failed to connect to server (Game Full)."); - return nullptr; - } - return peer; - } - - int getPeerIdFromServer(ENetHost* host) - { - int id = -1; - sf::Clock test; - ENetEvent event; - while (test.getElapsedTime().asSeconds() < 2.0f) { - enet_host_service(host, &event, 0); - if (event.type == ENET_EVENT_TYPE_RECEIVE) { - ClientCommand command; - sf::Packet packet; - packet.append(event.packet->data, event.packet->dataLength); - packet >> command; - if (command == ClientCommand::PeerId) { - peer_id_t peerId; - packet >> peerId; - id = peerId; - break; - } - } - } - return id; - } - - ENetPacket* createPacket(sf::Packet& packet, u32 flags) - { - return enet_packet_create(packet.getData(), packet.getDataSize(), flags); - } -} // namespace - -NetworkHost::NetworkHost(std::string&& name) - : m_name(std::move(name)) -{ -} - -NetworkHost::~NetworkHost() -{ - destroy(); -} - -void NetworkHost::destroy() -{ - if (mp_host) { - enet_host_destroy(mp_host); - mp_host = nullptr; - } -} - -std::optional NetworkHost::createAsClient(const std::string& ip) -{ - mp_host = createHost(0, 1); - if (!mp_host) { - LOG(m_name.c_str(), "Error: Failed to create host."); - return {}; - } - - auto server = connectHostTo(mp_host, ip); - - if (!server) { - LOG(m_name.c_str(), "Error: Failed to connect to server."); - return {}; - } - enet_host_flush(mp_host); - - int id = getPeerIdFromServer(mp_host); - if (id == -1) { - LOG(m_name.c_str(), "Error: Peer ID was not received from server."); - return {}; - } - m_peerId = static_cast(id); - return server; -} - -bool NetworkHost::createAsServer(int maxConnections) -{ - m_maxConnections = maxConnections; - ENetAddress address{}; - address.host = ENET_HOST_ANY; - address.port = DEFAULT_PORT; - mp_host = createHost(&address, maxConnections); - return mp_host; -} - -void NetworkHost::disconnectFromPeer(ENetPeer* peer) -{ - enet_peer_disconnect(peer, static_cast(m_peerId)); - ENetEvent event; - while (enet_host_service(mp_host, &event, 3000) > 0) { - switch (event.type) { - case ENET_EVENT_TYPE_RECEIVE: - enet_packet_destroy(event.packet); - break; - - case ENET_EVENT_TYPE_DISCONNECT: - enet_host_flush(mp_host); - return; - - default: - break; - } - } - enet_peer_reset(peer); -} - -void NetworkHost::disconnectAllPeers() -{ - int maxConnection = getConnectedPeerCount(); - int connections = maxConnection; - for (int i = 0; i < maxConnection; i++) { - enet_peer_disconnect(&mp_host->peers[i], 0); - } - - ENetEvent event; - LOG(m_name.c_str(), "Allowing 1 second to disconnect all peers.") - while (enet_host_service(mp_host, &event, 1000) > 0 && connections > 0) { - switch (event.type) { - case ENET_EVENT_TYPE_RECEIVE: - enet_packet_destroy(event.packet); - break; - - case ENET_EVENT_TYPE_DISCONNECT: - connections--; - break; - - default: - break; - } - } -} - -int NetworkHost::getConnectedPeerCount() const -{ - return mp_host->connectedPeers; -} - -peer_id_t NetworkHost::getPeerId() const -{ - return m_peerId; -} - -int NetworkHost::getMaxConnections() const -{ - return m_maxConnections; -} - -void NetworkHost::sendToPeer(ENetPeer* peer, sf::Packet& packet, u8 channel, u32 flags) -{ - ENetPacket* pkt = createPacket(packet, flags); - enet_peer_send(peer, channel, pkt); -} - -void NetworkHost::broadcastToPeers(sf::Packet& packet, u8 channel, u32 flags) -{ - ENetPacket* pkt = createPacket(packet, flags); - enet_host_broadcast(mp_host, channel, pkt); -} - -void NetworkHost::tick() -{ - assert(mp_host); - ENetEvent event; - while (enet_host_service(mp_host, &event, 0) > 0) { - switch (event.type) { - case ENET_EVENT_TYPE_CONNECT: - onPeerConnect(event.peer); - break; - - case ENET_EVENT_TYPE_RECEIVE: - onCommandRecieve(event.peer, *event.packet); - enet_packet_destroy(event.packet); - break; - - case ENET_EVENT_TYPE_DISCONNECT: - onPeerDisconnect(event.peer); - break; - - case ENET_EVENT_TYPE_DISCONNECT_TIMEOUT: - onPeerTimeout(event.peer); - break; - - case ENET_EVENT_TYPE_NONE: - break; - } - } -} - -void NetworkHost::onCommandRecieve(ENetPeer* peer, const ENetPacket& enetPacket) -{ - sf::Packet packet; - packet.append(enetPacket.data, enetPacket.dataLength); - command_t command; - packet >> command; - onCommandRecieve(peer, packet, command); -} diff --git a/src/common/common/network/net_host.h b/src/common/common/network/net_host.h deleted file mode 100644 index ae8d4416..00000000 --- a/src/common/common/network/net_host.h +++ /dev/null @@ -1,107 +0,0 @@ -#pragma once - -#include "../macros.h" -#include "enet.h" -#include "net_command.h" -#include "net_constants.h" -#include -#include -#include -#include -#include - -#include - -/** - * @brief Base class for network hosts (clients/ servers) - */ -class NetworkHost { - NON_COPYABLE(NetworkHost) - NON_MOVEABLE(NetworkHost) - - public: - NetworkHost(std::string&& name); - - virtual ~NetworkHost(); - - void destroy(); - - /** - * @brief Does 1 tick of the host, must be called once per frame. - * The function will do all the receving and handling of connections and - * commands - */ - void tick(); - - /** - * @brief Creates as a client, and connects the host to a server - * - * @param ip The IP address of the server - * @param timeout How long the client waits for a connection before closing - * @return std::optional The server peer, might not be - * successful connection hence optional - */ - std::optional createAsClient(const std::string& ip); - - /** - * @brief Sets up the host to be a server - * - * @param maxConnections The maximum number connections to allow - * @return true - * @return false - */ - bool createAsServer(int maxConnections); - - /** - * @brief Sends a disconnect request to the peer - * - * @param peer The peer to disconnect from - */ - void disconnectFromPeer(ENetPeer* peer); - - /** - * @brief Disconnects all peers from this host - */ - void disconnectAllPeers(); - - int getConnectedPeerCount() const; - peer_id_t getPeerId() const; - int getMaxConnections() const; - - protected: - /** - * @brief Send a packet to a peer - * - * @param peer The peer to send the packet to - * @param packet The packet to send - * @param channel The channel ID to send the packet on - * @param flags Flags for the packet (ENetPacketFlag) - * @return true The packet was sent successfully - * @return false The packet was not sent - */ - void sendToPeer(ENetPeer* peer, sf::Packet& packet, u8 channel, u32 flags); - - /** - * @brief Broadcasts a packet to all connected peers - * - * @param packet The packet to broadcast - * @param channel The channel ID to send the packet on - * @param flags Flags for the packet (ENetPacketFlag) - */ - void broadcastToPeers(sf::Packet& packet, u8 channel, u32 flags); - - private: - virtual void onPeerConnect(ENetPeer* peer) = 0; - virtual void onPeerDisconnect(ENetPeer* peer) = 0; - virtual void onPeerTimeout(ENetPeer* peer) = 0; - virtual void onCommandRecieve(ENetPeer* peer, sf::Packet& packet, - command_t command) = 0; - - void onCommandRecieve(ENetPeer* peer, const ENetPacket& packet); - - ENetHost* mp_host = nullptr; - const std::string m_name; - - peer_id_t m_peerId = 0; - int m_maxConnections = 0; -}; diff --git a/src/common/common/network/packet.cpp b/src/common/common/network/packet.cpp new file mode 100644 index 00000000..d7f94d0e --- /dev/null +++ b/src/common/common/network/packet.cpp @@ -0,0 +1,52 @@ +#include "packet.h" + +#include "../world/voxel_data.h" + +sf::Packet& operator>>(sf::Packet& packet, glm::vec3& vect) +{ + packet >> vect.x >> vect.y >> vect.z; + return packet; +} + +sf::Packet& operator<<(sf::Packet& packet, const glm::vec3& vect) +{ + packet << vect.x << vect.y << vect.z; + return packet; +} + +sf::Packet& operator>>(sf::Packet& packet, VoxelData& voxel) +{ + u8 meshStyle = 0; + u8 type = 0; + u8 isCollidable = 0; + + packet >> voxel.name; + packet >> voxel.topTexture; + packet >> voxel.sideTexture; + packet >> voxel.bottomTexture; + packet >> meshStyle; + packet >> type; + packet >> isCollidable; + + voxel.meshStyle = static_cast(meshStyle); + voxel.type = static_cast(type); + voxel.isCollidable = isCollidable; + + return packet; +} + +sf::Packet& operator<<(sf::Packet& packet, const VoxelData& voxel) +{ + u8 mesh = static_cast(voxel.meshStyle); + u8 type = static_cast(voxel.type); + u8 isCollidable = static_cast(voxel.isCollidable); + packet << voxel.name; + packet << voxel.topTexture; + packet << voxel.sideTexture; + packet << voxel.bottomTexture; + packet << mesh; + packet << type; + packet << isCollidable; + + return packet; +} diff --git a/src/common/common/network/packet.h b/src/common/common/network/packet.h new file mode 100644 index 00000000..f11166fe --- /dev/null +++ b/src/common/common/network/packet.h @@ -0,0 +1,88 @@ +#pragma once + +#include "enet.h" +#include "net_command.h" + +#include + +struct VoxelData; + +sf::Packet& operator>>(sf::Packet& packet, glm::vec3& vect); +sf::Packet& operator<<(sf::Packet& packet, const glm::vec3& vect); + +sf::Packet& operator>>(sf::Packet& packet, VoxelData& voxelData); +sf::Packet& operator<<(sf::Packet& packet, const VoxelData& voxelData); + +template +class Packet { + public: + Packet(ENetPacket* packet); + + Packet(Outgoing command, u32 salt); + + template + Packet& write(const T& t) + { + m_payload << t; + return *this; + } + + template + Packet& read(T& t) + { + m_payload >> t; + return *this; + } + + template + T read() + { + T t; + m_payload >> t; + return t; + } + + const sf::Packet& get() const; + Incoming command() const; + u32 getSalt() const; + + private: + Incoming m_command; + u32 m_salt = 0; + sf::Packet m_payload; +}; + +using ServerPacket = Packet; +using ClientPacket = Packet; + +template +inline Packet::Packet(ENetPacket* packet) +{ + m_payload.append(packet->data, packet->dataLength); + m_payload >> m_command >> m_salt; +} + +template +inline Packet::Packet(Outgoing command, u32 salt) + : m_salt(salt) +{ + m_payload << command << salt; +} + +template +inline const sf::Packet& Packet::get() const +{ + return m_payload; +} + +template +inline Incoming Packet::command() const +{ + return m_command; +} + +template +inline u32 Packet::getSalt() const +{ + return m_salt; +} diff --git a/src/common/common/world/chunk.h b/src/common/common/world/chunk.h index 16411214..2284be36 100644 --- a/src/common/common/world/chunk.h +++ b/src/common/common/world/chunk.h @@ -19,6 +19,11 @@ using VoxelArray = std::array; */ using CompressedVoxels = std::vector>; +struct VoxelUpdate { + VoxelPosition voxelPosition; + voxel_t voxel; +}; + /** * @brief Data structure for a "chunk" of voxels of the game * diff --git a/src/common/common/world/chunk_manager.h b/src/common/common/world/chunk_manager.h index 405daf03..af8449bf 100644 --- a/src/common/common/world/chunk_manager.h +++ b/src/common/common/world/chunk_manager.h @@ -17,6 +17,7 @@ class ChunkManager final { Chunk& addChunk(const ChunkPosition& chunk); const Chunk& getChunk(const ChunkPosition& chunk); + const Chunk& getChunk(const ChunkPosition& chunk) const; voxel_t getVoxel(const VoxelPosition& voxelPosition) const; void setVoxel(const VoxelPosition& voxelPosition, voxel_t voxel); diff --git a/src/common/common/world/entity_state.h b/src/common/common/world/entity_state.h new file mode 100644 index 00000000..04fe8399 --- /dev/null +++ b/src/common/common/world/entity_state.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +struct EntityState { + glm::vec3 position{0.0f}; + glm::vec3 rotation{0.0f}; + + glm::vec3 lastPosition{0.0f}; + glm::vec3 lastRotation{0.0f}; + + glm::vec3 velocity{0.0}; + + bool active = false; +}; \ No newline at end of file diff --git a/src/common/common/world/voxel_data.cpp b/src/common/common/world/voxel_data.cpp index b44c0499..3d80edf5 100644 --- a/src/common/common/world/voxel_data.cpp +++ b/src/common/common/world/voxel_data.cpp @@ -1,5 +1,7 @@ #include "voxel_data.h" +#include + VoxelDataManager::VoxelDataManager() : m_commonVoxels(static_cast(CommonVoxel::Count)) { @@ -7,10 +9,6 @@ VoxelDataManager::VoxelDataManager() void VoxelDataManager::initCommonVoxelTypes() { - // @TODO Eventually some of this won't be needed as "common voxels" as this - // stuff - // would be delagated to the Lua instead - // For now though, this can work as a placeholder m_commonVoxels[(u8)CommonVoxel::Air] = getVoxelId("openbuilder_air"); m_commonVoxels[(u8)CommonVoxel::Stone] = getVoxelId("openbuilder_stone"); m_commonVoxels[(u8)CommonVoxel::Sand] = getVoxelId("openbuilder_sand"); @@ -27,6 +25,7 @@ voxel_t VoxelDataManager::addVoxelData(const VoxelData& voxel) const VoxelData& VoxelDataManager::getVoxelData(voxel_t id) const { + return m_voxels.at(id); } diff --git a/src/main.cpp b/src/main.cpp index 5140c40b..b6f3e504 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -15,6 +15,8 @@ #include #include +#include "client/window.h" + // Enable nvidia #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN @@ -26,7 +28,7 @@ _declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001; namespace { enum class LaunchType { - Server, + OLD_SERVER, Client, }; @@ -67,7 +69,7 @@ namespace { // Set launch type to be server. // Option: MAX_CONNECTIONS 2-16 if (option.first == "-server") { - launchType = LaunchType::Server; + launchType = LaunchType::OLD_SERVER; try { int maxConnections = std::stoi(option.second); if (maxConnections < 2) { @@ -156,14 +158,21 @@ int main(int argc, char** argv) loadFromConfigFile(); switch (parseArgs(args)) { - case LaunchType::Server: { - ServerLauncher launcher(sf::seconds(0)); + case LaunchType::OLD_SERVER: { + ServerEngine launcher; launcher.run(); break; } case LaunchType::Client: { - runClientEngine(); + sf::Window window; + if (!Window::createWindowInitOpengl(window)) { + return -1; + } + ClientEngine client; + if (client.init(window)) { + client.runClient(); + } break; } } diff --git a/src/server/CMakeLists.txt b/src/server/CMakeLists.txt index 30274c30..9d156552 100644 --- a/src/server/CMakeLists.txt +++ b/src/server/CMakeLists.txt @@ -1,8 +1,13 @@ add_library(ob-server server_engine.cpp - network/server.cpp + network/server_packet_handling.cpp + network/client_session.cpp + network/pending_client_session.cpp + world/terrain_generation.cpp + world/server_world.cpp + lua/data_api.cpp lua/world_api.cpp lua/server_lua_callback.cpp diff --git a/src/server/network/client_session.cpp b/src/server/network/client_session.cpp new file mode 100644 index 00000000..32b17fa4 --- /dev/null +++ b/src/server/network/client_session.cpp @@ -0,0 +1,92 @@ +#include "client_session.h" +#include + +#include "../world/server_world.h" +#include + +void ClientSession::init(ENetPeer* peer, u32 salt, u32 playerId) +{ + m_clientConnection.salt = salt; + m_salt = salt; + m_clientConnection.peer = peer; + m_playerId = playerId; + m_isActive = true; +} + +void ClientSession::disconnect() +{ + if (m_isActive) { + enet_peer_disconnect(m_clientConnection.peer, 0); + m_isActive = false; + } +} + +void ClientSession::sendPacket(const ServerPacket& packet, u32 channel, u32 flags) +{ + m_clientConnection.send(packet.get(), channel, flags); +} + +void ClientSession::tick(ServerWorld& world) +{ + for (auto& chunk : world.getChunks()) { + if (m_sentChunks.find(chunk.second.getPosition()) == m_sentChunks.end()) { + sendAddChunk(chunk.second); + m_sentChunks.insert(chunk.second.getPosition()); + } + } +} + +bool ClientSession::verify(u32 salt) const +{ + return salt == m_clientConnection.salt; +} + +bool ClientSession::isActive() const +{ + return m_isActive; +} + +u32 ClientSession::getPlayerId() const +{ + return m_playerId; +} + +void ClientSession::sendAddChunk(const Chunk& chunk) +{ + ServerPacket outgoing(ClientCommand::AddChunk, m_salt); + auto compressed = compressVoxelData(chunk.voxels); + + outgoing.write(chunk.getPosition().x); + outgoing.write(chunk.getPosition().y); + outgoing.write(chunk.getPosition().z); + + outgoing.write(static_cast(compressed.size())); + for (auto& voxel : compressed) { + outgoing.write(voxel.first).write(voxel.second); + } + + m_clientConnection.send(outgoing.get(), 1, ENET_PACKET_FLAG_RELIABLE); +} + +void ClientSession::sendPlayerSpawnPoint(const glm::vec3& position) +{ + ServerPacket outgoing(ClientCommand::PlayerSpawnPoint, m_salt); + outgoing.write(position); + + m_clientConnection.send(outgoing.get(), 0, ENET_PACKET_FLAG_RELIABLE); +} + +void ClientSession::sendVoxelUpdate(const VoxelUpdate& update) +{ + auto chunkPosition = toChunkPosition(update.voxelPosition); + if (m_sentChunks.find(chunkPosition) != m_sentChunks.end()) { + ServerPacket outgoing(ClientCommand::VoxelUpdate, m_salt); + outgoing.write(update.voxelPosition.x); + outgoing.write(update.voxelPosition.y); + outgoing.write(update.voxelPosition.z); + + outgoing.write(update.voxel); + + m_clientConnection.send(outgoing.get(), 0, ENET_PACKET_FLAG_RELIABLE); + } +} diff --git a/src/server/network/client_session.h b/src/server/network/client_session.h new file mode 100644 index 00000000..92483c6f --- /dev/null +++ b/src/server/network/client_session.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include +#include + +struct VoxelUpdate; +class ServerWorld; +class Chunk; + +struct PendingClientSession { + Connection connection; + u32 salt = 0; + + void sendHandshakeChallenge(u32 serversalt); + void sendAcceptConnection(u32 playerId); + void sendRejectConnection(const char* reason); + + void sendGameData(const ServerWorld& world); +}; + +class ClientSession { + public: + void init(ENetPeer* peer, u32 salt, u32 playerId); + void disconnect(); + + void sendPacket(const ServerPacket& packet, u32 channel = 0, u32 flags = 0); + + void tick(ServerWorld& world); + + bool verify(u32 salt) const; + bool isActive() const; + u32 getPlayerId() const; + + void sendAddChunk(const Chunk& chunk); + void sendPlayerSpawnPoint(const glm::vec3& position); + void sendVoxelUpdate(const VoxelUpdate& update); + + private: + std::unordered_set m_sentChunks; + + Connection m_clientConnection; + u32 m_salt = 0; + + u32 m_playerId = 0; + + bool m_isActive = false; + + int dist = 4; +}; \ No newline at end of file diff --git a/src/server/network/pending_client_session.cpp b/src/server/network/pending_client_session.cpp new file mode 100644 index 00000000..0b837799 --- /dev/null +++ b/src/server/network/pending_client_session.cpp @@ -0,0 +1,51 @@ +#include "client_session.h" +#include + +#include "../world/server_world.h" + +void PendingClientSession::sendHandshakeChallenge(u32 serversalt) +{ + ServerPacket outgoing(ClientCommand::HandshakeChallenge, salt); + outgoing.write(serversalt); + connection.send(outgoing.get(), 0, ENET_PACKET_FLAG_RELIABLE); +} + +void PendingClientSession::sendRejectConnection(const char* reason) +{ + // 0 meaning accept + ServerPacket outgoing(ClientCommand::ConnectionAcceptance, salt); + outgoing.write((u8)0); + outgoing.write(std::string(reason)); + + connection.send(outgoing.get(), 0, ENET_PACKET_FLAG_RELIABLE); +} + +void PendingClientSession::sendGameData(const ServerWorld& world) +{ + ServerPacket outgoing(ClientCommand::GameData, salt); + + // Send game data about voxels + auto& voxels = world.getVoxelData().getVoxelData(); + outgoing.write(static_cast(voxels.size())); + std::cout << "sending " << voxels.size() << " voxels\n"; + for (const VoxelData& voxel : voxels) { + outgoing.write(voxel); + } + + // Send current world entities + world.serialiseEntities(outgoing); + + connection.send(outgoing.get(), 0, ENET_PACKET_FLAG_RELIABLE); +} + +void PendingClientSession::sendAcceptConnection(u32 playerId) +{ + // 1 meaning accept + ServerPacket outgoing(ClientCommand::ConnectionAcceptance, salt); + outgoing.write((u8)1); + + // Send the player ID + outgoing.write(playerId); + + connection.send(outgoing.get(), 0, ENET_PACKET_FLAG_RELIABLE); +} diff --git a/src/server/network/server.cpp b/src/server/network/server.cpp deleted file mode 100644 index d89ec14e..00000000 --- a/src/server/network/server.cpp +++ /dev/null @@ -1,333 +0,0 @@ -#include "server.h" - -#include "../lua/server_lua_api.h" -#include "../world/terrain_generation.h" -#include -#include -#include -#include -#include -#include - -Server::Server() - : NetworkHost("Server") - , m_worldSize(16) - , m_luaCallbacks(m_script) -{ - // clang-format off - m_commandDispatcher.addCommand(ServerCommand::VoxelEdit, &Server::onVoxelEdit); - m_commandDispatcher.addCommand(ServerCommand::PlayerPosition, &Server::onPlayerPosition); - m_commandDispatcher.addCommand(ServerCommand::PlayerSkin, &Server::onPlayerSkin); - // clang-format on - - // Add all the API needed to the Lua engine - // (Stuff that Lua calls that is defined on the C++ side) - luaInitDataApi(m_script, m_biomeData, m_voxelData); - luaInitWorldApi(m_script); - - // Load game in this order - // Voxels then Biomes - // Done this way as voxel types are a dependancy of biomes - m_script.lua["path"] = "game/"; - m_script.runLuaFile("game/voxels.lua"); - m_script.runLuaFile("game/biomes.lua"); - - m_voxelData.initCommonVoxelTypes(); - - int seed = generateSeed("test"); - - for (int z = 0; z < m_worldSize; z++) { - for (int x = 0; x < m_worldSize; x++) { - generateTerrain(m_world.chunks, x, z, m_voxelData, m_biomeData, seed, - m_worldSize); - } - } -} - -void Server::sendChunk(peer_id_t peerId, const ChunkPosition& position) -{ - if (!m_connectedClients[peerId].connected) { - return; - } - - const Chunk& chunk = [this, &position]() { - if (m_world.chunks.hasChunk(position)) { - return m_world.chunks.getChunk(position); - } - else { - Chunk& c = m_world.chunks.addChunk(position); - return c; - } - }(); - - // Create the chunk-data packet - sf::Packet packet; - packet << ClientCommand::ChunkData << chunk.getPosition().x << chunk.getPosition().y - << chunk.getPosition().z; - - auto compressedChunk = compressVoxelData(chunk.voxels); - packet << static_cast(compressedChunk.size()); - for (auto& voxel : compressedChunk) { - packet << voxel.first << voxel.second; - } - - // Send chunk data to client - sendToPeer(m_connectedClients[peerId].peer, packet, 1, ENET_PACKET_FLAG_RELIABLE); -} - -void Server::sendPlayerSkin(peer_id_t peerId, std::optional toPeer) -{ - sf::Packet skinPacket; - skinPacket << ClientCommand::NewPlayerSkin; - skinPacket << peerId; - - skinPacket.append(m_entities[peerId].m_skinData.data(), 8192); - - if (!toPeer.has_value()) { - broadcastToPeers(skinPacket, 0, ENET_PACKET_FLAG_RELIABLE); - } - else { - sendToPeer(m_connectedClients[toPeer.value()].peer, skinPacket, 0, - ENET_PACKET_FLAG_RELIABLE); - } -} - -void Server::sendGameData(peer_id_t peerId) -{ - sf::Packet packet; - packet << ClientCommand::GameRegistryData; - - auto& data = m_voxelData.getVoxelData(); - packet << static_cast(data.size()); - for (auto& voxel : data) { - u8 mesh = static_cast(voxel.meshStyle); - u8 type = static_cast(voxel.type); - u8 isCollidable = static_cast(voxel.isCollidable); - packet << voxel.name; - packet << voxel.topTexture; - packet << voxel.sideTexture; - packet << voxel.bottomTexture; - packet << mesh; - packet << type; - packet << isCollidable; - } - - sendToPeer(m_connectedClients[peerId].peer, packet, 0, ENET_PACKET_FLAG_RELIABLE); -} - -void Server::onPeerConnect(ENetPeer* peer) -{ - int slot = findEmptySlot(); - if (slot >= 0) { - peer_id_t id = static_cast(slot); - - // Send client back their id - sf::Packet packet; - packet << ClientCommand::PeerId << id; - NetworkHost::sendToPeer(peer, packet, 0, ENET_PACKET_FLAG_RELIABLE); - - // Add peer - addPeer(peer, id); - - // Send them the game data - sendGameData(id); - - // Broadcast the connection event - sf::Packet announcement; - announcement << ClientCommand::PlayerJoin << id; - broadcastToPeers(announcement, 0, ENET_PACKET_FLAG_RELIABLE); - - // Send the spawn chunks - sf::Packet spawn; - auto& player = m_entities[id]; - player.position = findPlayerSpawnPosition(); - player.m_skinData.resize(8192); - spawn << ClientCommand::SpawnPoint << player.position.x << player.position.y - << player.position.z; - sendToPeer(peer, spawn, 0, ENET_PACKET_FLAG_RELIABLE); - - m_luaCallbacks.runPlayerJoinCallbacks(); - - // Send chunks around the player to the client (Spawn chunks) - // "Radius" and "player chunk" position - int r = 2; - ChunkPosition pc = worldToChunkPosition(player.position); - for (int cy = pc.y - r; cy <= pc.y + r; cy++) { - for (int cz = pc.z - r; cz < pc.z + r; cz++) { - for (int cx = pc.x - r; cx < pc.z + r; cx++) { - sendChunk(id, {cx, cy, cz}); - } - } - } - - // Send the inital world to the client - for (auto& chunk : m_world.chunks.chunks()) { - sendChunk(id, chunk.second.getPosition()); - } - - // Send the peer other player's skins - // (Note: Could this overwhelm the player's buffer?) - for (unsigned p_id = 0; p_id < m_entities.size(); p_id++) { - if (m_entities[p_id].hasSkin) { - sendPlayerSkin(p_id, id); - } - } - } -} - -void Server::onPeerDisconnect(ENetPeer* peer) -{ - removePeer(peer->connectID); -} - -void Server::onPeerTimeout(ENetPeer* peer) -{ - removePeer(peer->connectID); -} - -void Server::onCommandRecieve([[maybe_unused]] ENetPeer* peer, sf::Packet& packet, - command_t command) -{ - m_commandDispatcher.execute(*this, command, packet); -} - -void Server::onPlayerPosition(sf::Packet& packet) -{ - peer_id_t id = 0; - packet >> id; - packet >> m_entities[id].position.x >> m_entities[id].position.y >> - m_entities[id].position.z; -} - -void Server::onVoxelEdit(sf::Packet& packet) -{ - VoxelPosition position; - voxel_t voxel; - packet >> position.x >> position.y >> position.z >> voxel; - m_world.voxelUpdates.push_back({position, voxel}); -} - -void Server::onPlayerSkin(sf::Packet& packet) -{ - if (packet.getDataSize() != (sizeof(command_t) + sizeof(peer_id_t) + 8192)) { - LOG("Server", "Player Skin Packet is of an invalid size"); - return; - } - - peer_id_t id = 0; - packet >> id; - - sf::Uint8* skinPixels = - (sf::Uint8*)packet.getData() + sizeof(command_t) + sizeof(peer_id_t); - std::vector newPixels(skinPixels, skinPixels + 8192); - - m_entities[id].m_skinData.swap(newPixels); - m_entities[id].hasSkin = true; - - sendPlayerSkin(id); -} - -void Server::update() -{ - { - sf::Packet packet; - if (m_world.voxelUpdates.size() > 0) { - - u16 size = static_cast(m_world.voxelUpdates.size()); - packet << ClientCommand::VoxelUpdate << size; - - for (auto& voxelUpdate : m_world.voxelUpdates) { - auto chunkPosition = toChunkPosition(voxelUpdate.position); - m_world.chunks.ensureNeighbours(chunkPosition); - m_world.chunks.setVoxel(voxelUpdate.position, voxelUpdate.voxel); - - packet << voxelUpdate.position.x << voxelUpdate.position.y - << voxelUpdate.position.z << voxelUpdate.voxel; - } - // @TODO: Try find a way to not send voxel updates to players - // that created them - broadcastToPeers(packet, 0, ENET_PACKET_FLAG_RELIABLE); - m_world.voxelUpdates.clear(); - } - } - - // Player positions - { - sf::Packet packet; - u16 count = NetworkHost::getConnectedPeerCount(); - packet << ClientCommand::Snapshot << count; - for (int i = 0; i < NetworkHost::getMaxConnections(); i++) { - if (m_connectedClients[i].connected) { - packet << static_cast(i) << m_entities[i].position.x - << m_entities[i].position.y << m_entities[i].position.z; - } - } - broadcastToPeers(packet, 0, 0); - } -} - -int Server::findEmptySlot() const -{ - for (int i = 0; i < NetworkHost::getMaxConnections(); i++) { - if (!m_connectedClients[i].connected) { - return i; - } - } - return -1; -} - -void Server::addPeer(ENetPeer* peer, peer_id_t id) -{ - LOGVAR("Server", "New Peer, Peer Id:", (int)id); - m_connectedClients[id].peer = peer; - m_connectedClients[id].connected = true; - m_connectedClients[id].entityId = id; -} - -void Server::removePeer(u32 connectionId) -{ - auto itr = std::find_if(m_connectedClients.begin(), m_connectedClients.end(), - [this, &connectionId](auto& conn) { - return conn.peer && conn.peer->connectID == connectionId; - }); - - assert(itr != m_connectedClients.cend()); - if (itr != m_connectedClients.cend()) { - LOGVAR("Server", "Client disconnected, Peer Id:", (int)itr->entityId); - m_luaCallbacks.runPlayerLeaveCallbacks(); - - m_entities[itr->entityId].active = false; - m_entities[itr->entityId].hasSkin = false; - itr->connected = false; - itr->peer = nullptr; - - // Broadcast the disconnection event - sf::Packet announcement; - announcement << ClientCommand::PlayerLeave << itr->entityId; - broadcastToPeers(announcement, 0, ENET_PACKET_FLAG_RELIABLE); - - itr->entityId = 0; - } -} - -glm::vec3 Server::findPlayerSpawnPosition() -{ - int x = (CHUNK_SIZE * m_worldSize) / 2; - int z = (CHUNK_SIZE * m_worldSize) / 2; - - for (int chunkY = m_worldSize - 1; chunkY >= 0; chunkY--) { - auto chunkPosition = worldToChunkPosition({x, 0, z}); - chunkPosition.y = chunkY; - auto& spawn = m_world.chunks.getChunk(chunkPosition); - - for (int voxelY = CHUNK_SIZE - 1; voxelY >= 0; voxelY--) { - auto voxelPosition = toLocalVoxelPosition({x, 0, z}); - voxelPosition.y = voxelY; - if (spawn.qGetVoxel(voxelPosition) != 0) { - auto worldY = chunkY * CHUNK_SIZE + voxelY + 3; - return {x, worldY, z}; - } - } - } - return {x, CHUNK_SIZE * m_worldSize, z}; -} diff --git a/src/server/network/server.h b/src/server/network/server.h deleted file mode 100644 index 99d1315d..00000000 --- a/src/server/network/server.h +++ /dev/null @@ -1,79 +0,0 @@ -#pragma once - -#include "../lua/server_lua_callback.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -struct ServerConfig; - -struct ServerEntity { - glm::vec3 position{0.0f}; - bool active = false; - - std::vector m_skinData; - bool hasSkin = false; -}; - -struct ConnectedClient { - ENetPeer* peer = nullptr; - peer_id_t entityId = 0; - bool connected = false; -}; - -class Server final : public NetworkHost { - struct VoxelUpdate { - VoxelPosition position; - voxel_t voxel; - }; - - public: - Server(); - - void update(); - - private: - glm::vec3 findPlayerSpawnPosition(); - - void sendChunk(peer_id_t peerId, const ChunkPosition& chunk); - void sendPlayerSkin(peer_id_t peerId, std::optional toPeer = std::nullopt); - void sendGameData(peer_id_t peerId); - - void onPeerConnect(ENetPeer* peer) override; - void onPeerDisconnect(ENetPeer* peer) override; - void onPeerTimeout(ENetPeer* peer) override; - void onCommandRecieve(ENetPeer* peer, sf::Packet& packet, command_t command) override; - - void onPlayerPosition(sf::Packet& packet); - void onVoxelEdit(sf::Packet& packet); - void onPlayerSkin(sf::Packet& packet); - - int findEmptySlot() const; - - void addPeer(ENetPeer* peer, peer_id_t id); - void removePeer(u32 connectionId); - - std::array m_entities; - std::array m_connectedClients{}; - - struct { - ChunkManager chunks; - std::vector voxelUpdates; - } m_world; - - bool m_isRunning = true; - const int m_worldSize; - - ScriptEngine m_script; - ServerLuaCallbacks m_luaCallbacks; - VoxelDataManager m_voxelData; - BiomeDataManager m_biomeData; - - CommandDispatcher m_commandDispatcher; -}; \ No newline at end of file diff --git a/src/server/network/server_packet_handling.cpp b/src/server/network/server_packet_handling.cpp new file mode 100644 index 00000000..9a860c06 --- /dev/null +++ b/src/server/network/server_packet_handling.cpp @@ -0,0 +1,149 @@ +#include "../server_engine.h" + +#define AUTHENTICATE_PACKET \ + auto itr = m_clientsMap.find(peer->incomingPeerID); \ + if (itr == m_clientsMap.end()) { \ + return; \ + } \ + auto& client = m_clients.at(itr->second); \ + if (!client.verify(packet.getSalt())) { \ + return; \ + } + +void ServerEngine::handlePacket(ServerPacket& packet, ENetPeer* peer) +{ + using Cmd = ServerCommand; + // clang-format off + switch (packet.command()) { + case Cmd::HandshakePartOne: onHandshakePartOne(packet, peer); break; + case Cmd::HandshakeResponse: onHandshakeResponse(packet, peer); break; + + case Cmd::Interaction: onInteraction(packet, peer); break; + case Cmd::MouseState: onMouseState(packet, peer); break; + case Cmd::PlayerState: onPlayerState(packet, peer); break; + case Cmd::SpawnRequest: onSpawnRequest(packet, peer); break; + } + // clang-format on +} + +void ServerEngine::onHandshakePartOne(ServerPacket& packet, ENetPeer* peer) +{ + for (auto& pending : m_pendingConnections) { + if (pending.connection.peer->incomingPeerID == peer->incomingPeerID) { + pending.salt = packet.getSalt(); + pending.sendHandshakeChallenge(m_salt); + } + } +} + +void ServerEngine::onHandshakeResponse(ServerPacket& packet, ENetPeer* peer) +{ + auto itr = findPendingSession(peer->incomingPeerID); + if (itr != m_pendingConnections.end()) { + auto& pending = *itr; + auto thepeer = pending.connection.peer; + if (thepeer->incomingPeerID == peer->incomingPeerID) { + u32 salt = itr->salt ^ m_salt; + if (salt == packet.getSalt()) { + itr->salt = salt; + int slot = createClientSession(peer, salt); + if (slot != -1) { + pending.sendAcceptConnection(m_clients[slot].getPlayerId()); + pending.sendGameData(m_world); + } + else { + pending.sendRejectConnection("Game Full"); + } + } + itr = m_pendingConnections.erase(itr); + } + } +} + +void ServerEngine::onInteraction(ServerPacket& packet, ENetPeer* peer) +{ + AUTHENTICATE_PACKET + auto& player = m_world.findEntity(client.getPlayerId()); + auto& position = player.position; + auto& rotation = player.rotation; + + // See if a block was clicked + auto result = m_world.tryInteract(InteractionKind::PlaceBlock, position, rotation); + if (result) { + VoxelUpdate update; + update.voxelPosition = *result; + update.voxel = m_world.getVoxelData().getVoxelId(CommonVoxel::Stone); + + // Send to clients that own this chunk + for (auto& session : m_clients) { + if (session.isActive()) { + session.sendVoxelUpdate(update); + } + } + } +} + +void ServerEngine::onMouseState(ServerPacket& packet, ENetPeer* peer) +{ + AUTHENTICATE_PACKET + auto& player = m_world.findEntity(client.getPlayerId()); + bool click = packet.read(); + if (click) { + auto& position = player.position; + auto& rotation = player.rotation; + + // See if a block was clicked + auto result = m_world.tryInteract(InteractionKind::DigBlock, position, rotation); + if (result) { + VoxelUpdate update; + update.voxelPosition = *result; + update.voxel = m_world.getVoxelData().getVoxelId(CommonVoxel::Air); + + // Send to clients that own this chunk + for (auto& session : m_clients) { + if (session.isActive()) { + session.sendVoxelUpdate(update); + } + } + } + } +} + +void ServerEngine::onPlayerState(ServerPacket& packet, ENetPeer* peer) +{ + AUTHENTICATE_PACKET + auto position = packet.read(); + auto rotation = packet.read(); + + auto& player = m_world.findEntity(client.getPlayerId()); + player.lastPosition = player.position; + player.lastRotation = player.rotation; + + player.position = position; + player.rotation = rotation; +} + +void ServerEngine::onSpawnRequest(ServerPacket& packet, ENetPeer* peer) +{ + AUTHENTICATE_PACKET + auto id = client.getPlayerId(); + client.sendPlayerSpawnPoint(m_world.getPlayerSpawnPosition(id)); +} + +void ServerEngine::handleDisconnection(ENetPeer* peer) +{ + auto itr = m_clientsMap.find(peer->incomingPeerID); + if (itr != m_clientsMap.end()) { + auto index = itr->second; + m_world.removeEntity(m_clients[index].getPlayerId()); + m_clients[index].disconnect(); + broadcastPlayerLeave(m_clients[index].getPlayerId()); + m_clientsMap.erase(itr); + } + else { + auto pending = findPendingSession(peer->incomingPeerID); + if (pending != m_pendingConnections.end()) { + m_pendingConnections.erase(pending); + } + } +} diff --git a/src/server/server_engine.cpp b/src/server/server_engine.cpp index 99923be7..37d3927b 100644 --- a/src/server/server_engine.cpp +++ b/src/server/server_engine.cpp @@ -1,34 +1,42 @@ #include "server_engine.h" -#include "network/server.h" #include #include #include +#include #include #include -ServerLauncher::ServerLauncher(sf::Time timeout) - : m_timeout(timeout) +const int MAX_CONNECTS = 10; + +ServerEngine::ServerEngine() + : m_world(MAX_CONNECTS) + , m_host({ENET_HOST_ANY, DEFAULT_PORT}, MAX_CONNECTS) + , m_clients(MAX_CONNECTS) + , m_maxConnections(MAX_CONNECTS) + , m_salt(createHandshakeRandom()) { + m_clientsMap.reserve(MAX_CONNECTS); } -ServerLauncher::~ServerLauncher() +ServerEngine::~ServerEngine() { stop(); } -void ServerLauncher::run() +bool ServerEngine::isSetup() const { + return m_host.handle != nullptr; +} - std::cout << "Server Console Commands:\n" - << "exit - Exits server and disconnects everyone\n\n"; - - m_serverThread = std::make_unique([this] { +void ServerEngine::run() +{ + printf("Type 'exit' to shutdown server"); + m_serverThread = std::thread([this] { std::string input; while (m_isServerRunning) { - std::cin >> input; - + std::getline(std::cin, input); if (input == "exit") { - std::cout << "Exiting Server.\n\n"; + std::cout << "Exiting server.\n\n"; m_isServerRunning = false; } } @@ -37,50 +45,155 @@ void ServerLauncher::run() launch(); } -void ServerLauncher::runAsThread() +void ServerEngine::runAsThread() { - m_serverThread = std::make_unique([this] { launch(); }); + m_serverThread = std::thread([this] { launch(); }); } -void ServerLauncher::launch() +void ServerEngine::launch() { - if (!m_server.createAsServer(16)) { - std::cout << "Failed to create server.\n\n"; + if (!isSetup()) { return; } - sf::Clock clock; + m_isServerRunning = true; - LOG("Server", "Server has been launched."); + while (m_isServerRunning) { + std::this_thread::sleep_for(std::chrono::milliseconds(20)); - // Server updates - m_server.tick(); - m_server.update(); + tick(); - // Exit the server if there is no connections - if (m_timeout > sf::seconds(0)) { - if (m_server.getConnectedPeerCount() == 0) { - m_isServerRunning = clock.getElapsedTime() < m_timeout; - } - else { - clock.restart(); - } + m_world.tick(); + + broadcastEntityStates(); + } +} + +void ServerEngine::tick() +{ + assert(m_host.handle); + NetEvent event; + while (m_host.pumpEvent(event)) { + switch (event.type) { + case NetEventType::Connection: + addPendingConnection(event.peer); + break; + + case NetEventType::Disconnect: + case NetEventType::Timeout: + handleDisconnection(event.peer); + break; + + case NetEventType::Data: { + ServerPacket packet(event.packet); + handlePacket(packet, event.peer); + enet_packet_destroy(event.packet); + } break; + + default: + break; + } + } + + for (auto& client : m_clients) { + if (client.isActive()) { + client.tick(m_world); + } + } +} + +void ServerEngine::stop() +{ + m_isServerRunning = false; + if (m_isServerRunning) { + + broadcastServerShutdown(); + + enet_host_flush(m_host.handle); + for (auto& session : m_clients) { + session.disconnect(); } } - LOG("Server", "Server stopped."); + if (m_serverThread.joinable()) { + m_serverThread.join(); + } } -void ServerLauncher::stop() +void ServerEngine::broadcastPacket(ServerPacket& packet, int channel, int flags) { - if (m_serverThread && m_serverThread->joinable()) { - m_isServerRunning = false; - if (m_serverThread->joinable()) { - m_serverThread->join(); + for (auto& client : m_clients) { + if (client.isActive()) + client.sendPacket(packet, channel, flags); + } +} + +std::vector::iterator ServerEngine::findPendingSession(u32 peerId) +{ + for (auto itr = m_pendingConnections.begin(); itr != m_pendingConnections.end();) { + auto thepeer = itr->connection.peer; + if (thepeer->incomingPeerID == peerId) { + return itr; } + } + return m_pendingConnections.end(); +} + +void ServerEngine::addPendingConnection(ENetPeer* peer) +{ + PendingClientSession session; + session.connection.peer = peer; + m_pendingConnections.push_back(session); +} - m_server.disconnectAllPeers(); - m_server.destroy(); - LOG("Server", "Server has exited."); +int ServerEngine::createClientSession(ENetPeer* peer, u32 salt) +{ + for (unsigned i = 0; i < m_clients.size(); i++) { + if (!m_clients[i].isActive()) { + u32 playerId = m_world.addEntity(); + m_clients[i].init(peer, salt, playerId); + m_clientsMap[peer->incomingPeerID] = i; + broadcastPlayerJoin(playerId); + return i; + } } + return -1; +} + +// +// B R O A D C A S T S +// +void ServerEngine::broadcastEntityStates() +{ + ServerPacket packet(ClientCommand::UpdateEntityStates, m_salt); + m_world.serialiseEntities(packet); + + broadcastPacket(packet); +} + +void ServerEngine::broadcastPlayerJoin(u32 playerId) +{ + ServerPacket packet(ClientCommand::AddEntity, m_salt); + packet.write((u32)1); + packet.write(playerId); + std::cout << "Player join " << playerId << std::endl; + auto& player = m_world.findEntity(playerId); + packet.write(player.position); + packet.write(player.rotation); + + broadcastToPeers(m_host.handle, packet.get(), 0, ENET_PACKET_FLAG_RELIABLE); +} + +void ServerEngine::broadcastPlayerLeave(u32 playerId) +{ + ServerPacket packet(ClientCommand::RemoveEntity, m_salt); + packet.write(playerId); + broadcastToPeers(m_host.handle, packet.get(), 0, ENET_PACKET_FLAG_RELIABLE); +} + +void ServerEngine::broadcastServerShutdown() +{ + ServerPacket packet(ClientCommand::ForceExitGame, m_salt); + packet.write(std::string("Host has exited the game.")); + broadcastToPeers(m_host.handle, packet.get(), 0, ENET_PACKET_FLAG_RELIABLE); } \ No newline at end of file diff --git a/src/server/server_engine.h b/src/server/server_engine.h index 24c8872a..c2e3ca18 100644 --- a/src/server/server_engine.h +++ b/src/server/server_engine.h @@ -1,34 +1,63 @@ #pragma once -#include "network/server.h" +#include "network/client_session.h" +#include "world/server_world.h" #include #include #include #include -class ServerLauncher { +class ServerEngine { public: - NON_COPYABLE(ServerLauncher) - NON_MOVEABLE(ServerLauncher) + NON_COPYABLE(ServerEngine) - /** - * @brief Construct a new Server Launcher object - * @param timeout Time to wait before server exit after no connections are - * connected - */ - ServerLauncher(sf::Time timeout); - ~ServerLauncher(); + ServerEngine(); + ~ServerEngine(); void run(); void runAsThread(); void stop(); + bool isSetup() const; + private: + void tick(); + void launch(); - Server m_server; - std::unique_ptr m_serverThread; + void broadcastEntityStates(); + void broadcastPacket(ServerPacket& packet, int channel = 0, int flags = 0); + + void handlePacket(ServerPacket& packet, ENetPeer* peer); + void addPendingConnection(ENetPeer* peer); + + void onHandshakePartOne(ServerPacket& packet, ENetPeer* peer); + void onHandshakeResponse(ServerPacket& packet, ENetPeer* peer); + + void onInteraction(ServerPacket& packet, ENetPeer* peer); + void onMouseState(ServerPacket& packet, ENetPeer* peer); + void onPlayerState(ServerPacket& packet, ENetPeer* peer); + void onSpawnRequest(ServerPacket& packet, ENetPeer* peer); + + void broadcastPlayerJoin(u32 playerId); + void broadcastPlayerLeave(u32 playerId); + void broadcastServerShutdown(); + + int createClientSession(ENetPeer* peer, u32 salt); + void handleDisconnection(ENetPeer* peer); + std::vector::iterator findPendingSession(u32 peerId); + + ServerWorld m_world; + std::thread m_serverThread; std::atomic_bool m_isServerRunning; - sf::Time m_timeout; + NetHost m_host; + std::vector m_clients; + std::unordered_map m_clientsMap; + + std::vector m_pendingConnections; + + int m_maxConnections = 0; + + u32 m_salt; }; diff --git a/src/server/world/server_world.cpp b/src/server/world/server_world.cpp new file mode 100644 index 00000000..c303697d --- /dev/null +++ b/src/server/world/server_world.cpp @@ -0,0 +1,175 @@ +#include "server_world.h" + +#include "../lua/server_lua_api.h" +#include + +#include "terrain_generation.h" +#include + +ServerWorld::ServerWorld(int size) + : m_luaCallbacks(m_lua) +{ + m_entities.resize(1024); + + // Initialise the Lua API + luaInitDataApi(m_lua, m_biomeData, m_voxelData); + luaInitWorldApi(m_lua); + + // Load game data. Must be in this order! + m_lua.lua["path"] = "game/"; + m_lua.runLuaFile("game/voxels.lua"); + m_lua.runLuaFile("game/biomes.lua"); + + m_voxelData.initCommonVoxelTypes(); + + for (int z = 0; z < size; z++) { + for (int x = 0; x < size; x++) { + auto chunks = + generateTerrain(m_chunks, x, z, m_voxelData, m_biomeData, 9000, size); + for (auto& chunk : chunks) + m_currentChunks.insert(chunk); + } + } +} + +void ServerWorld::tick() +{ + /* + while (!m_chunkGenerationQueue.empty()) { + auto pos = m_chunkGenerationQueue.front(); + m_chunkGenerationQueue.pop(); + std::cout << "Generating terrain\n"; + generateTerrain(m_chunks, pos.x, pos.z, m_voxelData, m_biomeData, 1234, 16); + m_currentChunks.insert(pos); + } + */ + /* + for (int i = 1; i < m_entities.size(); i++) { + EntityState& state = m_entities[i]; + if (state.active) { + } + } + */ +} + +const std::vector& ServerWorld::getEntities() const +{ + return m_entities; +} + +void ServerWorld::serialiseEntities(ServerPacket& packet) const +{ + packet.write(entityCount); + for (u32 i = 1; i < m_entities.size(); i++) { + const auto& state = m_entities[i]; + if (state.active) { + packet.write(static_cast(i)); + packet.write(state.position); + packet.write(state.rotation); + } + } +} + +u32 ServerWorld::addEntity() +{ + for (u32 i = 1; i < m_entities.size(); i++) { + auto& state = m_entities[i]; + if (!state.active) { + entityCount++; + state.active = true; + return i; + } + } + return 0; +} + +void ServerWorld::removeEntity(u32 id) +{ + assert(id < m_entities.size()); + m_entities[id].active = false; + entityCount--; +} + +EntityState& ServerWorld::findEntity(u32 id) +{ + assert(id < m_entities.size()); + return m_entities[id]; +} + +const VoxelDataManager& ServerWorld::getVoxelData() const +{ + return m_voxelData; +} + +const BiomeDataManager& ServerWorld::getBiomeData() const +{ + return m_biomeData; +} + +const Chunk* ServerWorld::getChunk(const ChunkPosition& chunkPosition) +{ + if (m_currentChunks.find(chunkPosition) != m_currentChunks.end()) { + return &m_chunks.getChunk(chunkPosition); + } + else { + m_chunkGenerationQueue.push(chunkPosition); + return nullptr; + } +} + +const ChunkPositionMap& ServerWorld::getChunks() const +{ + return m_chunks.chunks(); +} + +glm::vec3 ServerWorld::getPlayerSpawnPosition(u32 playerId) +{ + int x = CHUNK_SIZE * 4; + int z = CHUNK_SIZE * 4; + + for (int chunkY = 10 - 1; chunkY >= 0; chunkY--) { + auto chunkPosition = worldToChunkPosition({x, 0, z}); + chunkPosition.y = chunkY; + auto& spawn = m_chunks.getChunk(chunkPosition); + + for (int voxelY = CHUNK_SIZE - 1; voxelY >= 0; voxelY--) { + auto voxelPosition = toLocalVoxelPosition({x, 0, z}); + voxelPosition.y = voxelY; + if (spawn.qGetVoxel(voxelPosition) != 0) { + auto worldY = chunkY * CHUNK_SIZE + voxelY + 3; + return {x, worldY, z}; + } + } + } + return {x, CHUNK_SIZE * 3, z}; +} + +std::optional ServerWorld::tryInteract(InteractionKind kind, + const glm::vec3& position, + const glm::vec3& rotation) +{ + auto voxelPositions = getIntersectedVoxels(position, forwardsVector(rotation), 8); + if (voxelPositions.empty()) { + return {}; + } + VoxelPosition& previous = voxelPositions.at(0); + + for (auto& voxelPosition : voxelPositions) { + auto voxelId = m_chunks.getVoxel(voxelPosition); + auto type = m_voxelData.getVoxelData(voxelId).type; + if (type == VoxelType::Solid || type == VoxelType::Flora) { + auto air = m_voxelData.getVoxelId(CommonVoxel::Air); + auto stone = m_voxelData.getVoxelId(CommonVoxel::Stone); + + VoxelUpdate update; + update.voxel = kind == InteractionKind::DigBlock ? air : stone; + update.voxelPosition = + kind == InteractionKind::DigBlock ? voxelPosition : previous; + + m_chunks.setVoxel(update.voxelPosition, update.voxel); + return update.voxelPosition; + } + previous = voxelPosition; + } + return {}; +} diff --git a/src/server/world/server_world.h b/src/server/world/server_world.h new file mode 100644 index 00000000..72d956b9 --- /dev/null +++ b/src/server/world/server_world.h @@ -0,0 +1,53 @@ +#pragma once + +#include "../lua/server_lua_callback.h" +#include +#include +#include +#include +#include +#include +#include +#include + +enum class InteractionKind { PlaceBlock, DigBlock }; + +class ServerWorld { + public: + ServerWorld(int size); + + void tick(); + + const std::vector& getEntities() const; + void serialiseEntities(ServerPacket& packet) const; + + u32 addEntity(); + void removeEntity(u32 id); + EntityState& findEntity(u32 id); + + const VoxelDataManager& getVoxelData() const; + const BiomeDataManager& getBiomeData() const; + + const Chunk* getChunk(const ChunkPosition& chunkPosition); + const ChunkPositionMap& getChunks() const; + + glm::vec3 getPlayerSpawnPosition(u32 playerId); + + std::optional tryInteract(InteractionKind kind, + const glm::vec3& position, + const glm::vec3& rotation); + + private: + std::queue m_chunkGenerationQueue; + std::unordered_set m_currentChunks; + + std::vector m_entities; + u32 entityCount = 0; + ChunkManager m_chunks; + + ScriptEngine m_lua; + ServerLuaCallbacks m_luaCallbacks; + + VoxelDataManager m_voxelData; + BiomeDataManager m_biomeData; +}; \ No newline at end of file diff --git a/src/server/world/terrain_generation.cpp b/src/server/world/terrain_generation.cpp index 01d3a03f..9de94c0f 100644 --- a/src/server/world/terrain_generation.cpp +++ b/src/server/world/terrain_generation.cpp @@ -184,11 +184,13 @@ namespace { } // namespace -void generateTerrain(ChunkManager& chunkManager, int chunkX, int chunkZ, - const VoxelDataManager& voxelData, const BiomeDataManager& biomeData, - int seed, int worldSize) +std::vector generateTerrain(ChunkManager& chunkManager, int chunkX, + int chunkZ, const VoxelDataManager& voxelData, + const BiomeDataManager& biomeData, int seed, + int worldSize) { ChunkPosition position{chunkX, 0, chunkZ}; + std::vector positions; auto heightMap = createChunkHeightMap(position, worldSize, seed); auto biomeMap = createBiomeMap(position, 9876); @@ -198,7 +200,9 @@ void generateTerrain(ChunkManager& chunkManager, int chunkX, int chunkZ, Chunk& chunk = chunkManager.addChunk({chunkX, y, chunkZ}); createTerrain(chunk, heightMap, biomeMap, voxelData, biomeData, seed); chunkManager.ensureNeighbours(chunk.getPosition()); + positions.emplace_back(chunkX, y, chunkZ); } + return positions; } float generateSeed(const std::string& input) diff --git a/src/server/world/terrain_generation.h b/src/server/world/terrain_generation.h index 9bb17040..9952558b 100644 --- a/src/server/world/terrain_generation.h +++ b/src/server/world/terrain_generation.h @@ -3,13 +3,15 @@ #include #include #include +#include class ChunkManager; class BiomeDataManager; class VoxelDataManager; -void generateTerrain(ChunkManager& chunkManager, int chunkX, int chunkZ, - const VoxelDataManager& voxelData, const BiomeDataManager& biomeData, - int seed, int worldSize); +std::vector generateTerrain(ChunkManager& chunkManager, int chunkX, + int chunkZ, const VoxelDataManager& voxelData, + const BiomeDataManager& biomeData, int seed, + int worldSize); float generateSeed(const std::string& input); \ No newline at end of file diff --git a/tests/client/lua/client_control_api_test.cpp b/tests/client/lua/client_control_api_test.cpp index f8de72ee..429cf800 100644 --- a/tests/client/lua/client_control_api_test.cpp +++ b/tests/client/lua/client_control_api_test.cpp @@ -1,7 +1,7 @@ #include #include -#include +#include #include #include #include diff --git a/tests/client_server_tests.cpp b/tests/client_server_tests.cpp new file mode 100644 index 00000000..7e12be0a --- /dev/null +++ b/tests/client_server_tests.cpp @@ -0,0 +1,96 @@ +#include + +#include +#include +#include +#include +#include +#include + + + +struct ServerHelper { + ServerHelper(int max = 2) + { + server.runAsThread(); + } + + ~ServerHelper() + { + server.stop(); + } + + ServerEngine server; +}; + +void tickClient(Client& client) +{ + for (int i = 0; i < 100; i++) { + client.tick(); + } +} + +TEST_CASE("Client/Server connection tests") +{ + SECTION("The client can connect to the server") + { + ServerHelper server; + Client client; + client.connectTo(LOCAL_HOST); + + REQUIRE(client.getConnnectionState() == ConnectionState::Pending); + } + + SECTION("The client can disconnect from the server") + { + ServerHelper server; + Client client; + client.connectTo(LOCAL_HOST); + client.tick(); + REQUIRE(client.getConnnectionState() == ConnectionState::Pending); + client.disconnect(); + + REQUIRE(client.getConnnectionState() == ConnectionState::Disconnected); + } + + SECTION("The client can disconnect from the server, and then reconnect") + { + ServerHelper server; + Client client; + client.connectTo(LOCAL_HOST); + client.tick(); + client.disconnect(); + + REQUIRE(client.getConnnectionState() == ConnectionState::Disconnected); + + client.connectTo(LOCAL_HOST); + REQUIRE(client.getConnnectionState() == ConnectionState::Pending); + + client.tick(); + client.disconnect(); + + REQUIRE(client.getConnnectionState() == ConnectionState::Disconnected); + } +/* + SECTION("The client will be fully connected to a server if it is not full") + { + ServerHelper server; + Client client; + client.connectTo(LOCAL_HOST); + tickClient(client); + REQUIRE(client.getConnnectionState() == ConnectionState::Connected); + } + SECTION("The client will be disconnected from the server if it is full") + { + ServerHelper server(1); + Client client; + client.connectTo(LOCAL_HOST); + tickClient(client); + + Client client2; + client2.connectTo(LOCAL_HOST); + tickClient(client2); + REQUIRE(client2.getConnnectionState() == ConnectionState::Disconnected); + } + */ +} \ No newline at end of file diff --git a/tests/common/network/command_dispatcher_test.cpp b/tests/common/network/command_dispatcher_test.cpp deleted file mode 100644 index 25a15dcc..00000000 --- a/tests/common/network/command_dispatcher_test.cpp +++ /dev/null @@ -1,88 +0,0 @@ -#include - -#include -#include - -enum class Command { - Add = 0, - Subtract, - - COUNT -}; - -class Handler { - public: - Handler() - { - m_dispatcher.addCommand(Command::Add, &Handler::onAdd); - m_dispatcher.addCommand(Command::Subtract, &Handler::onSubtract); - } - - void handle(command_t command, sf::Packet& packet) - { - m_dispatcher.execute(*this, command, packet); - } - - u8 counter = 0; - - private: - void onAdd(sf::Packet& packet) - { - u8 adder = 0; - packet >> adder; - counter += adder; - } - - void onSubtract(sf::Packet& packet) - { - u8 subber = 0; - packet >> subber; - counter -= subber; - } - - CommandDispatcher m_dispatcher; -}; - -TEST_CASE("Command dispatcher tests") -{ - Handler handler; - - SECTION("The command dispatcher can handle one type of command") - { - u8 adder = 2; - sf::Packet packet; - packet << static_cast(Command::Add) << adder; - - // This would be done by net handler - command_t command; - packet >> command; - handler.handle(command, packet); - - REQUIRE(handler.counter == adder); - } - - SECTION("The command dispatcher can handle multiple types of command") - { - u8 adder = 12; - u8 subber = 8; - { - sf::Packet packet; - packet << static_cast(Command::Add) << adder; - - // This would be done by net handler - command_t command; - packet >> command; - handler.handle(command, packet); - } - { - sf::Packet packet; - packet << static_cast(Command::Subtract) << subber; - - // This would be done by net handler - command_t command; - packet >> command; - handler.handle(command, packet); - } - REQUIRE(handler.counter == (adder - subber)); - } -} \ No newline at end of file