15 Commits

Author SHA1 Message Date
sindoring ed6eea2f43 Merge feature/moonwell-quest-drop into development 2026-10-30 18:28:09 +04:00
sindoring a9c5cacec8 MoonWell: Rate.Drop.Item.Quest, quest item drop chance (production 2x)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 18:28:09 +04:00
sindoring 146f02f046 Merge remote-tracking branch 'upstream/development' into development 2026-10-30 18:22:24 +04:00
sindoring a2c532b676 Merge feature/moonwell-voice into development 2026-10-29 17:26:16 +04:00
sindoring 25ad97555f MoonWell: voice chat tickets and the state feed for the voice relay
Voice goes through a separate relay (moonwell-voice-relay, from the
MoonWell client repo), never the world socket. mangosd only vouches:
`.mwvoice` answers the player with an HMAC-signed ticket and the relay
address as an addon line (prefix MWV), and every 250 ms the world thread
sends the relay each character in the world: faction, map, instance,
position, group and guild, so no client can claim a place, group or
guild of its own. The feed is read after the map update, under the
lock that keeps async packets out. Sends are counted and logged every
30 s.

MoonWell.Voice.* in mangosd.conf (off by default); the production
compose adds the voice-relay container (UDP 3725) and the voice_secret
secret, deploy/local enables voice against a relay on the Windows host.

