Compare commits

...

4 Commits

Author SHA1 Message Date
5b42f25302 Comment connecting to server 2023-04-15 15:20:00 +02:00
00bb2fa4cf Implement state screens system 2023-04-14 20:40:55 +02:00
2523a91dfd .gitignore update 2023-04-14 20:40:21 +02:00
6dd8b27ae1 Add server console 2023-04-14 20:30:11 +02:00
14 changed files with 879 additions and 288 deletions

1
.gitignore vendored
View File

@ -46,6 +46,7 @@ Debug/
Release/
UnitTests/
open-builder-unit-tests/
external/
libenet.a
libglad.a

View File

@ -1,12 +1,11 @@
#include "engine.hpp"
#include "../audio/audio.hpp"
#include "game/audio/audio.hpp"
#include "game/world/chunk_renderer.hpp"
#include "common/network/network_manager.hpp"
#include "common/logger.hpp"
#include "game/world/chunk_renderer.hpp"
#include "game/screens/game_session_screen.hpp"
#include <GLFW/glfw3.h>
#include "vendor/imgui.h"
#include "vendor/imgui_impl_glfw.h"
#include "vendor/imgui_impl_opengl3.h"
@ -14,38 +13,14 @@
using namespace polygun::renderer;
using namespace polygun::world;
int sizex = SCR_WIDTH, sizey = SCR_HEIGHT;
static int sizex = SCR_WIDTH, sizey = SCR_HEIGHT;
namespace polygun::engine {
void framebuffer_size_callback(GLFWwindow* window, int width, int height) { glViewport(0, 0, width, height); glfwGetWindowSize(window, &sizex, &sizey);}
void Engine::connection_thread_func() {
network::NetworkManager network_manager("127.0.0.1", 1117);
network::NetworkPacket handshake = network_manager.create_packet(network::NetworkPacketFlag::FLAG_RELIABLE, network::NetworkPacketID::PACKET_HANDSHAKE);
network::NetworkPacket packet;
handshake.write("mrkubax10");
network_manager.send_packet(handshake);
network_manager.update();
while(m_running) {
if(network_manager.get_socket().recv(packet)) {
if((packet.get_flags() & network::NetworkPacketFlag::FLAG_RELIABLE) || packet.get_id()==network::NetworkPacketID::PACKET_ACK)
network_manager.push_packet(packet);
network_manager.update();
if(network_manager.recv_packet(packet)) {
// handle
}
}
packet = network_manager.create_packet(network::NetworkPacketFlag::FLAG_NONE, network::NetworkPacketID::PACKET_ACTIVITY);
network_manager.send_packet(packet);
}
packet = network_manager.create_packet(network::NetworkPacketFlag::FLAG_RELIABLE, network::NetworkPacketID::PACKET_EXIT);
network_manager.send_packet(packet);
while(network_manager.has_outgoing_packets()) {
if(network_manager.get_socket().recv(packet) && packet.get_id()==network::NetworkPacketID::PACKET_ACK)
network_manager.push_packet(packet);
network_manager.update();
}
}
Engine::Engine() :
m_screen_manager(*this)
{}
void Engine::init() {
audio::init();
@ -55,20 +30,17 @@ namespace polygun::engine {
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "Polygun", NULL, NULL);
if (window == NULL) {
std::cout << "Failed to create GLFW window" << std::endl;
m_window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "Polygun", NULL, NULL);
if (m_window == NULL) {
glfwTerminate();
return;
LOG_FATAL("Failed to create GLFW window");
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
glfwSetCursorPosCallback(window, m_camera.mouse_callback);
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
glfwMakeContextCurrent(m_window);
glfwSetFramebufferSizeCallback(m_window, framebuffer_size_callback);
if (glewInit() != GLEW_OK) {
std::cout << "GLEW init failed" << std::endl;
return;
glfwTerminate();
LOG_FATAL("GLEW init failed");
}
// Setup Dear ImGui context
@ -79,244 +51,30 @@ namespace polygun::engine {
ImGui::StyleColorsDark();
// Setup Platform/Renderer bindings
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplGlfw_InitForOpenGL(m_window, true);
ImGui_ImplOpenGL3_Init("#version 330");
glEnable(GL_DEPTH_TEST);
// Push game screen
std::unique_ptr<screens::GameSessionScreen> game_session_screen = std::make_unique<screens::GameSessionScreen>();
m_screen_manager.push_screen(std::move(game_session_screen));
Shader chunk_shader("shaders/chunk_vertex.glsl", "shaders/chunk_fragment.glsl");
float vertices[] = {
-0.1f, -0.1f, -0.1f, 0.0f, 0.0f,
0.1f, -0.1f, -0.1f, 1.0f, 0.0f,
0.1f, 0.1f, -0.1f, 1.0f, 1.0f,
0.1f, 0.1f, -0.1f, 1.0f, 1.0f,
-0.1f, 0.1f, -0.1f, 0.0f, 1.0f,
-0.1f, -0.1f, -0.1f, 0.0f, 0.0f,
-0.1f, -0.1f, 0.1f, 0.0f, 0.0f,
0.1f, -0.1f, 0.1f, 1.0f, 0.0f,
0.1f, 0.1f, 0.1f, 1.0f, 1.0f,
0.1f, 0.1f, 0.1f, 1.0f, 1.0f,
-0.1f, 0.1f, 0.1f, 0.0f, 1.0f,
-0.1f, -0.1f, 0.1f, 0.0f, 0.0f,
-0.1f, 0.1f, 0.1f, 1.0f, 0.0f,
-0.1f, 0.1f, -0.1f, 1.0f, 1.0f,
-0.1f, -0.1f, -0.1f, 0.0f, 1.0f,
-0.1f, -0.1f, -0.1f, 0.0f, 1.0f,
-0.1f, -0.1f, 0.1f, 0.0f, 0.0f,
-0.1f, 0.1f, 0.1f, 1.0f, 0.0f,
0.1f, 0.1f, 0.1f, 1.0f, 0.0f,
0.1f, 0.1f, -0.1f, 1.0f, 1.0f,
0.1f, -0.1f, -0.1f, 0.0f, 1.0f,
0.1f, -0.1f, -0.1f, 0.0f, 1.0f,
0.1f, -0.1f, 0.1f, 0.0f, 0.0f,
0.1f, 0.1f, 0.1f, 1.0f, 0.0f,
-0.1f, -0.1f, -0.1f, 0.0f, 1.0f,
0.1f, -0.1f, -0.1f, 1.0f, 1.0f,
0.1f, -0.1f, 0.1f, 1.0f, 0.0f,
0.1f, -0.1f, 0.1f, 1.0f, 0.0f,
-0.1f, -0.1f, 0.1f, 0.0f, 0.0f,
-0.1f, -0.1f, -0.1f, 0.0f, 1.0f,
-0.1f, 0.1f, -0.1f, 0.0f, 1.0f,
0.1f, 0.1f, -0.1f, 1.0f, 1.0f,
0.1f, 0.1f, 0.1f, 1.0f, 0.0f,
0.1f, 0.1f, 0.1f, 1.0f, 0.0f,
-0.1f, 0.1f, 0.1f, 0.0f, 0.0f,
-0.1f, 0.1f, -0.1f, 0.0f, 1.0f
};
Chunk chnk;
unsigned int VBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// position attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// texture coord attribute
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
unsigned int texture_atlas;
glGenTextures(1, &texture_atlas);
glBindTexture(GL_TEXTURE_2D, texture_atlas);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
int width, height, nrChannels;
stbi_set_flip_vertically_on_load(true);
unsigned char *data = stbi_load("res/textures/gold.png", &width, &height, &nrChannels, 0);
if (data)
while (!glfwWindowShouldClose(m_window))
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);
m_screen_manager.render();
m_screen_manager.update();
chunk_shader.bind();
chunk_shader.set_uniform("texture_atlas", 0);
m_delta_time = 0;
m_last_frame = 0;
bool is_esc_pressed = false;
bool was_camera_locked = false;
m_running = true;
// Note: uncomment to test connection with server
// m_connection_thread.reset(new std::thread(&Engine::connection_thread_func, this));
while (!glfwWindowShouldClose(window))
{
float current_frame = static_cast<float>(glfwGetTime());
m_delta_time = current_frame - m_last_frame;
m_last_frame = current_frame;
// input
// -----
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
if (!is_esc_pressed) {
if (glfwGetInputMode(window, GLFW_CURSOR) == GLFW_CURSOR_DISABLED)
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
else
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
was_camera_locked = true;
}
is_esc_pressed = true;
}
else
is_esc_pressed = false;
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
m_camera.process_movement(polygun::engine::FORWARD, m_delta_time);
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
m_camera.process_movement(polygun::engine::BACKWARD, m_delta_time);
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
m_camera.process_movement(LEFT, m_delta_time);
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
m_camera.process_movement(polygun::engine::RIGHT, m_delta_time);
if (glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS)
m_camera.process_movement(polygun::engine::UP, m_delta_time);
if (glfwGetKey(window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS)
m_camera.process_movement(polygun::engine::DOWN, m_delta_time);
if(glfwGetInputMode(window, GLFW_CURSOR) == GLFW_CURSOR_DISABLED) {
if(was_camera_locked) {
m_camera.update(true);
was_camera_locked = false;
}
else
m_camera.update(false);
}
// rendering
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// bind textures on corresponding texture units
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture_atlas);
// activate shader
chunk_shader.bind();
glm::mat4 projection = glm::perspective(glm::radians(m_camera.m_fov), (float)sizex / (float)sizey, 0.1f, 100.0f);
chunk_shader.set_uniform("projection", projection);
glm::mat4 view = m_camera.get_view_matrix();
chunk_shader.set_uniform("view", view);
glBindVertexArray(VAO);
for (unsigned int x = 0; x < 32; x++) {
for (unsigned int y = 0; y < 32; y++) {
for (unsigned int z = 0; z < 32; z++) {
if (chnk.get_node(glm::vec3(x,y,z))!= 0) {
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, glm::vec3(x/5.0f, y/5.0f, z/5.0f));
chunk_shader.set_uniform("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
}
}
}
// imgui
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
ImGui::SetNextWindowSize(ImVec2(340, 200));
ImGui::Begin("Debug");
ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);
// Camera pos
ImGui::Text("Player Pos: %.3f, %.3f, %.3f", m_camera.m_position.x, m_camera.m_position.y, m_camera.m_position.z);
// Camera rot
ImGui::Text("Player Rot: %.3f, %.3f", m_camera.m_yaw, m_camera.m_pitch);
int x_d, y_d, z_d, id_d;
ImGui::InputInt("ID", &id_d);
ImGui::InputInt("X", &x_d);
ImGui::InputInt("Y", &y_d);
ImGui::InputInt("Z", &z_d);
if (ImGui::Button("Place Block"))
{
chnk.add_node(id_d, glm::vec3(x_d, y_d, z_d));
}
ImGui::End();
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
ImGui::EndFrame();
glfwSwapBuffers(window);
glfwSwapBuffers(m_window);
glfwPollEvents();
}
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
m_running = false;
if(m_connection_thread->joinable()) {
LOG_VERBOSE("Waiting for connection thread to finish");
m_connection_thread->join();
}
glfwTerminate();
}
int Engine::get_screen_width() {
return sizex;
}
int Engine::get_screen_height() {
return sizey;
}
}

