13 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
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
15 changed files with 68 additions and 27 deletions
+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
+1
View File
@@ -69,6 +69,7 @@ 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
+4
View File
@@ -32,6 +32,10 @@ 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")
+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,
+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)
+1
View File
@@ -503,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);
+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,
+3 -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
+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);
}