Checked: the changed units build in vmangos-buildenv; a ticket and a
feed datagram made by this code verify in the relay tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 17:26:16 +04:00
sindoring 40b395db8f Merge feature/moonwell-quest-sync into development 2026-10-29 14:20:58 +04:00
ratkosrb 9b7fb4dfdb Replace new with make_shared in auth socket. 2026-10-28 15:09:18 +02:00
vibeXcode d9173eca3e Block character names with three or more consecutive identical characters.
Closes https://github.com/vmangos/core/issues/3569
2026-10-26 22:28:28 +02:00
FlagFlayer 36cde9a0bd Correct captured thistle bear timer (again) (#3582) 2026-10-26 15:19:20 +02:00
Wall 8af2ddc596 Remove extra space in function name. (#3581) 2026-10-26 15:19:05 +02:00
ratkosrb 316e886869 Correction to continent instantiate boundaries. 2026-10-26 00:59:02 +02:00
ratkosrb a87eb41eac Fix realmd crash when TOTP security info is empty.
Closes https://github.com/vmangos/core/issues/3580
2026-10-25 14:34:26 +02:00
andi79h-claude a9fa07aab5 Add fishing to secondary skills of custom profession npc (#3575) 2026-10-21 13:07:57 +03:00
Gamemechanic 4c51bb778d Dont allow multiple loot rolls for the same player (#3574) 2026-10-20 15:21:50 +03:00
Gamemechanic d0c3a62be2 Fix orientation bug in SPELL_EFFECT_TELEPORT_UNITS_FACE_CASTER. (#3573) 2026-10-20 15:21:25 +03:00
24 changed files with 535 additions and 29 deletions
+7
View File
@@ -26,6 +26,11 @@ services:
ports: !override ["127.0.0.1:8085:8085/tcp"]
environment:
CONSOLE_ENABLE: "1"
MOONWELL_VOICE_ENABLE: "1"
MOONWELL_VOICE_ADDRESS: 127.0.0.1:3725
MOONWELL_VOICE_FEED: host.docker.internal:3726
extra_hosts: ["host.docker.internal:host-gateway"]
secrets: [db_password, voice_secret]
stdin_open: true
tty: true
volumes:
@@ -40,3 +45,5 @@ secrets:
file: ../local/secrets/db_password
db_root_password:
file: ../local/secrets/db_root_password
voice_secret:
file: ../local/secrets/voice_secret
+1 -1
View File
@@ -3,7 +3,7 @@ set -euo pipefail
cd "$(dirname "$0")"
mkdir -p secrets data
chmod 0700 secrets
for secret in db_password db_root_password; do
for secret in db_password db_root_password voice_secret; do
if [[ ! -e "secrets/$secret" ]]; then
(umask 077; openssl rand -hex 32 > "secrets/$secret")
fi
+2
View File
@@ -9,3 +9,5 @@ BUILD_JOBS=2
PARTY_BOT_MAX_BOTS=4
# Fill battleground queues with bots when a real player queues.
BATTLE_BOT_AUTO_JOIN=0
# Quest item drop chance multiplier (1 = stock vanilla).
RATE_DROP_ITEM_QUEST=2
+22 -1
View File
@@ -22,7 +22,8 @@ mkdir -p secrets data
chmod 0700 secrets
(umask 077; openssl rand -hex 32 > secrets/db_password)
(umask 077; openssl rand -hex 32 > secrets/db_root_password)
chmod 0444 secrets/db_password secrets/db_root_password
(umask 077; openssl rand -hex 32 > secrets/voice_secret)
chmod 0444 secrets/db_password secrets/db_root_password secrets/voice_secret
./prepare-db.sh
# Необязательное дополнение к штатной русской локализации:
# python3 prepare-russian.py
@@ -212,3 +213,23 @@ world dump, всех миграций этого checkout и дополните
healthy; TCP 3724 отвечает. Проверены генерация world-конфига и backup.
Игровой вход, загрузка карт и полный запуск `mangosd` требуют клиентских
данных и пока не проверены.
## Голосовой чат MoonWell
Голос идёт через отдельный контейнер `voice-relay` (UDP 3725, наружу), mangosd только выдаёт
клиентам подписанные тикеты (`.mwvoice`) и передаёт relay положение, группу и гильдию каждого
персонажа по внутренней сети compose. Голос в mangosd выключен, пока `.env` не включит его:
```bash
# Бинарник relay собирается в репозитории клиента: cargo build --release -p moonwell-voice
# --bin moonwell-voice-relay (под Linux), и кладётся сюда:
mkdir -p voice && cp moonwell-voice-relay voice/ && chmod 0755 voice/moonwell-voice-relay
(umask 077; openssl rand -hex 32 > secrets/voice_secret); chmod 0444 secrets/voice_secret
# В .env:
# MOONWELL_VOICE_ENABLE=1
# MOONWELL_VOICE_ADDRESS=play.moon-well.online:3725
docker compose up -d voice-relay mangosd
```
Секрет `voice_secret` нужен compose всегда (он объявлен для mangosd), даже при выключенном голосе.
Открыть в файрволе хоста нужно только UDP 3725.
+23
View File
@@ -69,6 +69,11 @@ services:
TZ: Europe/Moscow
PARTY_BOT_MAX_BOTS: ${PARTY_BOT_MAX_BOTS:-4}
BATTLE_BOT_AUTO_JOIN: ${BATTLE_BOT_AUTO_JOIN:-0}
RATE_DROP_ITEM_QUEST: ${RATE_DROP_ITEM_QUEST:-2}
MOONWELL_VOICE_ENABLE: ${MOONWELL_VOICE_ENABLE:-0}
MOONWELL_VOICE_ADDRESS: ${MOONWELL_VOICE_ADDRESS:-}
MOONWELL_VOICE_FEED: voice-relay:3726
secrets: [db_password, voice_secret]
healthcheck:
test: [CMD, python3, -c, "import socket; socket.create_connection(('127.0.0.1',8085),5).close()"]
interval: 30s
@@ -81,6 +86,22 @@ services:
- ./data:/data:ro
- ./entrypoint.py:/opt/entrypoint.py:ro
- world_logs:/var/log/vmangos
voice-relay:
image: ubuntu:24.04@sha256:33ceb71981b602c1a7443a53469e4dba065f7503eab3078a2d7a57a2ab987517
restart: unless-stopped
init: true
read_only: true
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
user: "10001:10001"
command: [/opt/voice/moonwell-voice-relay, --listen, "0.0.0.0:3725", --feed, "0.0.0.0:3726", --secret-file, /run/secrets/voice_secret]
secrets: [voice_secret]
ports: ["3725:3725/udp"]
volumes:
- ./voice:/opt/voice:ro
logging:
driver: json-file
options: {max-size: 10m, max-file: "5"}
realm-config:
image: mariadb:10.11@sha256:7db29378d4fdab73f8123bbc2b48905c90d1a4b00cf848b028f1e81e623257f2
profiles: [tools]
@@ -103,3 +124,5 @@ secrets:
file: ./secrets/db_password
db_root_password:
file: ./secrets/db_root_password
voice_secret:
file: ./secrets/voice_secret
+21
View File
@@ -32,10 +32,31 @@ else:
values[key] = str(value)
values.update({"DataDir": '"/data"', "HonorDir": '"/var/log/vmangos"', "RealmID": "1", "RealmZone": "12", "DeclinedNames": "1", "Console.Enable": "0", "Ra.Enable": "0", "SOAP.Enabled": "0", "PlayerSave.Interval": "300000"})
values.update({"Warden.WinEnabled": "0", "Warden.OSXEnabled": "0"})
quest_drop = os.environ.get("RATE_DROP_ITEM_QUEST", "2")
if not re.fullmatch(r"[0-9]{1,2}(\.[0-9]{1,2})?", quest_drop) or not 0 < float(quest_drop) <= 10:
raise SystemExit("Invalid RATE_DROP_ITEM_QUEST")
values["Rate.Drop.Item.Quest"] = quest_drop
console = os.environ.get("CONSOLE_ENABLE", "0")
if console not in ("0", "1"):
raise SystemExit("Invalid CONSOLE_ENABLE")
values["Console.Enable"] = console
voice = os.environ.get("MOONWELL_VOICE_ENABLE", "0")
if voice not in ("0", "1"):
raise SystemExit("Invalid MOONWELL_VOICE_ENABLE")
values["MoonWell.Voice.Enable"] = voice
if voice == "1":
secret_file = Path("/run/secrets/voice_secret")
if not secret_file.is_file():
raise SystemExit("MOONWELL_VOICE_ENABLE=1 needs the voice_secret secret")
secret = secret_file.read_text().strip()
if not re.fullmatch(r"[A-Za-z0-9]{32,}", secret):
raise SystemExit("The voice secret must contain at least 32 alphanumeric characters")
values["MoonWell.Voice.Secret"] = f'"{secret}"'
for name, key in (("MOONWELL_VOICE_ADDRESS", "MoonWell.Voice.Address"), ("MOONWELL_VOICE_FEED", "MoonWell.Voice.Feed")):
address = os.environ.get(name, "")
if not re.fullmatch(r"[A-Za-z0-9.-]+:[0-9]{1,5}", address):
raise SystemExit("Invalid " + name)
values[key] = f'"{address}"'
for directory in ("5875/dbc", "maps", "vmaps", "mmaps"):
if not Path("/data", directory).is_dir():
raise SystemExit("Missing client data: /data/" + directory)
+2
View File
@@ -23,6 +23,7 @@ set (game_SRCS
Conditions.cpp
CreatureGroups.cpp
DungeonScaling.cpp
MoonWellVoice.cpp
GameEventMgr.cpp
GuardMgr.cpp
GMTicketMgr.cpp
@@ -444,6 +445,7 @@ set (game_SRCS
OutdoorPvP/OutdoorPvPSI.h
PacketBroadcast/MovementBroadcaster.h
PacketBroadcast/PlayerBroadcaster.h
MoonWellVoice.h
PlayerBots/CombatBotBaseAI.h
PlayerBots/PartyBotAI.h
PlayerBots/BattleBotAI.h
+1
View File
@@ -1240,6 +1240,7 @@ ChatCommand * ChatHandler::getCommandTable()
{ "gocorpse", SEC_TICKETMASTER, false, &ChatHandler::HandleGocorpseCommand, "", nullptr },
{ "commands", SEC_PLAYER, true, &ChatHandler::HandleCommandsCommand, "", nullptr },
{ "mwquests", SEC_PLAYER, false, &ChatHandler::HandleMoonWellQuestsCommand, "", nullptr },
{ "mwvoice", SEC_PLAYER, false, &ChatHandler::HandleMoonWellVoiceCommand, "", nullptr },
{ "demorph", SEC_GAMEMASTER, false, &ChatHandler::HandleDeMorphCommand, "", nullptr },
{ "namedie", SEC_GAMEMASTER, false, &ChatHandler::HandleNameDieCommand, "", nullptr },
{ "die", SEC_GAMEMASTER, false, &ChatHandler::HandleDieCommand, "", nullptr },
+1
View File
@@ -936,6 +936,7 @@ class ChatHandler
bool HandleHelpCommand(char* args);
bool HandleCommandsCommand(char* args);
bool HandleMoonWellQuestsCommand(char* args);
bool HandleMoonWellVoiceCommand(char* args);
bool HandleStartCommand(char* args);
bool HandleMountCommand(char* args);
bool HandleDismountCommand(char* args);
+9
View File
@@ -33,6 +33,7 @@
#include "InstanceData.h"
#include "MapManager.h"
#include "BattleGroundMgr.h"
#include "MoonWellVoice.h"
bool ChatHandler::HandleHelpCommand(char* args)
{
@@ -64,6 +65,14 @@ bool ChatHandler::HandleMoonWellQuestsCommand(char* /*args*/)
return true;
}
// .mwvoice: the MoonWell client asks for a voice ticket and the relay address (MoonWellVoice::SendTicket).
bool ChatHandler::HandleMoonWellVoiceCommand(char* /*args*/)
{
if (Player* player = m_session ? m_session->GetPlayer() : nullptr)
MoonWellVoice::SendTicket(player);
return true;
}
bool ChatHandler::HandleAuctionAllianceCommand(char* /*args*/)
{
m_session->GetPlayer()->SetAuctionAccessMode(m_session->GetPlayer()->GetTeam() != ALLIANCE ? -1 : 0);
+3
View File
@@ -947,6 +947,9 @@ bool Group::CountRollVote(ObjectGuid const& playerGUID, Rolls::iterator& rollI,
if (itr == roll->playerVote.end())
return true; // result used for need iterator ++, so avoid for end of list
if (itr->second != ROLL_NOT_EMITED_YET)
return false; // player already rolled for this loot!
if (Loot* pLoot = roll->getLoot())
if (pLoot->items.empty())
return false;
+4 -1
View File
@@ -270,7 +270,10 @@ bool LootStoreItem::Roll(bool rate, float chanceMod) const
ItemPrototype const* pProto = sObjectMgr.GetItemPrototype(itemid);
float qualityModifier = pProto && rate ? sWorld.getConfig(qualityToRate[pProto->Quality]) : 1.0f;
// MoonWell: a quest drop takes the quest rate in place of its quality's.
float qualityModifier = !rate ? 1.0f
: needs_quest ? sWorld.getConfig(CONFIG_FLOAT_RATE_DROP_ITEM_QUEST)
: pProto ? sWorld.getConfig(qualityToRate[pProto->Quality]) : 1.0f;
return roll_chance_f(chance * chanceMod * qualityModifier);
}
+2 -1
View File
@@ -701,7 +701,8 @@ uint32 MapManager::GetContinentInstanceId(uint32 mapId, float x, float y, bool*
-10670.61f, -1692.51f,
-10908.48f, -1563.87f,
-13006.40f, -1622.80f,
-12863.23f, -4798.42f
-12863.23f, -4798.42f,
-12863.23f, -5855.58f
};
static float const stormwindAreaSouthLimit[] = {
-8725.337891f, 3535.624023f,
+277
View File
@@ -0,0 +1,277 @@
/*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "MoonWellVoice.h"
#include "ByteBuffer.h"
#include "Chat.h"
#include "Config/Config.h"
#include "Group.h"
#include "Log.h"
#include "Player.h"
#include "WorldPacket.h"
#include "WorldSession.h"
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <chrono>
#include <cerrno>
#include <cstring>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <fcntl.h>
#include <netdb.h>
#include <sys/socket.h>
#include <unistd.h>
#endif
namespace MoonWellVoice
{
// Must match the relay: feed::MAX_PER_DATAGRAM keeps a datagram under a 1280-byte MTU.
static uint8 const VERSION = 1;
static size_t const MAC_LEN = 16;
static size_t const MAX_PER_DATAGRAM = 31;
// A failed socket or name lookup is retried this often, so the relay may start after mangosd.
static uint32 const REOPEN_MS = 10 * IN_MILLISECONDS;
static bool s_enabled = false;
static std::string s_secret;
static std::string s_address;
static std::string s_feed;
static uint32 s_interval = 250;
static uint32 s_ticketLifetime = HOUR;
static uint32 s_sinceFeed = 0;
static uint32 s_sinceOpen = REOPEN_MS;
// What the feed did since the last report, logged every REPORT_MS when it changed, so a feed
// that stops or fails shows in the log.
static uint32 const REPORT_MS = 30 * IN_MILLISECONDS;
static uint32 s_sinceReport = 0;
static uint32 s_sent = 0;
static uint32 s_failed = 0;
static int s_lastError = 0;
static uint32 s_lastCount = 0;
#ifdef _WIN32
static SOCKET s_socket = INVALID_SOCKET;
static bool IsOpen() { return s_socket != INVALID_SOCKET; }
static void CloseSocket() { if (IsOpen()) closesocket(s_socket); s_socket = INVALID_SOCKET; }
#else
static int s_socket = -1;
static bool IsOpen() { return s_socket >= 0; }
static void CloseSocket() { if (IsOpen()) close(s_socket); s_socket = -1; }
#endif
void LoadConfig()
{
CloseSocket();
s_enabled = sConfig.GetBoolDefault("MoonWell.Voice.Enable", false);
s_secret = sConfig.GetStringDefault("MoonWell.Voice.Secret", "");
s_address = sConfig.GetStringDefault("MoonWell.Voice.Address", "");
s_feed = sConfig.GetStringDefault("MoonWell.Voice.Feed", "127.0.0.1:3726");
s_interval = std::max(100, sConfig.GetIntDefault("MoonWell.Voice.FeedInterval", 250));
s_ticketLifetime = std::max(60, sConfig.GetIntDefault("MoonWell.Voice.TicketLifetime", HOUR));
s_sinceOpen = REOPEN_MS;
if (!s_enabled)
return;
// The relay refuses a shorter secret, so the core would sign what it never accepts.
if (s_secret.size() < 32 || s_address.empty())
{
sLog.Out(LOG_BASIC, LOG_LVL_ERROR, "MoonWell.Voice: Secret (32+ chars) and Address are required, voice disabled");
s_enabled = false;
}
}
bool IsEnabled()
{
return s_enabled;
}
// The relay's truncated HMAC-SHA256: the label first, so a ticket never verifies as a feed
// datagram nor the other way round.
static void Sign(char const* label, ByteBuffer& buffer)
{
std::vector<uint8> message(label, label + strlen(label));
message.insert(message.end(), buffer.contents(), buffer.contents() + buffer.size());
uint8 mac[EVP_MAX_MD_SIZE];
#if defined(OPENSSL_VERSION_MAJOR) && (OPENSSL_VERSION_MAJOR >= 3)
size_t macLen = 0;
EVP_Q_mac(nullptr, "HMAC", nullptr, "SHA256", nullptr, s_secret.data(), s_secret.size(),
message.data(), message.size(), mac, sizeof(mac), &macLen);
#else
unsigned int macLen = 0;
HMAC(EVP_sha256(), s_secret.data(), int(s_secret.size()), message.data(), message.size(), mac, &macLen);
#endif
buffer.append(mac, MAC_LEN);
}
static uint64 UnixMs()
{
using namespace std::chrono;
return uint64(duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count());
}
// The relay is named host:port; a host name is looked up here, so a container name works.
static void OpenSocket()
{
size_t colon = s_feed.rfind(':');
if (colon == std::string::npos)
{
sLog.Out(LOG_BASIC, LOG_LVL_ERROR, "MoonWell.Voice.Feed '%s' is not host:port", s_feed.c_str());
return;
}
std::string host = s_feed.substr(0, colon);
std::string port = s_feed.substr(colon + 1);
addrinfo hints = {};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM;
addrinfo* found = nullptr;
if (getaddrinfo(host.c_str(), port.c_str(), &hints, &found) != 0 || !found)
{
sLog.Out(LOG_BASIC, LOG_LVL_ERROR, "MoonWell.Voice: cannot resolve the relay %s", s_feed.c_str());
return;
}
s_socket = socket(found->ai_family, found->ai_socktype, found->ai_protocol);
// Connected, so a send needs no address; non-blocking, so a full buffer drops a tick and
// never stalls the world thread.
if (IsOpen() && connect(s_socket, found->ai_addr, int(found->ai_addrlen)) != 0)
CloseSocket();
freeaddrinfo(found);
if (!IsOpen())
return;
#ifdef _WIN32
u_long nonBlocking = 1;
ioctlsocket(s_socket, FIONBIO, &nonBlocking);
#else
fcntl(s_socket, F_SETFL, fcntl(s_socket, F_GETFL, 0) | O_NONBLOCK);
#endif
sLog.Out(LOG_BASIC, LOG_LVL_MINIMAL, "MoonWell.Voice: feeding the relay at %s", s_feed.c_str());
}
bool FeedDue(uint32 diff)
{
if (!s_enabled)
return false;
s_sinceFeed += diff;
s_sinceReport += diff;
if (s_sinceReport >= REPORT_MS)
{
s_sinceReport = 0;
if (s_sent || s_failed)
sLog.Out(LOG_BASIC, LOG_LVL_MINIMAL, "MoonWell.Voice: %u feed datagrams sent, %u failed (last error %d), %u characters in the last",
s_sent, s_failed, s_lastError, s_lastCount);
s_sent = 0;
s_failed = 0;
}
if (!IsOpen())
{
s_sinceOpen += diff;
if (s_sinceOpen < REOPEN_MS)
return false;
s_sinceOpen = 0;
OpenSocket();
if (!IsOpen())
return false;
}
if (s_sinceFeed < s_interval)
return false;
s_sinceFeed = 0;
return true;
}
static void SendDatagram(uint64 sentMs, ByteBuffer const& entries, uint8 count)
{
ByteBuffer datagram(entries.size() + 32);
datagram.append("MWVF", 4);
datagram << VERSION << sentMs << count;
datagram.append(entries.contents(), entries.size());
Sign("moonwell-voice feed", datagram);
if (send(s_socket, reinterpret_cast<char const*>(datagram.contents()), int(datagram.size()), 0) >= 0)
++s_sent;
else
{
++s_failed;
#ifdef _WIN32
s_lastError = WSAGetLastError();
#else
s_lastError = errno;
#endif
}
}
void SendFeed(std::vector<Player*> const& players)
{
if (!IsOpen())
return;
uint64 sentMs = UnixMs();
s_lastCount = uint32(players.size());
ByteBuffer entries(MAX_PER_DATAGRAM * 37);
uint8 count = 0;
for (Player* player : players)
{
Group const* group = player->GetGroup();
entries << uint64(player->GetObjectGuid().GetRawValue());
entries << uint8(player->GetTeam() == ALLIANCE ? 0 : 1);
entries << uint32(player->GetMapId());
entries << uint32(player->GetInstanceId());
entries << player->GetPositionX() << player->GetPositionY() << player->GetPositionZ();
entries << uint32(group ? group->GetId() : 0);
entries << uint32(player->GetGuildId());
if (++count == MAX_PER_DATAGRAM)
{
SendDatagram(sentMs, entries, count);
entries.clear();
count = 0;
}
}
if (count)
SendDatagram(sentMs, entries, count);
}
static void SendLine(Player* player, std::string const& payload)
{
std::string text = "MWV\t" + payload;
WorldPacket data;
ChatHandler::BuildChatPacket(data, CHAT_MSG_WHISPER, text.c_str(), LANG_ADDON, CHAT_TAG_NONE,
player->GetObjectGuid(), player->GetName());
player->GetSession()->SendPacket(&data);
}
// "T<ticket hex>\t<relay address>", or "off" when voice is disabled.
void SendTicket(Player* player)
{
if (!s_enabled)
{
SendLine(player, "off");
return;
}
ByteBuffer ticket(64);
ticket << VERSION << uint64(player->GetObjectGuid().GetRawValue()) << uint64(UnixMs() / 1000 + s_ticketLifetime);
Sign("moonwell-voice token", ticket);
static char const digits[] = "0123456789abcdef";
std::string hex;
for (size_t i = 0; i < ticket.size(); ++i)
{
hex += digits[ticket.contents()[i] >> 4];
hex += digits[ticket.contents()[i] & 0xF];
}
SendLine(player, "T" + hex + "\t" + s_address);
}
}
+51
View File
@@ -0,0 +1,51 @@
/*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#ifndef MANGOS_MOONWELL_VOICE_H
#define MANGOS_MOONWELL_VOICE_H
#include "Common.h"
#include <vector>
class Player;
// MoonWell voice chat. Voice goes to a separate relay (moonwell-voice-relay in the MoonWell
// client repo), never through the world socket; the core only vouches for who a client is and
// where every character stands:
// - `.mwvoice` answers the player with a signed ticket and the relay's address, as an addon
// line (LANG_ADDON, prefix MWV), so the client can open a voice session as that character;
// - every MoonWell.Voice.FeedInterval ms the world thread sends the relay each character in the
// world: faction, map, instance, position, group and guild. The relay decides who hears whom
// from that alone, so no client can claim a place, group or guild that is not its own.
// Both are signed with MoonWell.Voice.Secret, which the relay shares. The byte layouts are the
// relay's `token` and `feed` modules.
namespace MoonWellVoice
{
void LoadConfig();
bool IsEnabled();
// Counts the world's diff and says whether a feed is due; the caller then gathers the players
// under the lock that keeps async packets out, and calls SendFeed.
bool FeedDue(uint32 diff);
void SendFeed(std::vector<Player*> const& players);
void SendTicket(Player* player);
}
#endif
+6
View File
@@ -9590,6 +9590,12 @@ uint8 ObjectMgr::CheckPlayerName(std::string const& name, bool create)
if (wname.size() < minName)
return CHAR_NAME_TOO_SHORT;
for (size_t i = 2; i < wname.size(); ++i)
{
if (wname[i] == wname[i - 1] && wname[i] == wname[i - 2])
return CHAR_NAME_THREE_CONSECUTIVE;
}
uint32 strictMask = sWorld.getConfig(CONFIG_UINT32_STRICT_PLAYER_NAMES);
if (!isValidString(wname, strictMask, false, create))
return CHAR_NAME_MIXED_LANGUAGES;
+1 -1
View File
@@ -22770,7 +22770,7 @@ void Log::Player(WorldSession const* session, LogType logType, char const* subTy
// Player logs should never go to the console
}
void Log:: Player(uint32 accountId, LogType logType, LogLevel logLevel, char const* format, ...)
void Log::Player(uint32 accountId, LogType logType, LogLevel logLevel, char const* format, ...)
{
if (logType >= LOG_TYPE_MAX || logType < 0)
return;
+1 -1
View File
@@ -2953,7 +2953,7 @@ void Spell::EffectTeleUnitsFaceCaster(SpellEffectIndex effIdx)
m_caster->GetClosePoint(fx, fy, fz, unitTarget->GetObjectBoundingRadius(), dis);
}
unitTarget->NearTeleportTo(fx, fy, fz, -m_caster->GetOrientation(), TELE_TO_NOT_LEAVE_COMBAT | TELE_TO_NOT_UNSUMMON_PET | (unitTarget == m_caster ? TELE_TO_SPELL : 0));
unitTarget->NearTeleportTo(fx, fy, fz, m_caster->GetAngle(fx, fy) - M_PI_F, TELE_TO_NOT_LEAVE_COMBAT | TELE_TO_NOT_UNSUMMON_PET | (unitTarget == m_caster ? TELE_TO_SPELL : 0));
}
void Spell::EffectLearnSkill(SpellEffectIndex effIdx)
+22
View File
@@ -46,6 +46,7 @@
#include "Chat.h"
#include "DBCStores.h"
#include "MassMailMgr.h"
#include "MoonWellVoice.h"
#include "LootMgr.h"
#include "ItemEnchantmentMgr.h"
#include "MapManager.h"
@@ -502,6 +503,7 @@ void World::LoadConfigSettings(bool reload)
setConfigPos(CONFIG_FLOAT_RATE_DROP_ITEM_LEGENDARY, "Rate.Drop.Item.Legendary", 1.0f);
setConfigPos(CONFIG_FLOAT_RATE_DROP_ITEM_ARTIFACT, "Rate.Drop.Item.Artifact", 1.0f);
setConfigPos(CONFIG_FLOAT_RATE_DROP_ITEM_REFERENCED, "Rate.Drop.Item.Referenced", 1.0f);
setConfigPos(CONFIG_FLOAT_RATE_DROP_ITEM_QUEST, "Rate.Drop.Item.Quest", 1.0f);
setConfigPos(CONFIG_FLOAT_RATE_DROP_MONEY, "Rate.Drop.Money", 1.0f);
setConfig(CONFIG_FLOAT_RATE_XP_KILL, "Rate.XP.Kill", 1.0f);
setConfig(CONFIG_FLOAT_RATE_XP_KILL_ELITE, "Rate.XP.Kill.Elite", 1.0f);
@@ -673,6 +675,8 @@ void World::LoadConfigSettings(bool reload)
setConfigMinMax(CONFIG_FLOAT_DUNGEON_SCALING_DAMAGE_4, "DungeonScaling.Damage.4", 0.90f, 0.05f, 1.0f);
setConfigMinMax(CONFIG_FLOAT_DUNGEON_FULL_GROUP_RARE_GEAR_CHANCE, "DungeonScaling.FullGroup.RareGearChance", 1.2f, 1.0f, 10.0f);
MoonWellVoice::LoadConfig();
setConfig(CONFIG_UINT32_MAX_PRIMARY_TRADE_SKILL, "MaxPrimaryTradeSkill", 2);
setConfigMinMax(CONFIG_UINT32_MIN_PETITION_SIGNS, "MinPetitionSigns", 9, 0, 9);
@@ -2145,6 +2149,24 @@ void World::Update(uint32 diff)
// Update ban list if necessary
sAccountMgr.Update(diff);
// MoonWell voice: every character in the world to the relay. After the map update, and under
// the lock that keeps async packets out, so no group or guild changes while it is read.
if (MoonWellVoice::FeedDue(diff))
{
m_canProcessAsyncPackets = false;
std::vector<Player*> players;
{
std::lock_guard<std::mutex> lock(m_asyncPacketsMutex);
players.reserve(m_sessions.size());
for (auto const& itr : m_sessions)
if (Player* player = itr.second->GetPlayer())
if (player->IsInWorld())
players.push_back(player);
MoonWellVoice::SendFeed(players);
}
m_canProcessAsyncPackets = true;
}
{
m_canProcessAsyncPackets = false;
std::lock_guard<std::mutex> lock(m_asyncPacketsMutex);
+1
View File
@@ -408,6 +408,7 @@ enum eConfigFloatValues
CONFIG_FLOAT_RATE_DROP_ITEM_LEGENDARY,
CONFIG_FLOAT_RATE_DROP_ITEM_ARTIFACT,
CONFIG_FLOAT_RATE_DROP_ITEM_REFERENCED,
CONFIG_FLOAT_RATE_DROP_ITEM_QUEST,
CONFIG_FLOAT_RATE_DROP_MONEY,
CONFIG_FLOAT_RATE_XP_KILL,
CONFIG_FLOAT_RATE_XP_KILL_ELITE,
+39 -1
View File
@@ -2626,8 +2626,9 @@ Visibility.ForceActiveObjects = 1
# Rate.Drop.Item.Legendary
# Rate.Drop.Item.Artifact
# Rate.Drop.Item.Referenced
# Rate.Drop.Item.Quest
# Rate.Drop.Money
# Drop rates (items by quality and money)
# Drop rates (items by quality, quest items in place of their quality, and money)
# Default: 1
#
# Rate.XP.Kill
@@ -2805,6 +2806,7 @@ Rate.Drop.Item.Epic = 1
Rate.Drop.Item.Legendary = 1
Rate.Drop.Item.Artifact = 1
Rate.Drop.Item.Referenced = 1
Rate.Drop.Item.Quest = 1
Rate.Drop.Money = 1
Rate.XP.Kill = 1
Rate.XP.Kill.Elite = 1
@@ -2958,6 +2960,42 @@ Alterac.InitMaxPlayers = 0
OutdoorPvP.SI.Enable = 1
OutdoorPvP.EP.Enable = 1
###################################################################################################################
# MOONWELL VOICE
#
# MoonWell.Voice.Enable
# Vouch for MoonWell voice chat: answer .mwvoice with a signed ticket and feed the voice relay
# (moonwell-voice-relay) every character in the world. Voice itself never goes through mangosd.
# Default: 0 (disable)
#
# MoonWell.Voice.Secret
# Signs tickets and the feed; must equal the relay's MOONWELL_VOICE_SECRET, 32 chars or more.
#
# MoonWell.Voice.Address
# The relay's public host:port, as sent to clients.
#
# MoonWell.Voice.Feed
# Where the feed goes: the relay's --feed address. Keep it off the public interface.
# Default: "127.0.0.1:3726"
#
# MoonWell.Voice.FeedInterval
# Milliseconds between feeds (100 or more). A character kicked from a group or guild stops
# hearing it within one interval.
# Default: 250
#
# MoonWell.Voice.TicketLifetime
# Seconds a ticket opens a voice session for (60 or more); the client asks again before then.
# Default: 3600
#
###################################################################################################################
MoonWell.Voice.Enable = 0
MoonWell.Voice.Secret = ""
MoonWell.Voice.Address = ""
MoonWell.Voice.Feed = "127.0.0.1:3726"
MoonWell.Voice.FeedInterval = 250
MoonWell.Voice.TicketLifetime = 3600
###################################################################################################################
# DUNGEON SCALING
#
+37 -20
View File
@@ -171,7 +171,7 @@ void AuthSocket::DoRecvIncomingData()
std::shared_ptr<ByteBuffer> AuthSocket::GenerateLogonProofResponse(Crypto::Hash::SHA1::Digest const& shaDigest)
{
std::shared_ptr<ByteBuffer> pkt(new ByteBuffer());
auto pkt = std::make_shared<ByteBuffer>();
if (m_build < 6299) // before version 2.0.3 (exclusive)
{
@@ -587,7 +587,7 @@ void AuthSocket::_HandleLogonProof()
return;
}
std::shared_ptr<PINData> pinData(new PINData());
auto pinData = std::make_shared<PINData>();
self->m_socket.Read((char*) pinData.get(), sizeof(PINData), [self, lp, pinData](IO::NetworkError const& error, size_t)
{
self->_HandleLogonProof__PostRecv(lp, pinData);
@@ -620,7 +620,7 @@ void AuthSocket::_HandleLogonProof__PostRecv_HandleInvalidVersion(std::shared_pt
if (m_pendingPatchFile == nullptr)
{
// no patch found
std::shared_ptr<ByteBuffer> pkt(new ByteBuffer());
auto pkt = std::make_shared<ByteBuffer>();
*pkt << (uint8) CMD_AUTH_LOGON_CHALLENGE;
*pkt << (uint8) 0x00;
*pkt << (uint8) WOW_FAIL_VERSION_INVALID;
@@ -643,7 +643,7 @@ void AuthSocket::_HandleLogonProof__PostRecv_HandleInvalidVersion(std::shared_pt
std::string wowClientPathType = "Patch"; // Must be patch "Patch"
MANGOS_ASSERT(wowClientPathType.size() <= 255); // Filename must fit inside a byte
std::shared_ptr<ByteBuffer> pkt(new ByteBuffer());
auto pkt = std::make_shared<ByteBuffer>();
// packet 1
*pkt << (uint8) CMD_AUTH_LOGON_PROOF;
@@ -714,7 +714,7 @@ void AuthSocket::_HandleLogonProof__PostRecv(std::shared_ptr<sAuthLogonProof_C c
pinResult = VerifyPinData(std::stoi(m_securityInfo), *pinData);
}
}
catch (const std::exception& e)
catch (std::exception const& e)
{
sLog.Out(LOG_BASIC, LOG_LVL_ERROR, "[AuthChallenge] Account '%s' failed to parse FIXED_PIN from security info: %s", m_login.c_str(), e.what());
pinResult = false;
@@ -723,15 +723,31 @@ void AuthSocket::_HandleLogonProof__PostRecv(std::shared_ptr<sAuthLogonProof_C c
}
else if ((m_lockFlags & TOTP) == TOTP)
{
for (int i = -2; i != 2; ++i)
try
{
auto pin = GenerateTotpPin(m_securityInfo, i);
if (m_securityInfo.empty())
{
sLog.Out(LOG_BASIC, LOG_LVL_DEBUG, "[AuthChallenge] Account '%s' has empty security info for TOTP", m_login.c_str());
pinResult = false;
}
else
{
for (int i = -2; i != 2; ++i)
{
auto pin = GenerateTotpPin(m_securityInfo, i);
if (pin == uint32(-1))
break;
if (pin == uint32(-1))
break;
if ((pinResult = VerifyPinData(pin, *pinData)))
break;
if ((pinResult = VerifyPinData(pin, *pinData)))
break;
}
}
}
catch (std::exception const& e)
{
sLog.Out(LOG_BASIC, LOG_LVL_ERROR, "[AuthChallenge] Account '%s' failed to parse TOTP from security info: %s", m_login.c_str(), e.what());
pinResult = false;
}
}
else if (m_geoUnlockPIN)
@@ -746,13 +762,14 @@ void AuthSocket::_HandleLogonProof__PostRecv(std::shared_ptr<sAuthLogonProof_C c
}
// Check if SRP6 results match (password is correct), else send an error
if (!srp.Proof(lp->M1, 20) && pinResult)
bool const passwordResult = !srp.Proof(lp->M1, 20);
if (passwordResult && pinResult)
{
if (!VerifyVersion(lp->A, sizeof(lp->A), lp->crc_hash, false))
{
sLog.Out(LOG_BASIC, LOG_LVL_BASIC, "[AuthChallenge] Account %s tried to login with modified client!", m_login.c_str());
std::shared_ptr<ByteBuffer> pkt(new ByteBuffer());
auto pkt = std::make_shared<ByteBuffer>();
*pkt << (uint8) CMD_AUTH_LOGON_PROOF;
*pkt << (uint8) WOW_FAIL_VERSION_INVALID;
m_socket.Write(std::move(pkt), [self = shared_from_this()](IO::NetworkError const& error)
@@ -781,7 +798,7 @@ void AuthSocket::_HandleLogonProof__PostRecv(std::shared_ptr<sAuthLogonProof_C c
{
sLog.Out(LOG_BASIC, LOG_LVL_ERROR, "Unable to write geolock PIN for %s - account has not been locked", m_safelogin.c_str());
std::shared_ptr<ByteBuffer> pkt(new ByteBuffer());
auto pkt = std::make_shared<ByteBuffer>();
*pkt << (uint8) CMD_AUTH_LOGON_PROOF;
*pkt << (uint8) WOW_FAIL_DB_BUSY;
m_socket.Write(std::move(pkt), [self = shared_from_this()](IO::NetworkError const& error)
@@ -815,7 +832,7 @@ void AuthSocket::_HandleLogonProof__PostRecv(std::shared_ptr<sAuthLogonProof_C c
}
#endif
std::shared_ptr<ByteBuffer> pkt(new ByteBuffer());
auto pkt = std::make_shared<ByteBuffer>();
*pkt << (uint8) CMD_AUTH_LOGON_PROOF;
*pkt << (uint8) WOW_FAIL_PARENTCONTROL;
m_socket.Write(std::move(pkt), [self = shared_from_this()](IO::NetworkError const& error)
@@ -888,12 +905,12 @@ void AuthSocket::_HandleLogonProof__PostRecv(std::shared_ptr<sAuthLogonProof_C c
else
{
// We are here because the password was incorrect
sLog.Out(LOG_BASIC, LOG_LVL_BASIC, "[AuthChallenge] Account '%s' using IP '%s' tried to login with wrong password!", m_login.c_str (), GetRemoteIpString().c_str());
sLog.Out(LOG_BASIC, LOG_LVL_BASIC, "[AuthChallenge] Account '%s' using IP '%s' tried to login with wrong %s!", m_login.c_str (), GetRemoteIpString().c_str(), (passwordResult ? "pin" : "password"));
// Record the failure for the in-memory brute-force guard.
RecordWrongPasswordAttempt(GetRemoteIpString());
std::shared_ptr<ByteBuffer> pkt(new ByteBuffer());
auto pkt = std::make_shared<ByteBuffer>();
*pkt << (uint8) CMD_AUTH_LOGON_PROOF;
*pkt << (uint8) WOW_FAIL_UNKNOWN_ACCOUNT;
if (m_build > 6005) // > 1.12.2
@@ -953,7 +970,7 @@ void AuthSocket::_HandleReconnectProof()
m_status = STATUS_INVALID;
// Read the packet
std::shared_ptr<AUTH_RECONNECT_PROOF_C> lp(new AUTH_RECONNECT_PROOF_C());
auto lp = std::make_shared<AUTH_RECONNECT_PROOF_C>();
m_socket.Read((char*) lp.get(), sizeof(AUTH_RECONNECT_PROOF_C), [self = shared_from_this(), lp](IO::NetworkError const& error, size_t)
{
if (error)
@@ -1056,7 +1073,7 @@ void AuthSocket::_HandleRealmList()
ByteBuffer realmlistBuffer;
self->LoadRealmlistAndWriteIntoBuffer(realmlistBuffer);
std::shared_ptr<ByteBuffer> pkt(new ByteBuffer());
auto pkt = std::make_shared<ByteBuffer>();
*pkt << (uint8) CMD_REALM_LIST;
*pkt << (uint16)realmlistBuffer.size();
pkt->append(realmlistBuffer);
@@ -1370,7 +1387,7 @@ void AuthSocket::InitAndHandOverControlToPatchHandler()
{
MANGOS_ASSERT(m_pendingPatchFile);
std::shared_ptr<XFER_DATA_CHUNK> rawChunk(new XFER_DATA_CHUNK());
auto rawChunk = std::make_shared<XFER_DATA_CHUNK>();
rawChunk->cmd = CMD_XFER_DATA;
RepeatInternalXferLoop(rawChunk);
+1 -1
View File
@@ -952,7 +952,7 @@ bool GossipHello_ProfessionNPC(Player* player, Creature* creature)
void CompleteLearnProfession(Player *pPlayer, Creature *pCreature, SkillType skill)
{
if (pPlayer->GetFreePrimaryProfessionPoints() == 0 && !(skill == SKILL_COOKING || skill == SKILL_FIRST_AID))
if (pPlayer->GetFreePrimaryProfessionPoints() == 0 && !(skill == SKILL_COOKING || skill == SKILL_FIRST_AID || skill == SKILL_FISHING))
{
pPlayer->GetSession()->SendNotification("You already know two primary professions.");
}
+1 -1
View File
@@ -859,7 +859,7 @@ struct npc_rabid_thistle_bearAI : public FollowerAI
void StartFollowing(Player* pPlayer)
{
Captured_Timer = 300000;
Captured_Timer = 500000;
StartFollow(pPlayer);
}