mirror of
https://github.com/mod-playerbots/azerothcore-wotlk.git
synced 2026-01-25 22:56:24 +00:00
Merge branch 'master' into Playerbot
This commit is contained in:
149
src/server/apps/worldserver/ACSoap/ACSoap.cpp
Normal file
149
src/server/apps/worldserver/ACSoap/ACSoap.cpp
Normal file
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by the
|
||||
* Free Software Foundation; either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ACSoap.h"
|
||||
#include "AccountMgr.h"
|
||||
#include "Log.h"
|
||||
#include "World.h"
|
||||
#include "soapStub.h"
|
||||
|
||||
void ACSoapThread(const std::string& host, uint16 port)
|
||||
{
|
||||
struct soap soap;
|
||||
soap_init(&soap);
|
||||
soap_set_imode(&soap, SOAP_C_UTFSTRING);
|
||||
soap_set_omode(&soap, SOAP_C_UTFSTRING);
|
||||
|
||||
// check every 3 seconds if world ended
|
||||
soap.accept_timeout = 3;
|
||||
soap.recv_timeout = 5;
|
||||
soap.send_timeout = 5;
|
||||
|
||||
if (!soap_valid_socket(soap_bind(&soap, host.c_str(), port, 100)))
|
||||
{
|
||||
LOG_ERROR("network.soap", "ACSoap: couldn't bind to {}:{}", host, port);
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
LOG_INFO("network.soap", "ACSoap: bound to http://{}:{}", host, port);
|
||||
|
||||
while (!World::IsStopped())
|
||||
{
|
||||
if (!soap_valid_socket(soap_accept(&soap)))
|
||||
continue; // ran into an accept timeout
|
||||
|
||||
LOG_DEBUG("network.soap", "ACSoap: accepted connection from IP={}.{}.{}.{}", (int)(soap.ip >> 24) & 0xFF, (int)(soap.ip >> 16) & 0xFF, (int)(soap.ip >> 8) & 0xFF, (int)soap.ip & 0xFF);
|
||||
struct soap* thread_soap = soap_copy(&soap);// make a safe copy
|
||||
|
||||
process_message(thread_soap);
|
||||
}
|
||||
|
||||
soap_destroy(&soap);
|
||||
soap_end(&soap);
|
||||
soap_done(&soap);
|
||||
}
|
||||
|
||||
void process_message(struct soap* soap_message)
|
||||
{
|
||||
LOG_TRACE("network.soap", "SOAPWorkingThread::process_message");
|
||||
|
||||
soap_serve(soap_message);
|
||||
soap_destroy(soap_message); // dealloc C++ data
|
||||
soap_end(soap_message); // dealloc data and clean up
|
||||
soap_free(soap_message); // detach soap struct and fre up the memory
|
||||
}
|
||||
|
||||
/*
|
||||
Code used for generating stubs:
|
||||
int ns1__executeCommand(char* command, char** result);
|
||||
*/
|
||||
int ns1__executeCommand(soap* soap, char* command, char** result)
|
||||
{
|
||||
// security check
|
||||
if (!soap->userid || !soap->passwd)
|
||||
{
|
||||
LOG_DEBUG("network.soap", "ACSoap: Client didn't provide login information");
|
||||
return 401;
|
||||
}
|
||||
|
||||
uint32 accountId = AccountMgr::GetId(soap->userid);
|
||||
if (!accountId)
|
||||
{
|
||||
LOG_DEBUG("network", "ACSoap: Client used invalid username '{}'", soap->userid);
|
||||
return 401;
|
||||
}
|
||||
|
||||
if (!AccountMgr::CheckPassword(accountId, soap->passwd))
|
||||
{
|
||||
LOG_DEBUG("network.soap", "ACSoap: invalid password for account '{}'", soap->userid);
|
||||
return 401;
|
||||
}
|
||||
|
||||
if (AccountMgr::GetSecurity(accountId) < SEC_ADMINISTRATOR)
|
||||
{
|
||||
LOG_DEBUG("network.soap", "ACSoap: {}'s gmlevel is too low", soap->userid);
|
||||
return 403;
|
||||
}
|
||||
|
||||
if (!command || !*command)
|
||||
return soap_sender_fault(soap, "Command can not be empty", "The supplied command was an empty string");
|
||||
|
||||
LOG_DEBUG("network.soap", "ACSoap: got command '{}'", command);
|
||||
SOAPCommand connection;
|
||||
|
||||
// commands are executed in the world thread. We have to wait for them to be completed
|
||||
{
|
||||
// CliCommandHolder will be deleted from world, accessing after queueing is NOT save
|
||||
CliCommandHolder* cmd = new CliCommandHolder(&connection, command, &SOAPCommand::print, &SOAPCommand::commandFinished);
|
||||
sWorld->QueueCliCommand(cmd);
|
||||
}
|
||||
|
||||
// Wait until the command has finished executing
|
||||
connection.finishedPromise.get_future().wait();
|
||||
|
||||
// The command has finished executing already
|
||||
char* printBuffer = soap_strdup(soap, connection.m_printBuffer.c_str());
|
||||
if (connection.hasCommandSucceeded())
|
||||
{
|
||||
*result = printBuffer;
|
||||
return SOAP_OK;
|
||||
}
|
||||
else
|
||||
return soap_sender_fault(soap, printBuffer, printBuffer);
|
||||
}
|
||||
|
||||
void SOAPCommand::commandFinished(void* soapconnection, bool success)
|
||||
{
|
||||
SOAPCommand* con = (SOAPCommand*)soapconnection;
|
||||
con->setCommandSuccess(success);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Namespace Definition Table
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
struct Namespace namespaces[] =
|
||||
{
|
||||
{ "SOAP-ENV", "http://schemas.xmlsoap.org/soap/envelope/", nullptr, nullptr }, // must be first
|
||||
{ "SOAP-ENC", "http://schemas.xmlsoap.org/soap/encoding/", nullptr, nullptr }, // must be second
|
||||
{ "xsi", "http://www.w3.org/1999/XMLSchema-instance", "http://www.w3.org/*/XMLSchema-instance", nullptr },
|
||||
{ "xsd", "http://www.w3.org/1999/XMLSchema", "http://www.w3.org/*/XMLSchema", nullptr },
|
||||
{ "ns1", "urn:AC", nullptr, nullptr }, // "ns1" namespace prefix
|
||||
{ nullptr, nullptr, nullptr, nullptr }
|
||||
};
|
||||
64
src/server/apps/worldserver/ACSoap/ACSoap.h
Normal file
64
src/server/apps/worldserver/ACSoap/ACSoap.h
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by the
|
||||
* Free Software Foundation; either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef _ACSOAP_H
|
||||
#define _ACSOAP_H
|
||||
|
||||
#include "Define.h"
|
||||
#include <future>
|
||||
#include <mutex>
|
||||
|
||||
void process_message(struct soap* soap_message);
|
||||
void ACSoapThread(const std::string& host, uint16 port);
|
||||
|
||||
class SOAPCommand
|
||||
{
|
||||
public:
|
||||
SOAPCommand() :
|
||||
m_success(false) { }
|
||||
|
||||
~SOAPCommand() { }
|
||||
|
||||
void appendToPrintBuffer(std::string_view msg)
|
||||
{
|
||||
m_printBuffer += msg;
|
||||
}
|
||||
|
||||
void setCommandSuccess(bool val)
|
||||
{
|
||||
m_success = val;
|
||||
finishedPromise.set_value();
|
||||
}
|
||||
|
||||
bool hasCommandSucceeded() const
|
||||
{
|
||||
return m_success;
|
||||
}
|
||||
|
||||
static void print(void* callbackArg, std::string_view msg)
|
||||
{
|
||||
((SOAPCommand*)callbackArg)->appendToPrintBuffer(msg);
|
||||
}
|
||||
|
||||
static void commandFinished(void* callbackArg, bool success);
|
||||
|
||||
bool m_success;
|
||||
std::string m_printBuffer;
|
||||
std::promise<void> finishedPromise;
|
||||
};
|
||||
|
||||
#endif
|
||||
194
src/server/apps/worldserver/CommandLine/CliRunnable.cpp
Normal file
194
src/server/apps/worldserver/CommandLine/CliRunnable.cpp
Normal file
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by the
|
||||
* Free Software Foundation; either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/// \addtogroup Acored
|
||||
/// @{
|
||||
/// \file
|
||||
|
||||
#include "CliRunnable.h"
|
||||
#include "Config.h"
|
||||
#include "Errors.h"
|
||||
#include "ObjectMgr.h"
|
||||
#include "World.h"
|
||||
#include <fmt/core.h>
|
||||
|
||||
#if AC_PLATFORM != AC_PLATFORM_WINDOWS
|
||||
#include "Chat.h"
|
||||
#include "ChatCommand.h"
|
||||
#include <cstring>
|
||||
#include <readline/history.h>
|
||||
#include <readline/readline.h>
|
||||
#endif
|
||||
|
||||
static constexpr char CLI_PREFIX[] = "AC> ";
|
||||
|
||||
static inline void PrintCliPrefix()
|
||||
{
|
||||
fmt::print(CLI_PREFIX);
|
||||
}
|
||||
|
||||
#if AC_PLATFORM != AC_PLATFORM_WINDOWS
|
||||
namespace Acore::Impl::Readline
|
||||
{
|
||||
static std::vector<std::string> vec;
|
||||
char* cli_unpack_vector(char const*, int state)
|
||||
{
|
||||
static size_t i=0;
|
||||
if (!state)
|
||||
i = 0;
|
||||
if (i < vec.size())
|
||||
return strdup(vec[i++].c_str());
|
||||
else
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
char** cli_completion(char const* text, int /*start*/, int /*end*/)
|
||||
{
|
||||
::rl_attempted_completion_over = 1;
|
||||
vec = Acore::ChatCommands::GetAutoCompletionsFor(CliHandler(nullptr,nullptr), text);
|
||||
return ::rl_completion_matches(text, &cli_unpack_vector);
|
||||
}
|
||||
|
||||
int cli_hook_func()
|
||||
{
|
||||
if (World::IsStopped())
|
||||
::rl_done = 1;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void utf8print(void* /*arg*/, std::string_view str)
|
||||
{
|
||||
#if AC_PLATFORM == AC_PLATFORM_WINDOWS
|
||||
fmt::print(str);
|
||||
#else
|
||||
{
|
||||
fmt::print(str);
|
||||
fflush(stdout);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void commandFinished(void*, bool /*success*/)
|
||||
{
|
||||
PrintCliPrefix();
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
#ifdef linux
|
||||
// Non-blocking keypress detector, when return pressed, return 1, else always return 0
|
||||
int kb_hit_return()
|
||||
{
|
||||
struct timeval tv;
|
||||
fd_set fds;
|
||||
tv.tv_sec = 0;
|
||||
tv.tv_usec = 0;
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(STDIN_FILENO, &fds);
|
||||
select(STDIN_FILENO+1, &fds, nullptr, nullptr, &tv);
|
||||
return FD_ISSET(STDIN_FILENO, &fds);
|
||||
}
|
||||
#endif
|
||||
|
||||
/// %Thread start
|
||||
void CliThread()
|
||||
{
|
||||
#if AC_PLATFORM == AC_PLATFORM_WINDOWS
|
||||
// print this here the first time
|
||||
// later it will be printed after command queue updates
|
||||
PrintCliPrefix();
|
||||
#else
|
||||
::rl_attempted_completion_function = &Acore::Impl::Readline::cli_completion;
|
||||
{
|
||||
static char BLANK = '\0';
|
||||
::rl_completer_word_break_characters = &BLANK;
|
||||
}
|
||||
::rl_event_hook = &Acore::Impl::Readline::cli_hook_func;
|
||||
#endif
|
||||
|
||||
if (sConfigMgr->GetOption<bool>("BeepAtStart", true))
|
||||
printf("\a"); // \a = Alert
|
||||
|
||||
#if AC_PLATFORM == AC_PLATFORM_WINDOWS
|
||||
if (sConfigMgr->GetOption<bool>("FlashAtStart", true))
|
||||
{
|
||||
FLASHWINFO fInfo;
|
||||
fInfo.cbSize = sizeof(FLASHWINFO);
|
||||
fInfo.dwFlags = FLASHW_TRAY | FLASHW_TIMERNOFG;
|
||||
fInfo.hwnd = GetConsoleWindow();
|
||||
fInfo.uCount = 0;
|
||||
fInfo.dwTimeout = 0;
|
||||
FlashWindowEx(&fInfo);
|
||||
}
|
||||
#endif
|
||||
|
||||
///- As long as the World is running (no World::m_stopEvent), get the command line and handle it
|
||||
while (!World::IsStopped())
|
||||
{
|
||||
fflush(stdout);
|
||||
|
||||
std::string command;
|
||||
|
||||
#if AC_PLATFORM == AC_PLATFORM_WINDOWS
|
||||
wchar_t commandbuf[256];
|
||||
if (fgetws(commandbuf, sizeof(commandbuf), stdin))
|
||||
{
|
||||
if (!WStrToUtf8(commandbuf, wcslen(commandbuf), command))
|
||||
{
|
||||
PrintCliPrefix();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
#else
|
||||
char* command_str = readline(CLI_PREFIX);
|
||||
::rl_bind_key('\t', ::rl_complete);
|
||||
if (command_str != nullptr)
|
||||
{
|
||||
command = command_str;
|
||||
free(command_str);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!command.empty())
|
||||
{
|
||||
std::size_t nextLineIndex = command.find_first_of("\r\n");
|
||||
if (nextLineIndex != std::string::npos)
|
||||
{
|
||||
if (nextLineIndex == 0)
|
||||
{
|
||||
#if AC_PLATFORM == AC_PLATFORM_WINDOWS
|
||||
PrintCliPrefix();
|
||||
#endif
|
||||
continue;
|
||||
}
|
||||
|
||||
command.erase(nextLineIndex);
|
||||
}
|
||||
|
||||
fflush(stdout);
|
||||
sWorld->QueueCliCommand(new CliCommandHolder(nullptr, command.c_str(), &utf8print, &commandFinished));
|
||||
#if AC_PLATFORM != AC_PLATFORM_WINDOWS
|
||||
add_history(command.c_str());
|
||||
#endif
|
||||
}
|
||||
else if (feof(stdin))
|
||||
{
|
||||
World::StopNow(SHUTDOWN_EXIT_CODE);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/server/apps/worldserver/CommandLine/CliRunnable.h
Normal file
30
src/server/apps/worldserver/CommandLine/CliRunnable.h
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by the
|
||||
* Free Software Foundation; either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/// \addtogroup Acored
|
||||
/// @{
|
||||
/// \file
|
||||
|
||||
#ifndef __CLIRUNNABLE_H
|
||||
#define __CLIRUNNABLE_H
|
||||
|
||||
/// Command Line Interface handling thread
|
||||
void CliThread();
|
||||
|
||||
#endif
|
||||
|
||||
/// @}
|
||||
773
src/server/apps/worldserver/Main.cpp
Normal file
773
src/server/apps/worldserver/Main.cpp
Normal file
@@ -0,0 +1,773 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by the
|
||||
* Free Software Foundation; either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/// \addtogroup Acored Acore Daemon
|
||||
/// @{
|
||||
/// \file
|
||||
|
||||
#include "ACSoap.h"
|
||||
#include "AppenderDB.h"
|
||||
#include "AsyncAcceptor.h"
|
||||
#include "AsyncAuctionListing.h"
|
||||
#include "Banner.h"
|
||||
#include "BattlegroundMgr.h"
|
||||
#include "BigNumber.h"
|
||||
#include "CliRunnable.h"
|
||||
#include "Common.h"
|
||||
#include "Config.h"
|
||||
#include "DatabaseEnv.h"
|
||||
#include "DatabaseLoader.h"
|
||||
#include "DeadlineTimer.h"
|
||||
#include "GitRevision.h"
|
||||
#include "IoContext.h"
|
||||
#include "MapMgr.h"
|
||||
#include "Metric.h"
|
||||
#include "ModuleMgr.h"
|
||||
#include "ModulesScriptLoader.h"
|
||||
#include "MySQLThreading.h"
|
||||
#include "OpenSSLCrypto.h"
|
||||
#include "OutdoorPvPMgr.h"
|
||||
#include "ProcessPriority.h"
|
||||
#include "RASession.h"
|
||||
#include "RealmList.h"
|
||||
#include "Resolver.h"
|
||||
#include "ScriptLoader.h"
|
||||
#include "ScriptMgr.h"
|
||||
#include "SecretMgr.h"
|
||||
#include "SharedDefines.h"
|
||||
#include "World.h"
|
||||
#include "WorldSocket.h"
|
||||
#include "WorldSocketMgr.h"
|
||||
#include <boost/asio/signal_set.hpp>
|
||||
#include <boost/program_options.hpp>
|
||||
#include <csignal>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <openssl/crypto.h>
|
||||
#include <openssl/opensslv.h>
|
||||
|
||||
#if AC_PLATFORM == AC_PLATFORM_WINDOWS
|
||||
#include "ServiceWin32.h"
|
||||
char serviceName[] = "worldserver";
|
||||
char serviceLongName[] = "AzerothCore world service";
|
||||
char serviceDescription[] = "AzerothCore World of Warcraft emulator world service";
|
||||
/*
|
||||
* -1 - not in service mode
|
||||
* 0 - stopped
|
||||
* 1 - running
|
||||
* 2 - paused
|
||||
*/
|
||||
int m_ServiceStatus = -1;
|
||||
#endif
|
||||
|
||||
#ifndef _ACORE_CORE_CONFIG
|
||||
#define _ACORE_CORE_CONFIG "worldserver.conf"
|
||||
#endif
|
||||
|
||||
#define WORLD_SLEEP_CONST 10
|
||||
using namespace boost::program_options;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
class FreezeDetector
|
||||
{
|
||||
public:
|
||||
FreezeDetector(Acore::Asio::IoContext& ioContext, uint32 maxCoreStuckTime)
|
||||
: _timer(ioContext), _worldLoopCounter(0), _lastChangeMsTime(getMSTime()), _maxCoreStuckTimeInMs(maxCoreStuckTime) { }
|
||||
|
||||
static void Start(std::shared_ptr<FreezeDetector> const& freezeDetector)
|
||||
{
|
||||
freezeDetector->_timer.expires_from_now(boost::posix_time::seconds(5));
|
||||
freezeDetector->_timer.async_wait(std::bind(&FreezeDetector::Handler, std::weak_ptr<FreezeDetector>(freezeDetector), std::placeholders::_1));
|
||||
}
|
||||
|
||||
static void Handler(std::weak_ptr<FreezeDetector> freezeDetectorRef, boost::system::error_code const& error);
|
||||
|
||||
private:
|
||||
Acore::Asio::DeadlineTimer _timer;
|
||||
uint32 _worldLoopCounter;
|
||||
uint32 _lastChangeMsTime;
|
||||
uint32 _maxCoreStuckTimeInMs;
|
||||
};
|
||||
|
||||
void SignalHandler(boost::system::error_code const& error, int signalNumber);
|
||||
void ClearOnlineAccounts();
|
||||
bool StartDB();
|
||||
void StopDB();
|
||||
bool LoadRealmInfo(Acore::Asio::IoContext& ioContext);
|
||||
AsyncAcceptor* StartRaSocketAcceptor(Acore::Asio::IoContext& ioContext);
|
||||
void ShutdownCLIThread(std::thread* cliThread);
|
||||
void AuctionListingRunnable();
|
||||
void ShutdownAuctionListingThread(std::thread* thread);
|
||||
void WorldUpdateLoop();
|
||||
variables_map GetConsoleArguments(int argc, char** argv, fs::path& configFile, [[maybe_unused]] std::string& cfg_service);
|
||||
|
||||
/// Launch the Azeroth server
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
Acore::Impl::CurrentServerProcessHolder::_type = SERVER_PROCESS_WORLDSERVER;
|
||||
signal(SIGABRT, &Acore::AbortHandler);
|
||||
|
||||
// Command line parsing
|
||||
auto configFile = fs::path(sConfigMgr->GetConfigPath() + std::string(_ACORE_CORE_CONFIG));
|
||||
std::string configService;
|
||||
auto vm = GetConsoleArguments(argc, argv, configFile, configService);
|
||||
|
||||
// exit if help or version is enabled
|
||||
if (vm.count("help"))
|
||||
return 0;
|
||||
|
||||
#if AC_PLATFORM == AC_PLATFORM_WINDOWS
|
||||
if (configService.compare("install") == 0)
|
||||
return WinServiceInstall() == true ? 0 : 1;
|
||||
else if (configService.compare("uninstall") == 0)
|
||||
return WinServiceUninstall() == true ? 0 : 1;
|
||||
else if (configService.compare("run") == 0)
|
||||
WinServiceRun();
|
||||
#endif
|
||||
|
||||
// Add file and args in config
|
||||
sConfigMgr->Configure(configFile.generic_string(), {argv, argv + argc}, CONFIG_FILE_LIST);
|
||||
|
||||
if (!sConfigMgr->LoadAppConfigs())
|
||||
return 1;
|
||||
|
||||
std::shared_ptr<Acore::Asio::IoContext> ioContext = std::make_shared<Acore::Asio::IoContext>();
|
||||
|
||||
// Init all logs
|
||||
sLog->RegisterAppender<AppenderDB>();
|
||||
// If logs are supposed to be handled async then we need to pass the IoContext into the Log singleton
|
||||
sLog->Initialize(sConfigMgr->GetOption<bool>("Log.Async.Enable", false) ? ioContext.get() : nullptr);
|
||||
|
||||
Acore::Banner::Show("worldserver-daemon",
|
||||
[](std::string_view text)
|
||||
{
|
||||
LOG_INFO("server.worldserver", text);
|
||||
},
|
||||
[]()
|
||||
{
|
||||
LOG_INFO("server.worldserver", "> Using configuration file {}", sConfigMgr->GetFilename());
|
||||
LOG_INFO("server.worldserver", "> Using SSL version: {} (library: {})", OPENSSL_VERSION_TEXT, SSLeay_version(SSLEAY_VERSION));
|
||||
LOG_INFO("server.worldserver", "> Using Boost version: {}.{}.{}", BOOST_VERSION / 100000, BOOST_VERSION / 100 % 1000, BOOST_VERSION % 100);
|
||||
});
|
||||
|
||||
OpenSSLCrypto::threadsSetup();
|
||||
|
||||
std::shared_ptr<void> opensslHandle(nullptr, [](void*) { OpenSSLCrypto::threadsCleanup(); });
|
||||
|
||||
// Seed the OpenSSL's PRNG here.
|
||||
// That way it won't auto-seed when calling BigNumber::SetRand and slow down the first world login
|
||||
BigNumber seed;
|
||||
seed.SetRand(16 * 8);
|
||||
|
||||
/// worldserver PID file creation
|
||||
std::string pidFile = sConfigMgr->GetOption<std::string>("PidFile", "");
|
||||
if (!pidFile.empty())
|
||||
{
|
||||
if (uint32 pid = CreatePIDFile(pidFile))
|
||||
LOG_ERROR("server", "Daemon PID: {}\n", pid); // outError for red color in console
|
||||
else
|
||||
{
|
||||
LOG_ERROR("server", "Cannot create PID file {} (possible error: permission)\n", pidFile);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Set signal handlers (this must be done before starting IoContext threads, because otherwise they would unblock and exit)
|
||||
boost::asio::signal_set signals(*ioContext, SIGINT, SIGTERM);
|
||||
#if AC_PLATFORM == AC_PLATFORM_WINDOWS
|
||||
signals.add(SIGBREAK);
|
||||
#endif
|
||||
signals.async_wait(SignalHandler);
|
||||
|
||||
// Start the Boost based thread pool
|
||||
int numThreads = sConfigMgr->GetOption<int32>("ThreadPool", 1);
|
||||
std::shared_ptr<std::vector<std::thread>> threadPool(new std::vector<std::thread>(), [ioContext](std::vector<std::thread>* del)
|
||||
{
|
||||
ioContext->stop();
|
||||
for (std::thread& thr : *del)
|
||||
thr.join();
|
||||
|
||||
delete del;
|
||||
});
|
||||
|
||||
if (numThreads < 1)
|
||||
{
|
||||
numThreads = 1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < numThreads; ++i)
|
||||
{
|
||||
threadPool->push_back(std::thread([ioContext]()
|
||||
{
|
||||
ioContext->run();
|
||||
}));
|
||||
}
|
||||
|
||||
// Set process priority according to configuration settings
|
||||
SetProcessPriority("server.worldserver", sConfigMgr->GetOption<int32>(CONFIG_PROCESSOR_AFFINITY, 0), sConfigMgr->GetOption<bool>(CONFIG_HIGH_PRIORITY, false));
|
||||
|
||||
// Loading modules configs before scripts
|
||||
sConfigMgr->LoadModulesConfigs();
|
||||
|
||||
sScriptMgr->SetScriptLoader(AddScripts);
|
||||
sScriptMgr->SetModulesLoader(AddModulesScripts);
|
||||
|
||||
std::shared_ptr<void> sScriptMgrHandle(nullptr, [](void*)
|
||||
{
|
||||
sScriptMgr->Unload();
|
||||
//sScriptReloadMgr->Unload();
|
||||
});
|
||||
|
||||
LOG_INFO("server.loading", "Initializing Scripts...");
|
||||
sScriptMgr->Initialize();
|
||||
|
||||
// Start the databases
|
||||
if (!StartDB())
|
||||
return 1;
|
||||
|
||||
std::shared_ptr<void> dbHandle(nullptr, [](void*) { StopDB(); });
|
||||
|
||||
// set server offline (not connectable)
|
||||
LoginDatabase.DirectExecute("UPDATE realmlist SET flag = (flag & ~{}) | {} WHERE id = '{}'", REALM_FLAG_OFFLINE, REALM_FLAG_VERSION_MISMATCH, realm.Id.Realm);
|
||||
|
||||
LoadRealmInfo(*ioContext);
|
||||
|
||||
sMetric->Initialize(realm.Name, *ioContext, []()
|
||||
{
|
||||
METRIC_VALUE("online_players", sWorld->GetPlayerCount());
|
||||
METRIC_VALUE("db_queue_login", uint64(LoginDatabase.QueueSize()));
|
||||
METRIC_VALUE("db_queue_character", uint64(CharacterDatabase.QueueSize()));
|
||||
METRIC_VALUE("db_queue_world", uint64(WorldDatabase.QueueSize()));
|
||||
sScriptMgr->OnMetricLogging();
|
||||
});
|
||||
|
||||
METRIC_EVENT("events", "Worldserver started", "");
|
||||
|
||||
std::shared_ptr<void> sMetricHandle(nullptr, [](void*)
|
||||
{
|
||||
METRIC_EVENT("events", "Worldserver shutdown", "");
|
||||
sMetric->Unload();
|
||||
});
|
||||
|
||||
Acore::Module::SetEnableModulesList(AC_MODULES_LIST);
|
||||
|
||||
///- Initialize the World
|
||||
sSecretMgr->Initialize();
|
||||
sWorld->SetInitialWorldSettings();
|
||||
|
||||
std::shared_ptr<void> mapManagementHandle(nullptr, [](void*)
|
||||
{
|
||||
// unload battleground templates before different singletons destroyed
|
||||
sBattlegroundMgr->DeleteAllBattlegrounds();
|
||||
|
||||
sOutdoorPvPMgr->Die(); // unload it before MapMgr
|
||||
sMapMgr->UnloadAll(); // unload all grids (including locked in memory)
|
||||
|
||||
sScriptMgr->OnAfterUnloadAllMaps();
|
||||
});
|
||||
|
||||
// Start the Remote Access port (acceptor) if enabled
|
||||
std::unique_ptr<AsyncAcceptor> raAcceptor;
|
||||
if (sConfigMgr->GetOption<bool>("Ra.Enable", false))
|
||||
{
|
||||
raAcceptor.reset(StartRaSocketAcceptor(*ioContext));
|
||||
}
|
||||
|
||||
// Start soap serving thread if enabled
|
||||
std::shared_ptr<std::thread> soapThread;
|
||||
if (sConfigMgr->GetOption<bool>("SOAP.Enabled", false))
|
||||
{
|
||||
soapThread.reset(new std::thread(ACSoapThread, sConfigMgr->GetOption<std::string>("SOAP.IP", "127.0.0.1"), uint16(sConfigMgr->GetOption<int32>("SOAP.Port", 7878))),
|
||||
[](std::thread* thr)
|
||||
{
|
||||
thr->join();
|
||||
delete thr;
|
||||
});
|
||||
}
|
||||
|
||||
// Launch the worldserver listener socket
|
||||
uint16 worldPort = uint16(sWorld->getIntConfig(CONFIG_PORT_WORLD));
|
||||
std::string worldListener = sConfigMgr->GetOption<std::string>("BindIP", "0.0.0.0");
|
||||
|
||||
int networkThreads = sConfigMgr->GetOption<int32>("Network.Threads", 1);
|
||||
|
||||
if (networkThreads <= 0)
|
||||
{
|
||||
LOG_ERROR("server.worldserver", "Network.Threads must be greater than 0");
|
||||
World::StopNow(ERROR_EXIT_CODE);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!sWorldSocketMgr.StartWorldNetwork(*ioContext, worldListener, worldPort, networkThreads))
|
||||
{
|
||||
LOG_ERROR("server.worldserver", "Failed to initialize network");
|
||||
World::StopNow(ERROR_EXIT_CODE);
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::shared_ptr<void> sWorldSocketMgrHandle(nullptr, [](void*)
|
||||
{
|
||||
sWorld->KickAll(); // save and kick all players
|
||||
sWorld->UpdateSessions(1); // real players unload required UpdateSessions call
|
||||
|
||||
sWorldSocketMgr.StopNetwork();
|
||||
|
||||
///- Clean database before leaving
|
||||
ClearOnlineAccounts();
|
||||
});
|
||||
|
||||
// Set server online (allow connecting now)
|
||||
LoginDatabase.DirectExecute("UPDATE realmlist SET flag = flag & ~{}, population = 0 WHERE id = '{}'", REALM_FLAG_VERSION_MISMATCH, realm.Id.Realm);
|
||||
realm.PopulationLevel = 0.0f;
|
||||
realm.Flags = RealmFlags(realm.Flags & ~uint32(REALM_FLAG_VERSION_MISMATCH));
|
||||
|
||||
// Start the freeze check callback cycle in 5 seconds (cycle itself is 1 sec)
|
||||
std::shared_ptr<FreezeDetector> freezeDetector;
|
||||
if (int32 coreStuckTime = sConfigMgr->GetOption<int32>("MaxCoreStuckTime", 60))
|
||||
{
|
||||
freezeDetector = std::make_shared<FreezeDetector>(*ioContext, coreStuckTime * 1000);
|
||||
FreezeDetector::Start(freezeDetector);
|
||||
LOG_INFO("server.worldserver", "Starting up anti-freeze thread ({} seconds max stuck time)...", coreStuckTime);
|
||||
}
|
||||
|
||||
LOG_INFO("server.worldserver", "{} (worldserver-daemon) ready...", GitRevision::GetFullVersion());
|
||||
|
||||
sScriptMgr->OnStartup();
|
||||
|
||||
// Launch CliRunnable thread
|
||||
std::shared_ptr<std::thread> cliThread;
|
||||
#if AC_PLATFORM == AC_PLATFORM_WINDOWS
|
||||
if (sConfigMgr->GetOption<bool>("Console.Enable", true) && (m_ServiceStatus == -1)/* need disable console in service mode*/)
|
||||
#else
|
||||
if (sConfigMgr->GetOption<bool>("Console.Enable", true))
|
||||
#endif
|
||||
{
|
||||
cliThread.reset(new std::thread(CliThread), &ShutdownCLIThread);
|
||||
}
|
||||
|
||||
// Launch CliRunnable thread
|
||||
std::shared_ptr<std::thread> auctionLisingThread;
|
||||
auctionLisingThread.reset(new std::thread(AuctionListingRunnable),
|
||||
[](std::thread* thr)
|
||||
{
|
||||
thr->join();
|
||||
delete thr;
|
||||
});
|
||||
|
||||
WorldUpdateLoop();
|
||||
|
||||
// Shutdown starts here
|
||||
threadPool.reset();
|
||||
|
||||
sLog->SetSynchronous();
|
||||
|
||||
sScriptMgr->OnShutdown();
|
||||
|
||||
// set server offline
|
||||
LoginDatabase.DirectExecute("UPDATE realmlist SET flag = flag | {} WHERE id = '{}'", REALM_FLAG_OFFLINE, realm.Id.Realm);
|
||||
|
||||
LOG_INFO("server.worldserver", "Halting process...");
|
||||
|
||||
// 0 - normal shutdown
|
||||
// 1 - shutdown at error
|
||||
// 2 - restart command used, this code can be used by restarter for restart Warheadd
|
||||
|
||||
return World::GetExitCode();
|
||||
}
|
||||
|
||||
/// Initialize connection to the databases
|
||||
bool StartDB()
|
||||
{
|
||||
MySQL::Library_Init();
|
||||
|
||||
// Load databases
|
||||
DatabaseLoader loader("server.worldserver", DatabaseLoader::DATABASE_NONE, AC_MODULES_LIST);
|
||||
loader
|
||||
.AddDatabase(LoginDatabase, "Login")
|
||||
.AddDatabase(CharacterDatabase, "Character")
|
||||
.AddDatabase(WorldDatabase, "World");
|
||||
|
||||
if (!loader.Load())
|
||||
return false;
|
||||
|
||||
if (!sScriptMgr->OnDatabasesLoading())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
///- Get the realm Id from the configuration file
|
||||
realm.Id.Realm = sConfigMgr->GetOption<uint32>("RealmID", 0);
|
||||
if (!realm.Id.Realm)
|
||||
{
|
||||
LOG_ERROR("server.worldserver", "Realm ID not defined in configuration file");
|
||||
return false;
|
||||
}
|
||||
else if (realm.Id.Realm > 255)
|
||||
{
|
||||
/*
|
||||
* Due to the client only being able to read a realm.Id.Realm
|
||||
* with a size of uint8 we can "only" store up to 255 realms
|
||||
* anything further the client will behave anormaly
|
||||
*/
|
||||
LOG_ERROR("server.worldserver", "Realm ID must range from 1 to 255");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("server.loading", "Loading world information...");
|
||||
LOG_INFO("server.loading", "> RealmID: {}", realm.Id.Realm);
|
||||
|
||||
///- Clean the database before starting
|
||||
ClearOnlineAccounts();
|
||||
|
||||
///- Insert version info into DB
|
||||
WorldDatabase.Execute("UPDATE version SET core_version = '{}', core_revision = '{}'", GitRevision::GetFullVersion(), GitRevision::GetHash()); // One-time query
|
||||
|
||||
sWorld->LoadDBVersion();
|
||||
|
||||
LOG_INFO("server.loading", "> Version DB world: {}", sWorld->GetDBVersion());
|
||||
|
||||
sScriptMgr->OnAfterDatabasesLoaded(loader.GetUpdateFlags());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void StopDB()
|
||||
{
|
||||
CharacterDatabase.Close();
|
||||
WorldDatabase.Close();
|
||||
LoginDatabase.Close();
|
||||
|
||||
sScriptMgr->OnDatabasesClosing();
|
||||
|
||||
MySQL::Library_End();
|
||||
}
|
||||
|
||||
/// Clear 'online' status for all accounts with characters in this realm
|
||||
void ClearOnlineAccounts()
|
||||
{
|
||||
// Reset online status for all accounts with characters on the current realm
|
||||
// pussywizard: tc query would set online=0 even if logged in on another realm >_>
|
||||
LoginDatabase.DirectExecute("UPDATE account SET online = 0 WHERE online = {}", realm.Id.Realm);
|
||||
|
||||
// Reset online status for all characters
|
||||
CharacterDatabase.DirectExecute("UPDATE characters SET online = 0 WHERE online <> 0");
|
||||
}
|
||||
|
||||
void ShutdownCLIThread(std::thread* cliThread)
|
||||
{
|
||||
if (cliThread)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
// First try to cancel any I/O in the CLI thread
|
||||
if (!CancelSynchronousIo(cliThread->native_handle()))
|
||||
{
|
||||
// if CancelSynchronousIo() fails, print the error and try with old way
|
||||
DWORD errorCode = GetLastError();
|
||||
LPCSTR errorBuffer;
|
||||
|
||||
DWORD formatReturnCode = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
nullptr, errorCode, 0, (LPTSTR)&errorBuffer, 0, nullptr);
|
||||
if (!formatReturnCode)
|
||||
errorBuffer = "Unknown error";
|
||||
|
||||
LOG_DEBUG("server.worldserver", "Error cancelling I/O of CliThread, error code {}, detail: {}", uint32(errorCode), errorBuffer);
|
||||
|
||||
if (!formatReturnCode)
|
||||
LocalFree((LPSTR)errorBuffer);
|
||||
|
||||
// send keyboard input to safely unblock the CLI thread
|
||||
INPUT_RECORD b[4];
|
||||
HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE);
|
||||
b[0].EventType = KEY_EVENT;
|
||||
b[0].Event.KeyEvent.bKeyDown = TRUE;
|
||||
b[0].Event.KeyEvent.uChar.AsciiChar = 'X';
|
||||
b[0].Event.KeyEvent.wVirtualKeyCode = 'X';
|
||||
b[0].Event.KeyEvent.wRepeatCount = 1;
|
||||
|
||||
b[1].EventType = KEY_EVENT;
|
||||
b[1].Event.KeyEvent.bKeyDown = FALSE;
|
||||
b[1].Event.KeyEvent.uChar.AsciiChar = 'X';
|
||||
b[1].Event.KeyEvent.wVirtualKeyCode = 'X';
|
||||
b[1].Event.KeyEvent.wRepeatCount = 1;
|
||||
|
||||
b[2].EventType = KEY_EVENT;
|
||||
b[2].Event.KeyEvent.bKeyDown = TRUE;
|
||||
b[2].Event.KeyEvent.dwControlKeyState = 0;
|
||||
b[2].Event.KeyEvent.uChar.AsciiChar = '\r';
|
||||
b[2].Event.KeyEvent.wVirtualKeyCode = VK_RETURN;
|
||||
b[2].Event.KeyEvent.wRepeatCount = 1;
|
||||
b[2].Event.KeyEvent.wVirtualScanCode = 0x1c;
|
||||
|
||||
b[3].EventType = KEY_EVENT;
|
||||
b[3].Event.KeyEvent.bKeyDown = FALSE;
|
||||
b[3].Event.KeyEvent.dwControlKeyState = 0;
|
||||
b[3].Event.KeyEvent.uChar.AsciiChar = '\r';
|
||||
b[3].Event.KeyEvent.wVirtualKeyCode = VK_RETURN;
|
||||
b[3].Event.KeyEvent.wVirtualScanCode = 0x1c;
|
||||
b[3].Event.KeyEvent.wRepeatCount = 1;
|
||||
DWORD numb;
|
||||
WriteConsoleInput(hStdIn, b, 4, &numb);
|
||||
}
|
||||
#endif
|
||||
cliThread->join();
|
||||
delete cliThread;
|
||||
}
|
||||
}
|
||||
|
||||
void WorldUpdateLoop()
|
||||
{
|
||||
uint32 realCurrTime = 0;
|
||||
uint32 realPrevTime = getMSTime();
|
||||
|
||||
LoginDatabase.WarnAboutSyncQueries(true);
|
||||
CharacterDatabase.WarnAboutSyncQueries(true);
|
||||
WorldDatabase.WarnAboutSyncQueries(true);
|
||||
|
||||
sScriptMgr->OnDatabaseWarnAboutSyncQueries(true);
|
||||
|
||||
///- While we have not World::m_stopEvent, update the world
|
||||
while (!World::IsStopped())
|
||||
{
|
||||
++World::m_worldLoopCounter;
|
||||
realCurrTime = getMSTime();
|
||||
|
||||
uint32 diff = getMSTimeDiff(realPrevTime, realCurrTime);
|
||||
|
||||
sWorld->Update(diff);
|
||||
realPrevTime = realCurrTime;
|
||||
|
||||
uint32 executionTimeDiff = getMSTimeDiff(realCurrTime, getMSTime());
|
||||
|
||||
// we know exactly how long it took to update the world, if the update took less than WORLD_SLEEP_CONST, sleep for WORLD_SLEEP_CONST - world update time
|
||||
if (executionTimeDiff < WORLD_SLEEP_CONST)
|
||||
{
|
||||
std::this_thread::sleep_for(Milliseconds(WORLD_SLEEP_CONST - executionTimeDiff));
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
if (m_ServiceStatus == 0)
|
||||
World::StopNow(SHUTDOWN_EXIT_CODE);
|
||||
|
||||
while (m_ServiceStatus == 2)
|
||||
Sleep(1000);
|
||||
#endif
|
||||
}
|
||||
|
||||
sScriptMgr->OnDatabaseWarnAboutSyncQueries(false);
|
||||
|
||||
LoginDatabase.WarnAboutSyncQueries(false);
|
||||
CharacterDatabase.WarnAboutSyncQueries(false);
|
||||
WorldDatabase.WarnAboutSyncQueries(false);
|
||||
}
|
||||
|
||||
void SignalHandler(boost::system::error_code const& error, int /*signalNumber*/)
|
||||
{
|
||||
if (!error)
|
||||
World::StopNow(SHUTDOWN_EXIT_CODE);
|
||||
}
|
||||
|
||||
void FreezeDetector::Handler(std::weak_ptr<FreezeDetector> freezeDetectorRef, boost::system::error_code const& error)
|
||||
{
|
||||
if (!error)
|
||||
{
|
||||
if (std::shared_ptr<FreezeDetector> freezeDetector = freezeDetectorRef.lock())
|
||||
{
|
||||
uint32 curtime = getMSTime();
|
||||
|
||||
uint32 worldLoopCounter = World::m_worldLoopCounter;
|
||||
if (freezeDetector->_worldLoopCounter != worldLoopCounter)
|
||||
{
|
||||
freezeDetector->_lastChangeMsTime = curtime;
|
||||
freezeDetector->_worldLoopCounter = worldLoopCounter;
|
||||
}
|
||||
// possible freeze
|
||||
else if (getMSTimeDiff(freezeDetector->_lastChangeMsTime, curtime) > freezeDetector->_maxCoreStuckTimeInMs)
|
||||
{
|
||||
LOG_ERROR("server.worldserver", "World Thread hangs, kicking out server!");
|
||||
ABORT();
|
||||
}
|
||||
|
||||
freezeDetector->_timer.expires_from_now(boost::posix_time::seconds(1));
|
||||
freezeDetector->_timer.async_wait(std::bind(&FreezeDetector::Handler, freezeDetectorRef, std::placeholders::_1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AsyncAcceptor* StartRaSocketAcceptor(Acore::Asio::IoContext& ioContext)
|
||||
{
|
||||
uint16 raPort = uint16(sConfigMgr->GetOption<int32>("Ra.Port", 3443));
|
||||
std::string raListener = sConfigMgr->GetOption<std::string>("Ra.IP", "0.0.0.0");
|
||||
|
||||
AsyncAcceptor* acceptor = new AsyncAcceptor(ioContext, raListener, raPort);
|
||||
if (!acceptor->Bind())
|
||||
{
|
||||
LOG_ERROR("server.worldserver", "Failed to bind RA socket acceptor");
|
||||
delete acceptor;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
acceptor->AsyncAccept<RASession>();
|
||||
return acceptor;
|
||||
}
|
||||
|
||||
bool LoadRealmInfo(Acore::Asio::IoContext& ioContext)
|
||||
{
|
||||
QueryResult result = LoginDatabase.Query("SELECT id, name, address, localAddress, localSubnetMask, port, icon, flag, timezone, allowedSecurityLevel, population, gamebuild FROM realmlist WHERE id = {}", realm.Id.Realm);
|
||||
if (!result)
|
||||
return false;
|
||||
|
||||
Acore::Asio::Resolver resolver(ioContext);
|
||||
|
||||
Field* fields = result->Fetch();
|
||||
realm.Name = fields[1].Get<std::string>();
|
||||
|
||||
Optional<boost::asio::ip::tcp::endpoint> externalAddress = resolver.Resolve(boost::asio::ip::tcp::v4(), fields[2].Get<std::string>(), "");
|
||||
if (!externalAddress)
|
||||
{
|
||||
LOG_ERROR("server.worldserver", "Could not resolve address {}", fields[2].Get<std::string>());
|
||||
return false;
|
||||
}
|
||||
|
||||
realm.ExternalAddress = std::make_unique<boost::asio::ip::address>(externalAddress->address());
|
||||
|
||||
Optional<boost::asio::ip::tcp::endpoint> localAddress = resolver.Resolve(boost::asio::ip::tcp::v4(), fields[3].Get<std::string>(), "");
|
||||
if (!localAddress)
|
||||
{
|
||||
LOG_ERROR("server.worldserver", "Could not resolve address {}", fields[3].Get<std::string>());
|
||||
return false;
|
||||
}
|
||||
|
||||
realm.LocalAddress = std::make_unique<boost::asio::ip::address>(localAddress->address());
|
||||
|
||||
Optional<boost::asio::ip::tcp::endpoint> localSubmask = resolver.Resolve(boost::asio::ip::tcp::v4(), fields[4].Get<std::string>(), "");
|
||||
if (!localSubmask)
|
||||
{
|
||||
LOG_ERROR("server.worldserver", "Could not resolve address {}", fields[4].Get<std::string>());
|
||||
return false;
|
||||
}
|
||||
|
||||
realm.LocalSubnetMask = std::make_unique<boost::asio::ip::address>(localSubmask->address());
|
||||
|
||||
realm.Port = fields[5].Get<uint16>();
|
||||
realm.Type = fields[6].Get<uint8>();
|
||||
realm.Flags = RealmFlags(fields[7].Get<uint8>());
|
||||
realm.Timezone = fields[8].Get<uint8>();
|
||||
realm.AllowedSecurityLevel = AccountTypes(fields[9].Get<uint8>());
|
||||
realm.PopulationLevel = fields[10].Get<float>();
|
||||
realm.Build = fields[11].Get<uint32>();
|
||||
return true;
|
||||
}
|
||||
|
||||
void AuctionListingRunnable()
|
||||
{
|
||||
LOG_INFO("server", "Starting up Auction House Listing thread...");
|
||||
|
||||
while (!World::IsStopped())
|
||||
{
|
||||
if (AsyncAuctionListingMgr::IsAuctionListingAllowed())
|
||||
{
|
||||
uint32 diff = AsyncAuctionListingMgr::GetDiff();
|
||||
AsyncAuctionListingMgr::ResetDiff();
|
||||
|
||||
if (AsyncAuctionListingMgr::GetTempList().size() || AsyncAuctionListingMgr::GetList().size())
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(AsyncAuctionListingMgr::GetLock());
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(AsyncAuctionListingMgr::GetTempLock());
|
||||
|
||||
for (auto const& delayEvent : AsyncAuctionListingMgr::GetTempList())
|
||||
AsyncAuctionListingMgr::GetList().emplace_back(delayEvent);
|
||||
|
||||
AsyncAuctionListingMgr::GetTempList().clear();
|
||||
}
|
||||
|
||||
for (auto& itr : AsyncAuctionListingMgr::GetList())
|
||||
{
|
||||
if (itr._msTimer <= diff)
|
||||
itr._msTimer = 0;
|
||||
else
|
||||
itr._msTimer -= diff;
|
||||
}
|
||||
|
||||
for (std::list<AuctionListItemsDelayEvent>::iterator itr = AsyncAuctionListingMgr::GetList().begin(); itr != AsyncAuctionListingMgr::GetList().end(); ++itr)
|
||||
{
|
||||
if ((*itr)._msTimer != 0)
|
||||
continue;
|
||||
|
||||
if ((*itr).Execute())
|
||||
AsyncAuctionListingMgr::GetList().erase(itr);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::this_thread::sleep_for(1ms);
|
||||
}
|
||||
|
||||
LOG_INFO("server", "Auction House Listing thread exiting without problems.");
|
||||
}
|
||||
|
||||
void ShutdownAuctionListingThread(std::thread* thread)
|
||||
{
|
||||
if (thread)
|
||||
{
|
||||
thread->join();
|
||||
delete thread;
|
||||
}
|
||||
}
|
||||
|
||||
variables_map GetConsoleArguments(int argc, char** argv, fs::path& configFile, [[maybe_unused]] std::string& configService)
|
||||
{
|
||||
options_description all("Allowed options");
|
||||
all.add_options()
|
||||
("help,h", "print usage message")
|
||||
("version,v", "print version build info")
|
||||
("dry-run,d", "Dry run")
|
||||
("config,c", value<fs::path>(&configFile)->default_value(fs::path(sConfigMgr->GetConfigPath() + std::string(_ACORE_CORE_CONFIG))), "use <arg> as configuration file");
|
||||
|
||||
#if AC_PLATFORM == WARHEAD_PLATFORM_WINDOWS
|
||||
options_description win("Windows platform specific options");
|
||||
win.add_options()
|
||||
("service,s", value<std::string>(&configService)->default_value(""), "Windows service options: [install | uninstall]");
|
||||
|
||||
all.add(win);
|
||||
#endif
|
||||
|
||||
variables_map vm;
|
||||
|
||||
try
|
||||
{
|
||||
store(command_line_parser(argc, argv).options(all).allow_unregistered().run(), vm);
|
||||
notify(vm);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
std::cerr << e.what() << "\n";
|
||||
}
|
||||
|
||||
if (vm.count("help"))
|
||||
{
|
||||
std::cout << all << "\n";
|
||||
}
|
||||
else if (vm.count("dry-run"))
|
||||
{
|
||||
sConfigMgr->setDryRun(true);
|
||||
}
|
||||
|
||||
return vm;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by the
|
||||
* Free Software Foundation; either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Common.h"
|
||||
#include "Config.h"
|
||||
#include "DatabaseEnv.h"
|
||||
#include "Log.h"
|
||||
#include "Util.h"
|
||||
#include "World.h"
|
||||
#include "WorldSocket.h"
|
||||
223
src/server/apps/worldserver/RemoteAccess/RASession.cpp
Normal file
223
src/server/apps/worldserver/RemoteAccess/RASession.cpp
Normal file
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by the
|
||||
* Free Software Foundation; either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "RASession.h"
|
||||
#include "AccountMgr.h"
|
||||
#include "Config.h"
|
||||
#include "DatabaseEnv.h"
|
||||
#include "Duration.h"
|
||||
#include "Log.h"
|
||||
#include "SRP6.h"
|
||||
#include "ServerMotd.h"
|
||||
#include "Util.h"
|
||||
#include "World.h"
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/asio/read_until.hpp>
|
||||
#include <thread>
|
||||
|
||||
using boost::asio::ip::tcp;
|
||||
|
||||
void RASession::Start()
|
||||
{
|
||||
// wait 1 second for active connections to send negotiation request
|
||||
for (int counter = 0; counter < 10 && _socket.available() == 0; counter++)
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// Check if there are bytes available, if they are, then the client is requesting the negotiation
|
||||
if (_socket.available() > 0)
|
||||
{
|
||||
// Handle subnegotiation
|
||||
char buf[1024] = { };
|
||||
_socket.read_some(boost::asio::buffer(buf));
|
||||
|
||||
// Send the end-of-negotiation packet
|
||||
uint8 const reply[2] = { 0xFF, 0xF0 };
|
||||
_socket.write_some(boost::asio::buffer(reply));
|
||||
}
|
||||
|
||||
Send("Authentication Required\r\n");
|
||||
Send("Username: ");
|
||||
|
||||
std::string username = ReadString();
|
||||
|
||||
if (username.empty())
|
||||
return;
|
||||
|
||||
LOG_INFO("commands.ra", "Accepting RA connection from user {} (IP: {})", username, GetRemoteIpAddress());
|
||||
|
||||
Send("Password: ");
|
||||
|
||||
std::string password = ReadString();
|
||||
if (password.empty())
|
||||
return;
|
||||
|
||||
if (!CheckAccessLevel(username) || !CheckPassword(username, password))
|
||||
{
|
||||
Send("Authentication failed\r\n");
|
||||
_socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("commands.ra", "User {} (IP: {}) authenticated correctly to RA", username, GetRemoteIpAddress());
|
||||
|
||||
// Authentication successful, send the motd
|
||||
Send(std::string(std::string(Motd::GetMotd()) + "\r\n").c_str());
|
||||
|
||||
// Read commands
|
||||
for (;;)
|
||||
{
|
||||
Send("AC>");
|
||||
std::string command = ReadString();
|
||||
|
||||
if (ProcessCommand(command))
|
||||
break;
|
||||
}
|
||||
|
||||
_socket.close();
|
||||
}
|
||||
|
||||
int RASession::Send(std::string_view data)
|
||||
{
|
||||
std::ostream os(&_writeBuffer);
|
||||
os << data;
|
||||
size_t written = _socket.send(_writeBuffer.data());
|
||||
_writeBuffer.consume(written);
|
||||
return written;
|
||||
}
|
||||
|
||||
std::string RASession::ReadString()
|
||||
{
|
||||
boost::system::error_code error;
|
||||
size_t read = boost::asio::read_until(_socket, _readBuffer, "\r\n", error);
|
||||
if (!read)
|
||||
{
|
||||
_socket.close();
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string line;
|
||||
std::istream is(&_readBuffer);
|
||||
std::getline(is, line);
|
||||
|
||||
if (*line.rbegin() == '\r')
|
||||
line.erase(line.length() - 1);
|
||||
|
||||
return line;
|
||||
}
|
||||
|
||||
bool RASession::CheckAccessLevel(const std::string& user)
|
||||
{
|
||||
std::string safeUser = user;
|
||||
|
||||
Utf8ToUpperOnlyLatin(safeUser);
|
||||
|
||||
auto* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_ACCOUNT_ACCESS);
|
||||
stmt->SetData(0, safeUser);
|
||||
|
||||
PreparedQueryResult result = LoginDatabase.Query(stmt);
|
||||
if (!result)
|
||||
{
|
||||
LOG_INFO("commands.ra", "User {} does not exist in database", user);
|
||||
return false;
|
||||
}
|
||||
|
||||
Field* fields = result->Fetch();
|
||||
|
||||
if (fields[1].Get<uint8>() < sConfigMgr->GetOption<int32>("Ra.MinLevel", 3))
|
||||
{
|
||||
LOG_INFO("commands.ra", "User {} has no privilege to login", user);
|
||||
return false;
|
||||
}
|
||||
else if (fields[2].Get<int32>() != -1)
|
||||
{
|
||||
LOG_INFO("commands.ra", "User {} has to be assigned on all realms (with RealmID = '-1')", user);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RASession::CheckPassword(const std::string& user, const std::string& pass)
|
||||
{
|
||||
std::string safe_user = user;
|
||||
std::transform(safe_user.begin(), safe_user.end(), safe_user.begin(), ::toupper);
|
||||
Utf8ToUpperOnlyLatin(safe_user);
|
||||
|
||||
std::string safe_pass = pass;
|
||||
Utf8ToUpperOnlyLatin(safe_pass);
|
||||
std::transform(safe_pass.begin(), safe_pass.end(), safe_pass.begin(), ::toupper);
|
||||
|
||||
auto* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_CHECK_PASSWORD_BY_NAME);
|
||||
|
||||
stmt->SetData(0, safe_user);
|
||||
|
||||
if (PreparedQueryResult result = LoginDatabase.Query(stmt))
|
||||
{
|
||||
Acore::Crypto::SRP6::Salt salt = (*result)[0].Get<Binary, Acore::Crypto::SRP6::SALT_LENGTH>();
|
||||
Acore::Crypto::SRP6::Verifier verifier = (*result)[1].Get<Binary, Acore::Crypto::SRP6::VERIFIER_LENGTH>();
|
||||
|
||||
if (Acore::Crypto::SRP6::CheckLogin(safe_user, safe_pass, salt, verifier))
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG_INFO("commands.ra", "Wrong password for user: {}", user);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RASession::ProcessCommand(std::string& command)
|
||||
{
|
||||
if (command.length() == 0)
|
||||
return true;
|
||||
|
||||
LOG_INFO("commands.ra", "Received command: {}", command);
|
||||
|
||||
// handle quit, exit and logout commands to terminate connection
|
||||
if (command == "quit" || command == "exit" || command == "logout")
|
||||
{
|
||||
Send("Bye\r\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Obtain a new promise per command
|
||||
delete _commandExecuting;
|
||||
_commandExecuting = new std::promise<void>();
|
||||
|
||||
CliCommandHolder* cmd = new CliCommandHolder(this, command.c_str(), &RASession::CommandPrint, &RASession::CommandFinished);
|
||||
sWorld->QueueCliCommand(cmd);
|
||||
|
||||
// Wait for the command to finish
|
||||
_commandExecuting->get_future().wait();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void RASession::CommandPrint(void* callbackArg, std::string_view text)
|
||||
{
|
||||
if (text.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RASession* session = static_cast<RASession*>(callbackArg);
|
||||
session->Send(text);
|
||||
}
|
||||
|
||||
void RASession::CommandFinished(void* callbackArg, bool /*success*/)
|
||||
{
|
||||
RASession* session = static_cast<RASession*>(callbackArg);
|
||||
session->_commandExecuting->set_value();
|
||||
}
|
||||
58
src/server/apps/worldserver/RemoteAccess/RASession.h
Normal file
58
src/server/apps/worldserver/RemoteAccess/RASession.h
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by the
|
||||
* Free Software Foundation; either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef __RASESSION_H__
|
||||
#define __RASESSION_H__
|
||||
|
||||
#include "Common.h"
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/streambuf.hpp>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
|
||||
using boost::asio::ip::tcp;
|
||||
|
||||
const size_t bufferSize = 4096;
|
||||
|
||||
class RASession : public std::enable_shared_from_this<RASession>
|
||||
{
|
||||
public:
|
||||
RASession(tcp::socket&& socket) :
|
||||
_socket(std::move(socket)), _commandExecuting(nullptr) { }
|
||||
|
||||
void Start();
|
||||
|
||||
const std::string GetRemoteIpAddress() const { return _socket.remote_endpoint().address().to_string(); }
|
||||
unsigned short GetRemotePort() const { return _socket.remote_endpoint().port(); }
|
||||
|
||||
private:
|
||||
int Send(std::string_view data);
|
||||
std::string ReadString();
|
||||
bool CheckAccessLevel(const std::string& user);
|
||||
bool CheckPassword(const std::string& user, const std::string& pass);
|
||||
bool ProcessCommand(std::string& command);
|
||||
|
||||
static void CommandPrint(void* callbackArg, std::string_view text);
|
||||
static void CommandFinished(void* callbackArg, bool);
|
||||
|
||||
tcp::socket _socket;
|
||||
boost::asio::streambuf _readBuffer;
|
||||
boost::asio::streambuf _writeBuffer;
|
||||
std::promise<void>* _commandExecuting;
|
||||
};
|
||||
|
||||
#endif
|
||||
15
src/server/apps/worldserver/resource.h
Normal file
15
src/server/apps/worldserver/resource.h
Normal file
@@ -0,0 +1,15 @@
|
||||
//{{NO_DEPENDENCIES}}
|
||||
// Microsoft Visual C++ generated include file.
|
||||
// Used by SunwellCore.rc
|
||||
//
|
||||
|
||||
// Next default values for new objects
|
||||
//
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NEXT_RESOURCE_VALUE 101
|
||||
#define _APS_NEXT_COMMAND_VALUE 40001
|
||||
#define _APS_NEXT_CONTROL_VALUE 1000
|
||||
#define _APS_NEXT_SYMED_VALUE 101
|
||||
#endif
|
||||
#endif
|
||||
4038
src/server/apps/worldserver/worldserver.conf.dist
Normal file
4038
src/server/apps/worldserver/worldserver.conf.dist
Normal file
File diff suppressed because it is too large
Load Diff
BIN
src/server/apps/worldserver/worldserver.ico
Normal file
BIN
src/server/apps/worldserver/worldserver.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
93
src/server/apps/worldserver/worldserver.rc
Normal file
93
src/server/apps/worldserver/worldserver.rc
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by the
|
||||
* Free Software Foundation; either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "resource.h"
|
||||
#include "revision.h"
|
||||
|
||||
#define APSTUDIO_READONLY_SYMBOLS
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Generated from the TEXTINCLUDE 2 resource.
|
||||
//
|
||||
#include "windows.h"
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
#undef APSTUDIO_READONLY_SYMBOLS
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Icon
|
||||
//
|
||||
|
||||
// Icon with lowest ID value placed first to ensure application icon
|
||||
// remains consistent on all systems.
|
||||
IDI_APPICON ICON "worldserver.ico"
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Neutre (Par défaut système) resources
|
||||
|
||||
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_NEUSD)
|
||||
#ifdef _WIN32
|
||||
LANGUAGE LANG_NEUTRAL, SUBLANG_SYS_DEFAULT
|
||||
#pragma code_page(1252)
|
||||
#endif //_WIN32
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Version
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION VER_FILEVERSION
|
||||
PRODUCTVERSION VER_PRODUCTVERSION
|
||||
|
||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||
|
||||
#ifndef _DEBUG
|
||||
FILEFLAGS 0
|
||||
#else
|
||||
#define VER_PRERELEASE VS_FF_PRERELEASE
|
||||
#define VER_PRIVATEBUILD VS_FF_PRIVATEBUILD
|
||||
#define VER_DEBUG 0
|
||||
FILEFLAGS (VER_PRIVATEBUILD|VER_PRERELEASE|VER_DEBUG)
|
||||
#endif
|
||||
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
FILETYPE VFT_APP
|
||||
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "080004b0"
|
||||
BEGIN
|
||||
VALUE "CompanyName", VER_COMPANYNAME_STR
|
||||
VALUE "FileDescription", "World Server Daemon"
|
||||
VALUE "FileVersion", VER_FILEVERSION_STR
|
||||
VALUE "InternalName", "worldserver"
|
||||
VALUE "LegalCopyright", VER_LEGALCOPYRIGHT_STR
|
||||
VALUE "OriginalFilename", "worldserver.exe"
|
||||
VALUE "ProductName", "World Server"
|
||||
VALUE "ProductVersion", VER_PRODUCTVERSION_STR
|
||||
END
|
||||
END
|
||||
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x800, 1200
|
||||
END
|
||||
END
|
||||
#endif
|
||||
Reference in New Issue
Block a user