#include "../common/content/world_content_service.h" #include "../common/emu_constants.h" #include "../common/eqemu_logsys.h" #include "../common/http/httplib.h" #include "../common/http/uri.h" #include "../common/net/console_server.h" #include "../common/net/servertalk_server.h" #include "../common/repositories/character_expedition_lockouts_repository.h" #include "../common/repositories/character_task_timers_repository.h" #include "../common/rulesys.h" #include "../common/strings.h" #include "adventure_manager.h" #include "dynamic_zone_manager.h" #include "login_server_list.h" #include "shared_task_manager.h" #include "ucs.h" #include "wguild_mgr.h" #include "world_boot.h" #include "world_config.h" #include "world_event_scheduler.h" #include "world_server_cli.h" #include "../common/zone_store.h" #include "worlddb.h" #include "zonelist.h" #include "zoneserver.h" #include "../common/ip_util.h" #include "../common/zone_store.h" #include "../common/path_manager.h" #include "../common/database/database_update.h" #include "../common/repositories/zone_state_spawns_repository.h" extern WorldConfig Config; auto mutex = new Mutex; void WorldBoot::GMSayHookCallBackProcessWorld(uint16 log_category, const char *func, std::string message) { // we don't want to loop up with chat messages if (message.find("OP_SpecialMesg") != std::string::npos) { return; } // Cut messages down to 4000 max to prevent client crash if (!message.empty()) { message = message.substr(0, 4000); } // Replace Occurrences of % or MessageStatus will crash Strings::FindReplace(message, std::string("%"), std::string(".")); if (message.find('\n') != std::string::npos) { auto message_split = Strings::Split(message, '\n'); for (size_t iter = 0; iter < message_split.size(); ++iter) { ZSList::Instance()->SendEmoteMessage( 0, 0, AccountStatus::QuestTroupe, EQEmuLogSys::Instance()->GetGMSayColorFromCategory(log_category), fmt::format( " {}{}", (iter == 0 ? " ---" : ""), message_split[iter] ).c_str() ); } return; } ZSList::Instance()->SendEmoteMessage( 0, 0, AccountStatus::QuestTroupe, EQEmuLogSys::Instance()->GetGMSayColorFromCategory(log_category), "%s", fmt::format("[{}] [{}] {}", Logs::LogCategoryName[log_category], func, message).c_str() ); } bool WorldBoot::HandleCommandInput(int argc, char **argv) { // command handler if (argc > 1) { EQEmuLogSys::Instance()->SilenceConsoleLogging(); PathManager::Instance()->Init(); WorldConfig::LoadConfig(); LoadDatabaseConnections(); RuleManager::Instance()->LoadRules(&database, "default", false); EQEmuLogSys::Instance()->EnableConsoleLogging(); WorldserverCLI::CommandHandler(argc, argv); } // database version uint32 database_version = CURRENT_BINARY_DATABASE_VERSION; uint32 bots_database_version = RuleB(Bots, Enabled) ? CURRENT_BINARY_BOTS_DATABASE_VERSION : 0; if (argc >= 2) { if (strcasecmp(argv[1], "db_version") == 0) { std::cout << "Binary Database Version: " << database_version << " : " << bots_database_version << std::endl; return true; } } // check if we ran a valid command, this whole CLI handler needs to be improved at a later time std::string arg1 = argc >= 2 ? argv[1] : ""; if (argc >= 2 && !Strings::Contains(arg1, ":")) { std::cout << "Invalid command, use --help to see available commands" << std::endl; return true; } return false; } bool WorldBoot::LoadServerConfig() { LogInfo("Loading server configuration"); if (!WorldConfig::LoadConfig()) { LogError("Loading server configuration failed"); return false; } return true; } bool WorldBoot::LoadDatabaseConnections() { const auto c = EQEmuConfig::get(); LogInfo( "Connecting to MySQL [{}]@[{}]:[{}]", c->DatabaseUsername.c_str(), c->DatabaseHost.c_str(), c->DatabasePort ); if (!database.Connect( c->DatabaseHost.c_str(), c->DatabaseUsername.c_str(), c->DatabasePassword.c_str(), c->DatabaseDB.c_str(), c->DatabasePort )) { LogError("Cannot continue without a database connection"); return false; } // Multi-tenancy - content database if (!c->ContentDbHost.empty()) { if (!content_db.Connect( c->ContentDbHost.c_str(), c->ContentDbUsername.c_str(), c->ContentDbPassword.c_str(), c->ContentDbName.c_str(), c->ContentDbPort, "content" )) { LogError("Cannot continue without a content database connection"); return false; } } else { content_db.SetMySQL(database); // when database and content_db share the same underlying mysql connection // it needs to be protected by a shared mutex otherwise we produce concurrency issues // when database actions are occurring in different threads database.SetMutex(mutex); content_db.SetMutex(mutex); } return true; } int get_file_size(const std::string &filename) // path to file { FILE *p_file = NULL; p_file = fopen(filename.c_str(), "rb"); fseek(p_file, 0, SEEK_END); int size = ftell(p_file); fclose(p_file); return size; } void WorldBoot::RegisterLoginservers() { const auto c = EQEmuConfig::get(); if (c->LoginCount == 0) { if (c->LoginHost.length()) { LoginServerList::Instance()->Add( c->LoginHost.c_str(), c->LoginPort, c->LoginAccount.c_str(), c->LoginPassword.c_str(), c->LoginLegacy ); LogInfo("Added loginserver [{}]:[{}]", c->LoginHost.c_str(), c->LoginPort); } } else { LinkedList loginlist = c->loginlist; LinkedListIterator iterator(loginlist); iterator.Reset(); while (iterator.MoreElements()) { if (iterator.GetData()->LoginHost.length()) { LoginServerList::Instance()->Add( iterator.GetData()->LoginHost.c_str(), iterator.GetData()->LoginPort, iterator.GetData()->LoginAccount.c_str(), iterator.GetData()->LoginPassword.c_str(), iterator.GetData()->LoginLegacy ); LogInfo( "Added loginserver [{}]:[{}]", iterator.GetData()->LoginHost.c_str(), iterator.GetData()->LoginPort ); } iterator.Advance(); } } } bool WorldBoot::DatabaseLoadRoutines(int argc, char **argv) { // logging system init auto logging = EQEmuLogSys::Instance()->SetDatabase(&database) ->SetLogPath(PathManager::Instance()->GetLogPath()) ->LoadLogDatabaseSettings(); EQEmuLogSys::Instance()->SetDiscordHandler(&WorldBoot::DiscordWebhookMessageHandler); const auto c = EQEmuConfig::get(); if (c->auto_database_updates) { LogInfo("Checking Database Conversions"); auto *r = RuleManager::Instance(); r->LoadRules(&database, "default", false); if (!RuleB(Bots, Enabled) && database.DoesTableExist("bot_data")) { LogInfo("Bot tables found but rule not enabled, enabling"); r->SetRule("Bots:Enabled", "true", &database, true, true); } DatabaseUpdate update{}; update.SetDatabase(&database) ->SetContentDatabase(&content_db) ->CheckDbUpdates(); } if (RuleB(Logging, WorldGMSayLogging)) { logging->SetGMSayHandler(&WorldBoot::GMSayHookCallBackProcessWorld); } logging->StartFileLogs(); LogInfo("Loading variables"); database.LoadVariables(); std::string hotfix_name; if (database.GetVariable("hotfix_name", hotfix_name)) { if (!hotfix_name.empty()) { LogInfo("Current hotfix in use: [{}]", hotfix_name.c_str()); } } guild_mgr.SetDatabase(&database)->SetContentDatabase(&content_db); LogInfo("Purging expired data buckets"); database.PurgeAllDeletedDataBuckets(); LogInfo("Loading zones"); ZoneStore::Instance()->LoadZones(content_db); if (ZoneStore::Instance()->GetZones().empty()) { LogError("Failed to load zones data, check your schema for possible errors"); return 1; } LogInfo("Clearing groups"); database.ClearGroup(); LogInfo("Clearing raids"); database.ClearRaid(); database.ClearRaidDetails(); database.ClearRaidLeader(); LogInfo("Clearing guild online status"); database.ClearGuildOnlineStatus(); LogInfo("Clearing inventory snapshots"); database.ClearInvSnapshots(); LogInfo("Loading items"); LogInfo("Clearing trader table details"); database.ClearTraderDetails(); database.ClearBuyerDetails(); LogInfo("Clearing buyer table details"); if (RuleB(Bots, Enabled)) { LogInfo("Clearing [bot_pet_buffs] table of stale entries"); database.QueryDatabase( "DELETE FROM bot_pet_buffs WHERE NOT EXISTS (SELECT * FROM bot_pets WHERE bot_pets.pets_index = bot_pet_buffs.pets_index)" ); } if (!content_db.LoadItems(hotfix_name)) { LogError("Error: Could not load item data. But ignoring"); } guild_mgr.LoadGuilds(); guild_mgr.LoadTributes(); //rules: { if (!RuleManager::Instance()->UpdateOrphanedRules(&database)) { LogInfo("Failed to process 'Orphaned Rules' update operation."); } if (!RuleManager::Instance()->UpdateInjectedRules(&database, "default")) { LogInfo("Failed to process 'Injected Rules' for ruleset 'default' update operation."); } std::string tmp; if (database.GetVariable("RuleSet", tmp)) { LogInfo("Loading rule set [{}]", tmp.c_str()); if (!RuleManager::Instance()->LoadRules(&database, tmp, false)) { LogInfo("Failed to load ruleset [{}], falling back to defaults", tmp.c_str()); } } else { if (!RuleManager::Instance()->LoadRules(&database, "default", false)) { LogInfo("No rule set configured, using default rules"); } } if (!RuleManager::Instance()->RestoreRuleNotes(&database)) { LogInfo("Failed to process 'Restore Rule Notes' update operation."); } } EQ::InitializeDynamicLookups(); if (RuleB(World, ClearTempMerchantlist)) { LogInfo("Clearing temporary merchant lists"); database.ClearMerchantTemp(); } LogInfo("Loading EQ time of day"); TimeOfDay_Struct eqTime{}; time_t realtime; eqTime = database.LoadTime(realtime); ZSList::Instance()->worldclock.SetCurrentEQTimeOfDay(eqTime, realtime); LogInfo("Deleted [{}] stale player corpses from database", database.DeleteStalePlayerCorpses()); LogInfo("Loading adventures"); if (!AdventureManager::Instance()->LoadAdventureTemplates()) { LogInfo("Unable to load adventure templates"); } if (!AdventureManager::Instance()->LoadAdventureEntries()) { LogInfo("Unable to load adventure templates"); } AdventureManager::Instance()->LoadLeaderboardInfo(); LogInfo("Purging expired dynamic zones and members"); dynamic_zone_manager.PurgeExpiredDynamicZones(); LogInfo("Purging expired character expedition lockouts"); CharacterExpeditionLockoutsRepository::DeleteWhere(database, "expire_time <= NOW()"); LogInfo("Purging expired character task timers"); CharacterTaskTimersRepository::DeleteWhere(database, "expire_time <= NOW()"); LogInfo("Purging expired instances"); database.PurgeExpiredInstances(); LogInfo("Loading dynamic zones"); dynamic_zone_manager.LoadTemplates(); dynamic_zone_manager.CacheAllFromDatabase(); LogInfo("Loading char create info"); content_db.LoadCharacterCreateAllocations(); content_db.LoadCharacterCreateCombos(); LogInfo("Initializing [EventScheduler]"); WorldEventScheduler::Instance()->SetDatabase(&database)->LoadScheduledEvents(); LogInfo("Initializing [WorldContentService]"); WorldContentService::Instance()->SetDatabase(&database) ->SetContentDatabase(&content_db) ->SetExpansionContext() ->ReloadContentFlags(); LogInfo("Initializing [SharedTaskManager]"); SharedTaskManager::Instance()->SetDatabase(&database) ->SetContentDatabase(&content_db) ->LoadTaskData() ->LoadSharedTaskState(); LogInfo("Purging expired shared tasks"); SharedTaskManager::Instance()->PurgeExpiredSharedTasks(); LogInfo("Cleaning up instance corpses"); database.CleanupInstanceCorpses(); if (RuleB(Zone, StateSavingOnShutdown)) { ZoneStateSpawnsRepository::PurgeInvalidZoneStates(database); ZoneStateSpawnsRepository::PurgeOldZoneStates(database); } return true; } void WorldBoot::CheckForPossibleConfigurationIssues() { const auto c = EQEmuConfig::get(); if (c->DisableConfigChecks) { LogInfo("Configuration checking [disabled]"); return; } const std::string local_address = IpUtil::GetLocalIPAddress(); const std::string public_address = IpUtil::GetPublicIPAddress(); const std::string config_file = "eqemu_config.json"; const std::ifstream is_in_docker("/.dockerenv"); if (local_address.empty() && public_address.empty()) { LogInfo("Configuration check, probes failed for local and public address, returning"); return; } LogInfo("Checking for possible configuration issues"); LogInfo("To disable configuration checks, set [server.disable_config_checks] to [true] in [{}]", config_file); std::string config_address = c->WorldAddress; if (!IpUtil::IsIPAddress(config_address)) { config_address = IpUtil::DNSLookupSync(c->WorldAddress, 9000); LogInfo( "World config address using DNS [{}] resolves to [{}]", c->WorldAddress, config_address ); } std::cout << std::endl; // lan detection if (local_address != public_address && IpUtil::IsIpInPrivateRfc1918(local_address) && local_address != c->LocalAddress && !is_in_docker ) { LogWarning("# LAN detection (Configuration)"); LogWarning(""); LogWarning("You appear to be on a LAN and your localaddress may not be properly set!"); LogWarning("This can prevent local clients from properly connecting to your server"); LogWarning(""); LogWarning("Docs [https://docs.eqemu.io/server/installation/configure-your-eqemu_config/#world]"); LogWarning(""); LogWarning("Config file [{}] path [server.world] variable [localaddress]", config_file); LogWarning(""); LogWarning("Local address (eqemu_config) value [{}] detected value [{}]", c->LocalAddress, local_address); std::cout << std::endl; } // docker configuration if ( ( (c->LocalAddress.empty() && config_address.empty()) || (config_address != public_address) ) && is_in_docker ) { LogWarning("# Docker Configuration (Configuration)"); LogWarning(""); LogWarning("You appear to running EQEmu in a docker container"); LogWarning("In order for networking to work properly you will need to properly configure your server"); LogWarning(""); LogWarning( "If your Docker host is on a [LAN] or behind a NAT / Firewall, your [localaddress] variable under [server.world] will need to"); LogWarning( "be set to your LAN address on the host, not the container address. [address] will need to be your public address"); LogWarning(""); LogWarning( "If your Docker host is directly on the [public internet], your [localaddress] variable under [server.world] can be set to [127.0.0.1]." ); LogWarning(""); LogWarning("[address] will need to be your public address"); LogWarning(""); LogWarning("Docs [https://docs.eqemu.io/server/installation/configure-your-eqemu_config/#world]"); LogWarning(""); LogWarning("Config file [{}] path [server.world] variable(s) [localaddress] [address]", config_file); LogWarning(""); LogWarning("Local address (eqemu_config) value [{}] detected value [{}]", c->LocalAddress, local_address); LogWarning( "Public address (eqemu_config) value [{}] detected value [{}]", config_address, public_address ); std::cout << std::endl; } // docker LAN not set if (c->LocalAddress.empty() && is_in_docker) { LogWarning("# Docker LAN (Configuration)"); LogWarning(""); LogWarning("You appear to running EQEmu in a docker container"); LogWarning( "Your local address does not appear to be set, this may not be an issue if your deployment is not on a LAN" ); LogWarning(""); LogWarning( "If your Docker host is on a [LAN] or behind a NAT / Firewall, your [localaddress] variable under [server.world] will need to"); LogWarning( "be set to your LAN address on the host, not the container address. [address] will need to be your public address"); LogWarning(""); LogWarning( "If your Docker host is directly on the [public internet], your [localaddress] variable under [server.world] can be set to [127.0.0.1]." ); LogWarning(""); LogWarning("[address] will need to be your public address"); LogWarning(""); LogWarning("Docs [https://docs.eqemu.io/server/installation/configure-your-eqemu_config/#world]"); LogWarning(""); LogWarning("Config file [{}] path [server.world] variable(s) [localaddress] [address]", config_file); LogWarning(""); LogWarning("Local address (eqemu_config) value [{}] detected value [{}]", c->LocalAddress, local_address); LogWarning( "Public address (eqemu_config) value [{}] detected value [{}]", config_address, public_address ); std::cout << std::endl; } // public address different from configuration if (!config_address.empty() && public_address != config_address) { LogWarning("# Public address (Configuration)"); LogWarning(""); LogWarning("Your configured public address appears to be different from what's detected!"); LogWarning(""); LogWarning("Docs [https://docs.eqemu.io/server/installation/configure-your-eqemu_config/#world]"); LogWarning(""); LogWarning("Config file [{}] path [server.world] variable [address]", config_file); LogWarning(""); LogWarning( "Public address (eqemu_config) value [{}] detected value [{}]", config_address, public_address ); std::cout << std::endl; } // public address set to meta-address if (config_address == "0.0.0.0") { LogWarning("# Public meta-address (Configuration)"); LogWarning(""); LogWarning("Your configured public address is set to a meta-address (0.0.0.0) (all-interfaces)"); LogWarning( "The meta-address may not work properly and it is recommended you configure your public address explicitly"); LogWarning(""); LogWarning("Docs [https://docs.eqemu.io/server/installation/configure-your-eqemu_config/#world]"); LogWarning(""); LogWarning("Config file [{}] path [server.world] variable [address]", config_file); LogWarning(""); LogWarning( "Public address (eqemu_config) value [{}] detected value [{}]", config_address, public_address ); std::cout << std::endl; } // local address set to meta-address if (c->LocalAddress == "0.0.0.0") { LogWarning("# Local meta-address (Configuration)"); LogWarning(""); LogWarning("Your configured local address is set to a meta-address (0.0.0.0) (all-interfaces)"); LogWarning( "The meta-address may not work properly and it is recommended you configure your local address explicitly" ); LogWarning(""); LogWarning("Docs [https://docs.eqemu.io/server/installation/configure-your-eqemu_config/#world]"); LogWarning(""); LogWarning("Config file [{}] path [server.world] variable [localaddress]", config_file); LogWarning(""); LogWarning("Local address (eqemu_config) value [{}] detected value [{}]", c->LocalAddress, local_address); std::cout << std::endl; } // ucs (public) if ( (!config_address.empty() && c->GetUCSHost() != config_address) ) { LogWarning("# UCS Address Mailhost (Configuration)"); LogWarning(""); LogWarning( "UCS (Universal Chat Service) mail or chat appears to use a different address from your main world address" ); LogWarning("This can result in a chat service that doesn't network properly"); LogWarning(""); LogWarning("Docs [https://docs.eqemu.io/server/installation/configure-your-eqemu_config/#mailserver]"); LogWarning(""); LogWarning( "[server.world.address] value [{}] [server.ucs.host] [{}]", config_address, c->GetUCSHost() ); std::cout << std::endl; } } void WorldBoot::Shutdown() { safe_delete(mutex); } void WorldBoot::SendDiscordMessage(int webhook_id, const std::string &message) { if (UCSConnection::Instance()->IsConnected()) { auto pack = new ServerPacket(ServerOP_DiscordWebhookMessage, sizeof(DiscordWebhookMessage_Struct) + 1); auto *q = (DiscordWebhookMessage_Struct *) pack->pBuffer; strn0cpy(q->message, message.c_str(), 2000); q->webhook_id = webhook_id; UCSConnection::Instance()->SendPacket(pack); safe_delete(pack); } }