From e1a5a57fe6b487d5a9ac009498d1e76389e44c1b Mon Sep 17 00:00:00 2001 From: fir <29286243+usermicrodevices@users.noreply.github.com> Date: Sun, 7 Jun 2026 00:37:43 +0300 Subject: [PATCH 1/2] refactor cross-process architecture --- include/process/ProcessPool.hpp | 20 -------------------- src/network/ClientListener.cpp | 3 --- src/network/ConnectionManager.cpp | 14 +------------- src/network/MasterRouter.cpp | 2 -- src/process/IPCChannel.cpp | 9 +-------- src/process/ProcessPool.cpp | 4 ---- 6 files changed, 2 insertions(+), 50 deletions(-) diff --git a/include/process/ProcessPool.hpp b/include/process/ProcessPool.hpp index 4d02ef5..0e8c178 100644 --- a/include/process/ProcessPool.hpp +++ b/include/process/ProcessPool.hpp @@ -7,33 +7,13 @@ #include #include #include -#include -#include #include #include -#include -#include -#include #include -#include -#include -#include -#include -#include -#include #include #include -using asio::ip::tcp; -using asio::awaitable; -using asio::co_spawn; -using asio::detached; -using asio::redirect_error; -using asio::use_awaitable; - -#include - #include "logging/Logger.hpp" #include "config/ConfigManager.hpp" #include "network/BinaryProtocol.hpp" diff --git a/src/network/ClientListener.cpp b/src/network/ClientListener.cpp index b56a02a..a03d8ea 100644 --- a/src/network/ClientListener.cpp +++ b/src/network/ClientListener.cpp @@ -17,9 +17,7 @@ void ClientListener::Start() { std::thread([this]() { manager_->Start(); }).detach(); channel_->Start([this](const IPCEnvelope& env) { onMasterMessage(env); }); ioThread_ = std::thread([this]() { - Logger::Trace("ClientListener::Start: worker {} entering io_context::run()", workerId_); io_.run(); - Logger::Trace("ClientListener::Start: worker {} exiting io_context::run()", workerId_); }); } @@ -41,7 +39,6 @@ void ClientListener::onMasterMessage(const IPCEnvelope& env) { } void ClientListener::sendToMaster(uint32_t correlationId, uint64_t sessionId, uint16_t messageType, const std::vector& body) { - Logger::Trace("ClientListener::sendToMaster: corrId={}, session={}, type={}, bodySize={}", correlationId, sessionId, messageType, body.size()); IPCEnvelope env; env.correlationId = correlationId; env.sessionId = sessionId; diff --git a/src/network/ConnectionManager.cpp b/src/network/ConnectionManager.cpp index fff1a16..fb340b9 100644 --- a/src/network/ConnectionManager.cpp +++ b/src/network/ConnectionManager.cpp @@ -46,11 +46,7 @@ bool ConnectionManager::Start() { } void ConnectionManager::Shutdown() { - if (!running_.exchange(false)) - { - Logger::Trace("ConnectionManager::Shutdown already executed, wait finish..."); - return; - } + if (!running_.exchange(false)) return; acceptor_.close(); std::vector> sessions; { @@ -63,7 +59,6 @@ void ConnectionManager::Shutdown() { ioContext_.stop(); ioContext_.poll(); for (auto& t : workerThreads_) if (t.joinable()) t.join(); - Logger::Trace("ConnectionManager::Shutdown complete"); } void ConnectionManager::initSessionFactory() @@ -234,7 +229,6 @@ void ConnectionManager::doAccept() { void ConnectionManager::onClientMessage(uint64_t sessionId, uint16_t type, const std::vector& data) { uint32_t corrId = nextCorrelationId_++; pendingReplies_[corrId] = {sessionId, type}; - Logger::Trace("ConnectionManager::onClientMessage: sessionId={}, type={}, corrId={}, dataSize={}", sessionId, type, corrId, data.size()); masterSender_(corrId, sessionId, type, data); } @@ -436,11 +430,6 @@ nlohmann::json ConnectionManager::binaryToJson(uint16_t type, const std::vector< } void ConnectionManager::OnMasterPush(uint64_t sessionId, const std::vector& data) { - Logger::Trace("ConnectionManager::OnMasterPush: sessionId={}, dataSize={}", sessionId, data.size()); - if (data.size() >= 4) { - Logger::Trace("First 4 bytes: {:02x} {:02x} {:02x} {:02x}", - data[0], data[1], data[2], data[3]); - } auto sendToSession = [&](const std::shared_ptr& session, const std::vector& payload) { if (session->GetProtocolMode() == ProtocolMode::Binary) { if (payload.size() >= 2) { @@ -475,6 +464,5 @@ void ConnectionManager::OnMasterPush(uint64_t sessionId, const std::vector& data) { void MasterRouter::WireCallbacks() { gameLogic_.SetSendAuthenticationResponseCallback([this](uint64_t session_id, const std::string& message, uint64_t player_id) { - Logger::Trace("MasterRouter auth response: session={}, player={}, message={}", session_id, player_id, message); BinaryProtocol::BinaryWriter w; w.WriteUInt16(BinaryProtocol::MESSAGE_TYPE_AUTHENTICATION); w.WriteUInt64(gameLogic_.GetCurrentTimestamp()); diff --git a/src/process/IPCChannel.cpp b/src/process/IPCChannel.cpp index 10a3690..a1edb6e 100644 --- a/src/process/IPCChannel.cpp +++ b/src/process/IPCChannel.cpp @@ -68,13 +68,9 @@ void IPCChannel::doRead() { if (stopped_) return; auto self = shared_from_this(); auto lengthBuf = std::make_shared(0); - Logger::Trace("IPCChannel::doRead: starting async_read on fd={}, stopped={}", stream_.native_handle(), stopped_.load()); asio::async_read(stream_, asio::buffer(lengthBuf.get(), sizeof(uint32_t)), [self, lengthBuf](std::error_code ec, std::size_t) { - if (ec || self->stopped_) { - Logger::Trace("IPCChannel::doRead: async_read error ec={}, stopped={}", ec.value(), self->stopped_.load()); - return; - } + if (ec || self->stopped_) return; uint32_t msgLen = ntohl(*lengthBuf); if (msgLen == 0 || msgLen > BinaryProtocol::MAX_MESSAGE_SIZE) { self->doRead(); @@ -85,7 +81,6 @@ void IPCChannel::doRead() { [self, msgBuffer](std::error_code ec, std::size_t) { if (ec || self->stopped_) return; IPCEnvelope env = decodeFrame(msgBuffer->data(), msgBuffer->size()); - Logger::Trace("IPCChannel::doRead: received envelope, corrId={}, session={}, type={}, payloadSize={}", env.correlationId, env.sessionId, env.messageType, env.payload.size()); if (self->onMessage_) { self->onMessage_(env); } @@ -112,7 +107,6 @@ void IPCChannel::doWrite() { Logger::Error("IPCChannel::doWrite failed: {}", ec.message()); return; } - Logger::Trace("IPCChannel::doWrite: wrote {} bytes", data.size()); self->doWrite(); }); } @@ -128,7 +122,6 @@ void IPCChannel::SendAsync(const IPCEnvelope& envelope) { return; } } - Logger::Trace("IPCChannel::SendAsync: posting doWrite, frameSize={}, fd={}", frame.size(), stream_.native_handle()); asio::post(io_, [self = shared_from_this()]() { self->doWrite(); }); diff --git a/src/process/ProcessPool.cpp b/src/process/ProcessPool.cpp index 698db80..e57a8de 100644 --- a/src/process/ProcessPool.cpp +++ b/src/process/ProcessPool.cpp @@ -66,11 +66,8 @@ void ProcessPool::Run() { io_.run(); } void ProcessPool::Shutdown() { if (!running_.exchange(false)) return; - Logger::Trace("ProcessPool::Shutdown: running..."); for (auto& w : workers_) w->Shutdown(); - Logger::Trace("ProcessPool::Shutdown: workers shutdown finished"); io_.stop(); - Logger::Trace("ProcessPool::Shutdown: io_.stop finished"); } void ProcessPool::SetWorker(std::function func) { @@ -109,7 +106,6 @@ void ProcessPool::doSpawnWorkers() { if (std::string(err.what()) == "worker_function") { int fd = worker->GetMasterFd(); worker_(globalId, groups_[gi], fd); - Logger::Trace("ProcessPool::doSpawnWorkers: worker {} exiting cleanly", globalId); _exit(0); } else { Logger::Error("ProcessPool::doSpawnWorkers: worker start failed: {}", err.what()); From f109aac0e98ec1fce79777e1c5051bbe0697d11e Mon Sep 17 00:00:00 2001 From: fir <29286243+usermicrodevices@users.noreply.github.com> Date: Sun, 7 Jun 2026 00:38:32 +0300 Subject: [PATCH 2/2] add godot game client project --- clients/godot/.gitignore | 21 + clients/godot/config/loader.gd.uid | 1 + clients/godot/entities/ents.gd | 2 +- clients/godot/entities/ents.gd.uid | 1 + clients/godot/export_presets.cfg | 90 +++ clients/godot/icon.svg | 4 + clients/godot/main.gd | 5 + clients/godot/main.gd.uid | 1 + clients/godot/main.tscn | 36 ++ clients/godot/materials/mob.tres | 7 + clients/godot/materials/npc.tres | 7 + clients/godot/materials/player.tres | 7 + clients/godot/materials/stone.tres | 7 + clients/godot/materials/terrain.tres | 7 + clients/godot/materials/tree.tres | 7 + clients/godot/materials/water.tres | 8 + clients/godot/models/generator.gd | 2 +- clients/godot/models/generator.gd.uid | 1 + clients/godot/network/networks.gd | 726 +++++++++++++---------- clients/godot/network/networks.gd.uid | 1 + clients/godot/player/controller.gd | 6 +- clients/godot/player/controller.gd.uid | 1 + clients/godot/project.godot | 110 ++++ clients/godot/shaders/water.gdshader | 13 + clients/godot/shaders/water.gdshader.uid | 1 + clients/godot/textures/health_bar.svg | 4 + clients/godot/ui/uiface.gd | 4 +- clients/godot/ui/uiface.gd.uid | 1 + clients/godot/world/chunk.gd.uid | 1 + clients/godot/world/terrain.gd.uid | 1 + clients/godot/world/world.gd | 23 +- clients/godot/world/world.gd.uid | 1 + 32 files changed, 785 insertions(+), 322 deletions(-) create mode 100644 clients/godot/.gitignore create mode 100644 clients/godot/config/loader.gd.uid create mode 100644 clients/godot/entities/ents.gd.uid create mode 100644 clients/godot/export_presets.cfg create mode 100644 clients/godot/icon.svg create mode 100644 clients/godot/main.gd.uid create mode 100644 clients/godot/main.tscn create mode 100644 clients/godot/materials/mob.tres create mode 100644 clients/godot/materials/npc.tres create mode 100644 clients/godot/materials/player.tres create mode 100644 clients/godot/materials/stone.tres create mode 100644 clients/godot/materials/terrain.tres create mode 100644 clients/godot/materials/tree.tres create mode 100644 clients/godot/materials/water.tres create mode 100644 clients/godot/models/generator.gd.uid create mode 100644 clients/godot/network/networks.gd.uid create mode 100644 clients/godot/player/controller.gd.uid create mode 100644 clients/godot/project.godot create mode 100644 clients/godot/shaders/water.gdshader create mode 100644 clients/godot/shaders/water.gdshader.uid create mode 100644 clients/godot/textures/health_bar.svg create mode 100644 clients/godot/ui/uiface.gd.uid create mode 100644 clients/godot/world/chunk.gd.uid create mode 100644 clients/godot/world/terrain.gd.uid create mode 100644 clients/godot/world/world.gd.uid diff --git a/clients/godot/.gitignore b/clients/godot/.gitignore new file mode 100644 index 0000000..2e58676 --- /dev/null +++ b/clients/godot/.gitignore @@ -0,0 +1,21 @@ +# Godot 4 specific +.godot/ +*.import + +# Builds +build/ + +# User-specific +*.user +*.userprefs + +# OS files +.DS_Store +Thumbs.db + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ diff --git a/clients/godot/config/loader.gd.uid b/clients/godot/config/loader.gd.uid new file mode 100644 index 0000000..29c41f9 --- /dev/null +++ b/clients/godot/config/loader.gd.uid @@ -0,0 +1 @@ +uid://jq432e34g4ie diff --git a/clients/godot/entities/ents.gd b/clients/godot/entities/ents.gd index 05d0fb1..5ddbdf8 100644 --- a/clients/godot/entities/ents.gd +++ b/clients/godot/entities/ents.gd @@ -271,7 +271,7 @@ func add_interaction_area(entity_node: Node3D, entity_type: int, data: Dictionar func add_health_display(entity_node: Node3D, data: Dictionary): # Create health bar UI var health_bar = Sprite3D.new() - health_bar.texture = preload("res://textures/health_bar.png") + health_bar.texture = preload("res://textures/health_bar.svg") health_bar.pixel_size = 0.01 health_bar.billboard = BaseMaterial3D.BILLBOARD_ENABLED health_bar.position = Vector3(0, 1.5, 0) diff --git a/clients/godot/entities/ents.gd.uid b/clients/godot/entities/ents.gd.uid new file mode 100644 index 0000000..f9f9c85 --- /dev/null +++ b/clients/godot/entities/ents.gd.uid @@ -0,0 +1 @@ +uid://bwq4tcxq2qbgo diff --git a/clients/godot/export_presets.cfg b/clients/godot/export_presets.cfg new file mode 100644 index 0000000..82902e3 --- /dev/null +++ b/clients/godot/export_presets.cfg @@ -0,0 +1,90 @@ +[preset.0] + +name="Linux/X11" +platform="Linux" +runnable=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="build/linux/gameserver_client.x86_64" +encryption_include_filters="" +encryption_exclude_filters="" +encrypt_pck=false +encrypt_directory=false + +[preset.0.options] + +custom_template/debug="" +custom_template/release="" +debug/export_console_wrapper=1 +binary_format/embed_pck=false +texture_format/s3tc_bptc=true +texture_format/etc2_astc=false +binary_format/architecture="x86_64" + +[preset.1] + +name="Windows" +platform="Windows" +runnable=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="build/windows/gameserver_client.exe" +encryption_include_filters="" +encryption_exclude_filters="" +encrypt_pck=false +encrypt_directory=false + +[preset.1.options] + +custom_template/debug="" +custom_template/release="" +debug/export_console_wrapper=1 +binary_format/embed_pck=false +texture_format/s3tc_bptc=true +texture_format/etc2_astc=false +binary_format/architecture="x86_64" +codesign/enable=false + +[preset.2] + +name="Web" +platform="Web" +runnable=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="build/web/index.html" +encryption_include_filters="" +encryption_exclude_filters="" +encrypt_pck=false +encrypt_directory=false + +[preset.2.options] + +custom_template/debug="" +custom_template/release="" +variant/extensions_support=false +vram_texture_compression/for_desktop=true +vram_texture_compression/for_mobile=false +html/export_icon=true +html/custom_html_shell="" +html/head_include="" +html/canvas_resize_policy=2 +html/focus_canvas_on_start=true +html/experimental_virtual_keyboard=false +progressive_web_app/enabled=false +progressive_web_app/offline_page="" +progressive_web_app/display=1 +progressive_web_app/orientation=0 +progressive_web_app/icon_144x144="" +progressive_web_app/icon_180x180="" +progressive_web_app/icon_512x512="" +progressive_web_app/background_color=Color(0, 0, 0, 1) diff --git a/clients/godot/icon.svg b/clients/godot/icon.svg new file mode 100644 index 0000000..b3ba8bf --- /dev/null +++ b/clients/godot/icon.svg @@ -0,0 +1,4 @@ + + + GS + diff --git a/clients/godot/main.gd b/clients/godot/main.gd index f14f1ef..a412719 100644 --- a/clients/godot/main.gd +++ b/clients/godot/main.gd @@ -29,6 +29,11 @@ var chunk_height: int = 64 var lod_distances: Array = [20.0, 40.0, 80.0] var max_entities: int = 100 +func setup_input(): + # Input is already configured via project settings. + # Additional runtime input setup can go here. + pass + func _ready(): load_settings() setup_network_signals() diff --git a/clients/godot/main.gd.uid b/clients/godot/main.gd.uid new file mode 100644 index 0000000..f014eff --- /dev/null +++ b/clients/godot/main.gd.uid @@ -0,0 +1 @@ +uid://brbguaumulrvw diff --git a/clients/godot/main.tscn b/clients/godot/main.tscn new file mode 100644 index 0000000..bfac8f6 --- /dev/null +++ b/clients/godot/main.tscn @@ -0,0 +1,36 @@ +[gd_scene load_steps=2 format=3 uid="uid://c1234"] + +[ext_resource type="Script" path="res://main.gd" id="1"] + +[node name="GameClient" type="Node3D"] +script = ExtResource("1") + +[node name="World" type="Node3D" parent="."] + +[node name="Player" type="CharacterBody3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0) + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Player"] + +[node name="CameraPivot" type="Node3D" parent="Player"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6, 0) + +[node name="Camera3D" type="Camera3D" parent="Player/CameraPivot"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 3) +fov = 70.0 + +[node name="UI" type="Control" parent="."] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 + +[node name="NetworkManager" type="Node" parent="."] + +[node name="ModelGenerator" type="Node" parent="."] + +[node name="EntityManager" type="Node" parent="."] + +[node name="ConfigLoader" type="Node" parent="."] diff --git a/clients/godot/materials/mob.tres b/clients/godot/materials/mob.tres new file mode 100644 index 0000000..31467b2 --- /dev/null +++ b/clients/godot/materials/mob.tres @@ -0,0 +1,7 @@ +[gd_resource type="StandardMaterial3D" format=3] + +[resource] +resource_name = "Mob" +albedo_color = Color(0.8, 0.2, 0.2, 1) +roughness = 0.7 +metallic = 0.0 diff --git a/clients/godot/materials/npc.tres b/clients/godot/materials/npc.tres new file mode 100644 index 0000000..91c2886 --- /dev/null +++ b/clients/godot/materials/npc.tres @@ -0,0 +1,7 @@ +[gd_resource type="StandardMaterial3D" format=3] + +[resource] +resource_name = "NPC" +albedo_color = Color(0.8, 0.6, 0.2, 1) +roughness = 0.7 +metallic = 0.0 diff --git a/clients/godot/materials/player.tres b/clients/godot/materials/player.tres new file mode 100644 index 0000000..a984163 --- /dev/null +++ b/clients/godot/materials/player.tres @@ -0,0 +1,7 @@ +[gd_resource type="StandardMaterial3D" format=3] + +[resource] +resource_name = "Player" +albedo_color = Color(0.3, 0.5, 0.8, 1) +roughness = 0.7 +metallic = 0.0 diff --git a/clients/godot/materials/stone.tres b/clients/godot/materials/stone.tres new file mode 100644 index 0000000..c443f6a --- /dev/null +++ b/clients/godot/materials/stone.tres @@ -0,0 +1,7 @@ +[gd_resource type="StandardMaterial3D" format=3] + +[resource] +resource_name = "Stone" +albedo_color = Color(0.5, 0.5, 0.5, 1) +roughness = 0.9 +metallic = 0.1 diff --git a/clients/godot/materials/terrain.tres b/clients/godot/materials/terrain.tres new file mode 100644 index 0000000..d736353 --- /dev/null +++ b/clients/godot/materials/terrain.tres @@ -0,0 +1,7 @@ +[gd_resource type="StandardMaterial3D" format=3] + +[resource] +resource_name = "Terrain" +albedo_color = Color(0.4, 0.6, 0.3, 1) +roughness = 0.9 +metallic = 0.0 diff --git a/clients/godot/materials/tree.tres b/clients/godot/materials/tree.tres new file mode 100644 index 0000000..17f5528 --- /dev/null +++ b/clients/godot/materials/tree.tres @@ -0,0 +1,7 @@ +[gd_resource type="StandardMaterial3D" format=3] + +[resource] +resource_name = "Tree" +albedo_color = Color(0.3, 0.5, 0.2, 1) +roughness = 0.8 +metallic = 0.0 diff --git a/clients/godot/materials/water.tres b/clients/godot/materials/water.tres new file mode 100644 index 0000000..73dff6f --- /dev/null +++ b/clients/godot/materials/water.tres @@ -0,0 +1,8 @@ +[gd_resource type="StandardMaterial3D" format=3] + +[resource] +resource_name = "Water" +transparency = 1 +albedo_color = Color(0.2, 0.4, 0.8, 0.7) +metallic = 0.3 +roughness = 0.1 diff --git a/clients/godot/models/generator.gd b/clients/godot/models/generator.gd index 1d3da92..84c96cb 100644 --- a/clients/godot/models/generator.gd +++ b/clients/godot/models/generator.gd @@ -218,7 +218,7 @@ func generate_river_model(data: Dictionary) -> MeshInstance3D: mesh_instance.mesh = plane_mesh var material = ShaderMaterial.new() - var shader = preload("res://shaders/water.shader") + var shader = preload("res://shaders/water.gdshader") material.shader = shader if data.has("color"): diff --git a/clients/godot/models/generator.gd.uid b/clients/godot/models/generator.gd.uid new file mode 100644 index 0000000..0039c02 --- /dev/null +++ b/clients/godot/models/generator.gd.uid @@ -0,0 +1 @@ +uid://xqpklfwcaoh4 diff --git a/clients/godot/network/networks.gd b/clients/godot/network/networks.gd index 2ee918e..6b285f0 100644 --- a/clients/godot/network/networks.gd +++ b/clients/godot/network/networks.gd @@ -1,50 +1,56 @@ extends Node -class_name NetworkManager - # Protocol types enum ProtocolType { BINARY = 0, WEBSOCKET = 1, } -# Message types matching server +# Message types matching server BinaryProtocol.hpp exactly enum MessageType { MESSAGE_TYPE_INVALID = 0, - MESSAGE_TYPE_HEARTBEAT = 1, - MESSAGE_TYPE_PROTOCOL_NEGOTIATION = 2, - MESSAGE_TYPE_AUTHENTICATION = 3, - MESSAGE_TYPE_ERROR = 4, - MESSAGE_TYPE_SUCCESS = 5, - MESSAGE_TYPE_CHUNK_DATA = 100, - MESSAGE_TYPE_CHUNK_REQUEST = 101, - MESSAGE_TYPE_TERRAIN_HEIGHT = 102, - MESSAGE_TYPE_BIOME_DATA = 103, - MESSAGE_TYPE_ENTITY_SPAWN = 200, - MESSAGE_TYPE_ENTITY_UPDATE = 201, - MESSAGE_TYPE_ENTITY_DESPAWN = 202, - MESSAGE_TYPE_ENTITY_BATCH_UPDATE = 203, - MESSAGE_TYPE_PLAYER_POSITION = 300, - MESSAGE_TYPE_PLAYER_VELOCITY = 301, - MESSAGE_TYPE_PLAYER_ROTATION = 302, - MESSAGE_TYPE_PLAYER_STATE = 303, - MESSAGE_TYPE_PLAYER_POSITION_CORRECTION = 304, - MESSAGE_TYPE_NPC_SPAWN = 400, - MESSAGE_TYPE_NPC_UPDATE = 401, - MESSAGE_TYPE_NPC_DESPAWN = 402, - MESSAGE_TYPE_NPC_INTERACTION = 403, - MESSAGE_TYPE_COMBAT_EVENT = 500, - MESSAGE_TYPE_DAMAGE_EVENT = 501, - MESSAGE_TYPE_HEALTH_UPDATE = 502, - MESSAGE_TYPE_INVENTORY_UPDATE = 600, - MESSAGE_TYPE_LOOT_SPAWN = 601, - MESSAGE_TYPE_LOOT_PICKUP = 602, - MESSAGE_TYPE_CHAT_MESSAGE = 700, - MESSAGE_TYPE_SYSTEM_MESSAGE = 701, - MESSAGE_TYPE_CUSTOM_EVENT = 1000 + MESSAGE_TYPE_HEARTBEAT = 100, + MESSAGE_TYPE_PROTOCOL_NEGOTIATION = 102, + MESSAGE_TYPE_AUTHENTICATION = 103, + MESSAGE_TYPE_ERROR = 104, + MESSAGE_TYPE_SUCCESS = 105, + MESSAGE_TYPE_COLLISION_CHECK = 150, + MESSAGE_TYPE_CHUNK_PARAMS = 200, + MESSAGE_TYPE_CHUNK_DATA = 201, + MESSAGE_TYPE_BIOME_DATA = 202, + MESSAGE_TYPE_PLAYER_CONNECT = 300, + MESSAGE_TYPE_PLAYER_DISCONNECT = 301, + MESSAGE_TYPE_PLAYER_STATE = 302, + MESSAGE_TYPE_PLAYER_SPAWN = 303, + MESSAGE_TYPE_PLAYER_DESPAWN = 304, + MESSAGE_TYPE_PLAYER_UPDATE = 305, + MESSAGE_TYPE_PLAYER_VELOCITY = 306, + MESSAGE_TYPE_PLAYER_ROTATION = 307, + MESSAGE_TYPE_PLAYER_POSITION = 308, + MESSAGE_TYPE_PLAYER_POSITION_CORRECTION = 309, + MESSAGE_TYPE_PLAYERS_UPDATE = 350, + MESSAGE_TYPE_ENTITY_SPAWN = 400, + MESSAGE_TYPE_ENTITY_UPDATE = 401, + MESSAGE_TYPE_ENTITY_DESPAWN = 402, + MESSAGE_TYPE_ENTITY_BATCH_UPDATE = 403, + MESSAGE_TYPE_NPC_SPAWN = 500, + MESSAGE_TYPE_NPC_UPDATE = 501, + MESSAGE_TYPE_NPC_DESPAWN = 502, + MESSAGE_TYPE_NPC_INTERACTION = 503, + MESSAGE_TYPE_COMBAT_EVENT = 600, + MESSAGE_TYPE_DAMAGE_EVENT = 601, + MESSAGE_TYPE_HEALTH_UPDATE = 602, + MESSAGE_TYPE_LOOT_SPAWN = 700, + MESSAGE_TYPE_LOOT_PICKUP = 701, + MESSAGE_TYPE_INVENTORY_UPDATE = 702, + MESSAGE_TYPE_INVENTORY_MOVE = 703, + MESSAGE_TYPE_CHAT_MESSAGE = 800, + MESSAGE_TYPE_SYSTEM_MESSAGE = 801, + MESSAGE_TYPE_FAMILIAR_COMMAND = 900, + MESSAGE_TYPE_CUSTOM_EVENT = 1000, } -# Protocol flags +# Protocol flags matching server enum ProtocolFlags { FLAG_COMPRESSED = 0x01, FLAG_ENCRYPTED = 0x02, @@ -78,6 +84,7 @@ var protocol_type: int = ProtocolType.BINARY var use_ssl: bool = false var reconnect_attempts: int = 3 var reconnect_delay: float = 2.0 +var ws_path: String = "/game" # Network objects var tcp_client: StreamPeerTCP @@ -93,6 +100,8 @@ var sequence_number: int = 0 var pending_acks: Dictionary = {} var last_heartbeat: float = 0.0 var heartbeat_interval: float = 5.0 +var protocol_negotiated: bool = false +var recv_buffer: PackedByteArray = PackedByteArray() # Quality monitoring var network_quality: Dictionary = { @@ -130,13 +139,15 @@ func _process(delta): func connect_to_server(address: String, port: int, protocol: int = ProtocolType.BINARY): if connection_state != ConnectionState.DISCONNECTED: disconnect_from_server() - + server_address = address server_port = port protocol_type = protocol - + protocol_negotiated = false + recv_buffer = PackedByteArray() + set_connection_state(ConnectionState.CONNECTING) - + match protocol_type: ProtocolType.BINARY: connect_binary() @@ -151,7 +162,7 @@ func disconnect_from_server(): ProtocolType.WEBSOCKET: if websocket: websocket.close() - + set_connection_state(ConnectionState.DISCONNECTED) cleanup() @@ -160,109 +171,222 @@ func send_message(message_type: int, data: PackedByteArray, reliable: bool = tru outgoing_queue.append(message) func send_json_message(message_type: int, json_data: Dictionary): - var json_string = JSON.stringify(json_data) - var data = json_string.to_utf8_buffer() - send_message(message_type, data) + if protocol_type == ProtocolType.WEBSOCKET: + send_ws_json(json_data) + else: + var json_string = JSON.stringify(json_data) + var data = json_string.to_utf8_buffer() + send_message(message_type, data) func send_binary_message(message_type: int, binary_data: PackedByteArray): send_message(message_type, binary_data) func authenticate(username: String, password: String): - var auth_data = { - "username": username, - "password": password, - "client_version": "1.0.0" - } - send_json_message(MessageType.MESSAGE_TYPE_AUTHENTICATION, auth_data) + if protocol_type == ProtocolType.WEBSOCKET: + send_ws_json({ + "msg": "authentication", + "login": username, + "password": password, + }) + else: + var body = PackedByteArray() + body.append_array(_write_uint64(0)) + body.append_array(_write_string(username)) + body.append_array(_write_string(password)) + send_message(MessageType.MESSAGE_TYPE_AUTHENTICATION, body) set_connection_state(ConnectionState.AUTHENTICATING) func request_chunk(chunk_x: int, chunk_z: int): - var request_data = { - "chunk_x": chunk_x, - "chunk_z": chunk_z - } - send_json_message(MessageType.MESSAGE_TYPE_CHUNK_REQUEST, request_data) + if protocol_type == ProtocolType.WEBSOCKET: + send_ws_json({ + "msg": "get_chunk", + "x": chunk_x, + "z": chunk_z, + }) + else: + var body = PackedByteArray() + body.append_array(_write_int32(chunk_x)) + body.append_array(_write_int32(chunk_z)) + send_message(MessageType.MESSAGE_TYPE_CHUNK_DATA, body) func update_player_position(position: Vector3, rotation: Vector3, velocity: Vector3 = Vector3.ZERO): - var pos_data = { - "position": { + if protocol_type == ProtocolType.WEBSOCKET: + send_ws_json({ + "msg": "player_position", + "player_id": player_id, "x": position.x, "y": position.y, - "z": position.z - }, - "rotation": { - "x": rotation.x, - "y": rotation.y, - "z": rotation.z - }, - "velocity": { - "x": velocity.x, - "y": velocity.y, - "z": velocity.z - }, - "timestamp": Time.get_ticks_msec(), - "input_id": sequence_number - } - send_json_message(MessageType.MESSAGE_TYPE_PLAYER_POSITION, pos_data) - + "z": position.z, + "vx": velocity.x, + "vy": velocity.y, + "vz": velocity.z, + "timestamp": Time.get_ticks_msec(), + }) + else: + var body = PackedByteArray() + body.append_array(_write_uint64(player_id)) + body.append_array(_write_vector3(position)) + body.append_array(_write_vector3(velocity)) + body.append_array(_write_uint64(Time.get_ticks_msec())) + send_message(MessageType.MESSAGE_TYPE_PLAYER_POSITION, body) + if prediction_enabled: store_input(sequence_number, position, rotation, velocity) sequence_number += 1 -func send_chat_message(message: String, channel: String = "global"): - var chat_data = { - "message": message, - "channel": channel, - "timestamp": Time.get_ticks_msec() - } - send_json_message(MessageType.MESSAGE_TYPE_CHAT_MESSAGE, chat_data) +func send_chat_message(message: String, sender: String = "Player"): + if protocol_type == ProtocolType.WEBSOCKET: + send_ws_json({ + "msg": "chat_message", + "sender": sender, + "message": message, + "timestamp": Time.get_ticks_msec(), + }) + else: + var body = PackedByteArray() + body.append_array(_write_string(sender)) + body.append_array(_write_string(message)) + body.append_array(_write_uint64(Time.get_ticks_msec())) + send_message(MessageType.MESSAGE_TYPE_CHAT_MESSAGE, body) func interact_with_entity(entity_id: int, interaction_type: String = "use"): - var interaction_data = { - "entity_id": entity_id, - "interaction_type": interaction_type, - "timestamp": Time.get_ticks_msec() - } - send_json_message(MessageType.MESSAGE_TYPE_NPC_INTERACTION, interaction_data) + if protocol_type == ProtocolType.WEBSOCKET: + send_ws_json({ + "msg": "npc_interaction", + "npc_id": entity_id, + "npc_type": interaction_type, + }) + else: + var body = PackedByteArray() + body.append_array(_write_uint64(entity_id)) + body.append_array(_write_string(interaction_type)) + send_message(MessageType.MESSAGE_TYPE_NPC_INTERACTION, body) # Message handlers func setup_message_handlers(): - # System messages message_handlers[MessageType.MESSAGE_TYPE_HEARTBEAT] = _handle_heartbeat message_handlers[MessageType.MESSAGE_TYPE_SUCCESS] = _handle_success message_handlers[MessageType.MESSAGE_TYPE_ERROR] = _handle_error - - # World messages message_handlers[MessageType.MESSAGE_TYPE_CHUNK_DATA] = _handle_chunk_data - message_handlers[MessageType.MESSAGE_TYPE_TERRAIN_HEIGHT] = _handle_terrain_height message_handlers[MessageType.MESSAGE_TYPE_BIOME_DATA] = _handle_biome_data - - # Entity messages message_handlers[MessageType.MESSAGE_TYPE_ENTITY_SPAWN] = _handle_entity_spawn message_handlers[MessageType.MESSAGE_TYPE_ENTITY_UPDATE] = _handle_entity_update message_handlers[MessageType.MESSAGE_TYPE_ENTITY_DESPAWN] = _handle_entity_despawn - - # Player messages message_handlers[MessageType.MESSAGE_TYPE_PLAYER_POSITION_CORRECTION] = _handle_position_correction + message_handlers[MessageType.MESSAGE_TYPE_PLAYER_STATE] = _handle_player_state message_handlers[MessageType.MESSAGE_TYPE_HEALTH_UPDATE] = _handle_health_update - - # Inventory messages message_handlers[MessageType.MESSAGE_TYPE_INVENTORY_UPDATE] = _handle_inventory_update message_handlers[MessageType.MESSAGE_TYPE_LOOT_SPAWN] = _handle_loot_spawn - - # Chat messages message_handlers[MessageType.MESSAGE_TYPE_CHAT_MESSAGE] = _handle_chat_message message_handlers[MessageType.MESSAGE_TYPE_SYSTEM_MESSAGE] = _handle_system_message -# Private methods +# --- Binary protocol helpers (big-endian body fields, matching server BinaryReader) --- + +func _write_uint8(value: int) -> PackedByteArray: + return PackedByteArray([value & 0xFF]) + +func _write_uint16(value: int) -> PackedByteArray: + return PackedByteArray([(value >> 8) & 0xFF, value & 0xFF]) + +func _write_uint32(value: int) -> PackedByteArray: + return PackedByteArray([ + (value >> 24) & 0xFF, + (value >> 16) & 0xFF, + (value >> 8) & 0xFF, + value & 0xFF + ]) + +func _write_uint64(value: int) -> PackedByteArray: + return PackedByteArray([ + (value >> 56) & 0xFF, + (value >> 48) & 0xFF, + (value >> 40) & 0xFF, + (value >> 32) & 0xFF, + (value >> 24) & 0xFF, + (value >> 16) & 0xFF, + (value >> 8) & 0xFF, + value & 0xFF + ]) + +func _write_int32(value: int) -> PackedByteArray: + return _write_uint32(value) + +func _write_float(value: float) -> PackedByteArray: + var int_val = int(value) + return _write_uint32(int_val) + +func _write_vector3(v: Vector3) -> PackedByteArray: + var buf = PackedByteArray() + buf.append_array(_write_float(v.x)) + buf.append_array(_write_float(v.y)) + buf.append_array(_write_float(v.z)) + return buf + +func _write_string(s: String) -> PackedByteArray: + var encoded = s.to_utf8_buffer() + var buf = PackedByteArray() + buf.append_array(_write_uint16(encoded.size())) + buf.append_array(encoded) + return buf + +func _read_uint8(data: PackedByteArray, offset: int) -> int: + return data[offset] + +func _read_uint16_be(data: PackedByteArray, offset: int) -> int: + return (data[offset] << 8) | data[offset + 1] + +func _read_uint32_be(data: PackedByteArray, offset: int) -> int: + return (data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3] + +func _read_uint64_be(data: PackedByteArray, offset: int) -> int: + var val: int = 0 + for i in range(8): + val = (val << 8) | data[offset + i] + return val + +func _read_int32_be(data: PackedByteArray, offset: int) -> int: + return _read_uint32_be(data, offset) + +func _read_float_be(data: PackedByteArray, offset: int) -> float: + return float(_read_uint32_be(data, offset)) + +func _read_vector3_be(data: PackedByteArray, offset: int) -> Vector3: + return Vector3( + _read_float_be(data, offset), + _read_float_be(data, offset + 4), + _read_float_be(data, offset + 8) + ) + +func _read_string_be(data: PackedByteArray, offset: int) -> String: + var length = _read_uint16_be(data, offset) + offset += 2 + if offset + length > data.size(): + return "" + return data.slice(offset, offset + length).get_string_from_utf8() + +# --- CRC32 (matching server polynomial 0xEDB88320) --- + +func _crc32(data: PackedByteArray) -> int: + var crc: int = 0xFFFFFFFF + for i in range(data.size()): + crc ^= data[i] + for _j in range(8): + if crc & 1: + crc = (crc >> 1) ^ 0xEDB88320 + else: + crc = crc >> 1 + return ~crc & 0xFFFFFFFF + +# --- Connection --- + func connect_binary(): tcp_client = StreamPeerTCP.new() var error = tcp_client.connect_to_host(server_address, server_port) - + if error == OK: if use_ssl: ssl_context = StreamPeerTLS.new() - error = ssl_context.connect_to_stream(tcp_client) + error = ssl_context.connect_to_stream(tcp_client, server_address) if error == OK: set_connection_state(ConnectionState.HANDSHAKE) else: @@ -276,18 +400,20 @@ func connect_binary(): func connect_websocket(): websocket = WebSocketPeer.new() - var url = "ws://%s:%d" % [server_address, server_port] + var url = "ws://%s:%d%s" % [server_address, server_port, ws_path] if use_ssl: - url = "wss://%s:%d" % [server_address, server_port] - + url = "wss://%s:%d%s" % [server_address, server_port, ws_path] + var error = websocket.connect_to_url(url) - + if error == OK: set_connection_state(ConnectionState.HANDSHAKE) else: set_connection_state(ConnectionState.ERROR) emit_signal("error_occurred", error, "WebSocket connection failed") +# --- Network processing --- + func process_network(): match protocol_type: ProtocolType.BINARY: @@ -298,30 +424,65 @@ func process_network(): func process_binary(): if not tcp_client or tcp_client.get_status() != StreamPeerTCP.STATUS_CONNECTED: return - - # Check for incoming data - var available_bytes = tcp_client.get_available_bytes() - if available_bytes > 0: - var data = tcp_client.get_data(available_bytes) - if data[0] == OK: - parse_binary_data(data[1]) + + var available = tcp_client.get_available_bytes() + if available > 0: + var result = tcp_client.get_data(available) + if result[0] == OK: + var new_data = result[1] + recv_buffer.append_array(new_data) + process_recv_buffer() + +func process_recv_buffer(): + while recv_buffer.size() >= 20: + var msg_type = _read_uint16_be(recv_buffer, 2) + var length = _read_uint32_be(recv_buffer, 12) + + if length > 10 * 1024 * 1024: + recv_buffer = PackedByteArray() + return + + var total = 20 + length + if recv_buffer.size() < total: + return + + var message = { + "version": recv_buffer[0], + "flags": recv_buffer[1], + "message_type": msg_type, + "sequence": _read_uint32_be(recv_buffer, 4), + "timestamp": _read_uint32_be(recv_buffer, 8), + "length": length, + } + + if length > 0: + message.data = recv_buffer.slice(20, total) + else: + message.data = PackedByteArray() + + recv_buffer = recv_buffer.slice(total, recv_buffer.size()) + + if message.flags & ProtocolFlags.FLAG_RELIABLE: + handle_ack(message.sequence) + + incoming_queue.append(message) func process_websocket(): if not websocket: return - + websocket.poll() - + var state = websocket.get_ready_state() match state: WebSocketPeer.STATE_OPEN: if connection_state != ConnectionState.CONNECTED: set_connection_state(ConnectionState.CONNECTED) - + while websocket.get_available_packet_count(): var packet = websocket.get_packet() parse_websocket_data(packet) - + WebSocketPeer.STATE_CLOSED: var code = websocket.get_close_code() var reason = websocket.get_close_reason() @@ -329,193 +490,122 @@ func process_websocket(): emit_signal("error_occurred", code, "WebSocket closed: %s" % reason) func process_queues(): - # Process incoming messages while incoming_queue.size() > 0: var message = incoming_queue.pop_front() handle_incoming_message(message) - - # Process outgoing messages + while outgoing_queue.size() > 0 and connection_state == ConnectionState.CONNECTED: var message = outgoing_queue.pop_front() send_network_message(message) -func check_heartbeat(delta: float): - last_heartbeat += delta - if last_heartbeat >= heartbeat_interval: - send_heartbeat() - last_heartbeat = 0.0 - -func send_heartbeat(): - var heartbeat_data = PackedByteArray() - send_message(MessageType.MESSAGE_TYPE_HEARTBEAT, heartbeat_data) - -func create_message(message_type: int, data: PackedByteArray, reliable: bool, priority: int) -> Dictionary: - var flags: int = 0 - if reliable: - flags |= ProtocolFlags.FLAG_RELIABLE - - match priority: - 1: - flags |= ProtocolFlags.FLAG_PRIORITY_HIGH - -1: - flags |= ProtocolFlags.FLAG_PRIORITY_LOW - - var message = { - "version": 1, - "flags": flags, - "message_type": message_type, - "sequence": get_next_sequence(), - "timestamp": Time.get_ticks_msec(), - "data": data - } - - if reliable: - pending_acks[message.sequence] = { - "message": message, - "timestamp": Time.get_ticks_msec(), - "retries": 0 - } - - return message - -func get_next_sequence() -> int: - var seq = sequence_number - sequence_number = (sequence_number + 1) % 65536 - return seq +# --- Serialization --- func send_network_message(message: Dictionary): - var packet = serialize_message(message) - match protocol_type: ProtocolType.BINARY: + var packet = serialize_binary_message(message) if use_ssl and ssl_context: ssl_context.put_data(packet) elif tcp_client: tcp_client.put_data(packet) - + ProtocolType.WEBSOCKET: - if websocket: - websocket.send(packet) + pass -func serialize_message(message: Dictionary) -> PackedByteArray: +func send_ws_json(json_data: Dictionary): + if websocket: + websocket.send(JSON.stringify(json_data).to_utf8_buffer()) + +func serialize_binary_message(message: Dictionary) -> PackedByteArray: var buffer = PackedByteArray() - - # Header (28 bytes) - buffer.append(message.version) # version - buffer.append(message.flags) # flags - - # message_type (2 bytes) - buffer.append((message.message_type >> 8) & 0xFF) + + # Header: all little-endian matching server memcpy on x86 + buffer.append(message.version & 0xFF) + buffer.append(message.flags & 0xFF) + + # message_type (2 bytes, little-endian) buffer.append(message.message_type & 0xFF) - - # sequence (4 bytes) + buffer.append((message.message_type >> 8) & 0xFF) + + # sequence (4 bytes, little-endian) for i in range(4): buffer.append((message.sequence >> (i * 8)) & 0xFF) - - # timestamp (4 bytes) + + # timestamp (4 bytes, little-endian) var timestamp = message.timestamp for i in range(4): buffer.append((timestamp >> (i * 8)) & 0xFF) - - # length (4 bytes) + + # length (4 bytes, little-endian) var length = message.data.size() for i in range(4): buffer.append((length >> (i * 8)) & 0xFF) - - # checksum (4 bytes) - placeholder + + # checksum (4 bytes, placeholder, little-endian) for i in range(4): buffer.append(0) - + # Data buffer.append_array(message.data) - - # Calculate and update checksum - update_checksum(buffer) - + + # Calculate CRC32 checksum over body only (matching server) + var body_bytes = message.data + if body_bytes.size() > 0: + var checksum = _crc32(body_bytes) + for i in range(4): + buffer[20 + i] = (checksum >> (i * 8)) & 0xFF + return buffer -func update_checksum(buffer: PackedByteArray): - # Simple XOR checksum for now - var checksum: int = 0 - for i in range(24, buffer.size()): # Skip header - checksum ^= buffer[i] - - # Update checksum in buffer (positions 24-27) - for i in range(4): - buffer[24 + i] = (checksum >> (i * 8)) & 0xFF +# --- Incoming parsing --- + +func parse_websocket_data(data: PackedByteArray): + var json_string = data.get_string_from_utf8() + if json_string == "": + return + + var json = JSON.new() + var error = json.parse(json_string) + if error != OK: + return -func parse_binary_data(data: PackedByteArray): - if data.size() < 28: + var message_data = json.data + if not message_data is Dictionary: return - + var message = { - "version": data[0], - "flags": data[1], - "message_type": (data[2] << 8) | data[3], - "sequence": (data[4] << 24) | (data[5] << 16) | (data[6] << 8) | data[7], - "timestamp": (data[8] << 24) | (data[9] << 16) | (data[10] << 8) | data[11], - "length": (data[12] << 24) | (data[13] << 16) | (data[14] << 8) | data[15] + "message_type": 0, + "data": message_data, + "flags": 0, + "sequence": 0, + "timestamp": 0 } - - # Verify checksum - if not verify_checksum(data): - print("Checksum verification failed for message type: ", message.message_type) - return - - # Extract data - if message.length > 0 and data.size() >= 28 + message.length: - message.data = data.slice(28, 28 + message.length) - incoming_queue.append(message) - - # Handle ACKs - if message.flags & ProtocolFlags.FLAG_RELIABLE: - handle_ack(message.sequence) -func parse_websocket_data(data: PackedByteArray): - # For WebSocket, assume JSON or custom binary format - # Try to parse as JSON first - var json_string = data.get_string_from_utf8() - if json_string != "": - var json = JSON.new() - var error = json.parse(json_string) - if error == OK: - var message_data = json.data - if message_data.has("type") and message_data.has("data"): - var message = { - "message_type": message_data.type, - "data": message_data.data, - "flags": 0, - "sequence": message_data.get("sequence", 0), - "timestamp": message_data.get("timestamp", 0) - } - incoming_queue.append(message) - else: - # Try to parse as binary protocol - parse_binary_data(data) +# --- Checksum --- func verify_checksum(data: PackedByteArray) -> bool: - # Simple XOR checksum verification - var checksum: int = 0 - for i in range(24, data.size()): - checksum ^= data[i] - - var stored_checksum: int = 0 + if data.size() < 20: + return false + var length = _read_uint32_be(data, 12) + if data.size() < 20 + length: + return false + var stored = 0 for i in range(4): - stored_checksum |= (data[24 + i] << (i * 8)) - - return checksum == stored_checksum + stored |= (data[20 + i] << (i * 8)) + var body = data.slice(20, 20 + length) + return _crc32(body) == stored + +# --- Message dispatch --- func handle_incoming_message(message: Dictionary): var message_type = message.message_type - + if message_handlers.has(message_type): message_handlers[message_type].call(message.data) else: if default_message_handler: default_message_handler.call(message_type, message.data) - else: - print("Unhandled message type: ", message_type) func handle_ack(sequence: int): if pending_acks.has(sequence): @@ -526,7 +616,7 @@ func set_connection_state(new_state: int): if connection_state != new_state: connection_state = new_state emit_signal("connection_state_changed", new_state) - + if new_state == ConnectionState.CONNECTED: set_process(true) elif new_state == ConnectionState.DISCONNECTED: @@ -536,22 +626,69 @@ func cleanup(): tcp_client = null websocket = null ssl_context = null + recv_buffer = PackedByteArray() incoming_queue.clear() outgoing_queue.clear() pending_acks.clear() sequence_number = 0 player_id = 0 auth_token = "" + protocol_negotiated = false func update_network_quality(success: bool): - # Update network quality metrics pass -# Prediction system +# --- Heartbeat --- + +func check_heartbeat(delta: float): + last_heartbeat += delta + if last_heartbeat >= heartbeat_interval: + send_heartbeat() + last_heartbeat = 0.0 + +func send_heartbeat(): + send_message(MessageType.MESSAGE_TYPE_HEARTBEAT, PackedByteArray()) + +func create_message(message_type: int, data: PackedByteArray, reliable: bool, priority: int) -> Dictionary: + var flags: int = 0 + if reliable: + flags |= ProtocolFlags.FLAG_RELIABLE + + match priority: + 1: + flags |= ProtocolFlags.FLAG_PRIORITY_HIGH + -1: + flags |= ProtocolFlags.FLAG_PRIORITY_LOW + + var message = { + "version": 1, + "flags": flags, + "message_type": message_type, + "sequence": get_next_sequence(), + "timestamp": Time.get_ticks_msec(), + "data": data + } + + if reliable: + pending_acks[message.sequence] = { + "message": message, + "timestamp": Time.get_ticks_msec(), + "retries": 0 + } + + return message + +func get_next_sequence() -> int: + var seq = sequence_number + sequence_number = (sequence_number + 1) % 65536 + return seq + +# --- Prediction system --- + func store_input(input_id: int, position: Vector3, rotation: Vector3, velocity: Vector3): if not prediction_enabled: return - + var input = { "input_id": input_id, "position": position, @@ -559,36 +696,29 @@ func store_input(input_id: int, position: Vector3, rotation: Vector3, velocity: "velocity": velocity, "timestamp": Time.get_ticks_msec() } - + input_buffer.append(input) - - # Keep buffer size reasonable + if input_buffer.size() > 100: input_buffer.pop_front() func update_prediction(delta: float): if not prediction_enabled or input_buffer.size() == 0: return - - # Apply prediction to local entities - pass func reconcile_with_server(server_state: Dictionary): if not prediction_enabled: return - - # Find the last confirmed input + var last_confirmed_input_id = server_state.get("last_input_id", 0) - - # Remove processed inputs from buffer + var i = 0 while i < input_buffer.size(): if input_buffer[i].input_id <= last_confirmed_input_id: input_buffer.remove_at(i) else: i += 1 - - # Apply correction if needed + var correction = server_state.get("correction", null) if correction: emit_signal("player_position_corrected", Vector3( @@ -597,27 +727,26 @@ func reconcile_with_server(server_state: Dictionary): correction.z )) -# Message handlers implementation -func _handle_heartbeat(data: PackedByteArray): - # Update last heartbeat time +# --- Message handlers --- + +func _handle_heartbeat(_data: PackedByteArray): last_heartbeat = 0.0 update_network_quality(true) func _handle_success(data: PackedByteArray): + if connection_state == ConnectionState.HANDSHAKE or connection_state == ConnectionState.CONNECTING: + protocol_negotiated = true + set_connection_state(ConnectionState.CONNECTED) + return + var json_string = data.get_string_from_utf8() if json_string: var json = JSON.new() if json.parse(json_string) == OK: var success_data = json.data - if connection_state == ConnectionState.AUTHENTICATING: - if success_data.get("type") == "authentication": - auth_token = success_data.get("auth_token", "") - player_id = success_data.get("player_id", 0) - session_id = success_data.get("session_id", 0) - set_connection_state(ConnectionState.CONNECTED) - emit_signal("authentication_result", true, success_data.get("message", "Authenticated")) - + emit_signal("authentication_result", true, success_data.get("message", "Authenticated")) + set_connection_state(ConnectionState.CONNECTED) emit_signal("message_received", MessageType.MESSAGE_TYPE_SUCCESS, success_data) func _handle_error(data: PackedByteArray): @@ -626,19 +755,14 @@ func _handle_error(data: PackedByteArray): var json = JSON.new() if json.parse(json_string) == OK: var error_data = json.data - if connection_state == ConnectionState.AUTHENTICATING: emit_signal("authentication_result", false, error_data.get("message", "Authentication failed")) - emit_signal("error_occurred", error_data.get("code", -1), error_data.get("message", "Unknown error")) emit_signal("message_received", MessageType.MESSAGE_TYPE_ERROR, error_data) func _handle_chunk_data(data: PackedByteArray): emit_signal("message_received", MessageType.MESSAGE_TYPE_CHUNK_DATA, data) -func _handle_terrain_height(data: PackedByteArray): - emit_signal("message_received", MessageType.MESSAGE_TYPE_TERRAIN_HEIGHT, data) - func _handle_biome_data(data: PackedByteArray): emit_signal("message_received", MessageType.MESSAGE_TYPE_BIOME_DATA, data) @@ -652,22 +776,10 @@ func _handle_entity_despawn(data: PackedByteArray): emit_signal("message_received", MessageType.MESSAGE_TYPE_ENTITY_DESPAWN, data) func _handle_position_correction(data: PackedByteArray): - var json_string = data.get_string_from_utf8() - if json_string: - var json = JSON.new() - if json.parse(json_string) == OK: - var correction_data = json.data - - # Apply prediction reconciliation - if correction_data.has("server_state"): - reconcile_with_server(correction_data.server_state) - - # Emit correction signal - if correction_data.has("position"): - var pos = correction_data.position - emit_signal("player_position_corrected", Vector3(pos.x, pos.y, pos.z)) - - emit_signal("message_received", MessageType.MESSAGE_TYPE_PLAYER_POSITION_CORRECTION, correction_data) + emit_signal("message_received", MessageType.MESSAGE_TYPE_PLAYER_POSITION_CORRECTION, data) + +func _handle_player_state(data: PackedByteArray): + emit_signal("message_received", MessageType.MESSAGE_TYPE_PLAYER_STATE, data) func _handle_health_update(data: PackedByteArray): emit_signal("message_received", MessageType.MESSAGE_TYPE_HEALTH_UPDATE, data) @@ -679,7 +791,17 @@ func _handle_loot_spawn(data: PackedByteArray): emit_signal("message_received", MessageType.MESSAGE_TYPE_LOOT_SPAWN, data) func _handle_chat_message(data: PackedByteArray): - emit_signal("message_received", MessageType.MESSAGE_TYPE_CHAT_MESSAGE, data) + var result = {"sender": "", "message": "", "timestamp": 0} + if data.size() > 0: + var offset = 0 + result.sender = _read_string_be(data, offset) + offset += 2 + result.sender.to_utf8_buffer().size() + if offset < data.size(): + result.message = _read_string_be(data, offset) + offset += 2 + result.message.to_utf8_buffer().size() + if offset + 8 <= data.size(): + result.timestamp = _read_uint64_be(data, offset) + emit_signal("message_received", MessageType.MESSAGE_TYPE_CHAT_MESSAGE, result) func _handle_system_message(data: PackedByteArray): - emit_signal("message_received", MessageType.MESSAGE_TYPE_SYSTEM_MESSAGE, data) \ No newline at end of file + emit_signal("message_received", MessageType.MESSAGE_TYPE_SYSTEM_MESSAGE, data) diff --git a/clients/godot/network/networks.gd.uid b/clients/godot/network/networks.gd.uid new file mode 100644 index 0000000..fc295c5 --- /dev/null +++ b/clients/godot/network/networks.gd.uid @@ -0,0 +1 @@ +uid://cvmgfnqjs26jl diff --git a/clients/godot/player/controller.gd b/clients/godot/player/controller.gd index 53f7b6d..be95b7c 100644 --- a/clients/godot/player/controller.gd +++ b/clients/godot/player/controller.gd @@ -29,7 +29,7 @@ var move_input: Vector2 = Vector2.ZERO var look_input: Vector2 = Vector2.ZERO # Network -var network_manager: NetworkManager +var network_manager var last_sent_position: Vector3 = Vector3.ZERO var position_update_rate: float = 0.1 # 10 times per second var last_update_time: float = 0.0 @@ -68,7 +68,7 @@ func _input(event): toggle_mouse_capture() elif event.keycode == KEY_SHIFT: is_sprinting = true - elif event.keycode == KEY_CONTROL: + elif event.keycode == KEY_CTRL: toggle_crouch() func process_movement(delta): @@ -222,7 +222,7 @@ func send_position_update(): ) last_sent_position = current_position -func set_network_manager(manager: NetworkManager): +func set_network_manager(manager): network_manager = manager func apply_position_correction(corrected_position: Vector3): diff --git a/clients/godot/player/controller.gd.uid b/clients/godot/player/controller.gd.uid new file mode 100644 index 0000000..a8eb174 --- /dev/null +++ b/clients/godot/player/controller.gd.uid @@ -0,0 +1 @@ +uid://cuc8f6a3di2d5 diff --git a/clients/godot/project.godot b/clients/godot/project.godot new file mode 100644 index 0000000..ea75db4 --- /dev/null +++ b/clients/godot/project.godot @@ -0,0 +1,110 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[animation] + +compatibility/default_parent_skeleton_in_mesh_instance_3d=true + +[application] + +config/name="GameServer Client" +config/description="MMORPG client connecting to gameserver via binary/WebSocket protocol" +run/main_scene="res://main.tscn" +config/features=PackedStringArray("4.6") +config/icon="res://icon.svg" + +[autoload] + +NetworkManager="*res://network/networks.gd" + +[display] + +window/size/viewport_width=1280 +window/size/viewport_height=720 +window/stretch/mode="canvas_items" +window/stretch/aspect="expand" + +[input] + +move_forward={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null) +] +} +move_backward={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null) +] +} +move_left={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null) +] +} +move_right={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null) +] +} +jump={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null) +] +} +sprint={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194325,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +crouch={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":67,"key_label":0,"unicode":99,"location":0,"echo":false,"script":null) +] +} +interact={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null) +] +} +chat={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":84,"key_label":0,"unicode":116,"location":0,"echo":false,"script":null) +] +} +toggle_debug={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194344,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +toggle_wireframe={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194345,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +toggle_prediction={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194346,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} + +[layer_names] + +3d_physics/layer_1="World" +3d_physics/layer_2="Player" +3d_physics/layer_3="Entity" +3d_physics/layer_4="Interactable" + +[rendering] + +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility" +textures/default_filters/anisotropic_filtering_level=4 +anti_aliasing/quality/msaa_3d=2 +environment/defaults/default_clear_color=Color(0.3, 0.4, 0.5, 1) diff --git a/clients/godot/shaders/water.gdshader b/clients/godot/shaders/water.gdshader new file mode 100644 index 0000000..a9d8146 --- /dev/null +++ b/clients/godot/shaders/water.gdshader @@ -0,0 +1,13 @@ +shader_type canvas_item; + +uniform vec4 water_color : source_color = vec4(0.2, 0.4, 0.8, 0.7); +uniform float wave_speed = 1.0; +uniform float wave_amplitude = 0.02; +uniform float wave_frequency = 2.0; + +void fragment() { + vec2 uv = UV; + float wave = sin(uv.x * wave_frequency + TIME * wave_speed) * wave_amplitude; + uv.y += wave; + COLOR = water_color; +} diff --git a/clients/godot/shaders/water.gdshader.uid b/clients/godot/shaders/water.gdshader.uid new file mode 100644 index 0000000..24a6194 --- /dev/null +++ b/clients/godot/shaders/water.gdshader.uid @@ -0,0 +1 @@ +uid://cr4davplssiqu diff --git a/clients/godot/textures/health_bar.svg b/clients/godot/textures/health_bar.svg new file mode 100644 index 0000000..b6258cc --- /dev/null +++ b/clients/godot/textures/health_bar.svg @@ -0,0 +1,4 @@ + + + + diff --git a/clients/godot/ui/uiface.gd b/clients/godot/ui/uiface.gd index 58b1b04..07620d9 100644 --- a/clients/godot/ui/uiface.gd +++ b/clients/godot/ui/uiface.gd @@ -12,7 +12,7 @@ class_name UIManager @onready var debug_info = $DebugInfo # Game references -var network_manager: NetworkManager +var network_manager var player_node: Node3D # UI state @@ -40,7 +40,7 @@ func _input(event): elif event.keycode == KEY_TAB: toggle_minimap() -func setup_references(network: NetworkManager, player: Node3D): +func setup_references(network, player: Node3D): network_manager = network player_node = player diff --git a/clients/godot/ui/uiface.gd.uid b/clients/godot/ui/uiface.gd.uid new file mode 100644 index 0000000..9c30fd2 --- /dev/null +++ b/clients/godot/ui/uiface.gd.uid @@ -0,0 +1 @@ +uid://b1387b4oh4cm diff --git a/clients/godot/world/chunk.gd.uid b/clients/godot/world/chunk.gd.uid new file mode 100644 index 0000000..1fc9d66 --- /dev/null +++ b/clients/godot/world/chunk.gd.uid @@ -0,0 +1 @@ +uid://cp83g6n426w1c diff --git a/clients/godot/world/terrain.gd.uid b/clients/godot/world/terrain.gd.uid new file mode 100644 index 0000000..d39dae6 --- /dev/null +++ b/clients/godot/world/terrain.gd.uid @@ -0,0 +1 @@ +uid://bc3l0s7jj3s7k diff --git a/clients/godot/world/world.gd b/clients/godot/world/world.gd index f7cddc5..0faccea 100644 --- a/clients/godot/world/world.gd +++ b/clients/godot/world/world.gd @@ -1,18 +1,15 @@ extends Node3D -class_name ChunkLoader - -func load_chunk(chunk_x: int, chunk_z: int, chunk_size: int) -> Dictionary: - # This would load chunk data from disk or network - # For now, return empty data - return { - "chunk_x": chunk_x, - "chunk_z": chunk_z, - "heightmap": [], - "biomes": [] - } - -class_name GameWorld +class ChunkLoader: + func load_chunk(chunk_x: int, chunk_z: int, chunk_size: int) -> Dictionary: + # This would load chunk data from disk or network + # For now, return empty data + return { + "chunk_x": chunk_x, + "chunk_z": chunk_z, + "heightmap": [], + "biomes": [] + } # World parameters @export var chunk_size: int = 16 diff --git a/clients/godot/world/world.gd.uid b/clients/godot/world/world.gd.uid new file mode 100644 index 0000000..4c111eb --- /dev/null +++ b/clients/godot/world/world.gd.uid @@ -0,0 +1 @@ +uid://d1wm6d3xe6pg7