From 370a2f60d07bbc09fc1e436a7ea11d638c459fea Mon Sep 17 00:00:00 2001 From: tovjemam Date: Fri, 2 Jan 2026 14:52:18 +0100 Subject: [PATCH] 1.1. --- CMakeLists.txt | 118 +++++++++++++++++++++++++------- src/assets/cache.cpp | 5 +- src/assets/cache.hpp | 46 ++++++++++--- src/assets/map.cpp | 17 +++++ src/assets/map.hpp | 9 +++ src/assets/mesh_builder.cpp | 2 +- src/assets/mesh_builder.hpp | 2 +- src/assets/model.cpp | 85 +++++++++++++++++------ src/assets/model.hpp | 38 +++++++++- src/assets/vehiclemdl.cpp | 43 ++++++++++++ src/assets/vehiclemdl.hpp | 45 ++++++++++++ src/client/app.cpp | 52 +++++++++++++- src/client/app.hpp | 41 +++++++---- src/client/main.cpp | 77 ++++++++++++++++----- src/collision/dynamicsworld.cpp | 52 ++++++++++++++ src/collision/dynamicsworld.hpp | 42 ++++++++++++ src/collision/motionstate.hpp | 26 +++++++ src/collision/trianglemesh.hpp | 2 +- src/game/entity.cpp | 5 ++ src/game/entity.hpp | 33 +++++++++ src/game/game.cpp | 8 +++ src/game/game.hpp | 20 ++++++ src/game/player.cpp | 114 ++++++++++++++++++++++++++++++ src/game/player.hpp | 59 ++++++++++++++++ src/game/player_input.hpp | 22 +++--- src/game/transform_node.hpp | 4 +- src/game/vehicle.cpp | 34 +++++++++ src/game/vehicle.hpp | 25 +++++++ src/game/world.cpp | 44 ++++++++++++ src/game/world.hpp | 45 ++++++++++++ src/gameview/client_session.cpp | 46 +++++++++++++ src/gameview/client_session.hpp | 24 +++++++ src/gameview/entityview.hpp | 10 ++- src/gameview/worldview.cpp | 69 +++++++++++++++++++ src/gameview/worldview.hpp | 21 ++++++ src/gfx/draw_list.hpp | 13 ++-- src/gfx/renderer.cpp | 12 +++- src/gfx/shader_sources.cpp | 3 +- src/net/defs.hpp | 29 ++++++-- src/net/fixed_str.hpp | 9 ++- src/net/inmessage.hpp | 16 +++++ src/net/msg_producer.cpp | 16 +++++ src/net/msg_producer.hpp | 24 +++++++ src/net/outmessage.hpp | 12 ++++ src/server/client.cpp | 62 +++++++++++++++++ src/server/client.hpp | 48 +++++++++++++ src/server/main.cpp | 19 +++++ src/server/server.cpp | 77 +++++++++++++++++++++ src/server/server.hpp | 44 ++++++++++++ src/server/wsserver.cpp | 107 +++++++++++++++++++++++++++++ src/server/wsserver.hpp | 69 +++++++++++++++++++ src/utils/allocnum.hpp | 24 +++++++ src/utils/defs.hpp | 13 ++++ src/utils/transform.hpp | 17 +++++ src/utils/validate.hpp | 34 +++++++++ 55 files changed, 1811 insertions(+), 122 deletions(-) create mode 100644 src/assets/vehiclemdl.cpp create mode 100644 src/assets/vehiclemdl.hpp create mode 100644 src/collision/dynamicsworld.cpp create mode 100644 src/collision/dynamicsworld.hpp create mode 100644 src/collision/motionstate.hpp create mode 100644 src/game/entity.cpp create mode 100644 src/game/entity.hpp create mode 100644 src/game/game.cpp create mode 100644 src/game/game.hpp create mode 100644 src/game/player.cpp create mode 100644 src/game/player.hpp create mode 100644 src/game/vehicle.cpp create mode 100644 src/game/vehicle.hpp create mode 100644 src/game/world.cpp create mode 100644 src/game/world.hpp create mode 100644 src/gameview/client_session.cpp create mode 100644 src/gameview/worldview.cpp create mode 100644 src/net/msg_producer.cpp create mode 100644 src/net/msg_producer.hpp create mode 100644 src/server/client.cpp create mode 100644 src/server/client.hpp create mode 100644 src/server/main.cpp create mode 100644 src/server/server.cpp create mode 100644 src/server/server.hpp create mode 100644 src/server/wsserver.cpp create mode 100644 src/server/wsserver.hpp create mode 100644 src/utils/allocnum.hpp create mode 100644 src/utils/defs.hpp create mode 100644 src/utils/validate.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2b23f0f..c904012 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,19 +26,13 @@ set(SOURCES "src/assets/skeleton.hpp" "src/assets/skeleton.cpp" "src/collision/trianglemesh.cpp" - # "src/game/entity.hpp" - # "src/game/entity.cpp" - # "src/game/meshinstance.hpp" - # "src/game/meshinstance.cpp" "src/game/player_input.hpp" - # "src/game/player.hpp" - # "src/game/player.cpp" "src/game/transform_node.hpp" - # "src/game/world.hpp" - # "src/game/world.cpp" "src/gameview/client_session.hpp" + "src/gameview/client_session.cpp" "src/gameview/entityview.hpp" "src/gameview/worldview.hpp" + "src/gameview/worldview.cpp" "src/gfx/buffer_object.cpp" "src/gfx/buffer_object.hpp" "src/gfx/draw_list.hpp" @@ -58,8 +52,11 @@ set(SOURCES "src/net/defs.hpp" "src/net/fixed_str.hpp" "src/net/inmessage.hpp" + "src/net/msg_producer.hpp" + "src/net/msg_producer.cpp" "src/net/outmessage.hpp" "src/net/quantized.hpp" + "src/utils/defs.hpp" "src/utils/files.hpp" "src/utils/files.cpp" "src/utils/transform.hpp" @@ -79,9 +76,23 @@ endif() # Include directories -target_include_directories(${MAIN_NAME} PRIVATE - "src" -) +target_include_directories(${MAIN_NAME} PRIVATE "src") + +add_subdirectory(external/glm) +target_link_libraries(${MAIN_NAME} PRIVATE glm) +target_include_directories(${MAIN_NAME} PRIVATE "external/stb") + +set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +set(BUILD_BULLET2_DEMOS OFF CACHE BOOL "" FORCE) +set(BUILD_UNIT_TESTS OFF CACHE BOOL "" FORCE) +set(BUILD_PYBULLET OFF CACHE BOOL "" FORCE) +set(BUILD_OPENGL3_DEMOS OFF CACHE BOOL "" FORCE) + +add_subdirectory(external/bullet3) +target_link_libraries(${MAIN_NAME} PRIVATE BulletCollision BulletDynamics LinearMath Bullet3Common) +target_include_directories(${MAIN_NAME} PRIVATE "external/bullet3/src") + +target_compile_definitions(${MAIN_NAME} PRIVATE CLIENT) # Platform-specific SDL2 handling if (CMAKE_SYSTEM_NAME STREQUAL Emscripten) @@ -134,18 +145,75 @@ endif() add_subdirectory(external/glad) target_link_libraries(${MAIN_NAME} PRIVATE glad) + # add easywsclient + set(EASYWSCLIENT_PATH "external/easywsclient") + + add_library(easywsclient STATIC + ${EASYWSCLIENT_PATH}/easywsclient.cpp + ) + + target_include_directories(easywsclient PUBLIC ${EASYWSCLIENT_PATH}) + + target_link_libraries(${MAIN_NAME} PRIVATE easywsclient) + + # build server + set(ASIO_INCLUDE_DIR "external/asio/include") + include_directories(${ASIO_INCLUDE_DIR}) + + add_subdirectory(external/Crow) + set(SERVER_SOURCES + "src/assets/animation.hpp" + "src/assets/animation.cpp" + "src/assets/cache.hpp" + "src/assets/cache.cpp" + "src/assets/cmdfile.hpp" + "src/assets/cmdfile.cpp" + "src/assets/map.hpp" + "src/assets/map.cpp" + "src/assets/model.hpp" + "src/assets/model.cpp" + "src/assets/mesh_builder.cpp" + "src/assets/skeleton.hpp" + "src/assets/skeleton.cpp" + "src/collision/aabb.hpp" + "src/collision/dynamicsworld.hpp" + "src/collision/dynamicsworld.cpp" + "src/collision/trianglemesh.hpp" + "src/collision/trianglemesh.cpp" + "src/game/entity.hpp" + "src/game/entity.cpp" + "src/game/game.hpp" + "src/game/game.cpp" + "src/game/player_input.hpp" + "src/game/player.hpp" + "src/game/player.cpp" + "src/game/transform_node.hpp" + "src/game/world.hpp" + "src/game/world.cpp" + "src/net/defs.hpp" + "src/net/fixed_str.hpp" + "src/net/inmessage.hpp" + "src/net/msg_producer.hpp" + "src/net/msg_producer.cpp" + "src/net/outmessage.hpp" + "src/net/quantized.hpp" + "src/server/client.hpp" + "src/server/client.cpp" + "src/server/main.cpp" + "src/server/server.hpp" + "src/server/server.cpp" + "src/server/wsserver.hpp" + "src/server/wsserver.cpp" + "src/utils/defs.hpp" + "src/utils/files.hpp" + "src/utils/files.cpp" + "src/utils/transform.hpp" + ) + + set(SERVER_NAME server) + add_executable(${SERVER_NAME} ${SERVER_SOURCES}) + target_link_libraries(${SERVER_NAME} PRIVATE Crow glm BulletCollision BulletDynamics LinearMath Bullet3Common) + target_include_directories(${SERVER_NAME} PRIVATE "src" "external/bullet3/src") + target_compile_definitions(${SERVER_NAME} PRIVATE SERVER) + endif() - -add_subdirectory(external/glm) -target_link_libraries(${MAIN_NAME} PRIVATE glm) -target_include_directories(${MAIN_NAME} PRIVATE "external/stb") - -set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) -set(BUILD_BULLET2_DEMOS OFF CACHE BOOL "" FORCE) -set(BUILD_UNIT_TESTS OFF CACHE BOOL "" FORCE) -set(BUILD_PYBULLET OFF CACHE BOOL "" FORCE) -set(BUILD_OPENGL3_DEMOS OFF CACHE BOOL "" FORCE) - -add_subdirectory(external/bullet3) -target_link_libraries(${MAIN_NAME} PRIVATE BulletCollision LinearMath Bullet3Common) -target_include_directories(${MAIN_NAME} PRIVATE "external/bullet3/src") diff --git a/src/assets/cache.cpp b/src/assets/cache.cpp index 9d366d5..b7890b2 100644 --- a/src/assets/cache.cpp +++ b/src/assets/cache.cpp @@ -1,5 +1,8 @@ #include "cache.hpp" -assets::TextureCache assets::CacheManager::texture_cache_; assets::SkeletonCache assets::CacheManager::skeleton_cache_; assets::ModelCache assets::CacheManager::model_cache_; +assets::MapCache assets::CacheManager::map_cache_; +assets::VehicleCache assets::CacheManager::vehicle_cache_; + +CLIENT_ONLY(assets::TextureCache assets::CacheManager::texture_cache_;) \ No newline at end of file diff --git a/src/assets/cache.hpp b/src/assets/cache.hpp index ac728f6..af5fff2 100644 --- a/src/assets/cache.hpp +++ b/src/assets/cache.hpp @@ -1,8 +1,15 @@ #pragma once +#include "map.hpp" #include "model.hpp" #include "skeleton.hpp" +#include "vehiclemdl.hpp" + +#include "utils/defs.hpp" + +#ifdef CLIENT #include "gfx/texture.hpp" +#endif namespace assets { @@ -36,11 +43,13 @@ private: std::map> cache_; }; +#ifdef CLIENT class TextureCache final : public Cache { protected: PtrType Load(const std::string& key) override { return gfx::Texture::LoadFromFile(key); } }; +#endif // CLIENT class SkeletonCache final : public Cache { @@ -54,26 +63,45 @@ protected: PtrType Load(const std::string& key) override { return Model::LoadFromFile(key); } }; +class MapCache final : public Cache +{ +protected: + PtrType Load(const std::string& key) override { return Map::LoadFromFile(key); } +}; + +class VehicleCache final : public Cache +{ +protected: + PtrType Load(const std::string& key) override { return VehicleModel::LoadFromFile(key); } +}; + class CacheManager { public: - static std::shared_ptr GetTexture(const std::string& filename) { - return texture_cache_.Get(filename); - } - - static std::shared_ptr GetSkeleton(const std::string& filename) { + static std::shared_ptr GetSkeleton(const std::string& filename) + { return skeleton_cache_.Get(filename); } - static std::shared_ptr GetModel(const std::string& filename) { - return model_cache_.Get(filename); + static std::shared_ptr GetModel(const std::string& filename) { return model_cache_.Get(filename); } + + static std::shared_ptr GetMap(const std::string& filename) { return map_cache_.Get(filename); } + + static std::shared_ptr GetVehicleModel(const std::string& filename) { return vehicle_cache_.Get(filename); } + +#ifdef CLIENT + static std::shared_ptr GetTexture(const std::string& filename) + { + return texture_cache_.Get(filename); } +#endif private: - static TextureCache texture_cache_; static SkeletonCache skeleton_cache_; static ModelCache model_cache_; - + static MapCache map_cache_; + static VehicleCache vehicle_cache_; + CLIENT_ONLY(static TextureCache texture_cache_;) }; } // namespace assets \ No newline at end of file diff --git a/src/assets/map.cpp b/src/assets/map.cpp index 68ca82f..2856e06 100644 --- a/src/assets/map.cpp +++ b/src/assets/map.cpp @@ -47,3 +47,20 @@ std::shared_ptr assets::Map::LoadFromFile(const std::string& return map; } + +#ifdef CLIENT +void assets::Map::Draw(gfx::DrawList& dlist) const +{ + if (!basemodel_ || !basemodel_->GetMesh()) + return; + + const auto& surfaces = basemodel_->GetMesh()->surfaces; + + for (const auto& surface : surfaces) + { + gfx::DrawSurfaceCmd cmd; + cmd.surface = &surface; + dlist.AddSurface(cmd); + } +} +#endif // CLIENT \ No newline at end of file diff --git a/src/assets/map.hpp b/src/assets/map.hpp index 347b53b..acc9307 100644 --- a/src/assets/map.hpp +++ b/src/assets/map.hpp @@ -6,6 +6,10 @@ #include "model.hpp" #include "utils/transform.hpp" +#ifdef CLIENT +#include "gfx/draw_list.hpp" +#endif // CLIENT + namespace assets { @@ -22,6 +26,11 @@ public: Map() = default; static std::shared_ptr LoadFromFile(const std::string& filename); + const std::shared_ptr& GetBaseModel() const { return basemodel_; } + const std::vector& GetStaticObjects() const { return static_objects_; } + + CLIENT_ONLY(void Draw(gfx::DrawList& dlist) const;) + private: std::shared_ptr basemodel_; std::vector static_objects_; diff --git a/src/assets/mesh_builder.cpp b/src/assets/mesh_builder.cpp index d016794..525ce3d 100644 --- a/src/assets/mesh_builder.cpp +++ b/src/assets/mesh_builder.cpp @@ -13,7 +13,7 @@ void assets::MeshBuilder::BeginSurface(gfx::SurfaceFlags sflags, const std::stri gfx::Surface surface; surface.sflags = sflags; - surface.texture = texture; + surface.texture = std::move(texture); surface.first = tris_.size(); mesh_->surfaces.push_back(surface); diff --git a/src/assets/mesh_builder.hpp b/src/assets/mesh_builder.hpp index dc28863..2be9f83 100644 --- a/src/assets/mesh_builder.hpp +++ b/src/assets/mesh_builder.hpp @@ -50,7 +50,7 @@ public: void SetMeshFlag(gfx::MeshFlags flag) { mflags_ |= flag; } std::shared_ptr GetMesh() const { return mesh_; } - + private: void FinalizeSurface(); diff --git a/src/assets/model.cpp b/src/assets/model.cpp index 75e8a80..452ecd8 100644 --- a/src/assets/model.cpp +++ b/src/assets/model.cpp @@ -6,32 +6,53 @@ std::shared_ptr assets::Model::LoadFromFile(const std::string& filename) { auto model = std::make_shared(); - - MeshBuilder mb(gfx::MF_NONE); + std::vector vert_pos; // rember for collision trimesh + + CLIENT_ONLY(MeshBuilder mb(gfx::MF_NONE);) LoadCMDFile(filename, [&](const std::string& command, std::istringstream& iss) { if (command == "v") { - MeshVertex v; - iss >> v.pos.x >> v.pos.y >> v.pos.z; - iss >> v.normal.x >> v.normal.y >> v.normal.z; - iss >> v.uv.x >> v.uv.y; + glm::vec3 pos; + iss >> pos.x >> pos.y >> pos.z; - // TODO: LUV & bone data + CLIENT_ONLY( + MeshVertex v; + v.pos = pos; + iss >> v.normal.x >> v.normal.y >> v.normal.z; + iss >> v.uv.x >> v.uv.y; + + // TODO: LUV & bone data + + mb.AddVertex(v); + ) - mb.AddVertex(v); + if (model->cmesh_) + vert_pos.emplace_back(pos); } else if (command == "f") { - MeshTriangle t; - iss >> t.vert[0] >> t.vert[1] >> t.vert[2]; + uint32_t indices[3]; + iss >> indices[0] >> indices[1] >> indices[2]; + + CLIENT_ONLY( + MeshTriangle t; + t.vert[0] = indices[0]; + t.vert[1] = indices[1]; + t.vert[2] = indices[2]; + mb.AddTriangle(t); + ) - mb.AddTriangle(t); + if (model->cmesh_) + { + // FIXME: possible index segfault + model->cmesh_->AddTriangle(vert_pos[indices[0]], vert_pos[indices[1]], vert_pos[indices[2]]); + } } else if (command == "surface") { std::string surface_name, texture_name; - gfx::SurfaceFlags sflags = gfx::SF_NONE; + CLIENT_ONLY(gfx::SurfaceFlags sflags = gfx::SF_NONE;) iss >> surface_name; @@ -40,22 +61,36 @@ std::shared_ptr assets::Model::LoadFromFile(const std::stri while (iss >> flag) { if (flag == "+texture") + { iss >> texture_name; + } else if (flag == "+doublesided") - sflags |= gfx::SF_DOUBLE_SIDED; + { + CLIENT_ONLY(sflags |= gfx::SF_DOUBLE_SIDED;) + } else if (flag == "+transparent") - sflags |= gfx::SF_TRANSPARENT; + { + CLIENT_ONLY(sflags |= gfx::SF_TRANSPARENT;) + } else if (flag == "+ocolor") - sflags |= gfx::SF_OBJECT_COLOR; + { + CLIENT_ONLY(sflags |= gfx::SF_OBJECT_COLOR;) + } } - std::shared_ptr texture; - if (!texture_name.empty()) - { - texture = CacheManager::GetTexture("data/" + surface_name + ".png"); - } - - mb.BeginSurface(sflags, surface_name, texture); + CLIENT_ONLY( + std::shared_ptr texture; + if (!texture_name.empty()) + { + texture = CacheManager::GetTexture("data/" + surface_name + ".png"); + } + + mb.BeginSurface(sflags, surface_name, texture); + ) + } + else if (command == "makecoltrimesh") + { + model->cmesh_ = std::make_unique(); } else { @@ -65,7 +100,13 @@ std::shared_ptr assets::Model::LoadFromFile(const std::stri // TODO: skeleton }); + CLIENT_ONLY( + mb.Build(); + model->mesh_ = mb.GetMesh(); + ) + if (model->cmesh_) + model->cmesh_->Build(); return model; } diff --git a/src/assets/model.hpp b/src/assets/model.hpp index 72d707e..d31b0ff 100644 --- a/src/assets/model.hpp +++ b/src/assets/model.hpp @@ -1,21 +1,55 @@ #pragma once #include +#include + +#include "utils/defs.hpp" + +#include "collision/trianglemesh.hpp" + +#ifdef CLIENT #include "mesh_builder.hpp" +#endif namespace assets { +enum ModelCollisionShapeType +{ + MCS_NONE, + + MCS_BOX, + MCS_SPHERE, +}; + +struct ModelCollisionShape +{ + ModelCollisionShapeType type = MCS_NONE; + glm::vec3 origin = glm::vec3(0.0f); + union + { + float radius; + glm::vec3 half_extents; + }; +}; + class Model { public: Model() = default; static std::shared_ptr LoadFromFile(const std::string& filename); - const std::shared_ptr& GetMesh() const { return mesh_; } + const collision::TriangleMesh* GetColMesh() const { return cmesh_.get(); } + const std::vector& GetColShapes() const { return cshapes_; } + + CLIENT_ONLY(const std::shared_ptr& GetMesh() const { return mesh_; }) private: - std::shared_ptr mesh_; + std::unique_ptr cmesh_; + std::vector cshapes_; + + CLIENT_ONLY(std::shared_ptr mesh_;) + }; } \ No newline at end of file diff --git a/src/assets/vehiclemdl.cpp b/src/assets/vehiclemdl.cpp new file mode 100644 index 0000000..0ad9d13 --- /dev/null +++ b/src/assets/vehiclemdl.cpp @@ -0,0 +1,43 @@ +#include "vehiclemdl.hpp" + +#include "cache.hpp" +#include "cmdfile.hpp" + +std::shared_ptr assets::VehicleModel::LoadFromFile(const std::string& filename) +{ + auto veh = std::shared_ptr(); + + LoadCMDFile(filename, [&](const std::string& command, std::istringstream& iss) { + if (command == "basemodel") + { + std::string model_name; + iss >> model_name; + + veh->basemodel_ = CacheManager::GetModel("data/" + model_name + ".mdl"); + } + else if (command == "wheel") + { + VehicleWheel wheel; + + std::string type_str; + std::string model_name; + iss >> type_str >> model_name; + + if (type_str == "FL") + wheel.type = WHEEL_FL; + else if (type_str == "FR") + wheel.type = WHEEL_FR; + else if (type_str == "RL") + wheel.type = WHEEL_RL; + else if (type_str == "RR") + wheel.type = WHEEL_RR; + + wheel.model = assets::CacheManager::GetModel("data/" + model_name + ".mdl"); + + iss >> wheel.position.x >> wheel.position.y >> wheel.position.z; + iss >> wheel.radius; + + veh->wheels_.emplace_back(wheel); + } + }); +} \ No newline at end of file diff --git a/src/assets/vehiclemdl.hpp b/src/assets/vehiclemdl.hpp new file mode 100644 index 0000000..2c6236c --- /dev/null +++ b/src/assets/vehiclemdl.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include "model.hpp" +#include "utils/transform.hpp" + +namespace assets +{ + +enum VehicleWheelType +{ + WHEEL_REAR = 1, + WHEEL_RIGHT = 2, + + WHEEL_FL = 0, + WHEEL_FR = WHEEL_RIGHT, + WHEEL_RL = WHEEL_REAR | WHEEL_RIGHT, + WHEEL_RR = WHEEL_REAR | WHEEL_RIGHT, +}; + +struct VehicleWheel +{ + VehicleWheelType type = WHEEL_FL; + std::shared_ptr model; + glm::vec3 position; + float radius; +}; + +class VehicleModel +{ +public: + VehicleModel() = default; + + static std::shared_ptr LoadFromFile(const std::string& filename); + + const std::shared_ptr& GetModel() { return basemodel_; } + const std::vector& GetWheels() { return wheels_; } + +private: + std::shared_ptr basemodel_; + std::vector wheels_; + + +}; + +} \ No newline at end of file diff --git a/src/client/app.cpp b/src/client/app.cpp index 86a1568..20c4c65 100644 --- a/src/client/app.cpp +++ b/src/client/app.cpp @@ -2,6 +2,11 @@ #include +#include "net/defs.hpp" +#include "net/outmessage.hpp" + +#include "gameview/worldview.hpp" + App::App() { std::cout << "Initializing App..." << std::endl; @@ -30,8 +35,53 @@ void App::Frame() float aspect = static_cast(viewport_size_.x) / static_cast(viewport_size_.y); renderer_.Begin(viewport_size_.x, viewport_size_.y); + renderer_.ClearColor(glm::vec3(0.3f, 0.9f, 1.0f)); + renderer_.ClearDepth(); - // TODO: draw world + dlist_.Clear(); + + const game::view::WorldView* world; + if (session_ && (world = session_->GetWorld())) + { + world->Draw(dlist_); + + glm::mat4 view = glm::lookAt(glm::vec3(80.0f, 0.0f, 10.0f), glm::vec3(40.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)); + glm::mat4 proj = glm::perspective(glm::radians(45.0f), aspect, 0.1f, 3000.0f); + + gfx::DrawListParams params; + params.view_proj = proj * view; + + renderer_.DrawList(dlist_, params); + } +} + +void App::Connected() +{ + std::cout << "WS connected" << std::endl; + + // init session + session_ = std::make_unique(); + + // send login + auto msg = BeginMsg(net::MSG_ID); + net::PlayerName name; + msg.Write(name); +} + +void App::ProcessMessage(net::InMessage& msg) +{ + if (!session_) + return; + + session_->ProcessMessage(msg); +} + +void App::Disconnected(const std::string& reason) +{ + std::cout << "WS disconnected" << std::endl; + + // close session + session_.reset(); } void App::MouseMove(const glm::vec2& delta) diff --git a/src/client/app.hpp b/src/client/app.hpp index 48f86bc..264948f 100644 --- a/src/client/app.hpp +++ b/src/client/app.hpp @@ -1,29 +1,46 @@ #pragma once +#include + #include "game/player_input.hpp" #include "gfx/renderer.hpp" +#include "net/msg_producer.hpp" +#include "net/inmessage.hpp" -class App +#include "gameview/client_session.hpp" + +class App : public net::MsgProducer { - float time_ = 0.0f; - glm::ivec2 viewport_size_ = { 800, 600 }; - game::PlayerInputFlags input_ = 0; - game::PlayerInputFlags prev_input_ = 0; - - float prev_time_ = 0.0f; - - gfx::Renderer renderer_; public: App(); void Frame(); + void Connected(); + void ProcessMessage(net::InMessage& msg); + void Disconnected(const std::string& reason); + void SetTime(float time) { time_ = time; } - void SetViewportSize(int width, int height) { viewport_size_ = { width, height }; } - void SetInput(game::PlayerInputFlags input) { input_ = input; } + void SetViewportSize(int width, int height) { viewport_size_ = {width, height}; } + void SetInput(game::PlayerInputFlags input) { input_ = input; } void MouseMove(const glm::vec2& delta); ~App(); -}; +private: + void Send(std::vector data); + +private: + float time_ = 0.0f; + glm::ivec2 viewport_size_ = {800, 600}; + game::PlayerInputFlags input_ = 0; + game::PlayerInputFlags prev_input_ = 0; + + float prev_time_ = 0.0f; + + gfx::Renderer renderer_; + gfx::DrawList dlist_; + + std::unique_ptr session_; +}; diff --git a/src/client/main.cpp b/src/client/main.cpp index 86bbd17..f7072f1 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -1,13 +1,16 @@ #include #include #include -#include "app.hpp" +#include #ifdef EMSCRIPTEN #include #include -#endif +#else +#include +#endif // EMSCRIPTEN +#include "app.hpp" #include "gl.hpp" static SDL_Window *s_window = nullptr; @@ -155,39 +158,39 @@ static void Frame() const uint8_t* kbd_state = SDL_GetKeyboardState(nullptr); if (kbd_state[SDL_GetScancodeFromKey(SDLK_w)]) - input |= game::PI_FORWARD; + input |= game::IN_FORWARD; if (kbd_state[SDL_GetScancodeFromKey(SDLK_s)]) - input |= game::PI_BACKWARD; + input |= game::IN_BACKWARD; if (kbd_state[SDL_GetScancodeFromKey(SDLK_a)]) - input |= game::PI_LEFT; + input |= game::IN_LEFT; if (kbd_state[SDL_GetScancodeFromKey(SDLK_d)]) - input |= game::PI_RIGHT; + input |= game::IN_RIGHT; if (kbd_state[SDL_GetScancodeFromKey(SDLK_SPACE)]) - input |= game::PI_JUMP; + input |= game::IN_JUMP; if (kbd_state[SDL_GetScancodeFromKey(SDLK_LCTRL)]) - input |= game::PI_CROUCH; + input |= game::IN_CROUCH; if (kbd_state[SDL_GetScancodeFromKey(SDLK_e)]) - input |= game::PI_USE; + input |= game::IN_USE; if (kbd_state[SDL_GetScancodeFromKey(SDLK_F3)]) - input |= game::PI_DEBUG1; + input |= game::IN_DEBUG1; if (kbd_state[SDL_GetScancodeFromKey(SDLK_F4)]) - input |= game::PI_DEBUG2; + input |= game::IN_DEBUG2; if (kbd_state[SDL_GetScancodeFromKey(SDLK_F5)]) - input |= game::PI_DEBUG3; + input |= game::IN_DEBUG3; int mouse_state = SDL_GetMouseState(nullptr, nullptr); if (mouse_state & SDL_BUTTON(SDL_BUTTON_LEFT)) - input |= game::PI_ATTACK; + input |= game::IN_ATTACK; s_app->SetInput(input); @@ -216,16 +219,58 @@ static void Main() { #ifdef EMSCRIPTEN emscripten_set_main_loop(Frame, 0, true); #else - while (!s_quit) { - Frame(); + using namespace easywsclient; + + auto ws = std::unique_ptr(WebSocket::from_url("ws://127.0.0.1:8080/ws")); + bool connected = false; + + std::vector data; + + while (!s_quit) + { + ws->poll(); + + auto ws_state = ws->getReadyState(); + if (ws_state == WebSocket::OPEN && !connected) + { + connected = true; + s_app->Connected(); + } + else if (ws_state != WebSocket::CLOSED && connected) + { + connected = false; + s_app->Disconnected("WS closed"); + } + + ws->dispatchBinary([&](const std::vector& data) { + net::InMessage msg(reinterpret_cast(data.data()), data.size()); + s_app->ProcessMessage(msg); + }); + + Frame(); + + if (connected) + { + auto msg = s_app->GetMsg(); + if (!msg.empty()) + { + data.resize(msg.size_bytes()); + memcpy(data.data(), msg.data(), msg.size_bytes()); + ws->sendBinary(data); + } + } + + s_app->ResetMsg(); + } + } s_app.reset(); ShutdownGL(); ShutdownSDL(); -#endif +#endif // EMSCRIPTEN } int main(int argc, char *argv[]) diff --git a/src/collision/dynamicsworld.cpp b/src/collision/dynamicsworld.cpp new file mode 100644 index 0000000..c4d38d8 --- /dev/null +++ b/src/collision/dynamicsworld.cpp @@ -0,0 +1,52 @@ +#include "dynamicsworld.hpp" + +#include + +collision::DynamicsWorld::DynamicsWorld(std::shared_ptr map) + : map_(std::move(map)), bt_dispatcher_(&bt_cfg_), + bt_world_(&bt_dispatcher_, &bt_broadphase_, &bt_solver_, &bt_cfg_), bt_veh_raycaster_(&bt_world_) +{ + AddMapCollision(); +} + +void collision::DynamicsWorld::AddMapCollision() +{ + if (!map_) // is perfectly possible that there is no map in this world + return; + + // add basemodel + const auto& basemodel = map_->GetBaseModel(); + if (basemodel) + { + Transform identity; + AddModelInstance(*basemodel, identity); + } + + // add static objects + for (const auto& sobjs = map_->GetStaticObjects(); const auto& sobj : sobjs) + { + AddModelInstance(*sobj.model, sobj.transform); + } +} + +void collision::DynamicsWorld::AddModelInstance(const assets::Model& model, const Transform& trans) +{ + if (auto cmesh = model.GetColMesh(); cmesh) + { + // create trimesh object + auto obj = std::make_unique(); + obj->setCollisionShape(cmesh->GetShape()); + + // set transform + obj->setWorldTransform(trans.ToBtTransform()); + + // add to world + bt_world_.addCollisionObject(obj.get()); + static_objs_.emplace_back(std::move(obj)); + } + + for (const auto& shapes = model.GetColShapes(); const auto& shape : shapes) + { + // TODO: add basic shapes + } +} diff --git a/src/collision/dynamicsworld.hpp b/src/collision/dynamicsworld.hpp new file mode 100644 index 0000000..0a6664f --- /dev/null +++ b/src/collision/dynamicsworld.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include + +#include + +#include "assets/map.hpp" + +namespace collision +{ + +class DynamicsWorld +{ +public: + DynamicsWorld(std::shared_ptr map); + + btDynamicsWorld& GetBtWorld() { return bt_world_; } + const btDynamicsWorld& GetBtWorld() const { return bt_world_; } + btVehicleRaycaster& GetVehicleRaycaster() { return bt_veh_raycaster_; } + + const std::shared_ptr& GetMap() const { return map_; } + +private: + void AddMapCollision(); + void AddModelInstance(const assets::Model& model, const Transform& trans); + +private: + // this is BEFORE bt_world_!!! + std::shared_ptr map_; + std::vector> static_objs_; + // ^----- + + btDefaultCollisionConfiguration bt_cfg_; + btCollisionDispatcher bt_dispatcher_; + btDbvtBroadphase bt_broadphase_; + btSequentialImpulseConstraintSolver bt_solver_; + btDiscreteDynamicsWorld bt_world_; + btDefaultVehicleRaycaster bt_veh_raycaster_; + +}; + +} // namespace collision \ No newline at end of file diff --git a/src/collision/motionstate.hpp b/src/collision/motionstate.hpp new file mode 100644 index 0000000..9b8a7f2 --- /dev/null +++ b/src/collision/motionstate.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "utils/transform.hpp" +#include + +namespace collision +{ + +class MotionState : public btMotionState +{ +public: + MotionState(Transform& transform) : transform_(transform) {} + + void getWorldTransform(btTransform& bt_trans) const override { + bt_trans = transform_.ToBtTransform(); + } + + void setWorldTransform(const btTransform& bt_trans) override { + transform_.SetBtTransform(bt_trans); + } + +private: + Transform& transform_; +}; + +} \ No newline at end of file diff --git a/src/collision/trianglemesh.hpp b/src/collision/trianglemesh.hpp index 72cb95b..e021324 100644 --- a/src/collision/trianglemesh.hpp +++ b/src/collision/trianglemesh.hpp @@ -22,6 +22,6 @@ namespace collision void AddTriangle(const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2); void Build(); - btBvhTriangleMeshShape* GetShape() { return bt_shape_.get(); } + btBvhTriangleMeshShape* GetShape() const { return bt_shape_.get(); } }; } \ No newline at end of file diff --git a/src/game/entity.cpp b/src/game/entity.cpp new file mode 100644 index 0000000..34f558e --- /dev/null +++ b/src/game/entity.cpp @@ -0,0 +1,5 @@ +#include "entity.hpp" + +#include "world.hpp" + +game::Entity::Entity(World& world, net::EntType viewtype) : world_(world), entnum_(world.GetNewEntnum()), viewtype_(viewtype) {} diff --git a/src/game/entity.hpp b/src/game/entity.hpp new file mode 100644 index 0000000..c158202 --- /dev/null +++ b/src/game/entity.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include "net/msg_producer.hpp" +#include "transform_node.hpp" + +namespace game +{ + +class World; +class Player; + +class Entity : public net::MsgProducer +{ +public: + Entity(World& world, net::EntType viewtype); + + net::EntNum GetEntNum() const { return entnum_; } + net::EntType GetViewType() const { return viewtype_; } + + virtual void Update() { ResetMsg(); } + virtual void SendInitData(Player& player, net::OutMessage& msg) const {} + +protected: + World& world_; + const net::EntNum entnum_; + const net::EntType viewtype_; + + TransformNode root_; +}; + +} \ No newline at end of file diff --git a/src/game/game.cpp b/src/game/game.cpp new file mode 100644 index 0000000..c60ccd0 --- /dev/null +++ b/src/game/game.cpp @@ -0,0 +1,8 @@ +#include "game.hpp" + +game::Game::Game() +{ + default_world_ = std::make_shared("openworld"); + + +} \ No newline at end of file diff --git a/src/game/game.hpp b/src/game/game.hpp new file mode 100644 index 0000000..07d86b8 --- /dev/null +++ b/src/game/game.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +#include "world.hpp" + +namespace game +{ + +class Game +{ +public: + Game(); + +private: + std::shared_ptr default_world_; + +}; + +} \ No newline at end of file diff --git a/src/game/player.cpp b/src/game/player.cpp new file mode 100644 index 0000000..46e1e93 --- /dev/null +++ b/src/game/player.cpp @@ -0,0 +1,114 @@ +#include "player.hpp" + +#include "world.hpp" + +game::Player::Player(Game& game, std::string name) : game_(game), name_(std::move(name)) {} + +bool game::Player::ProcessMsg(net::MessageType type, net::InMessage& msg) +{ + switch (type) + { + case net::MSG_IN: + return ProcessInputMsg(msg); + default: + return false; + } +} + +void game::Player::Update() +{ + if (world_ != known_world_) + { + SendWorldMsg(); + known_world_ = world_; + known_ents_.clear(); + } + + if (world_) + SyncEntities(); +} + +void game::Player::SendWorldMsg() +{ + auto msg = BeginMsg(net::MSG_CHWORLD); + msg.Write(net::MapName(world_->GetMapName())); +} + +void game::Player::SyncEntities() +{ + const auto& ents = world_->GetEntities(); + + auto ent_it = ents.begin(); + auto know_it = known_ents_.begin(); + + while (ent_it != ents.end() || know_it != known_ents_.end()) + { + const net::EntNum entnum = (ent_it != ents.end() ? ent_it->first : std::numeric_limits::max()); + const net::EntNum knownum = (know_it != known_ents_.end() ? *know_it : std::numeric_limits::max()); + + if (entnum == knownum) // ----- entity exists and is currently known ----- + { + const Entity& e = *ent_it->second; + if (ShouldSeeEntity(e)) // still visible? + { + SendUpdateEntity(e); // 2) update + ++ent_it; + ++know_it; + } + else // vanished for player + { + SendDestroyEntity(knownum); // 3) destroy + know_it = known_ents_.erase(know_it); // remove from known + ++ent_it; + } + } + else if (entnum < knownum) // ----- entity exists, player does NOT know it ----- + { + const Entity& e = *ent_it->second; + if (ShouldSeeEntity(e)) // 1) become visible + { + SendInitEntity(e); + known_ents_.insert(entnum); // add to known + } // else: stays invisible, nothing to do + ++ent_it; + } + else // ----- player knows it, but it no longer exists ----- + { + SendDestroyEntity(knownum); // 3) destroy + know_it = known_ents_.erase(know_it); // remove from known + } + } +} + +bool game::Player::ShouldSeeEntity(const Entity& entity) const +{ + return true; // TODO: check distance? +} + +void game::Player::SendInitEntity(const Entity& entity) +{ + auto msg = BeginMsg(net::MSG_ENTSPAWN); + msg.Write(entity.GetEntNum()); + msg.Write(entity.GetViewType()); + entity.SendInitData(*this, msg); +} + +void game::Player::SendUpdateEntity(const Entity& entity) +{ + auto msg = BeginMsg(); // no CMD here, these are already included in entity message payload! + msg.Write(entity.GetMsg()); +} + +void game::Player::SendDestroyEntity(net::EntNum entnum) +{ + auto msg = BeginMsg(net::MSG_ENTDESTROY); + msg.Write(entnum); +} + +bool game::Player::ProcessInputMsg(net::InMessage& msg) +{ + if (!msg.Read(in_)) + return false; + + return true; +} diff --git a/src/game/player.hpp b/src/game/player.hpp new file mode 100644 index 0000000..5bc3e8f --- /dev/null +++ b/src/game/player.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include + +#include "server/client.hpp" +#include "net/msg_producer.hpp" + +#include "player_input.hpp" +#include "game.hpp" + +namespace game +{ + +class World; +class Entity; + +enum PlayerState +{ + PS_NONE, + PS_CHARACTER, + PS_VEHICLE, +}; + +class Player : public net::MsgProducer +{ +public: + Player(Game& game, std::string name); + + bool ProcessMsg(net::MessageType type, net::InMessage& msg); + void Update(); + +private: + void SendWorldMsg(); + + // entities sync + void SyncEntities(); + bool ShouldSeeEntity(const Entity& entity) const; + void SendInitEntity(const Entity& entity); + void SendUpdateEntity(const Entity& entity); + void SendDestroyEntity(net::EntNum entnum); + + // msg handlers + bool ProcessInputMsg(net::InMessage& msg); + +private: + Game& game_; + std::string name_; + + World* world_ = nullptr; + World* known_world_ = nullptr; + std::set known_ents_; + + PlayerInputFlags in_; + + PlayerState state_; + +}; + +} \ No newline at end of file diff --git a/src/game/player_input.hpp b/src/game/player_input.hpp index a5eeb89..2243bf3 100644 --- a/src/game/player_input.hpp +++ b/src/game/player_input.hpp @@ -8,16 +8,16 @@ namespace game enum PlayerInputFlag : PlayerInputFlags { - PI_FORWARD = 1 << 0, - PI_BACKWARD = 1 << 1, - PI_LEFT = 1 << 2, - PI_RIGHT = 1 << 3, - PI_JUMP = 1 << 4, - PI_CROUCH = 1 << 5, - PI_USE = 1 << 6, - PI_ATTACK = 1 << 7, - PI_DEBUG1 = 1 << 8, - PI_DEBUG2 = 1 << 9, - PI_DEBUG3 = 1 << 10, + IN_FORWARD = 1 << 0, + IN_BACKWARD = 1 << 1, + IN_LEFT = 1 << 2, + IN_RIGHT = 1 << 3, + IN_JUMP = 1 << 4, + IN_CROUCH = 1 << 5, + IN_USE = 1 << 6, + IN_ATTACK = 1 << 7, + IN_DEBUG1 = 1 << 8, + IN_DEBUG2 = 1 << 9, + IN_DEBUG3 = 1 << 10, }; } diff --git a/src/game/transform_node.hpp b/src/game/transform_node.hpp index 8c45682..ca04019 100644 --- a/src/game/transform_node.hpp +++ b/src/game/transform_node.hpp @@ -7,7 +7,7 @@ namespace game struct TransformNode { const TransformNode* parent = nullptr; - Transform local_transform; + Transform local; glm::mat4 matrix = glm::mat4(1.0f); // Global TransformNode() @@ -17,7 +17,7 @@ namespace game void UpdateMatrix() { - matrix = local_transform.ToMatrix(); + matrix = local.ToMatrix(); if (parent) { diff --git a/src/game/vehicle.cpp b/src/game/vehicle.cpp new file mode 100644 index 0000000..27a8b19 --- /dev/null +++ b/src/game/vehicle.cpp @@ -0,0 +1,34 @@ +#include "vehicle.hpp" + +#include "assets/cache.hpp" + +static std::shared_ptr LoadVehicleModelByName(const std::string& model_name) +{ + return assets::CacheManager::GetVehicleModel("data/" + model_name + ".map"); +} + + + +game::Vehicle::Vehicle(World& world, std::string model_name) : + Entity(world, net::ET_VEHICLE), + model_name_(model_name), + model_(LoadVehicleModelByName(model_name)), + motion_(root_.local) +{ + // setup chassis rigidbody + float mass = 300.0f; + static btBoxShape shape(btVector3(1, 1, 1)); + + btVector3 local_inertia(0, 0, 0); + shape.calculateLocalInertia(mass, local_inertia); + + btRigidBody::btRigidBodyConstructionInfo rb_info(mass, &motion_, &shape, local_inertia); + body_ = std::make_unique(rb_info); + + // setup vehicle + btRaycastVehicle::btVehicleTuning tuning; + vehicle_ = std::make_unique(tuning, body_.get(), &world_.GetVehicleRaycaster()); + vehicle_->setCoordinateSystem(0, 2, 1); + +} + diff --git a/src/game/vehicle.hpp b/src/game/vehicle.hpp new file mode 100644 index 0000000..1da0ed5 --- /dev/null +++ b/src/game/vehicle.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "entity.hpp" +#include "world.hpp" +#include "assets/vehiclemdl.hpp" +#include "collision/motionstate.hpp" + +namespace game +{ + +class Vehicle : public Entity +{ +public: + Vehicle(World& world, std::string model_name); + +private: + std::string model_name_; + std::shared_ptr model_; + + collision::MotionState motion_; + std::unique_ptr body_; + std::unique_ptr vehicle_; +}; + +} \ No newline at end of file diff --git a/src/game/world.cpp b/src/game/world.cpp new file mode 100644 index 0000000..1d31152 --- /dev/null +++ b/src/game/world.cpp @@ -0,0 +1,44 @@ +#include "world.hpp" + +#include + +#include "assets/cache.hpp" +#include "utils/allocnum.hpp" + +static std::shared_ptr LoadMapByName(const std::string& mapname) +{ + return assets::CacheManager::GetMap("data/" + mapname + ".map"); +} + +game::World::World(std::string mapname) : DynamicsWorld(LoadMapByName(mapname)), mapname_(std::move(mapname)) {} + +net::EntNum game::World::GetNewEntnum() +{ + auto entnum = utils::AllocNum(ents_, last_entnum_); + + if (!entnum) + throw std::runtime_error("Max entities reached"); + + return entnum; +} + +void game::World::RegisterEntity(std::unique_ptr ent) +{ + auto& entslot = ents_[ent->GetEntNum()]; + + if (entslot) + throw std::runtime_error("Attempted to register entity with an occupied entnum"); + + entslot = std::move(ent); +} + +void game::World::Update(int64_t delta_time) +{ + time_ms_ += delta_time; + GetBtWorld().stepSimulation(static_cast(delta_time) * 1000.0f); + + for (auto& [entnum, ent] : ents_) + { + ent->Update(); + } +} diff --git a/src/game/world.hpp b/src/game/world.hpp new file mode 100644 index 0000000..0e4f63e --- /dev/null +++ b/src/game/world.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include + +#include "assets/map.hpp" +#include "collision/dynamicsworld.hpp" +#include "entity.hpp" +#include "net/defs.hpp" + +namespace game +{ + +class World : public collision::DynamicsWorld +{ +public: + World(std::string mapname); + + // spawn entity of type T + template T, typename... TArgs> + T& Spawn(TArgs&&... args) + { + auto ent = std::make_unique(*this, std::forward(args)...); + auto& ref = *ent; + RegisterEntity(std::move(ent)); + return ref; + } + + net::EntNum GetNewEntnum(); + void RegisterEntity(std::unique_ptr ent); + + void Update(int64_t delta_time); + + const std::string& GetMapName() const { return mapname_; } + const std::map>& GetEntities() const { return ents_; } + int64_t GetTime() const { return time_ms_; } + +private: + std::string mapname_; + std::map> ents_; + net::EntNum last_entnum_ = 0; + + int64_t time_ms_ = 0; +}; + +} // namespace game \ No newline at end of file diff --git a/src/gameview/client_session.cpp b/src/gameview/client_session.cpp new file mode 100644 index 0000000..dbe30a0 --- /dev/null +++ b/src/gameview/client_session.cpp @@ -0,0 +1,46 @@ +#include "client_session.hpp" + +game::view::ClientSession::ClientSession(App& app) : app_(app) {} + +bool game::view::ClientSession::ProcessMessage(net::InMessage& msg) +{ + while (true) + { + net::MessageType type = net::MSG_NONE; + if (!msg.Read(type)) + break; + + if (type == net::MSG_NONE || type >= net::MSG_COUNT) + return false; + + ProcessSingleMessage(type, msg); + } +} + +bool game::view::ClientSession::ProcessSingleMessage(net::MessageType type, net::InMessage& msg) +{ + switch (type) + { + case net::MSG_CHWORLD: + return ProcessWorldMsg(msg); + + default: + // try pass the msg to world + if (world_ && world_->ProcessMsg(type, msg)) + return true; + + return false; + } +} + +bool game::view::ClientSession::ProcessWorldMsg(net::InMessage& msg) +{ + net::MapName mapname; + if (!msg.Read(mapname)) + return false; + + // TODO: pass mapname + world_ = std::make_unique(); + + return true; +} diff --git a/src/gameview/client_session.hpp b/src/gameview/client_session.hpp index b73828e..8432a7f 100644 --- a/src/gameview/client_session.hpp +++ b/src/gameview/client_session.hpp @@ -1,12 +1,36 @@ #pragma once +#include + +#include "worldview.hpp" + +#include "client/app.hpp" +#include "gfx/draw_list.hpp" +#include "net/defs.hpp" +#include "net/inmessage.hpp" + namespace game::view { class ClientSession { public: + ClientSession(App& app); + + bool ProcessMessage(net::InMessage& msg); + bool ProcessSingleMessage(net::MessageType type, net::InMessage& msg); + + const WorldView* GetWorld() const { return world_.get(); } + private: + // msg handlers + bool ProcessWorldMsg(net::InMessage& msg); + +private: + App& app_; + + std::unique_ptr world_; + }; } // namespace game::view \ No newline at end of file diff --git a/src/gameview/entityview.hpp b/src/gameview/entityview.hpp index 5b825f9..dd01333 100644 --- a/src/gameview/entityview.hpp +++ b/src/gameview/entityview.hpp @@ -1,8 +1,13 @@ #pragma once +#include + #include "game/transform_node.hpp" #include "gfx/draw_list.hpp" +#include "net/defs.hpp" +#include "net/inmessage.hpp" + class World; namespace game::view @@ -13,9 +18,12 @@ class EntityView public: EntityView(World& world) : world_(world) {} - virtual void Draw(gfx::DrawList& dlist) {} + virtual bool ProcessMsg( net::InMessage& msg) { return false; } + virtual void Update() {} + virtual void Draw(gfx::DrawList& dlist) {} + protected: World& world_; TransformNode root_; diff --git a/src/gameview/worldview.cpp b/src/gameview/worldview.cpp new file mode 100644 index 0000000..47c42c1 --- /dev/null +++ b/src/gameview/worldview.cpp @@ -0,0 +1,69 @@ +#include "worldview.hpp" + +#include "assets/cache.hpp" + +game::view::WorldView::WorldView() +{ + map_ = assets::CacheManager::GetMap("data/openworld.map"); +} + +bool game::view::WorldView::ProcessMsg(net::MessageType type, net::InMessage& msg) +{ + switch (type) + { + case net::MSG_ENTSPAWN: + return ProcessEntSpawnMsg(msg); + + case net::MSG_ENTMSG: + return ProcessEntMsgMsg(msg); + + case net::MSG_ENTDESTROY: + return ProcessEntDestroyMsg(msg); + + default: + return false; + } +} + +void game::view::WorldView::Draw(gfx::DrawList& dlist) const +{ + if (map_) + map_->Draw(dlist); +} + +bool game::view::WorldView::ProcessEntSpawnMsg(net::InMessage& msg) +{ + net::EntNum entnum; + net::EntType type; + + if (!msg.Read(entnum) || !msg.Read(type)) + return false; + + auto& entslot = ents_[entnum]; + if (entslot) + entslot.reset(); + + switch (type) + { + + default: + return false; + } + + return true; +} + +bool game::view::WorldView::ProcessEntMsgMsg(net::InMessage& msg) +{ + return false; +} + +bool game::view::WorldView::ProcessEntDestroyMsg(net::InMessage& msg) +{ + net::EntNum entnum; + if (!msg.Read(entnum)) + return false; + + ents_.erase(entnum); + return true; +} diff --git a/src/gameview/worldview.hpp b/src/gameview/worldview.hpp index 5a561e9..e72675a 100644 --- a/src/gameview/worldview.hpp +++ b/src/gameview/worldview.hpp @@ -1,13 +1,34 @@ #pragma once +#include "assets/map.hpp" +#include "gfx/draw_list.hpp" +#include "net/defs.hpp" +#include "net/inmessage.hpp" + +#include "entityview.hpp" + namespace game::view { class WorldView { public: + WorldView(); + + bool ProcessMsg(net::MessageType type, net::InMessage& msg); + + void Draw(gfx::DrawList& dlist) const; private: + // msg handlers + bool ProcessEntSpawnMsg(net::InMessage& msg); + bool ProcessEntMsgMsg(net::InMessage& msg); + bool ProcessEntDestroyMsg(net::InMessage& msg); + +private: + std::shared_ptr map_; + + std::map> ents_; }; diff --git a/src/gfx/draw_list.hpp b/src/gfx/draw_list.hpp index 3deaee0..751d5c6 100644 --- a/src/gfx/draw_list.hpp +++ b/src/gfx/draw_list.hpp @@ -2,7 +2,6 @@ #include -#include "assets/map.hpp" #include "assets/skeleton.hpp" #include "surface.hpp" @@ -11,12 +10,12 @@ namespace gfx struct DrawSurfaceCmd { - const Surface* surface; - const glm::mat4* matrices; // model matrix, continues in array of matrices for skeletal meshes - const glm::vec4* color; // optional tint - uint32_t first; // first triangle index - uint32_t count; // num triangles - float dist; // distance to camera - for transparnt sorting + const Surface* surface = nullptr; + const glm::mat4* matrices = nullptr; // model matrix, continues in array of matrices for skeletal meshes + const glm::vec4* color = nullptr; // optional tint + uint32_t first = 0; // first triangle index + uint32_t count = 0; // num triangles + float dist = 0.0f; // distance to camera - for transparnt sorting }; struct DrawList diff --git a/src/gfx/renderer.cpp b/src/gfx/renderer.cpp index 99f237c..5e8f3fd 100644 --- a/src/gfx/renderer.cpp +++ b/src/gfx/renderer.cpp @@ -96,6 +96,8 @@ void gfx::Renderer::DrawSurfaceList(std::span list, const DrawLi if (auto cmp = sa->va.get() <=> sb->va.get(); cmp != 0) return cmp < 0; + + return false; }); glEnable(GL_DEPTH_TEST); @@ -140,7 +142,15 @@ void gfx::Renderer::DrawSurfaceList(std::span list, const DrawLi SetupMeshShader(mshader, params); // set model matrix - glUniformMatrix4fv(mshader.shader->U(gfx::SU_MODEL), 1, GL_FALSE, &cmd.matrices[0][0][0]); + if (cmd.matrices) + { + glUniformMatrix4fv(mshader.shader->U(gfx::SU_MODEL), 1, GL_FALSE, &cmd.matrices[0][0][0]); + } + else + { // use identity if no matrix provided + static const glm::mat4 identity(1.0f); + glUniformMatrix4fv(mshader.shader->U(gfx::SU_MODEL), 1, GL_FALSE, &identity[0][0]); + } // set color glm::vec4 color = (object_color_flag && cmd.color) ? glm::vec4(*cmd.color) : glm::vec4(1.0f); diff --git a/src/gfx/shader_sources.cpp b/src/gfx/shader_sources.cpp index df35b23..628c278 100644 --- a/src/gfx/shader_sources.cpp +++ b/src/gfx/shader_sources.cpp @@ -85,7 +85,8 @@ void main() { gl_Position = u_view_proj * world_pos; v_uv = vec2(a_uv.x, 1.0 - a_uv.y); - v_color = ComputeLights(world_pos.xyz, world_normal) * a_color; + // v_color = ComputeLights(world_pos.xyz, world_normal) * a_color; + v_color = a_color; } )GLSL", diff --git a/src/net/defs.hpp b/src/net/defs.hpp index 90414e9..e85b5f4 100644 --- a/src/net/defs.hpp +++ b/src/net/defs.hpp @@ -14,7 +14,10 @@ enum MessageType : uint8_t MSG_NONE, /*~~~~~~~~ Client->Server ~~~~~~~~*/ - // IN + // ID + MSG_ID, + + // IN MSG_IN, /*~~~~~~~~ World ~~~~~~~~*/ @@ -26,17 +29,22 @@ enum MessageType : uint8_t MSG_ENTSPAWN, // ENTMSG data... MSG_ENTMSG, - // ENTRM - MSG_ENTRM, + // ENTDESTROY + MSG_ENTDESTROY, /*~~~~~~~~~~~~~~~~*/ MSG_COUNT, }; +using PlayerName = FixedStr<24>; using MapName = FixedStr<32>; -using ViewYaw = Quantized; -using ViewPitch = Quantized; +// pi approx fraction +constexpr long long PI_N = 245850922; +constexpr long long PI_D = 78256779; + +using ViewYawQ = Quantized; +using ViewPitchQ = Quantized; using EntNum = uint16_t; @@ -44,10 +52,17 @@ enum EntType : uint8_t { ET_NONE, - ET_PAWN, - ET_CAR, + ET_CHARACTER, + ET_VEHICLE, ET_COUNT, }; +enum EntMsgType : uint8_t +{ + EMSG_NONE, + + EMSG_UPDATE, +}; + } // namespace net \ No newline at end of file diff --git a/src/net/fixed_str.hpp b/src/net/fixed_str.hpp index 7a1d78e..102b73d 100644 --- a/src/net/fixed_str.hpp +++ b/src/net/fixed_str.hpp @@ -14,8 +14,13 @@ struct FixedStr size_t len = 0; char str[N]; - size_t MaxLen() const { return N; } + FixedStr() = default; + FixedStr(const std::string& stdstr) + { + *this = stdstr; + } + FixedStr& operator=(const std::string& stdstr) { size_t putsize = std::min(N, stdstr.size()); @@ -23,6 +28,8 @@ struct FixedStr memcpy(str, stdstr.data(), putsize); } + size_t MaxLen() const { return N; } + operator std::string() { return std::string(str, len); } operator std::string_view() { return std::string_view(str, len); } }; diff --git a/src/net/inmessage.hpp b/src/net/inmessage.hpp index 249e9bc..22b518b 100644 --- a/src/net/inmessage.hpp +++ b/src/net/inmessage.hpp @@ -24,6 +24,11 @@ public: return ptr_ + n <= end_; } + bool Eof() const + { + return ptr_ < end_; + } + bool Read(char* dest, size_t n) { if (!CheckAvail(n)) @@ -45,6 +50,17 @@ public: return true; } + template requires (std::is_enum_v && std::integral>) + bool Read(T& value) + { + std::underlying_type_t und; + if (!Read(und)) + return false; + + value = und; + return true; + } + template bool Read(FixedStr& str) { diff --git a/src/net/msg_producer.cpp b/src/net/msg_producer.cpp new file mode 100644 index 0000000..aa0b16b --- /dev/null +++ b/src/net/msg_producer.cpp @@ -0,0 +1,16 @@ +#include "msg_producer.hpp" + +#include + +void net::MsgProducer::ResetMsg() +{ + message_buf_.clear(); +} + +net::OutMessage net::MsgProducer::BeginMsg(net::MessageType type) +{ + OutMessage msg(message_buf_); + if (type != net::MSG_NONE) + msg.Write(type); + return msg; +} diff --git a/src/net/msg_producer.hpp b/src/net/msg_producer.hpp new file mode 100644 index 0000000..4f9d2de --- /dev/null +++ b/src/net/msg_producer.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include + +#include "defs.hpp" +#include "outmessage.hpp" + +namespace net +{ + +class MsgProducer +{ +public: + MsgProducer() = default; + + void ResetMsg(); + OutMessage BeginMsg(MessageType type = MSG_NONE); + std::span GetMsg() const { return message_buf_; }; + +private: + std::vector message_buf_; +}; + +} \ No newline at end of file diff --git a/src/net/outmessage.hpp b/src/net/outmessage.hpp index 789606f..9edbf3f 100644 --- a/src/net/outmessage.hpp +++ b/src/net/outmessage.hpp @@ -2,6 +2,7 @@ #include #include +#include #include "fixed_str.hpp" #include "quantized.hpp" @@ -34,6 +35,12 @@ public: WriteAt(Reserve(), value); } + template requires (std::is_enum_v && std::integral>) + void Write(T value) + { + Write(static_cast>(value)); + } + void Write(const char* str, size_t n) { size_t pos = buffer_.size(); @@ -41,6 +48,11 @@ public: memcpy(&buffer_[pos], str, n); } + void Write(std::span data) + { + Write(data.data(), data.size()); + } + template void Write(const FixedStr& str) { diff --git a/src/server/client.cpp b/src/server/client.cpp new file mode 100644 index 0000000..0f54422 --- /dev/null +++ b/src/server/client.cpp @@ -0,0 +1,62 @@ +#include "client.hpp" + +#include "server.hpp" +#include "utils/validate.hpp" + +sv::Client::Client(Server& server, WSConnId id) : server_(server), id_(id) {} + +bool sv::Client::ProcessMessage(net::InMessage& msg) +{ + while (true) + { + net::MessageType type = net::MSG_NONE; + if (!msg.Read(type)) + break; + + if (type == net::MSG_NONE || type >= net::MSG_COUNT) + return false; + + ProcessSingleMessage(type, msg); + } +} + +bool sv::Client::ProcessSingleMessage(net::MessageType type, net::InMessage& msg) +{ + if (state_ == CS_PLAYER) + return player_->ProcessMsg(type, msg); + + if (state_ == CS_INIT) + { + // allow only login + if (type != net::MSG_ID) + return false; + + net::PlayerName name; + if (!msg.Read(name) || !utils::IsAlphanumeric(name)) + return false; + + player_ = std::make_unique(server_.GetGame(), name); + state_ = CS_PLAYER; + return true; + } + + return false; +} + +void sv::Client::Update() +{ + if (player_) + { + player_->ResetMsg(); + player_->Update(); + + auto msg = player_->GetMsg(); + if (!msg.empty()) + Send(std::string(msg.data(), msg.size())); + } +} + +void sv::Client::Send(std::string msg) +{ + server_.Send(*this, std::move(msg)); +} diff --git a/src/server/client.hpp b/src/server/client.hpp new file mode 100644 index 0000000..28c3962 --- /dev/null +++ b/src/server/client.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include "wsserver.hpp" +#include "net/inmessage.hpp" +#include "game/player.hpp" + +namespace sv +{ + +class Server; + +enum ClientState +{ + CS_INIT, + CS_PLAYER, + CS_CLOSED, +}; + +class Client +{ +public: + Client(Server& server, WSConnId id); + + bool ProcessMessage(net::InMessage& msg); + bool ProcessSingleMessage(net::MessageType type, net::InMessage& msg); + + void Update(); + + // void Disconnect(const std::string& reason); + + WSConnId GetConnId() const { return id_; } + ClientState GetState() const { return state_; } + + game::Player* GetPlayer() { return player_.get(); } + const game::Player* GetPlayer() const { return player_.get(); } + +private: + void Send(std::string msg); + +private: + Server& server_; + WSConnId id_ = 0; + ClientState state_ = CS_INIT; + + std::unique_ptr player_; +}; + +} diff --git a/src/server/main.cpp b/src/server/main.cpp new file mode 100644 index 0000000..0c697bc --- /dev/null +++ b/src/server/main.cpp @@ -0,0 +1,19 @@ +#include +#include + +#include "server.hpp" + +int main() +{ + try + { + sv::Server server(11200); + server.Run(); + } catch (const std::exception& e) + { + std::cerr << "FATAL ERROR: " << e.what() << std::endl; + return 1; + } + + return 0; +} diff --git a/src/server/server.cpp b/src/server/server.cpp new file mode 100644 index 0000000..8e5f749 --- /dev/null +++ b/src/server/server.cpp @@ -0,0 +1,77 @@ +#include "server.hpp" + +sv::Server::Server(uint16_t port) : ws_(port) {} + +void sv::Server::Run() +{ + bool exit = false; + while (!exit) + { + PollWSEvents(); + } +} + +void sv::Server::Send(Client& client, std::string msg) +{ + ws_.Send(client.GetConnId(), std::move(msg)); +} + +void sv::Server::PollWSEvents() +{ + WSEvent event; + while (ws_.PollEvent(event)) + { + switch (event.type) + { + case WSE_CONNECTED: + HandleWSConnect(event.conn); + break; + + case WSE_MESSAGE: + HandleWSMessage(event.conn, event.data); + break; + + case WSE_DISCONNECTED: + HandleWSDisconnect(event.conn); + break; + + case WSE_EXIT: + exit_ = true; + break; + + default: + break; + } + } +} + +void sv::Server::HandleWSConnect(WSConnId conn) +{ + clients_[conn] = std::make_unique(); +} + +void sv::Server::HandleWSMessage(WSConnId conn, const std::string& data) +{ + net::InMessage msg(data.data(), data.size()); + if (!clients_.at(conn)->ProcessMessage(msg)) + { + // TODO: disconnect + } +} + +void sv::Server::HandleWSDisconnect(WSConnId conn) +{ + clients_.erase(conn); +} + +void sv::Server::Update() +{ + // update game + + + // update players + for (const auto& [conn, client] : clients_) + { + client->Update(); + } +} diff --git a/src/server/server.hpp b/src/server/server.hpp new file mode 100644 index 0000000..52c6402 --- /dev/null +++ b/src/server/server.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include + +#include "wsserver.hpp" +#include "client.hpp" + +#include "game/game.hpp" + +namespace sv +{ + +class Server +{ +public: + Server(uint16_t port); + + void Run(); + + void Send(Client& client, std::string msg); + + game::Game& GetGame() { return game_; } + +private: + void PollWSEvents(); + void HandleWSConnect(WSConnId conn); + void HandleWSMessage(WSConnId conn, const std::string& data); + void HandleWSDisconnect(WSConnId conn); + + void Update(); + +private: + WSServer ws_; + bool exit_ = false; + + game::Game game_; + std::unordered_map> clients_; + + +}; + +} \ No newline at end of file diff --git a/src/server/wsserver.cpp b/src/server/wsserver.cpp new file mode 100644 index 0000000..f577999 --- /dev/null +++ b/src/server/wsserver.cpp @@ -0,0 +1,107 @@ +#include "wsserver.hpp" + +#include + +#include "utils/allocnum.hpp" + +sv::WSServer::WSServer(uint16_t port) +{ + ws_thread_ = std::make_unique([=]() { + crow::SimpleApp app; + app_ptr_ = (void*)&app; + + CROW_WEBSOCKET_ROUTE(app, "/ws") + .onopen([&](crow::websocket::connection& conn) { + CROW_LOG_INFO << "new websocket connection from " << conn.get_remote_ip(); + + std::lock_guard lock(mtx_); + + WSConnId conn_id = utils::AllocNum(id2conn_, last_id_); + + // register connection + id2conn_[conn_id] = &conn; + conn.userdata((void*)conn_id); + // push connection event + events_.emplace_back(WSE_CONNECTED, conn_id); + }) + .onclose([&](crow::websocket::connection& conn, const std::string& reason, uint16_t) { + CROW_LOG_INFO << "websocket connection closed: " << reason; + + std::lock_guard lock(mtx_); + + WSConnId conn_id = (WSConnId)conn.userdata(); + + // push disonnected event + events_.emplace_back(WSE_DISCONNECTED, conn_id); + + id2conn_.erase(conn_id); + }) + .onmessage([&](crow::websocket::connection& conn, const std::string& data, bool is_binary) { + if (!is_binary) + return; // only accept binary messages here + + std::lock_guard lock(mtx_); + + WSConnId conn_id = (WSConnId)conn.userdata(); + events_.emplace_back(WSE_MESSAGE, conn_id, data); + + }); + + // CROW_ROUTE(app, "/") + // ([] { + // crow::mustache::context x; + // x["servername"] = "127.0.0.1"; + // auto page = crow::mustache::load("ws.html"); + // return page.render(x); + // }); + + app.port(port).run(); + + // push exit event + std::lock_guard lock(mtx_); + events_.emplace_back(WSE_EXIT); + + app_ptr_ = nullptr; + + }); +} + +bool sv::WSServer::PollEvent(WSEvent &out_event) +{ + std::lock_guard lock(mtx_); + + if (events_.empty()) + return false; + + out_event = std::move(events_.front()); + events_.pop_front(); + return true; +} + +void sv::WSServer::Send(WSConnId conn_id, std::string data) +{ + std::lock_guard lock(mtx_); + + auto it = id2conn_.find(conn_id); + if (it == id2conn_.end()) + { + std::cerr << "attempted to send message to unknown conn ID " << conn_id <second).send_binary(std::move(data)); + +} + +void sv::WSServer::Exit() +{ + std::lock_guard lock(mtx_); + if (app_ptr_) + ((crow::SimpleApp*)app_ptr_)->stop(); +} + +sv::WSServer::~WSServer() +{ + if (ws_thread_ && ws_thread_->joinable()) + ws_thread_->join(); +} diff --git a/src/server/wsserver.hpp b/src/server/wsserver.hpp new file mode 100644 index 0000000..86e3911 --- /dev/null +++ b/src/server/wsserver.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace crow::websocket +{ + class connection; +} + +namespace sv +{ + +enum WSEventType +{ + WSE_NONE, + WSE_EXIT, + WSE_CONNECTED, + WSE_MESSAGE, + WSE_DISCONNECTED, +}; + +using WSConnId = uint32_t; + +struct WSEvent +{ + WSEventType type = WSE_NONE; + WSConnId conn = 0; + std::string data; +}; + +// using WSEventHandler = std::function; + +class WSServer +{ +public: + WSServer(uint16_t port); + + WSServer(const WSServer& other) = delete; + WSServer(WSServer&& other) = delete; + WSServer& operator=(const WSServer& other) = delete; + WSServer& operator=(WSServer&& other) = delete; + + bool PollEvent(WSEvent& out_event); + void Send(WSConnId conn_id, std::string data); + // void Close(WSConnId conn_id); + + void Exit(); + + ~WSServer(); + +private: + std::deque events_; + std::mutex mtx_; + + std::unique_ptr ws_thread_; + void* app_ptr_ = nullptr; + + std::unordered_map id2conn_; + WSConnId last_id_ = 0; + +}; + +} \ No newline at end of file diff --git a/src/utils/allocnum.hpp b/src/utils/allocnum.hpp new file mode 100644 index 0000000..e775bd8 --- /dev/null +++ b/src/utils/allocnum.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include + +namespace utils +{ + +template +TNum AllocNum(const TMap& map, TNum& num) +{ + constexpr auto MAX_NUM = std::numeric_limits().max(); + if (ents_.size() >= MAX_NUM - 2) // 0 & MAX reserved + return 0; + + // this is stupid but whatever + do + { + ++num; + } while (num == 0 || num == MAX_ENTNUM || ents_.find(num) != ents_.end()); + + return num; +} + +} \ No newline at end of file diff --git a/src/utils/defs.hpp b/src/utils/defs.hpp new file mode 100644 index 0000000..045ef10 --- /dev/null +++ b/src/utils/defs.hpp @@ -0,0 +1,13 @@ +#pragma once + +#ifdef SERVER +#define SERVER_ONLY(...) __VA_ARGS__ +#else +#define SERVER_ONLY(...) +#endif // SERVER + +#ifdef CLIENT +#define CLIENT_ONLY(...) __VA_ARGS__ +#else +#define CLIENT_ONLY(...) +#endif diff --git a/src/utils/transform.hpp b/src/utils/transform.hpp index 3dc16c5..b7e6e38 100644 --- a/src/utils/transform.hpp +++ b/src/utils/transform.hpp @@ -3,6 +3,8 @@ #include #include +#include + struct Transform { glm::vec3 position = glm::vec3(0.0f); @@ -41,12 +43,27 @@ struct Transform ); } + btTransform ToBtTransform() const + { + btQuaternion bt_rotation(rotation.x, rotation.y, rotation.z, rotation.w); + btVector3 bt_position(position.x, position.y, position.z); + return btTransform(bt_rotation, bt_position); + } + void SetAngles(const glm::vec3& angles_deg) { glm::vec3 angles_rad = glm::radians(angles_deg); rotation = glm::quat(angles_rad); } + void SetBtTransform(const btTransform& bt_trans) + { + btVector3 bt_position = bt_trans.getOrigin(); + btQuaternion bt_rotation = bt_trans.getRotation(); + position = glm::vec3(bt_position.x(), bt_position.y(), bt_position.z()); + rotation = glm::quat(bt_rotation.w(), bt_rotation.x(), bt_rotation.y(), bt_rotation.z()); + } + static Transform Lerp(const Transform& a, const Transform& b, float t) { Transform result; diff --git a/src/utils/validate.hpp b/src/utils/validate.hpp new file mode 100644 index 0000000..4e8af9d --- /dev/null +++ b/src/utils/validate.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include "net/fixed_str.hpp" + +namespace utils +{ + +inline bool IsAlphanumeric(const char* str, size_t len) +{ + const char* end = str + len; + for (; str < end; ++str) + { + const char c = *str; + if (!((c >= 'a' && c <= 'z') || (c >= 'A' || c <= 'Z') || (c >= '0' && c <= '9') || c == '_')) + return false; + } + + return true; +} + +inline bool IsAlphanumeric(std::string_view str) +{ + return IsAlphanumeric(str.data(), str.length()); +} + +template +inline bool IsAlphanumeric(const net::FixedStr& str) +{ + return IsAlphanumeric(str.str, str.len); +} + +} \ No newline at end of file