From 78f4a6e63bd97a076d1028beb45cb8a39be8ac9d Mon Sep 17 00:00:00 2001 From: Devn00b Date: Mon, 5 Feb 2024 19:04:10 -0800 Subject: [PATCH] Added Discord chat support. --- DB/updates/DiscordBridgeOptions-2-5-24.sql | 4 + EQ2/source/WorldServer/Chat/Chat.cpp | 60 +++++++++++++ EQ2/source/WorldServer/Chat/Chat.h | 9 ++ EQ2/source/WorldServer/Chat/ChatChannel.cpp | 30 +++++-- EQ2/source/WorldServer/Rules/Rules.cpp | 6 ++ EQ2/source/WorldServer/Rules/Rules.h | 10 ++- EQ2/source/WorldServer/makefile.discord | 97 +++++++++++++++++++++ EQ2/source/WorldServer/net.cpp | 86 +++++++++++++++++- 8 files changed, 289 insertions(+), 13 deletions(-) create mode 100755 DB/updates/DiscordBridgeOptions-2-5-24.sql create mode 100644 EQ2/source/WorldServer/makefile.discord diff --git a/DB/updates/DiscordBridgeOptions-2-5-24.sql b/DB/updates/DiscordBridgeOptions-2-5-24.sql new file mode 100755 index 000000000..2b5b7370e --- /dev/null +++ b/DB/updates/DiscordBridgeOptions-2-5-24.sql @@ -0,0 +1,4 @@ +INSERT INTO `ruleset_details` (`ruleset_id`, `rule_category`, `rule_type`, `rule_value`, `description`) VALUES (1, 'R_Discord', 'DiscordEnabled', '1', 'Enable (1) or Disable(0) the Discord Bridge System.'); +INSERT INTO `ruleset_details` (`ruleset_id`, `rule_category`, `rule_type`, `rule_value`, `description`) VALUES (1, 'R_Discord', 'DiscordWebhookURL', 'https://example.com', 'Webhook url for EQ2 -> Discord coms.'); +INSERT INTO `ruleset_details` (`ruleset_id`, `rule_category`, `rule_type`, `rule_value`, `description`) VALUES (1, 'R_Discord', 'DiscordBotToken', '0', 'This is the token for the bot, given in the discord developer site.'); +INSERT INTO `ruleset_details` (`ruleset_id`, `rule_category`, `rule_type`, `rule_value`, `description`) VALUES (1, 'R_Discord', 'DiscordListenChan', '0', 'Channe ID you want to listen to chat from. this is for Discord -> EQ2 coms.'); \ No newline at end of file diff --git a/EQ2/source/WorldServer/Chat/Chat.cpp b/EQ2/source/WorldServer/Chat/Chat.cpp index fdee8a6fe..8c41c254a 100644 --- a/EQ2/source/WorldServer/Chat/Chat.cpp +++ b/EQ2/source/WorldServer/Chat/Chat.cpp @@ -22,9 +22,24 @@ #include "../../common/Log.h" #include "../../common/ConfigReader.h" #include "../../common/PacketStruct.h" + #include "../Rules/Rules.h" + extern RuleManager rule_manager; + +//devn00b +#ifdef DISCORD + #ifndef WIN32 + #include + #include "ChatChannel.h" + + extern ChatChannel channel; + #endif +#endif + extern ConfigReader configReader; + + Chat::Chat() { m_channels.SetName("Chat::Channels"); } @@ -262,6 +277,8 @@ bool Chat::LeaveAllChannels(Client *client) { bool Chat::TellChannel(Client *client, const char *channel_name, const char *message, const char* name) { vector::iterator itr; bool ret = false; + bool enablediscord = rule_manager.GetGlobalRule(R_Discord, DiscordEnabled)->GetBool(); + const char* discordchan = rule_manager.GetGlobalRule(R_Discord, DiscordChannel)->GetString(); m_channels.readlock(__FUNCTION__, __LINE__); for (itr = channels.begin(); itr != channels.end(); itr++) { @@ -271,6 +288,21 @@ bool Chat::TellChannel(Client *client, const char *channel_name, const char *mes else ret = (*itr)->TellChannel(client, message, name); + if(enablediscord == true && client){ + + if (strcmp(channel_name, discordchan) != 0){ + m_channels.releasereadlock(__FUNCTION__, __LINE__); + return ret; + } +#ifdef DISCORD + if (client) { + std::string whofrom = client->GetPlayer()->GetName(); + std::string msg = string(message); + ret = PushDiscordMsg(msg.c_str(), whofrom.c_str()); + } +#endif + } + break; } } @@ -310,3 +342,31 @@ ChatChannel* Chat::GetChannel(const char *channel_name) { return ret; } + +#ifdef DISCORD +//this sends chat from EQ2EMu to Discord. Currently using webhooks. Makes things simpler code wise. +int Chat::PushDiscordMsg(const char* msg, const char* from) { + bool enablediscord = rule_manager.GetGlobalRule(R_Discord, DiscordEnabled)->GetBool(); + + if(enablediscord == false) { + LogWrite(INIT__INFO, 0,"Discord","Bot Disabled By Rule..."); + return 0; + } + + m_channels.readlock(__FUNCTION__, __LINE__); + const char* hook = rule_manager.GetGlobalRule(R_Discord, DiscordWebhookURL)->GetString(); + std::string servername = net.GetWorldName(); + char ourmsg[4096]; + + //form our message + sprintf(ourmsg,"[%s] [%s] Says: %s",from, servername.c_str(), msg); + + /* send a message with this webhook */ + dpp::cluster bot(""); + dpp::webhook wh(hook); + bot.execute_webhook(wh, dpp::message(ourmsg)); + m_channels.releasereadlock(__FUNCTION__, __LINE__); + + return 1; +} +#endif \ No newline at end of file diff --git a/EQ2/source/WorldServer/Chat/Chat.h b/EQ2/source/WorldServer/Chat/Chat.h index ccdb54a17..fe6c4e2cb 100644 --- a/EQ2/source/WorldServer/Chat/Chat.h +++ b/EQ2/source/WorldServer/Chat/Chat.h @@ -27,6 +27,13 @@ #include "../client.h" #include "ChatChannel.h" +#ifdef DISCORD + #ifndef WIN32 + #pragma once + #include + #endif +#endif + using namespace std; /* @@ -100,6 +107,8 @@ public: bool LeaveAllChannels(Client *client); bool TellChannel(Client *client, const char *channel_name, const char *message, const char* name = 0); bool SendChannelUserList(Client *client, const char *channel_name); + //devn00b + int PushDiscordMsg(const char*, const char*); ChatChannel* GetChannel(const char* channel_name); private: diff --git a/EQ2/source/WorldServer/Chat/ChatChannel.cpp b/EQ2/source/WorldServer/Chat/ChatChannel.cpp index eb6991add..e0fbf9e97 100644 --- a/EQ2/source/WorldServer/Chat/ChatChannel.cpp +++ b/EQ2/source/WorldServer/Chat/ChatChannel.cpp @@ -135,23 +135,36 @@ bool ChatChannel::TellChannel(Client *client, const char *message, const char* n packet_struct->setDataByName("from_spawn_id", 0xFFFFFFFF); packet_struct->setDataByName("to_spawn_id", 0xFFFFFFFF); - if (client) + if (client != NULL){ packet_struct->setDataByName("from", client->GetPlayer()->GetName()); - else - packet_struct->setDataByName("from", name2); + } else { + char name3[128]; + sprintf(name3,"[%s] from discord",name2); + packet_struct->setDataByName("from", name3); + } packet_struct->setDataByName("to", to_client->GetPlayer()->GetName()); packet_struct->setDataByName("channel", to_client->GetMessageChannelColor(CHANNEL_CUSTOM_CHANNEL)); - packet_struct->setDataByName("language", client->GetPlayer()->GetCurrentLanguage()); + + if(client != NULL){ + packet_struct->setDataByName("language", client->GetPlayer()->GetCurrentLanguage()); + }else{ + packet_struct->setDataByName("language", 0); + } packet_struct->setDataByName("message", message); packet_struct->setDataByName("channel_name", name); packet_struct->setDataByName("show_bubble", 1); - - if (client->GetPlayer()->GetCurrentLanguage() == 0 || to_client->GetPlayer()->HasLanguage(client->GetPlayer()->GetCurrentLanguage())) { + + if(client != NULL){ + if (client->GetPlayer()->GetCurrentLanguage() == 0 || to_client->GetPlayer()->HasLanguage(client->GetPlayer()->GetCurrentLanguage())) { + packet_struct->setDataByName("understood", 1); + } + } else { packet_struct->setDataByName("understood", 1); } + packet_struct->setDataByName("unknown4", 0); - + to_client->QueuePacket(packet_struct->serialize()); safe_delete(packet_struct); } @@ -210,4 +223,5 @@ bool ChatChannel::SendChannelUserList(Client *client) { safe_delete(packet_struct); return true; -} +} + diff --git a/EQ2/source/WorldServer/Rules/Rules.cpp b/EQ2/source/WorldServer/Rules/Rules.cpp index 08dac36c9..4645d0757 100644 --- a/EQ2/source/WorldServer/Rules/Rules.cpp +++ b/EQ2/source/WorldServer/Rules/Rules.cpp @@ -376,6 +376,12 @@ void RuleManager::Init() RULE_INIT(R_World, DatabaseVersion, "0"); + //devn00b + RULE_INIT(R_Discord, DiscordEnabled, "0"); //Enable/Disable built in discord bot. + RULE_INIT(R_Discord, DiscordWebhookURL, "None"); //Webhook url used for server -> discord messages. + RULE_INIT(R_Discord, DiscordBotToken, "None"); //Bot token used to connect to discord and provides discord -> server messages. + RULE_INIT(R_Discord, DiscordChannel, "Discord"); // in-game channel used for server -> discord messages. + RULE_INIT(R_Discord, DiscordListenChan, "0"); // Discord ChannelID used for discord->server messages. #undef RULE_INIT } diff --git a/EQ2/source/WorldServer/Rules/Rules.h b/EQ2/source/WorldServer/Rules/Rules.h index 01eafa6bb..d040b4d98 100644 --- a/EQ2/source/WorldServer/Rules/Rules.h +++ b/EQ2/source/WorldServer/Rules/Rules.h @@ -40,7 +40,8 @@ enum RuleCategory { R_Zone, R_Loot, R_Spells, - R_Expansion + R_Expansion, + R_Discord }; enum RuleType { @@ -231,7 +232,12 @@ enum RuleType { DatabaseVersion, SkipLootGrayMob, - LootDistributionTime + LootDistributionTime, + DiscordEnabled, + DiscordWebhookURL, + DiscordBotToken, + DiscordChannel, + DiscordListenChan }; class Rule { diff --git a/EQ2/source/WorldServer/makefile.discord b/EQ2/source/WorldServer/makefile.discord new file mode 100644 index 000000000..739f4e097 --- /dev/null +++ b/EQ2/source/WorldServer/makefile.discord @@ -0,0 +1,97 @@ +# Programs +CC = gcc +CXX = g++ +LINKER = g++ + + +# Configuration +Build_Dir = build +Source_Dir = .. +APP = eq2world + + +# LUA flags +Lua_C_Flags = -DLUA_COMPAT_ALL -DLUA_USE_LINUX +Lua_W_Flags = -Wall + + +# World flags +C_Flags = -I/usr/include/mariadb -I../depends/fmt/include -I../depends/recastnavigation/Detour/Include -I/usr/local/include/boost -I../depends/glm/ -march=native -pipe -pthread -std=c++17 +LD_Flags = -L/usr/lib/x86_64-linux-gnu -lz -lpthread -lmariadbclient -L../depends/recastnavigation/RecastDemo/Build/gmake/lib/Debug -lDebugUtils -lDetour -lDetourCrowd -lDetourTileCache -lRecast -L/usr/local/lib -rdynamic -lm -Wl,-E -ldl -lreadline -lboost_system -lboost_filesystem -lboost_iostreams -lboost_regex +W_Flags = -Wall -Wno-reorder +D_Flags = -DEQ2 -DWORLD -D_GNU_SOURCE + + +# Setup Debug or Release build +ifeq ($(BUILD),debug) + # "Debug" build - minimum optimization, and debugging symbols + C_Flags += -O -ggdb + D_Flags += -DDEBUG -DDISCORD + LD_Flags += -ldpp + Current_Build_Dir := $(Build_Dir)/debug + App_Filename = $(APP)_debug +else + # "Release" build - optimization, and no debug symbols + C_Flags += -O2 -s -DNDEBUG + Current_Build_Dir := $(Build_Dir)/release + App_Filename = $(APP) +endif + + +# File lists +World_Source = $(wildcard $(Source_Dir)/WorldServer/*.cpp) $(wildcard $(Source_Dir)/WorldServer/*/*.cpp) +World_Objects = $(patsubst %.cpp,$(Current_Build_Dir)/%.o,$(subst $(Source_Dir)/,,$(World_Source))) +Common_Source = $(wildcard $(Source_Dir)/common/*.cpp) +Common_Objects = $(patsubst %.cpp,$(Current_Build_Dir)/%.o,$(subst $(Source_Dir)/,,$(Common_Source))) +Lua_Source = $(wildcard $(Source_Dir)/LUA/*.c) +Lua_Objects = $(patsubst %.c,$(Current_Build_Dir)/%.o,$(subst $(Source_Dir)/,,$(Lua_Source))) + + +# Receipes +all: $(APP) + +$(APP): $(Common_Objects) $(World_Objects) $(Lua_Objects) + @echo Linking... + @$(LINKER) $(W_Flags) $^ $(LD_Flags) -o $(App_Filename) + @test -e $(APP) || /bin/true + #@ln -s $(App_Filename) $(APP) || /bin/true + @echo Finished building world. + +$(Current_Build_Dir)/LUA/%.o: $(Source_Dir)/LUA/%.c + @mkdir -p $(dir $@) + $(CC) -c $(Lua_C_Flags) $(Lua_W_Flags) $< -o $@ + +$(Current_Build_Dir)/%.o: $(Source_Dir)/%.cpp + @mkdir -p $(dir $@) + $(CXX) -c $(C_Flags) $(D_Flags) $(W_Flags) $< -o $@ + +#setup: +# @test ! -e volumes.phys && ln -s $(Conf_Dir)/volumes.phys . || /bin/true +# @test ! -e vgemu-structs.xml && ln -s $(Conf_Dir)/vgemu-structs.xml . || /bin/true +# @$(foreach folder,$(wildcard $(Content_Dir)/scripts/*),test -d $(Content_Dir)/scripts && test ! -e $(notdir $(folder)) && ln -s $(folder) . || /bin/true) +# @echo "Symlinks have been created." +# @cp -n $(Conf_Dir)/vgemu-world.xml . +# @echo "You need to edit your config file: vgemu-world.xml" + +release: + @$(MAKE) "BUILD=release" + +debug: + @$(MAKE) "BUILD=debug" + +clean: + rm -rf $(filter-out %Lua,$(foreach folder,$(wildcard $(Current_Build_Dir)/*),$(folder))) $(App_Filename) $(APP) + +cleanlua: + rm -rf $(Current_Build_Dir)/Lua + +cleanall: + rm -rf $(Build_Dir) $(App_Filename) $(APP) + +#cleansetup: +# rm volumes.phys vgemu-structs.xml $(foreach folder,$(wildcard $(Content_Dir)/scripts/*),$(notdir $(folder))) + +#docs: docs-world + +#docs-world: +# @cd ../../doc; doxygen Doxyfile-World diff --git a/EQ2/source/WorldServer/net.cpp b/EQ2/source/WorldServer/net.cpp index e04f1b782..f8eacefde 100644 --- a/EQ2/source/WorldServer/net.cpp +++ b/EQ2/source/WorldServer/net.cpp @@ -57,6 +57,16 @@ using namespace std; #include "Transmute.h" #include "Zone/ChestTrap.h" +//devn00b +#ifdef DISCORD + //linux only for the moment. + #ifndef WIN32 + #include + #include "Chat/Chat.h" + extern Chat chat; + #endif +#endif + double frame_time = 0.0; #ifdef WIN32 @@ -113,6 +123,12 @@ extern map EQOpcodeVersions; ThreadReturnType ItemLoad (void* tmp); ThreadReturnType AchievmentLoad (void* tmp); ThreadReturnType SpellLoad (void* tmp); +//devn00b +#ifdef DISCORD + #ifndef WIN32 + ThreadReturnType StartDiscord (void* tmp); + #endif +#endif int main(int argc, char** argv) { #ifdef PROFILER @@ -238,9 +254,12 @@ int main(int argc, char** argv) { pthread_t thread2; pthread_create(&thread2, NULL, SpellLoad, &world); pthread_detach(thread2); - //pthread_t thread3; - //pthread_create(&thread3, NULL, AchievmentLoad, &world); - //pthread_detach(thread3); + //devn00b + #ifdef DISCORD + pthread_t thread3; + pthread_create(&thread3, NULL, StartDiscord, &world); + pthread_detach(thread3); + #endif #endif } @@ -930,3 +949,64 @@ void NetConnection::WelcomeHeader() fflush(stdout); } + +#ifdef DISCORD +ThreadReturnType StartDiscord(void* tmp) +{ +#ifndef DISCORD + THREAD_RETURN(NULL); +#endif + if (tmp == 0) { + ThrowError("StartDiscord: tmp = 0!"); + THREAD_RETURN(NULL); + } + +#ifdef WIN32 + SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_NORMAL); +#endif + + bool enablediscord = rule_manager.GetGlobalRule(R_Discord, DiscordEnabled)->GetBool(); + + if(enablediscord == false) { + LogWrite(INIT__INFO, 0,"Discord","Bot Disabled By Rule..."); + THREAD_RETURN(NULL); + } + + LogWrite(INIT__INFO, 0, "Discord", "Starting Discord Bridge..."); + const char* bottoken = rule_manager.GetGlobalRule(R_Discord, DiscordBotToken)->GetString(); + + if(strlen(bottoken)== 0) { + LogWrite(INIT__INFO, 0,"Discord","Bot Token Was Empty..."); + THREAD_RETURN(NULL); + } + + dpp::cluster bot(bottoken, dpp::i_default_intents | dpp::i_message_content); + + //if we have debug on, go ahead and show DPP logs. + #ifdef DEBUG + bot.on_log([&bot](const dpp::log_t & event) { + std::cout << "[" << dpp::utility::loglevel(event.severity) << "] " << event.message << "\n"; + }); + #endif + + bot.on_message_create([&bot](const dpp::message_create_t& event) { + if (event.msg.author.is_bot() == false) { + std::string chanid = event.msg.channel_id.str(); + std::string listenchan = rule_manager.GetGlobalRule(R_Discord, DiscordListenChan)->GetString(); + + if(chanid.compare(listenchan) != 0 || !chanid.size() || !listenchan.size()) { + return; + } + chat.TellChannel(NULL, listenchan.c_str(), event.msg.content.c_str(), event.msg.author.username.c_str()); + } + }); + + while(true) { + bot.start(dpp::st_wait); + //wait 30s for reconnect. prevents hammering discord and a potential ban. + std::this_thread::sleep_for(std::chrono::milliseconds(30000)); + } + + THREAD_RETURN(NULL); +} +#endif \ No newline at end of file