diff --git a/config/core.json b/config/core.json index 103d290..db15cd0 100644 --- a/config/core.json +++ b/config/core.json @@ -53,64 +53,64 @@ }, "npcs": { - "initialNPCCount": 50, - "maxNPCsPerChunk": 5, - "spawnInterval": 5.0, - "despawnDistance": 150.0 + "initial_count": 50, + "max_per_chunk": 5, + "spawn_interval": 5.0, + "despawn_distance": 150.0 }, "mobs": { "enabled": true, - "spawnZones": [ + "spawn_zones": [ { "name": "goblin_forest", "center": [100.0, 10.0, 100.0], "radius": 50.0, - "mobType": 0, - "minLevel": 1, - "maxLevel": 5, - "maxMobs": 15, - "respawnTime": 30.0 + "type": 0, + "max": 15, + "min_level": 1, + "max_level": 5, + "respawn_time": 30.0 }, { "name": "orc_camp", "center": [500.0, 15.0, 500.0], "radius": 75.0, - "mobType": 1, - "minLevel": 5, - "maxLevel": 15, - "maxMobs": 10, - "respawnTime": 45.0 + "type": 1, + "max": 10, + "min_level": 5, + "max_level": 15, + "respawn_time": 45.0 }, { "name": "dragon_lair", "center": [1000.0, 30.0, 1000.0], "radius": 100.0, - "mobType": 2, - "minLevel": 20, - "maxLevel": 30, - "maxMobs": 3, - "respawnTime": 300.0 + "type": 2, + "max": 3, + "min_level": 20, + "max_level": 30, + "respawn_time": 300.0 }, { "name": "slime_swamp", "center": [-200.0, 5.0, -200.0], "radius": 60.0, - "mobType": 3, - "minLevel": 1, - "maxLevel": 10, - "maxMobs": 20, - "respawnTime": 20.0 + "type": 3, + "max": 20, + "min_level": 1, + "max_level": 10, + "respawn_time": 20.0 } ], - "experienceMultiplier": 1.0, - "lootDropChance": 1.0 + "experience_multiplier": 1.0, + "loot_drop_chance": 1.0 }, "collision": { "enabled": true, - "gridCellSize": 10.0, - "maxCollisionChecks": 1000 + "cell_size": 10.0, + "max_checks": 1000 }, "database": { @@ -120,10 +120,10 @@ "name": "data/game.db", "user": "gameuser", "password": "password", - "connection_pool":{ + "pool":{ "enabled": false, - "pool_size": 10, - "pool_threads": 2, + "size": 10, + "threads": 2, "reconnect_attempts": 3, "min_connections": 5, "max_connections": 20, @@ -144,10 +144,12 @@ "console_output": true }, - "pythonScripting": { - "enabled": true, - "scriptDirectory": "scripts", - "hotReload": true + "scripting": { + "python": { + "enabled": true, + "directory": "scripts", + "hot_reload": true + } }, "game": { diff --git a/include/config/ConfigManager.hpp b/include/config/ConfigManager.hpp index 4626a26..b13a596 100644 --- a/include/config/ConfigManager.hpp +++ b/include/config/ConfigManager.hpp @@ -112,18 +112,21 @@ class ConfigManager { bool GetBool(const std::string& key, bool defaultValue = false) const; std::string GetString(const std::string& key, const std::string& defaultValue = "") const; std::vector GetStringArray(const std::string& key) const; - nlohmann::json GetJson(const std::string& key) const; + nlohmann::json GetJson(const std::string& key, const nlohmann::json& default_value = nlohmann::json()) const; bool HasKey(const std::string& key) const; private: + mutable std::mutex configMutex_; + nlohmann::json config_; + std::string configPath_; + + std::pair> ParseSegment(const std::string& seg) const; + std::vector SplitPath(const std::string& path) const; + ConfigManager() = default; ConfigManager(const ConfigManager&) = delete; ConfigManager& operator=(const ConfigManager&) = delete; - + bool HasProcessConfig() const; bool ValidateConfig(const nlohmann::json& config) const; - - mutable std::mutex configMutex_; - nlohmann::json config_; - std::string configPath_; }; diff --git a/include/game/GameData.hpp b/include/game/GameData.hpp index 70eddc7..2f40f60 100644 --- a/include/game/GameData.hpp +++ b/include/game/GameData.hpp @@ -145,6 +145,13 @@ struct PortalData { bool active = false; }; +struct ChunkParams { + uint64_t timestamp; + uint64_t session_id; + int size; + float spacing; +}; + struct ChunkData { uint64_t timestamp; uint64_t session_id; diff --git a/include/game/GameLogic.hpp b/include/game/GameLogic.hpp index 2a91b79..fca00f0 100644 --- a/include/game/GameLogic.hpp +++ b/include/game/GameLogic.hpp @@ -66,6 +66,7 @@ class GameLogic : public LogicCore, public std::enable_shared_from_this cb); + void SetSendChunkParamsCallback(std::function cb); void SetSendChunkCallback(std::function cb); void SetSendCollisionResponseCallback(std::function cb); @@ -81,7 +82,8 @@ class GameLogic : public LogicCore, public std::enable_shared_from_this cb); void OnAuthentication(const AuthenticationData& data); - void OnChunkRequest(const ChunkData& data); + void OnChunkParams(const ChunkParams& req); + void OnChunkData(const ChunkData& data); void OnCollisionCheck(const CollisionData& data); void OnPlayerPosition(const PlayerPositionData& data); void OnPlayerState(const PlayerStateData& data); @@ -131,6 +133,7 @@ class GameLogic : public LogicCore, public std::enable_shared_from_this sendAuthResponseCb_; + std::function sendChunkParamsCb_; std::function sendChunkCb_; std::function sendCollisionResponseCb_; diff --git a/include/network/BinaryProtocol.hpp b/include/network/BinaryProtocol.hpp index ef1d5e2..44eec12 100644 --- a/include/network/BinaryProtocol.hpp +++ b/include/network/BinaryProtocol.hpp @@ -26,10 +26,9 @@ namespace BinaryProtocol { MESSAGE_TYPE_COLLISION_CHECK = 50, // World messages - MESSAGE_TYPE_CHUNK_DATA = 100, - MESSAGE_TYPE_CHUNK_REQUEST = 101, - MESSAGE_TYPE_TERRAIN_HEIGHT = 102, - MESSAGE_TYPE_BIOME_DATA = 103, + MESSAGE_TYPE_CHUNK_PARAMS = 100, + MESSAGE_TYPE_CHUNK_DATA = 101, + MESSAGE_TYPE_BIOME_DATA = 102, // Player messages MESSAGE_TYPE_PLAYER_POSITION = 200, diff --git a/include/process/ProcessPool.hpp b/include/process/ProcessPool.hpp index 85646ea..2a1616e 100644 --- a/include/process/ProcessPool.hpp +++ b/include/process/ProcessPool.hpp @@ -57,6 +57,8 @@ class ProcessPool { bool SendToWorker(int workerId, const std::string& message); std::string ReceiveFromMaster(); + bool IsWorkersReady() const; + void WaitForWorkers(); bool IsWorkerAlive(int workerId) const; void RestartWorker(int workerId); @@ -69,24 +71,6 @@ class ProcessPool { void UnblockSignals(const sigset_t* oldset); private: - struct WorkerInfo { - pid_t pid; - int groupIdx; - int localWorkerId; - WorkerGroupConfig config; - }; - - void MasterProcess(); - void WorkerProcess(int globalWorkerId, const WorkerGroupConfig& config); - void SetupSignalHandlers(); - void CleanupDeadWorkers(); - void CloseAllPipes(); - void CreateWorkerPipe(int globalWorkerId); - - bool WriteAll(int fd, const void* buffer, size_t count); - bool ReadAll(int fd, void* buffer, size_t count, bool nonBlocking = false); - bool DrainPipe(int fd, size_t bytesToDrain); - std::vector groups_; int totalWorkers_; @@ -99,6 +83,13 @@ class ProcessPool { std::atomic running_{false}; std::atomic shutdownRequested_{false}; + struct WorkerInfo { + pid_t pid; + int groupIdx; + int localWorkerId; + WorkerGroupConfig config; + }; + std::vector workers_; std::vector workerPipes_; @@ -109,4 +100,17 @@ class ProcessPool { uint32_t maxMessageSize_{1024 * 1024}; uint32_t receiveTimeoutMs_{1000}; + + std::atomic workersReady_{false}; + + void MasterProcess(); + void WorkerProcess(int globalWorkerId, const WorkerGroupConfig& config); + void SetupSignalHandlers(); + void CleanupDeadWorkers(); + void CloseAllPipes(); + void CreateWorkerPipe(int globalWorkerId); + + bool WriteAll(int fd, const void* buffer, size_t count); + bool ReadAll(int fd, void* buffer, size_t count, bool nonBlocking = false); + bool DrainPipe(int fd, size_t bytesToDrain); }; diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index b69d52d..a8f754f 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -105,7 +105,7 @@ int ConfigManager::GetTotalWorkerCount() const { int ConfigManager::GetTotalThreadCount() const { int total = 0; for (const auto& g : GetWorkerGroups()) - total += g.threads * g.count; // each worker in group has its own threads + total += g.threads * g.count; return total; } @@ -189,14 +189,58 @@ bool ConfigManager::ValidateConfig(const nlohmann::json& config) const { Logger::Info("Configuration validation passed"); return true; - } catch (const std::exception& e) { - Logger::Critical("Configuration validation failed: {}", e.what()); + } catch (const std::exception& err) { + Logger::Critical("Configuration validation failed: {}", err.what()); return false; } } +std::pair> ConfigManager::ParseSegment(const std::string& seg) const { + auto bracketPos = seg.find('['); + if (bracketPos == std::string::npos) { + return {seg, std::nullopt}; + } + std::string field = seg.substr(0, bracketPos); + auto closePos = seg.find(']', bracketPos); + if (closePos == std::string::npos || closePos <= bracketPos + 1) { + throw std::invalid_argument("Invalid path segment (missing or empty index): " + seg); + } + std::string indexStr = seg.substr(bracketPos + 1, closePos - bracketPos - 1); + size_t index; + try { + index = std::stoull(indexStr); + } catch (...) { + throw std::invalid_argument("Invalid array index in segment: " + seg); + } + return {field, index}; +} + +std::vector ConfigManager::SplitPath(const std::string& path) const { + std::vector segments; + std::string current; + bool escape = false; + for (char c : path) { + if (escape) { + current += c; // the escaped character (e.g., '.', '\' itself, or any char) + escape = false; + } else if (c == '\\') { + escape = true; // next char is literal + } else if (c == '.') { + segments.push_back(current); + current.clear(); + } else { + current += c; + } + } + if (!current.empty() || !segments.empty() || path.empty()) { + segments.push_back(current); + } + return segments; +} + + // -------------------------------------------------------------------------- -// Setters (unchanged) +// Setters // -------------------------------------------------------------------------- void ConfigManager::SetBool(const std::string& key, bool value) { std::lock_guard lock(configMutex_); @@ -239,7 +283,7 @@ void ConfigManager::SetJson(const std::string& key, const nlohmann::json& value) } // -------------------------------------------------------------------------- -// Database configuration getters (unchanged) +// Database configuration getters // -------------------------------------------------------------------------- std::string ConfigManager::GetDatabaseHost() const { std::lock_guard lock(configMutex_); @@ -298,7 +342,7 @@ std::string ConfigManager::GetDatabaseBackend() const { int ConfigManager::GetDatabasePoolSize() const { std::lock_guard lock(configMutex_); try { - return config_.at("database").at("pool_size").get(); + return config_.at("database").at("pool").at("size").get(); } catch (const std::exception& err) {Logger::Warn("missed: {}", err.what()); return 10; } @@ -329,7 +373,7 @@ int ConfigManager::GetShardCount(int default_value) const { } // -------------------------------------------------------------------------- -// Game configuration getters (unchanged) +// Game configuration getters // -------------------------------------------------------------------------- int ConfigManager::GetMaxPlayersPerSession() const { std::lock_guard lock(configMutex_); @@ -359,7 +403,7 @@ int ConfigManager::GetSessionTimeout() const { } // -------------------------------------------------------------------------- -// World configuration getters (unchanged) +// World configuration getters // -------------------------------------------------------------------------- int ConfigManager::GetWorldSeed() const { std::lock_guard lock(configMutex_); @@ -443,7 +487,7 @@ int ConfigManager::GetWorldPreloadRadius() const { } // -------------------------------------------------------------------------- -// Logging configuration getters (unchanged) +// Logging configuration getters // -------------------------------------------------------------------------- std::string ConfigManager::GetLogLevel() const { std::lock_guard lock(configMutex_); @@ -492,8 +536,20 @@ bool ConfigManager::GetConsoleOutput() const { } } + +bool ConfigManager::HasKey(const std::string& key) const { + std::lock_guard lock(configMutex_); + try { + std::string keyPath = key; + std::replace(keyPath.begin(), keyPath.end(), '.', '/'); + return config_.contains(nlohmann::json::json_pointer("/" + keyPath)); + } catch (const std::exception& err) {//Logger::Warn("missed: {}", err.what()); + return false; + } +} + // -------------------------------------------------------------------------- -// Generic config accessors (unchanged) +// Generic config accessors // -------------------------------------------------------------------------- int ConfigManager::GetInt(const std::string& key, int defaultValue) const { std::lock_guard lock(configMutex_); @@ -558,24 +614,35 @@ std::vector ConfigManager::GetStringArray(const std::string& key) c return result; } -nlohmann::json ConfigManager::GetJson(const std::string& key) const { +nlohmann::json ConfigManager::GetJson(const std::string& key, + const nlohmann::json& default_value) const { std::lock_guard lock(configMutex_); + std::vector segments; try { - std::string keyPath = key; - std::replace(keyPath.begin(), keyPath.end(), '.', '/'); - return config_.at(nlohmann::json::json_pointer("/" + keyPath)); - } catch (const std::exception& err) {Logger::Warn("missed: {}", err.what()); - return nlohmann::json(); + segments = SplitPath(key); + } catch (const std::exception& e) { + Logger::Warn("Invalid path '{}': {}", key, e.what()); + return default_value; } -} - -bool ConfigManager::HasKey(const std::string& key) const { - std::lock_guard lock(configMutex_); - try { - std::string keyPath = key; - std::replace(keyPath.begin(), keyPath.end(), '.', '/'); - return config_.contains(nlohmann::json::json_pointer("/" + keyPath)); - } catch (const std::exception& err) {Logger::Warn("missed: {}", err.what()); - return false; + const nlohmann::json* current = &config_; + for (const auto& seg : segments) { + auto [field, index] = ParseSegment(seg); + if (current->is_object()) { + if (!current->contains(field)) { + Logger::Warn("Missing key '{}' in path '{}'", field, key); + return default_value; + } + current = ¤t->at(field); + } else if (current->is_array() && index.has_value()) { + if (index.value() >= current->size()) { + Logger::Warn("Index out of bounds in path '{}'", key); + return default_value; + } + current = ¤t->at(index.value()); + } else { + Logger::Warn("Path segment '{}' cannot navigate a {} node", seg, current->type_name()); + return default_value; + } } + return *current; } diff --git a/src/database/DbManager.cpp b/src/database/DbManager.cpp index 34d8884..09a7c25 100644 --- a/src/database/DbManager.cpp +++ b/src/database/DbManager.cpp @@ -264,7 +264,7 @@ bool DbManager::LoadConfiguration(const std::string& configPath) { {"name", config_.value("name", "game_db")}, {"user", config_.value("user", "postgres")}, {"password", config_.value("password", "")}, - {"connection_pool", { + {"pool", { {"enabled", poolConfig.value("enabled", true)}, {"min_connections", poolConfig.value("min", 5)}, {"max_connections", poolConfig.value("max", 20)} @@ -289,24 +289,18 @@ bool DbManager::LoadConfiguration(const std::string& configPath) { bool DbManager::ValidateConfiguration(const nlohmann::json& config) const { try { - // Check required fields if (config.contains("host") && !config["host"].is_string()) { Logger::Error("Invalid 'host' in database configuration (must be string)"); return false; } - if (!config.contains("name") || !config["name"].is_string()) { Logger::Error("Missing or invalid 'name' in database configuration"); return false; } - if (!config.contains("user") || !config["user"].is_string()) { Logger::Error("Missing or invalid 'user' in database configuration"); return false; } - - // Validate port - // Port is optional (default 5432), but if present must be valid if (config.contains("port")) { if (!config["port"].is_number()) { Logger::Error("Invalid 'port' in database configuration (must be number)"); @@ -318,10 +312,8 @@ bool DbManager::ValidateConfiguration(const nlohmann::json& config) const { return false; } } - - // Validate connection pool settings if present - if (config.contains("connection_pool")) { - const auto& pool = config["connection_pool"]; + if (config.contains("pool")) { + const auto& pool = config["pool"]; if (pool.contains("min_connections") && pool["min_connections"] <= 0) { Logger::Error("Invalid min_connections in connection pool"); return false; @@ -338,11 +330,9 @@ bool DbManager::ValidateConfiguration(const nlohmann::json& config) const { } } } - return true; - - } catch (const std::exception& e) { - Logger::Error("Configuration validation error: {}", e.what()); + } catch (const std::exception& err) { + Logger::Error("Configuration validation error: {}", err.what()); return false; } } @@ -354,22 +344,13 @@ bool DbManager::SetBackend(BackendType backendType, const nlohmann::json& config Logger::Error("Invalid configuration for new backend"); return false; } - - // Temporarily store old backend (not needed but kept for safety) std::unique_ptr oldBackend = std::move(backend_); - - // Update current type and config currentType_ = backendType; config_ = config; - - // Reload SQL for the new backend if (!LoadSQLForBackend()) { Logger::Error("Failed to load SQL queries for new backend"); - // Restore old backend? We'll just return false and leave backend_ empty return false; } - - // Create new backend with the provider switch (backendType) { case SQLITE: #ifdef USE_SQLITE @@ -399,16 +380,12 @@ bool DbManager::SetBackend(BackendType backendType, const nlohmann::json& config Logger::Error("Unsupported database backend"); return false; } - - connected_ = false; // Will need to reconnect + connected_ = false; Logger::Info("Database backend changed to {}", BackendTypeToString(currentType_)); return true; } std::string DbManager::EscapeString(const std::string& input) { - // Use your database client's escaping function. - // For PostgreSQL via libpq, you might use PQescapeLiteral. - // For simplicity, this example doubles single quotes. std::string escaped; for (char c : input) { if (c == '\'') escaped += "''"; @@ -426,19 +403,14 @@ bool DbManager::Connect() { Logger::Error("DbManager not initialized"); return false; } - if (!backend_) { Logger::Error("No database backend available"); return false; } - if (connected_) { Logger::Debug("Already connected to database"); return true; } - - // The master process has already ensured the database exists. - // Simply attempt to connect. try { if (backend_->Connect()) { connected_ = true; @@ -448,8 +420,8 @@ bool DbManager::Connect() { Logger::Error("Failed to connect to database"); return false; } - } catch (const std::exception& e) { - Logger::Error("Connection error: {}", e.what()); + } catch (const std::exception& err) { + Logger::Error("Connection error: {}", err.what()); return false; } } @@ -458,20 +430,16 @@ bool DbManager::Reconnect() { if (!initialized_ || !backend_) { return false; } - Logger::Info("Attempting to reconnect to database..."); - if (connected_) { backend_->Disconnect(); connected_ = false; } - if (backend_->Reconnect()) { connected_ = true; Logger::Info("Reconnected to database"); return true; } - Logger::Error("Failed to reconnect to database"); return false; } @@ -504,41 +472,32 @@ int DbManager::GetTotalShards() const { nlohmann::json DbManager::GetStatistics() const { nlohmann::json stats; - - // Basic info stats["backend"] = BackendTypeToString(currentType_); stats["initialized"] = initialized_.load(); stats["connected"] = connected_.load(); - if (backend_) { stats["connection_info"] = backend_->GetConnectionInfo(); stats["database_stats"] = backend_->GetDatabaseStats(); stats["active_connections"] = backend_->GetActiveConnections(); stats["idle_connections"] = backend_->GetIdleConnections(); } - - // Manager statistics auto now = std::chrono::steady_clock::now(); auto uptime = std::chrono::duration_cast(now - stats_.startTime).count(); - stats["uptime_seconds"] = uptime; stats["queries_executed"] = stats_.queriesExecuted.load(); stats["queries_failed"] = stats_.queriesFailed.load(); stats["transactions_committed"] = stats_.transactionsCommitted.load(); stats["transactions_rolled_back"] = stats_.transactionsRolledBack.load(); stats["bytes_transferred"] = stats_.bytesTransferred.load(); - if (stats_.queriesExecuted > 0) { double successRate = 100.0 * (1.0 - (double)stats_.queriesFailed / stats_.queriesExecuted); stats["success_rate_percent"] = successRate; } - return stats; } void DbManager::PrintStatistics() const { auto stats = GetStatistics(); - Logger::Info("=== Database Statistics ==="); Logger::Info(" Backend: {}", stats["backend"].get()); Logger::Info(" Status: {}", stats["connected"].get() ? "Connected" : "Disconnected"); @@ -554,7 +513,6 @@ void DbManager::PrintStatistics() const { Logger::Info(" Transactions Rolled Back: {}", stats["transactions_rolled_back"].get()); Logger::Info(" Bytes Transferred: {}", stats["bytes_transferred"].get()); Logger::Info(" "); - if (stats.contains("database_stats")) { Logger::Info(" Database Statistics:"); for (const auto& [key, value] : stats["database_stats"].items()) { @@ -569,21 +527,15 @@ bool DbManager::RunMigrations() { Logger::Error("Cannot run migrations: not connected to database"); return false; } - try { Logger::Info("Running database migrations..."); - - // Check if migrations table exists nlohmann::json result = backend_->Query( "SELECT EXISTS (SELECT FROM information_schema.tables " "WHERE table_name = 'schema_migrations')"); - bool migrationsTableExists = false; if (!result.empty() && result[0].contains("exists")) { migrationsTableExists = result[0]["exists"].get(); } - - // Create migrations table if it doesn't exist if (!migrationsTableExists) { Logger::Info("Creating migrations table..."); backend_->Execute( @@ -594,25 +546,20 @@ bool DbManager::RunMigrations() { " checksum VARCHAR(64)" ")"); } - - // Get current migration version int currentVersion = 0; result = backend_->Query("SELECT MAX(version) as current_version FROM schema_migrations"); if (!result.empty() && result[0].contains("current_version") && !result[0]["current_version"].is_null()) { currentVersion = result[0]["current_version"].get(); } - Logger::Info("Current migration version: {}", currentVersion); // TODO: Load migration files from disk and apply them - // This would be implemented based on your migration system Logger::Info("Migrations completed up to version {}", currentVersion); return true; - - } catch (const std::exception& e) { - Logger::Error("Migration error: {}", e.what()); + } catch (const std::exception& err) { + Logger::Error("Migration error: {}", err.what()); return false; } } @@ -621,12 +568,10 @@ bool DbManager::CheckMigrationStatus() { if (!IsConnected()) { return false; } - try { nlohmann::json result = backend_->Query( "SELECT version, name, applied_at FROM schema_migrations " "ORDER BY version DESC LIMIT 10"); - if (result.empty()) { Logger::Info("No migrations have been applied"); } else { @@ -639,11 +584,9 @@ bool DbManager::CheckMigrationStatus() { } Logger::Info("======================="); } - return true; - - } catch (const std::exception& e) { - Logger::Error("Failed to check migration status: {}", e.what()); + } catch (const std::exception& err) { + Logger::Error("Failed to check migration status: {}", err.what()); return false; } } @@ -653,13 +596,9 @@ bool DbManager::RollbackMigration(int version) { Logger::Error("Cannot rollback migration: not connected to database"); return false; } - - // Path to migrations directory – could be configurable const std::string migrationsDir = "migrations/"; - std::string downFileName = "U" + std::to_string(version) + "*.sql"; // wildcard for description - + std::string downFileName = "U" + std::to_string(version) + "*.sql"; try { - // Find the actual down file (there should be exactly one) std::vector downFiles; for (const auto& entry : std::filesystem::directory_iterator(migrationsDir)) { if (entry.is_regular_file()) { @@ -667,10 +606,9 @@ bool DbManager::RollbackMigration(int version) { if (filename.rfind("U" + std::to_string(version), 0) == 0 && filename.size() > 3 && filename.substr(filename.size() - 4) == ".sql") { downFiles.push_back(entry.path()); - } + } } } - if (downFiles.empty()) { Logger::Error("No down migration found for version {}", version); return false; @@ -679,8 +617,6 @@ bool DbManager::RollbackMigration(int version) { Logger::Error("Multiple down migrations found for version {}: {}", version, downFiles.size()); return false; } - - // Read the down SQL script std::ifstream file(downFiles[0]); if (!file.is_open()) { Logger::Error("Failed to open down migration file: {}", downFiles[0].string()); @@ -689,22 +625,14 @@ bool DbManager::RollbackMigration(int version) { std::stringstream buffer; buffer << file.rdbuf(); std::string downSql = buffer.str(); - Logger::Info("Rolling back migration version {} using file: {}", version, downFiles[0].string()); - - // Execute the down script within a transaction if (!backend_->BeginTransaction()) { Logger::Error("Failed to begin transaction for rollback"); return false; } - bool success = false; try { - // Execute the down SQL (may contain multiple statements) - // Simple approach: execute whole script. If your backend doesn't support multiple statements, - // you may need to split by ';'. We'll assume Execute handles batches. if (backend_->Execute(downSql)) { - // Remove the migration record std::string deleteSql = "DELETE FROM schema_migrations WHERE version = " + std::to_string(version); if (backend_->Execute(deleteSql)) { success = true; @@ -714,7 +642,6 @@ bool DbManager::RollbackMigration(int version) { } else { Logger::Error("Failed to execute down migration SQL for version {}", version); } - if (success) { if (!backend_->CommitTransaction()) { Logger::Error("Failed to commit transaction during rollback"); @@ -723,19 +650,18 @@ bool DbManager::RollbackMigration(int version) { } else { backend_->RollbackTransaction(); } - } catch (const std::exception& e) { - Logger::Error("Exception during rollback: {}", e.what()); + } catch (const std::exception& err) { + Logger::Error("Exception during rollback: {}", err.what()); backend_->RollbackTransaction(); return false; } - if (success) { Logger::Info("Migration version {} rolled back successfully", version); } return success; - } catch (const std::exception& e) { - Logger::Error("Rollback error: {}", e.what()); + } catch (const std::exception& err) { + Logger::Error("Rollback error: {}", err.what()); return false; } } @@ -743,7 +669,6 @@ bool DbManager::RollbackMigration(int version) { BackendType DbManager::ParseBackendType(const std::string& backendStr) const { std::string lowerType = backendStr; std::transform(lowerType.begin(), lowerType.end(), lowerType.begin(), ::tolower); - if (lowerType == "sqlite") { return SQLITE; } else if (lowerType == "postgresql" || lowerType == "postgres") { @@ -751,7 +676,6 @@ BackendType DbManager::ParseBackendType(const std::string& backendStr) const { } else if (lowerType == "citus") { return CITUS; } - return INVALID; } @@ -792,8 +716,8 @@ bool DbManager::TableExists(const std::string& tableName) { } return false; } - } catch (const std::exception& e) { - Logger::Error("Failed to check existence of table {}: {}", tableName, e.what()); + } catch (const std::exception& err) { + Logger::Error("Failed to check existence of table {}: {}", tableName, err.what()); return false; } } @@ -803,19 +727,17 @@ bool DbManager::ExecuteCreateTable(const std::string& tableName, const std::stri Logger::Debug("Table '{}' already exists, skipping creation.", tableName); return true; } - Logger::Info("Creating table '{}'...", tableName); try { backend_->Execute(createSql); Logger::Info("Table '{}' created successfully.", tableName); return true; - } catch (const std::exception& e) { - Logger::Error("Failed to create table '{}': {}", tableName, e.what()); + } catch (const std::exception& err) { + Logger::Error("Failed to create table '{}': {}", tableName, err.what()); return false; } } - bool DbManager::CreateDefaultTablesIfNotExist() { if (!IsConnected() && !Connect()) return false; @@ -830,7 +752,6 @@ bool DbManager::CreateDefaultTablesIfNotExist() { "create_table_loot_tables", "create_table_schema_migrations" }; - for (const auto& key : tableQueries) { std::string sql = sqlProvider_.GetQuery(key); if (sql.empty()) { @@ -843,8 +764,7 @@ bool DbManager::CreateDefaultTablesIfNotExist() { success = false; } } - - #ifdef USE_CITUS +#ifdef USE_CITUS if (currentType_ == CITUS) { std::vector distQueries = { "create_distributed_table_players", @@ -857,12 +777,11 @@ bool DbManager::CreateDefaultTablesIfNotExist() { for (const auto& key : distQueries) { std::string sql = sqlProvider_.GetQuery(key); if (!sql.empty()) { - backend_->Execute(sql); // ignore failure if table already distributed + backend_->Execute(sql); } } } - #endif - +#endif return success; } diff --git a/src/database/PostgreSqlClient.cpp b/src/database/PostgreSqlClient.cpp index d224b5f..bad1565 100644 --- a/src/database/PostgreSqlClient.cpp +++ b/src/database/PostgreSqlClient.cpp @@ -32,11 +32,11 @@ bool PostgreSqlClient::Connect() { if (poolInitialized_) return true; try { - if (config_.contains("connection_pool") && - config_["connection_pool"].value("enabled", true)) { + if (config_.contains("pool") && + config_["pool"].value("enabled", true)) { - size_t minConn = config_["connection_pool"].value("min_connections", 5); - size_t maxConn = config_["connection_pool"].value("max_connections", 20); + size_t minConn = config_["pool"].value("min_connections", 5); + size_t maxConn = config_["pool"].value("max_connections", 20); if (!InitializeConnectionPool(minConn, maxConn)) { Logger::Error("Failed to initialize connection pool"); @@ -993,8 +993,8 @@ nlohmann::json PostgreSqlClient::GetDatabaseStats() { stats["failed_queries"] = stats_.failedQueries.load(); stats["total_transactions"] = stats_.totalTransactions.load(); stats["connection_errors"] = stats_.connectionErrors.load(); - stats["connection_pool_hits"] = stats_.connectionPoolHits.load(); - stats["connection_pool_misses"] = stats_.connectionPoolMisses.load(); + stats["pool_hits"] = stats_.connectionPoolHits.load(); + stats["pool_misses"] = stats_.connectionPoolMisses.load(); stats["active_connections"] = GetActiveConnections(); stats["idle_connections"] = GetIdleConnections(); stats["total_connections"] = connections_.size(); diff --git a/src/game/EntityManager.cpp b/src/game/EntityManager.cpp index ac24a26..c21c77d 100644 --- a/src/game/EntityManager.cpp +++ b/src/game/EntityManager.cpp @@ -377,7 +377,7 @@ bool EntityManager::InitializePython() { Py_Initialize(); if (!Py_IsInitialized()) { - Logger::Error("Failed to initialize Python interpreter"); + Logger::Warn("Failed to initialize Python interpreter"); return false; } diff --git a/src/game/GameLogic.cpp b/src/game/GameLogic.cpp index 998eca3..7d49924 100644 --- a/src/game/GameLogic.cpp +++ b/src/game/GameLogic.cpp @@ -52,21 +52,25 @@ void GameLogic::Initialize() { if (!LoadGameData()) { Logger::Error("Failed to load game data"); } - pythonEnabled_ = config.GetBool("python.enabled", false); - if (pythonEnabled_) { - auto& pythonScripting = PythonScripting::GetInstance(); - if (pythonScripting.Initialize()) { - Logger::Info("Python scripting initialized"); - RegisterPythonEventHandlers(); - bool hotReloadEnabled = config.GetBool("python.hot_reload", true); - if (hotReloadEnabled) { - std::string scriptDir = config.GetString("python.script_dir", "./scripts"); - g_hotReloader = std::make_unique(scriptDir, 2000); - g_hotReloader->Start(); + if (config.HasKey("scripting.python")) + { + nlohmann::json json_py_conf = config.GetJson("scripting.python"); + pythonEnabled_ = json_py_conf.value("enabled", false); + if (pythonEnabled_) { + auto& pythonScripting = PythonScripting::GetInstance(); + if (pythonScripting.Initialize()) { + Logger::Info("Python scripting initialized"); + RegisterPythonEventHandlers(); + bool hotReloadEnabled = json_py_conf.value("hot_reload", true); + if (hotReloadEnabled) { + std::string scriptDir = json_py_conf.value("directory", "./scripts"); + g_hotReloader = std::make_unique(scriptDir, 2000); + g_hotReloader->Start(); + } + } else { + Logger::Warn("Failed to initialize Python scripting"); + pythonEnabled_ = false; } - } else { - Logger::Warn("Failed to initialize Python scripting"); - pythonEnabled_ = false; } } Logger::Info("GameLogic world system initialized successfully"); @@ -420,6 +424,10 @@ void GameLogic::SetSendAuthenticationResponseCallback(std::function cb) { + sendChunkParamsCb_ = std::move(cb); +} + void GameLogic::SetSendChunkCallback(std::function cb) { sendChunkCb_ = std::move(cb); } @@ -501,7 +509,19 @@ void GameLogic::OnAuthentication(const AuthenticationData& data) { } } -void GameLogic::OnChunkRequest(const ChunkData& req) { +void GameLogic::OnChunkParams(const ChunkParams& req) { + ChunkParams resp; + resp.size = WorldChunk::DEFAULT_SIZE; + resp.spacing = WorldChunk::DEFAULT_SPACING; + resp.timestamp = GetCurrentTimestamp(); + if (sendChunkParamsCb_) { + sendChunkParamsCb_(req.session_id, resp); + } else { + Logger::Error("No sendChunkParamsCb_ set in GameLogic"); + } +} + +void GameLogic::OnChunkData(const ChunkData& req) { auto chunk = GetOrCreateChunk(req.x, req.z); if (!chunk) { Logger::Error("Failed to get chunk ({},{}) for session {}", req.x, req.z, req.session_id); diff --git a/src/game/LogicCore.cpp b/src/game/LogicCore.cpp index 22a1698..f5c8fa6 100644 --- a/src/game/LogicCore.cpp +++ b/src/game/LogicCore.cpp @@ -33,7 +33,7 @@ void LogicCore::Initialize() { Logger::Info("Initializing LogicCore..."); auto& config = ConfigManager::GetInstance(); RegisterDefaultHandlers(); - pythonEnabled_ = config.GetBool("python.enabled", false); + pythonEnabled_ = config.GetBool("scripting.python.enabled", false); running_ = true; gameLoopThread_ = RAIIThread([this]() { GameLoop(); }); spawnerThread_ = RAIIThread([this]() { SpawnerLoop(); }); diff --git a/src/game/WorldGenerator.cpp b/src/game/WorldGenerator.cpp index 045e9c9..4f1db41 100644 --- a/src/game/WorldGenerator.cpp +++ b/src/game/WorldGenerator.cpp @@ -79,17 +79,21 @@ std::unique_ptr WorldGenerator::GenerateChunk(int chunkX, int chunkZ return chunk; } +// float WorldGenerator::GetTerrainHeight(float x, float z) { +// float baseHeight = FractalNoise(x / config_.terrainScale, z / config_.terrainScale); +// float detail = Noise(x / (config_.terrainScale * 0.5f), z / (config_.terrainScale * 0.5f)) * 0.2f; +// float normalizedHeight = (baseHeight + detail + 1.0f) * 0.5f; +// normalizedHeight = std::pow(normalizedHeight, 1.5f); +// float result = normalizedHeight * config_.terrainHeight; +// return result; +// } + +//TODO: replace all constants to parameters from config and also send to client with MESSAGE_TYPE_CHUNK_PARAMS float WorldGenerator::GetTerrainHeight(float x, float z) { - float baseHeight = FractalNoise(x / config_.terrainScale, z / config_.terrainScale); - float detail = Noise(x / (config_.terrainScale * 0.5f), z / (config_.terrainScale * 0.5f)) * 0.2f; - float normalizedHeight = (baseHeight + detail + 1.0f) * 0.5f; - normalizedHeight = std::pow(normalizedHeight, 1.5f); - float result = normalizedHeight * config_.terrainHeight; - // if (std::abs(x) < 1.0f && std::abs(z) < 1.0f) { - // Logger::Trace("GetTerrainHeight({:.2f}, {:.2f}) = {:.4f} (base={:.4f}, norm={:.4f})", - // x, z, result, baseHeight, normalizedHeight); - // } - return result; + return (sin(x * 0.1) * cos(z * 0.1) + + 0.3f * sin(x * 0.3f + 1.2f) + + 0.3f * cos(z * 0.3f + 2.4f) + + 0.2f * sin((x * 0.6f + z * 0.4f) * 0.8f)) * 2.0f + 0.5f; } BiomeType WorldGenerator::GetBiomeAt(float x, float z) { diff --git a/src/main.cpp b/src/main.cpp index 40c22ae..569e866 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -69,9 +69,9 @@ void worker(int workerId, const WorkerGroupConfig& groupConfig, ProcessPool* pro dbConfig["user"] = config.GetDatabaseUser(); dbConfig["password"] = config.GetDatabasePassword(); - dbConfig["max_connections"] = config.GetInt("database.connection_pool.max_connections", 20); - dbConfig["min_connections"] = config.GetInt("database.connection_pool.min_connections", 5); - dbConfig["connection_timeout_ms"] = config.GetInt("database.connection_pool.connection_timeout_ms", 5000); + dbConfig["max_connections"] = config.GetInt("database.pool.max_connections", 20); + dbConfig["min_connections"] = config.GetInt("database.pool.min_connections", 5); + dbConfig["connection_timeout_ms"] = config.GetInt("database.pool.connection_timeout_ms", 5000); if (!dbManager.Initialize(path_config)) { Logger::Error("Worker {} failed to initialize database", workerId); @@ -107,7 +107,7 @@ void worker(int workerId, const WorkerGroupConfig& groupConfig, ProcessPool* pro gameLogic.SetConnectionManager(ConnectionManager::GetInstancePtr()); gameLogic.Initialize(); - DatabaseService dbService(server.GetIoContext(), config.GetInt("database.pool_threads", 2)); + DatabaseService dbService(server.GetIoContext(), config.GetInt("database.pool.threads", 2)); gameLogic.SetDatabaseService(&dbService); if (config.ShouldPreloadWorld()) { @@ -283,6 +283,7 @@ int main(int argc, char* argv[]) { Logger::Info("Starting {} worker processes", processPool.GetTotalWorkerCount()); processPool.Run(); + processPool.WaitForWorkers(); // Master messaging thread – reduced frequency to avoid pipe overload std::thread masterMessagingThread([&processPool]() { @@ -293,8 +294,8 @@ int main(int argc, char* argv[]) { int totalWorkers = processPool.GetTotalWorkerCount(); for (int i = 0; i < totalWorkers; i++) { if (!processPool.IsWorkerAlive(i)) { - Logger::Warn("Master skipping welcome message to dead worker {}", i); - continue; + if (processPool.IsWorkersReady()) + Logger::Warn("Master skipping welcome message to dead worker {}", i); } nlohmann::json testMsg; diff --git a/src/network/BinarySession.cpp b/src/network/BinarySession.cpp index 590ff73..19e921e 100644 --- a/src/network/BinarySession.cpp +++ b/src/network/BinarySession.cpp @@ -323,7 +323,7 @@ void BinarySession::HandleBinaryMessage(const BinaryProtocol::BinaryMessage& mes case BinaryProtocol::MESSAGE_TYPE_PROTOCOL_NEGOTIATION: HandleProtocolNegotiation(message.data); return; - case BinaryProtocol::MESSAGE_TYPE_CHUNK_REQUEST: { + case BinaryProtocol::MESSAGE_TYPE_CHUNK_DATA: { BinaryProtocol::BinaryReader reader(message.data.data(), message.data.size()); ChunkData req; req.x = reader.ReadInt32(); @@ -333,7 +333,7 @@ void BinarySession::HandleBinaryMessage(const BinaryProtocol::BinaryMessage& mes req.player_y = reader.ReadFloat(); req.player_z = reader.ReadFloat(); req.session_id = sessionId_; - GameLogic::GetInstance().OnChunkRequest(req); + GameLogic::GetInstance().OnChunkData(req); break; } case BinaryProtocol::MESSAGE_TYPE_ERROR: diff --git a/src/network/GameServer.cpp b/src/network/GameServer.cpp index c6d23a4..674b82a 100644 --- a/src/network/GameServer.cpp +++ b/src/network/GameServer.cpp @@ -170,7 +170,14 @@ void GameServer::InitSessionFactory(int workerId, ProcessPool* processPool, Game game_logic.OnAuthentication(authData); break; } - case BinaryProtocol::MESSAGE_TYPE_CHUNK_REQUEST: { + case BinaryProtocol::MESSAGE_TYPE_CHUNK_PARAMS: { + BinaryProtocol::BinaryReader reader(data.data(), data.size()); + ChunkParams req; + req.session_id = session->GetSessionId(); + game_logic.OnChunkParams(req); + break; + } + case BinaryProtocol::MESSAGE_TYPE_CHUNK_DATA: { BinaryProtocol::BinaryReader reader(data.data(), data.size()); ChunkData req; req.x = reader.ReadInt32(); @@ -180,7 +187,7 @@ void GameServer::InitSessionFactory(int workerId, ProcessPool* processPool, Game req.player_y = reader.ReadFloat(); req.player_z = reader.ReadFloat(); req.session_id = session->GetSessionId(); - game_logic.OnChunkRequest(req); + game_logic.OnChunkData(req); break; } case BinaryProtocol::MESSAGE_TYPE_COLLISION_CHECK: { @@ -302,6 +309,11 @@ void GameServer::InitSessionFactory(int workerId, ProcessPool* processPool, Game authData.session_id = session->GetSessionId(); game_logic.OnAuthentication(authData); } + else if (msgType == "chunk_params") { + ChunkParams req; + req.session_id = session->GetSessionId(); + game_logic.OnChunkParams(req); + } else if (msgType == "get_chunk") { ChunkData req; req.x = msg.value("x", 0); @@ -311,7 +323,7 @@ void GameServer::InitSessionFactory(int workerId, ProcessPool* processPool, Game req.player_y = msg.value("player_y", 0.0f); req.player_z = msg.value("player_z", 0.0f); req.session_id = session->GetSessionId(); - game_logic.OnChunkRequest(req); + game_logic.OnChunkData(req); } else if (msgType == "collision") { CollisionData req; @@ -507,11 +519,21 @@ void GameServer::RegisterCallbacks(const std::string& protocol, GameLogic& game_ session->Send(BinaryProtocol::MESSAGE_TYPE_AUTHENTICATION, writer.GetBuffer()); } }); + game_logic.SetSendChunkParamsCallback([&](uint64_t session_id, const ChunkParams& data) { + BinaryProtocol::BinaryWriter writer; + writer.WriteUInt32(static_cast(data.size)); + writer.WriteFloat(data.spacing); + auto session = ConnectionManager::GetInstance().GetSession(session_id); + if (session) { + session->Send(BinaryProtocol::MESSAGE_TYPE_CHUNK_PARAMS, writer.GetBuffer()); + } + }); game_logic.SetSendChunkCallback([&](uint64_t session_id, const ChunkData& data) { BinaryProtocol::BinaryWriter writer; writer.WriteInt32(data.x); writer.WriteInt32(data.z); writer.WriteInt32(data.size); + writer.WriteFloat(data.spacing); uint32_t vertexDataSize = static_cast(data.vertices.size() * sizeof(float)); writer.WriteUInt32(vertexDataSize); writer.WriteBytes(reinterpret_cast(data.vertices.data()), vertexDataSize); @@ -671,6 +693,18 @@ void GameServer::RegisterCallbacks(const std::string& protocol, GameLogic& game_ session->SendJson(response); } }); + game_logic.SetSendChunkParamsCallback([&](uint64_t session_id, const ChunkParams& data) { + nlohmann::json msg = { + {"msg", "chunk_params"}, + {"size", data.size}, + {"spacing", data.spacing}, + {"timestamp", data.timestamp} + }; + auto session = ConnectionManager::GetInstance().GetSession(session_id); + if (session) { + session->SendJson(msg); + } + }); game_logic.SetSendChunkCallback([&](uint64_t session_id, const ChunkData& data) { nlohmann::json msg = { {"msg", "get_chunk"}, diff --git a/src/process/ProcessPool.cpp b/src/process/ProcessPool.cpp index f3dd694..35acb81 100644 --- a/src/process/ProcessPool.cpp +++ b/src/process/ProcessPool.cpp @@ -141,13 +141,21 @@ void ProcessPool::MasterProcess() { } } + workersReady_.store(true); UnblockSignals(&oldset); while (running_.load() && !shutdownRequested_.load()) { CleanupDeadWorkers(); for (int i = 0; i < 10 && running_.load() && !shutdownRequested_.load(); ++i) { + if (g_shutdown.load()) { + shutdownRequested_.store(true); + break; + } std::this_thread::sleep_for(std::chrono::milliseconds(100)); } + if (shutdownRequested_.load()) { + break; + } } constexpr int SHUTDOWN_TIMEOUT_SEC = 30; @@ -201,6 +209,17 @@ void ProcessPool::MasterProcess() { Logger::Info("Master process shutdown complete"); } +bool ProcessPool::IsWorkersReady() const +{ + return workersReady_.load(); +} + +void ProcessPool::WaitForWorkers() { + while (!workersReady_.load() && running_.load() && !shutdownRequested_.load()) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } +} + void ProcessPool::WorkerProcess(int globalWorkerId, const WorkerGroupConfig& config) { this->workerId_ = globalWorkerId; this->groupConfig_ = config; @@ -227,7 +246,7 @@ void ProcessPool::CleanupDeadWorkers() { workerPipes_[write_idx] = -1; } - if (!shutdownRequested_.load()) { + if (!shutdownRequested_.load() && !g_shutdown.load()) { RestartWorker(i); } else { std::lock_guard lock(healthMutex_); diff --git a/src/scripting/PythonScripting.cpp b/src/scripting/PythonScripting.cpp index 522d74e..7b855c0 100644 --- a/src/scripting/PythonScripting.cpp +++ b/src/scripting/PythonScripting.cpp @@ -27,7 +27,7 @@ bool PythonScripting::Initialize() { // Get configuration auto& config = ConfigManager::GetInstance(); - std::string pythonHome = config.GetString("python.home", ""); + std::string pythonHome = config.GetString("scripting.python.directory", ""); if (!pythonHome.empty()) { pythonHome_ = pythonHome; @@ -35,7 +35,7 @@ bool PythonScripting::Initialize() { // Initialize Python if (!InitializePython()) { - Logger::Error("Failed to initialize Python interpreter"); + Logger::Warn("Failed to initialize Python interpreter"); return false; } @@ -52,7 +52,7 @@ bool PythonScripting::Initialize() { } // Add additional paths from config - auto pythonPaths = config.GetStringArray("python.paths"); + auto pythonPaths = config.GetStringArray("scripting.python.paths"); for (const auto& path : pythonPaths) { AddPythonPath(path); } @@ -61,7 +61,7 @@ bool PythonScripting::Initialize() { PythonAPI::Initialize(); // Load default modules - std::string scriptDir = config.GetString("python.script_dir", "./scripts"); + std::string scriptDir = config.GetString("scripting.python.directory", "./scripts"); if (fs::exists(scriptDir)) { for (const auto& entry : fs::directory_iterator(scriptDir)) { if (entry.path().extension() == ".py") { @@ -127,7 +127,7 @@ bool PythonScripting::InitializePython() { PyConfig_Clear(&config); if (!Py_IsInitialized()) { - Logger::Error("Failed to initialize Python interpreter"); + Logger::Warn("Failed to initialize Python interpreter"); return false; }