View File

@ -1,35 +1,32 @@
#ifndef POLYGUN_ENGINE_HPP
#define POLYGUN_ENGINE_HPP
#include <atomic>
#define GLFW_INCLUDE_NONE // don't include OpenGL context as it's not needed application-wide
#include <GLFW/glfw3.h>
#include "game/core.hpp"
#include "game/renderer/shader.hpp"
#include "game/engine/player_camera.hpp"
#include "game/engine/screen_manager.hpp"
namespace polygun::engine {
class Engine {
private:
ScreenManager m_screen_manager;
GLFWwindow* m_window;
Camera m_camera;
float m_delta_time;
float m_last_frame;
std::unique_ptr<std::thread> m_connection_thread;
std::atomic<bool> m_running;
private:
void connection_thread_func();
public:
Engine() = default;
Engine();
~Engine() = default;
void init();
void render();
void update();
ScreenManager& get_screen_manager() { return m_screen_manager; }
GLFWwindow* get_window() { return m_window; }
static int get_screen_width();
static int get_screen_height();
};
}

View File

@ -0,0 +1,47 @@
/*
PolyGun
Copyright (c) 2023 mrkubax10 <mrkubax10@onet.pl>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#ifndef POLYGUN_ENGINE_SCREEN_HPP
#define POLYGUN_ENGINE_SCREEN_HPP
namespace polygun::engine {
class Engine;
class Screen {
friend class ScreenManager;
public:
// for some reason this is required
// GCC doesn't show warning about this but Clang does
virtual ~Screen(){}
virtual void begin(){}
virtual void render(){}
virtual void update(double delta){}
virtual void finish(){}
protected:
Engine* m_engine;
};
}
#endif // POLYGUN_ENGINE_SCREEN_HPP

View File

@ -0,0 +1,83 @@
/*
PolyGun
Copyright (c) 2023 mrkubax10 <mrkubax10@onet.pl>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include "game/engine/screen_manager.hpp"
#include <GLFW/glfw3.h>
#include <iostream>
using namespace polygun::engine;
ScreenManager::ScreenManager(Engine& engine) :
m_engine(engine),
m_screen_stack(),
m_prev_time(glfwGetTime())
{}
ScreenManager::~ScreenManager() {
if(m_screen_stack.size()>0) {
for(ssize_t i = m_screen_stack.size()-1; i>=0; i--) {
m_screen_stack[i]->finish();
}
}
}
void ScreenManager::push_screen(std::unique_ptr<Screen> screen) {
Screen* current_screen = get_current_screen();
if(current_screen)
current_screen->finish();
screen->m_engine = &m_engine;
screen->begin();
m_screen_stack.push_back(std::move(screen));
}
void ScreenManager::pop_screen() {
Screen* current_screen = get_current_screen();
if(!current_screen)
return;
current_screen->finish();
m_screen_stack.pop_back();
}
void ScreenManager::render() {
Screen* current_screen = get_current_screen();
if(current_screen)
current_screen->render();
}
void ScreenManager::update() {
Screen* current_screen = get_current_screen();
double tm = glfwGetTime();
if(current_screen)
current_screen->update(tm-m_prev_time);
m_prev_time = tm;
}
Screen* ScreenManager::get_current_screen() {
if(m_screen_stack.size()==0)
return nullptr;
return m_screen_stack.back().get();
}

View File

@ -0,0 +1,56 @@
/*
PolyGun
Copyright (c) 2023 mrkubax10 <mrkubax10@onet.pl>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#ifndef POLYGUN_ENGINE_SCREEN_MANAGER_HPP
#define POLYGUN_ENGINE_SCREEN_MANAGER_HPP
#include <memory>
#include <vector>
#include "game/engine/screen.hpp"
namespace polygun::engine {
class Engine;
class Screen;
class ScreenManager final {
public:
ScreenManager(Engine& engine);
~ScreenManager();
void push_screen(std::unique_ptr<Screen> screen);
void pop_screen();
void render();
void update();
Screen* get_current_screen();
private:
Engine& m_engine;
std::vector<std::unique_ptr<Screen>> m_screen_stack;
double m_prev_time;
};
}
#endif // POLYGUN_ENGINE_SCREEN_MANAGER_HPP

View File

@ -0,0 +1,288 @@
/*
PolyGun
Copyright (c) 2023 mrkubax10 <mrkubax10@onet.pl>
2023 kacperks
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include "game/screens/game_session_screen.hpp"
#include <GL/gl.h>
#include "common/network/network_manager.hpp"
#include "common/logger.hpp"
#include "game/engine/engine.hpp"
using namespace polygun::screens;
static const float vertices[] = {
-0.1f, -0.1f, -0.1f, 0.0f, 0.0f,
0.1f, -0.1f, -0.1f, 1.0f, 0.0f,
0.1f, 0.1f, -0.1f, 1.0f, 1.0f,
0.1f, 0.1f, -0.1f, 1.0f, 1.0f,
-0.1f, 0.1f, -0.1f, 0.0f, 1.0f,
-0.1f, -0.1f, -0.1f, 0.0f, 0.0f,
-0.1f, -0.1f, 0.1f, 0.0f, 0.0f,
0.1f, -0.1f, 0.1f, 1.0f, 0.0f,
0.1f, 0.1f, 0.1f, 1.0f, 1.0f,
0.1f, 0.1f, 0.1f, 1.0f, 1.0f,
-0.1f, 0.1f, 0.1f, 0.0f, 1.0f,
-0.1f, -0.1f, 0.1f, 0.0f, 0.0f,
-0.1f, 0.1f, 0.1f, 1.0f, 0.0f,
-0.1f, 0.1f, -0.1f, 1.0f, 1.0f,
-0.1f, -0.1f, -0.1f, 0.0f, 1.0f,
-0.1f, -0.1f, -0.1f, 0.0f, 1.0f,
-0.1f, -0.1f, 0.1f, 0.0f, 0.0f,
-0.1f, 0.1f, 0.1f, 1.0f, 0.0f,
0.1f, 0.1f, 0.1f, 1.0f, 0.0f,
0.1f, 0.1f, -0.1f, 1.0f, 1.0f,
0.1f, -0.1f, -0.1f, 0.0f, 1.0f,
0.1f, -0.1f, -0.1f, 0.0f, 1.0f,
0.1f, -0.1f, 0.1f, 0.0f, 0.0f,
0.1f, 0.1f, 0.1f, 1.0f, 0.0f,
-0.1f, -0.1f, -0.1f, 0.0f, 1.0f,
0.1f, -0.1f, -0.1f, 1.0f, 1.0f,
0.1f, -0.1f, 0.1f, 1.0f, 0.0f,
0.1f, -0.1f, 0.1f, 1.0f, 0.0f,
-0.1f, -0.1f, 0.1f, 0.0f, 0.0f,
-0.1f, -0.1f, -0.1f, 0.0f, 1.0f,
-0.1f, 0.1f, -0.1f, 0.0f, 1.0f,
0.1f, 0.1f, -0.1f, 1.0f, 1.0f,
0.1f, 0.1f, 0.1f, 1.0f, 0.0f,
0.1f, 0.1f, 0.1f, 1.0f, 0.0f,
-0.1f, 0.1f, 0.1f, 0.0f, 0.0f,
-0.1f, 0.1f, -0.1f, 0.0f, 1.0f
};
GameSessionScreen::GameSessionScreen() :
m_chunk_shader("shaders/chunk_vertex.glsl", "shaders/chunk_fragment.glsl"),
m_chnk(),
m_camera(),
m_vbo(0),
m_vao(0),
m_texture_atlas(0),
m_is_esc_pressed(false),
m_was_camera_locked(false),
m_connection_thread(),
m_running(true)
{}
void GameSessionScreen::begin() {
glEnable(GL_DEPTH_TEST);
glGenVertexArrays(1, &m_vao);
glGenBuffers(1, &m_vbo);
glBindVertexArray(m_vao);
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// position attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// texture coord attribute
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
glGenTextures(1, &m_texture_atlas);
glBindTexture(GL_TEXTURE_2D, m_texture_atlas);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
int width, height, nrChannels;
stbi_set_flip_vertically_on_load(true);
unsigned char *data = stbi_load("res/textures/gold.png", &width, &height, &nrChannels, 0);
if (data) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else {
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);
m_chunk_shader.bind();
m_chunk_shader.set_uniform("texture_atlas", 0);
glfwSetCursorPosCallback(m_engine->get_window(), m_camera.mouse_callback);
glfwSetInputMode(m_engine->get_window(), GLFW_CURSOR, GLFW_CURSOR_DISABLED);
// Note: uncomment to test connection with server
// m_connection_thread.reset(new std::thread(&GameSessionScreen::connection_thread_func, this));
}
void GameSessionScreen::render() {
// rendering
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// bind textures on corresponding texture units
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_texture_atlas);
// activate shader
m_chunk_shader.bind();
glm::mat4 projection = glm::perspective(glm::radians(m_camera.m_fov), (float)engine::Engine::get_screen_width() / (float)engine::Engine::get_screen_height(), 0.1f, 100.0f);
m_chunk_shader.set_uniform("projection", projection);
glm::mat4 view = m_camera.get_view_matrix();
m_chunk_shader.set_uniform("view", view);
glBindVertexArray(m_vao);
for (unsigned int x = 0; x < 32; x++) {
for (unsigned int y = 0; y < 32; y++) {
for (unsigned int z = 0; z < 32; z++) {
if (m_chnk.get_node(glm::vec3(x,y,z))!= 0) {
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, glm::vec3(x/5.0f, y/5.0f, z/5.0f));
m_chunk_shader.set_uniform("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
}
}
}
// imgui
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
ImGui::SetNextWindowSize(ImVec2(340, 200));
ImGui::Begin("Debug");
ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);
// Camera pos
ImGui::Text("Player Pos: %.3f, %.3f, %.3f", m_camera.m_position.x, m_camera.m_position.y, m_camera.m_position.z);
// Camera rot
ImGui::Text("Player Rot: %.3f, %.3f", m_camera.m_yaw, m_camera.m_pitch);
int x_d, y_d, z_d, id_d;
ImGui::InputInt("ID", &id_d);
ImGui::InputInt("X", &x_d);
ImGui::InputInt("Y", &y_d);
ImGui::InputInt("Z", &z_d);
if (ImGui::Button("Place Block")) {
m_chnk.add_node(id_d, glm::vec3(x_d, y_d, z_d));
}
ImGui::End();
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
ImGui::EndFrame();
}
void GameSessionScreen::update(double delta) {
// input
// -----
if (glfwGetKey(m_engine->get_window(), GLFW_KEY_ESCAPE) == GLFW_PRESS) {
if (!m_is_esc_pressed) {
if (glfwGetInputMode(m_engine->get_window(), GLFW_CURSOR) == GLFW_CURSOR_DISABLED)
glfwSetInputMode(m_engine->get_window(), GLFW_CURSOR, GLFW_CURSOR_NORMAL);
else
glfwSetInputMode(m_engine->get_window(), GLFW_CURSOR, GLFW_CURSOR_DISABLED);
m_was_camera_locked = true;
}
m_is_esc_pressed = true;
}
else
m_is_esc_pressed = false;
if (glfwGetKey(m_engine->get_window(), GLFW_KEY_W) == GLFW_PRESS)
m_camera.process_movement(polygun::engine::FORWARD, delta);
if (glfwGetKey(m_engine->get_window(), GLFW_KEY_S) == GLFW_PRESS)
m_camera.process_movement(polygun::engine::BACKWARD, delta);
if (glfwGetKey(m_engine->get_window(), GLFW_KEY_A) == GLFW_PRESS)
m_camera.process_movement(polygun::engine::LEFT, delta);
if (glfwGetKey(m_engine->get_window(), GLFW_KEY_D) == GLFW_PRESS)
m_camera.process_movement(polygun::engine::RIGHT, delta);
if (glfwGetKey(m_engine->get_window(), GLFW_KEY_SPACE) == GLFW_PRESS)
m_camera.process_movement(polygun::engine::UP, delta);
if (glfwGetKey(m_engine->get_window(), GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS)
m_camera.process_movement(polygun::engine::DOWN, delta);
if(glfwGetInputMode(m_engine->get_window(), GLFW_CURSOR) == GLFW_CURSOR_DISABLED) {
if(m_was_camera_locked) {
m_camera.update(true);
m_was_camera_locked = false;
}
else
m_camera.update(false);
}
}
void GameSessionScreen::finish() {
glDeleteVertexArrays(1, &m_vao);
glDeleteBuffers(1, &m_vbo);
m_running = false;
if(m_connection_thread.get() && m_connection_thread->joinable()) {
LOG_VERBOSE("Waiting for connection thread to finish");
m_connection_thread->join();
}
}
void GameSessionScreen::connection_thread_func() {
polygun::network::NetworkManager network_manager("127.0.0.1", 1117);
polygun::network::NetworkPacket handshake = network_manager.create_packet(polygun::network::NetworkPacketFlag::FLAG_RELIABLE, polygun::network::NetworkPacketID::PACKET_HANDSHAKE);
polygun::network::NetworkPacket packet;
handshake.write("mrkubax10");
network_manager.send_packet(handshake);
network_manager.update();
while(m_running) {
if(network_manager.get_socket().recv(packet)) {
if((packet.get_flags() & polygun::network::NetworkPacketFlag::FLAG_RELIABLE) || packet.get_id()==polygun::network::NetworkPacketID::PACKET_ACK)
network_manager.push_packet(packet);
network_manager.update();
if(network_manager.recv_packet(packet)) {
// handle
}
}
packet = network_manager.create_packet(polygun::network::NetworkPacketFlag::FLAG_NONE, polygun::network::NetworkPacketID::PACKET_ACTIVITY);
network_manager.send_packet(packet);
}
packet = network_manager.create_packet(polygun::network::NetworkPacketFlag::FLAG_RELIABLE, polygun::network::NetworkPacketID::PACKET_EXIT);
network_manager.send_packet(packet);
while(network_manager.has_outgoing_packets()) {
if(network_manager.get_socket().recv(packet) && packet.get_id()==polygun::network::NetworkPacketID::PACKET_ACK)
network_manager.push_packet(packet);
network_manager.update();
}
}

View File

@ -0,0 +1,64 @@
/*
PolyGun
Copyright (c) 2023 mrkubax10 <mrkubax10@onet.pl>
2023 kacperks
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#ifndef POLYGUN_SCREENS_GAME_SESSION_SCREEN_HPP
#define POLYGUN_SCREENS_GAME_SESSION_SCREEN_HPP
#include "game/engine/screen.hpp"
#include <atomic>
#include <thread>
#include "game/engine/player_camera.hpp"
#include "game/renderer/shader.hpp"
#include "game/world/chunk.hpp"
namespace polygun::screens {
class GameSessionScreen final : public engine::Screen {
public:
GameSessionScreen();
~GameSessionScreen() = default;
virtual void begin() override;
virtual void render() override;
virtual void update(double delta) override;
virtual void finish() override;
private:
renderer::Shader m_chunk_shader;
world::Chunk m_chnk;
engine::Camera m_camera;
unsigned int m_vbo, m_vao;
unsigned int m_texture_atlas;
bool m_is_esc_pressed, m_was_camera_locked;
std::unique_ptr<std::thread> m_connection_thread;
std::atomic<bool> m_running;
private:
void connection_thread_func();
};
}
#endif // POLYGUN_SCREENS_GAME_SESSION_SCREEN_HPP

View File

@ -0,0 +1,43 @@
/*
PolyGun
Copyright (c) 2023 mrkubax10 <mrkubax10@onet.pl>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include "game/screens/main_menu_screen.hpp"
using namespace polygun::screens;
void MainMenuScreen::begin() {
}
void MainMenuScreen::render() {
}
void MainMenuScreen::update(double delta) {
}
void MainMenuScreen::finish() {
}

View File

@ -0,0 +1,40 @@
/*
PolyGun
Copyright (c) 2023 mrkubax10 <mrkubax10@onet.pl>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#ifndef POLYGUN_SCREENS_MAIN_MENU_SCREEN_HPP
#define POLYGUN_SCREENS_MAIN_MENU_SCREEN_HPP
#include "game/engine/screen.hpp"
namespace polygun::screens {
class MainMenuScreen final : public engine::Screen {
public:
virtual void begin() override;
virtual void render() override;
virtual void update(double delta) override;
virtual void finish() override;
};
}
#endif

View File

@ -0,0 +1,88 @@
/*
PolyGun
Copyright (c) 2023 mrkubax10 <mrkubax10@onet.pl>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include "server/command_parser.hpp"
#include <iostream>
#include "server/server.hpp"
#include "common/logger.hpp"
using namespace polygun::server;
CommandParser::CommandParser(Server& server) :
m_server(server)
{}
void CommandParser::parse(const std::string& line) {
std::vector<std::string> tokens;
std::string current_token;
bool inside_quotes = false;
bool escaped = false;
for(size_t i = 0; i<line.length(); i++) {
char ch = line[i];
switch(ch) {
case '\\':
if(!escaped) {
escaped = true;
continue;
}
case '"':
if(!escaped) {
inside_quotes = !inside_quotes;
continue;
}
break;
case ' ':
if(!current_token.empty() && !inside_quotes) {
tokens.push_back(current_token);
current_token.clear();
continue;
}
break;
}
current_token+=ch;
if(escaped)
escaped = false;
}
if(!current_token.empty())
tokens.push_back(current_token);
if(tokens.empty())
return;
const std::string& command = tokens[0];
if(command=="quit")
m_server.m_running = false;
else if(command=="help")
handle_help_command();
else
LOG_ERROR("Unknown command '%s'", command.c_str());
}
void CommandParser::handle_help_command() {
std::cout<<"Available commands: "<<std::endl;
std::cout<<" quit - shutdowns server"<<std::endl;
std::cout<<" help - shows this message"<<std::endl;
}

View File

@ -0,0 +1,47 @@
/*
PolyGun
Copyright (c) 2023 mrkubax10 <mrkubax10@onet.pl>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#ifndef POLYGUN_SERVER_COMMAND_PARSER_HPP
#define POLYGUN_SERVER_COMMAND_PARSER_HPP
#include <string>
namespace polygun::server {
class Server;
class CommandParser final {
public:
CommandParser(Server& server);
~CommandParser() = default;
void parse(const std::string& line);
private:
Server& m_server;
private:
void handle_help_command();
};
}
#endif

View File

@ -26,25 +26,43 @@ SOFTWARE.
#include <cstring>
#include <GLFW/glfw3.h>
#include <iostream>
#include <csignal>
#if defined(__unix__)
#include <sys/ioctl.h>
#endif
#include "common/logger.hpp"
#include "server/command_parser.hpp"
using namespace polygun::server;
static Server* g_current_server = nullptr;
Server::Server() :
m_running(true),
m_clients(),
m_latest_client(),
m_server_socket(new polygun::network::UDPSocket)
m_server_socket(new polygun::network::UDPSocket),
m_command_thread()
{
LOG_INFO("Starting server");
glfwInit();
srand(time(nullptr));
m_server_socket->bind(1117);
g_current_server = this;
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
signal(SIGQUIT, signal_handler);
signal(SIGSTOP, signal_handler);
m_command_thread.reset(new std::thread(&Server::command_thread_func, this));
}
Server::~Server() {
LOG_INFO("Shutting down server");
LOG_VERBOSE("Waiting for command thread to finish");
if(m_command_thread->joinable())
m_command_thread->join();
glfwTerminate();
}
@ -104,6 +122,32 @@ void Server::run() {
}
}
void Server::command_thread_func() {
std::string line;
bool has_input_data = false;
bool show_input_prompt = true;
CommandParser parser(*this);
while(m_running) {
if(show_input_prompt) {
std::cout<<"> ";
std::cout.flush();
show_input_prompt = false;
}
#if defined(__unix__)
int byte_count = 0;
ioctl(0, FIONREAD, &byte_count);
if(byte_count>0)
has_input_data = true;
#endif
if(has_input_data) {
getline(std::cin, line);
parser.parse(line);
has_input_data = false;
show_input_prompt = true;
}
}
}
bool Server::handle_packet(polygun::network::NetworkPacket& packet, Client* client) {
switch(packet.get_id()) {
case polygun::network::NetworkPacketID::PACKET_HANDSHAKE:
@ -173,3 +217,29 @@ void Server::handle_player_timeout(const Client* client) {
void Server::handle_player_activity(Client* client) {
client->update_activity();
}
void Server::signal_handler(int sig) {
if(!g_current_server)
return;
std::string signal_name;
switch(sig) {
case SIGINT:
signal_name = "SIGINT";
break;
case SIGTERM:
signal_name = "SIGTERM";
break;
case SIGQUIT:
signal_name = "SIGQUIT";
break;
case SIGSTOP:
signal_name = "SIGSTOP";
break;
default:
signal_name = "SIGUNKNOWN";
}
LOG_INFO("Received %s signal", signal_name.c_str());
g_current_server->m_running = false;
}

View File

@ -25,10 +25,14 @@ SOFTWARE.
#ifndef POLYGUN_SERVER_SERVER_HPP
#define POLYGUN_SERVER_SERVER_HPP
#include <atomic>
#include <thread>
#include "server/client.hpp"
namespace polygun::server {
class Server final {
friend class CommandParser;
public:
Server();
~Server();
@ -36,18 +40,23 @@ namespace polygun::server {
void run();
private:
bool m_running;
std::atomic<bool> m_running;
std::vector<std::unique_ptr<Client>> m_clients;
std::unique_ptr<Client> m_latest_client;
std::shared_ptr<polygun::network::UDPSocket> m_server_socket;
std::unique_ptr<std::thread> m_command_thread;
private:
void command_thread_func();
bool handle_packet(polygun::network::NetworkPacket& packet, Client* client = nullptr);
bool handle_new_player(polygun::network::NetworkPacket& packet);
void handle_player_disconnected(const Client* client);
void handle_player_timeout(const Client* client);
void handle_player_activity(Client* client);
void handle_invalid_packet(const Client& client, const polygun::network::NetworkPacket& packet);
static void signal_handler(int sig);
};
}