eqemu-server/world/world_boot.cpp
hg 55155ff800
[Expeditions] Move expedition code into DynamicZone (#4672)
This removes the separate Expedition class and moves lockout code and
/dz command handlers into DynamicZone classes. It also refactors some
code to reduce bloat and some database usage.

This completes the effort of moving everything to DynamicZone that
started when implementing shared tasks. It also makes sense to do this
since expeditions are just dynamic zones internally despite dzs being
used for other types. Expedition specific things are just handled with
dz type checks.

Functionally nothing should change. This is mainly internal refactoring
and moving code around along with some bug fixes and reduced database
usage.

Main changes:

 - The `expeditions` database table has been removed

 - Expeditions no longer use a separate id, the expedition id is just the dz id

 - Expedition lock state and replay timer option were moved to the
   `dynamic_zones` table

 - Expeditions no longer have a separate cache from dynamic zones

 - Expedition creation no longer has every zone query the database to cache it

 - Expedition internal lockouts are now stored on DynamicZone

 - The `expedition_lockouts` table has been renamed to `dynamic_zone_lockouts`

 - Fixed a small bug with the UpdateLockoutDuration api where the
   internal lockout would get the time added twice in memory in the
   initiating zone (this api is likely rarely used)

 - Fixed an issue where use of the group/raid DoesAnyMemberHaveExpeditionLockout
   api would query once for every out of zone character.

   - This api now checks all members in the current zone first and only
     performs a single bulk query for out of zone members if that check
     is exhausted

 - Deprecated the max_check_count param of DoesAnyMemberHaveExpeditionLockout,
   the quest api still exists to avoid api break but a passed arg has no effect
2025-02-15 18:40:35 -06:00

631 lines
19 KiB
C++

#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"
extern ZSList zoneserver_list;
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) {
zoneserver_list.SendEmoteMessage(
0,
0,
AccountStatus::QuestTroupe,
LogSys.GetGMSayColorFromCategory(log_category),
fmt::format(
" {}{}",
(iter == 0 ? " ---" : ""),
message_split[iter]
).c_str()
);
}
return;
}
zoneserver_list.SendEmoteMessage(
0,
0,
AccountStatus::QuestTroupe,
LogSys.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) {
LogSys.SilenceConsoleLogging();
path.LoadPaths();
WorldConfig::LoadConfig();
LoadDatabaseConnections();
RuleManager::Instance()->LoadRules(&database, "default", false);
LogSys.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;
}
}
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;
}
extern LoginServerList loginserverlist;
void WorldBoot::RegisterLoginservers()
{
const auto c = EQEmuConfig::get();
if (c->LoginCount == 0) {
if (c->LoginHost.length()) {
loginserverlist.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<LoginConfig *> loginlist = c->loginlist;
LinkedListIterator<LoginConfig *> iterator(loginlist);
iterator.Reset();
while (iterator.MoreElements()) {
if (iterator.GetData()->LoginHost.length()) {
loginserverlist.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();
}
}
}
extern SharedTaskManager shared_task_manager;
extern AdventureManager adventure_manager;
extern WorldEventScheduler event_scheduler;
bool WorldBoot::DatabaseLoadRoutines(int argc, char **argv)
{
// logging system init
auto logging = LogSys.SetDatabase(&database)
->SetLogPath(path.GetLogPath())
->LoadLogDatabaseSettings();
LogSys.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");
zone_store.LoadZones(content_db);
if (zone_store.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);
zoneserver_list.worldclock.SetCurrentEQTimeOfDay(eqTime, realtime);
LogInfo("Deleted [{}] stale player corpses from database", database.DeleteStalePlayerCorpses());
LogInfo("Loading adventures");
if (!adventure_manager.LoadAdventureTemplates()) {
LogInfo("Unable to load adventure templates");
}
if (!adventure_manager.LoadAdventureEntries()) {
LogInfo("Unable to load adventure templates");
}
adventure_manager.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]");
event_scheduler.SetDatabase(&database)->LoadScheduledEvents();
LogInfo("Initializing [WorldContentService]");
content_service.SetDatabase(&database)
->SetContentDatabase(&content_db)
->SetExpansionContext()
->ReloadContentFlags();
LogInfo("Initializing [SharedTaskManager]");
shared_task_manager.SetDatabase(&database)
->SetContentDatabase(&content_db)
->LoadTaskData()
->LoadSharedTaskState();
LogInfo("Purging expired shared tasks");
shared_task_manager.PurgeExpiredSharedTasks();
LogInfo("Cleaning up instance corpses");
database.CleanupInstanceCorpses();
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 (UCSLink.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;
UCSLink.SendPacket(pack);
safe_delete(pack);
}
}