eqemu-server/world/world_boot.cpp
Mitch Freeman 91f5932c6d
[Feature] Add RoF2 Guild features (#3699)
* [Feature] Add additional Guild Features

This adds the following guild features and design pattern
- the existing guild system was used
- guild features are based on RoF2 within source with translaters used to converted between client differences
- backward compatible with Ti and UF, and allows for mixed client servers
- Guild Back for Ti and UF is based on RoF2 Permissions for banking if Guild Leader does not use Ti/UF
- Guild Ranks and Permissions are enabled.
- Guild Tributes are enabled.
- Event logging via rules for donating tribute items and plat
- Rules to limit Guild Tributes based on max level of server
- Rewrote guild communications to client using specific opcodes
-- Server no longer sends a guild member list on each zone
-- Guild window is updated when a member levels, rank changes, zone changes, banker/alt status using individual opcodes
-- When a member is removed or added to a guild, a single opcode is sent to each guild member
-- This reduces network traffic considerably

Known issues:
- Visual bug only. Guild Tributes window will display a 0 for level if tribute is above max level rule setting.
- Visual bug only. Guild Mgmt Window will not display an online member if the player has 'show offline' unchecked and a guild member zones within the Notes/Tribute tab.  This is resolved by selecting and de-selecting the 'Show Offline' checkbox.

* Updated RoF2 Guild Comms

Updated RoF2 Guild Comms
Update RoF2 Opcodes
Rewrote RoF2 Guild Communications using specific opcodes.
Added database changes - they are irreversible

* Formatting

* Update base_guild_members_repository.h

* Format GuildInfo

* Format GuildAction enum

* Formatting in clientlist

* quantity vs quantity

* desc vs description

* Format structs

* Inline struct values

* Formatting

* Formatting

* Formatting fixes

* Formatting items

* Formatting

* Formatting

* struct formatting updates

* Updated formatting

* Updated
- std:string items
- naming conventions
- magic numbers

* Repo refactors
Other formatting updates

* Remove test guild commands

* Updated #guild info command

* Add new repo methods for Neckolla ReplaceOne and ReplaceMany

* Fix guild_tributes repo

* Update database_update_manifest.cpp

* Phase 1 of final testing with RoF2 -> RoF2.
Next phase will be inter compatibility review

* Remove #guild testing commands

* Fix uf translator error
Rewrite LoadGuilds

* Use extended repository

* FIx guild window on member add

* LoadGuild Changes

* Update guild_base.cpp

* Few small fixes for display issue with UF

* Update guild_base.cpp

* Update guild_members_repository.h

* Update zoneserver.cpp

* Update guild.cpp

* Update entity.h

* Switch formatting

* Formatting

* Update worldserver.cpp

* Switch formatting

* Formatting switch statement

* Update guild.cpp

* Formatting in guild_base

* We don't need to validate m_db everywhere

* More formatting / spacing issues

* Switch format

* Update guild_base.cpp

* Fix an UF issue displaying incorrect guildtag as <>

* Updated several constants, fixed a few issues with Ti/UF and guild tributes not being removed or sent when a member is removed/disbands from a guild.

* Formatting and logging updates

* Fix for Loadguilds and permissions after repo updates.

* Cleanup unnecessary m_db checks

* Updated logging to use player_event_logs

* Updated to use the single opcodes for guild traffic for Ti/UF/RoF2.  Several enhancements for guild functionality for more reusable code and readability.

* Update to fix Demote Self and guild invites declining when option set to not accept guild invites

* Potential fix for guild notes/tribute display issues when client has 'Show Offline' unchecked.

* Updates to fox recent master changes

Updates to fix recent master changes

* Updates in response to comments

* Further Updates in response to comments

* Comment updates and refactor for SendAppearance functions

* Comment updates

* Update client spawn process for show guild name

Add show guild tag to default spawn process

* Update to use zone spawn packets for RoF2
Removed several unused functions as a result
Updated MemberRankUpdate to properly update guild_show on rank change.
Updated OP_GuildURLAndChannel opcode for UF/RoF2

* Cleanup of world changes
Created function for repetitive zonelist sendpackets to only booted zones
Re-Inserted accidental delete of scanclosemobs

* Fixes

* Further world cleanup

* Fix a few test guild bank cases for backward compat
Removed a duplicate db call
Fixed a fallthrough issue

* Update guild_mgr.cpp

* Cleanup

---------

Co-authored-by: Akkadius <akkadius1@gmail.com>
2024-02-10 03:27:58 -06:00

622 lines
18 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_task_timers_repository.h"
#include "../common/rulesys.h"
#include "../common/strings.h"
#include "adventure_manager.h"
#include "dynamic_zone_manager.h"
#include "expedition_database.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);
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 inventory snapshots");
database.ClearInvSnapshots();
LogInfo("Loading items");
if (!content_db.LoadItems(hotfix_name)) {
LogError("Error: Could not load item data. But ignoring");
}
if (!content_db.LoadSkillCaps(std::string(hotfix_name))) {
LogError("Error: Could not load skill cap 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 expeditions");
ExpeditionDatabase::PurgeExpiredExpeditions();
ExpeditionDatabase::PurgeExpiredCharacterLockouts();
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)
->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);
}
}