перевод текстов

This commit is contained in:
2026-07-26 22:48:15 +04:00
parent 4eb59b7245
commit a71eb7bee7
9 changed files with 218 additions and 10 deletions
+39
View File
@@ -0,0 +1,39 @@
# Solo Random Dungeon client patch
The server recognizes five custom `LFGDungeons.dbc` entries:
| Custom ID | Stock category |
| --- | --- |
| 1258 | Random Classic Dungeon (258) |
| 1259 | Random Burning Crusade Dungeon (259) |
| 1260 | Random Burning Crusade Heroic (260) |
| 1261 | Random Lich King Dungeon (261) |
| 1262 | Random Lich King Heroic (262) |
The generated server DBC is at:
`client_patch/enUS/DBFilesClient/LFGDungeons.dbc`
It is mounted into `ac-worldserver` by `docker-compose.override.yml`.
## Client installation
Use the DBC extracted from the exact locale of the target 3.3.5a client:
```bash
./tools/build-solo-lfg-dbc.py \
/path/to/client/DBFilesClient/LFGDungeons.dbc \
/tmp/solo-rdf/DBFilesClient/LFGDungeons.dbc
```
Put the generated `DBFilesClient/LFGDungeons.dbc` into a client MPQ patch
loaded after the stock locale archives. For a Russian client this is normally
a new archive under `Data/ruRU/` named like `patch-ruRU-S.MPQ`.
After installing the patch, delete the client's `Cache` directory. The
Dungeon Finder list will contain separate solo random categories appropriate
for the character's level.
Do not replace a localized client DBC with the bundled enUS server DBC. Run
the generator against that client's own DBC so all stock localized rows remain
intact.
Binary file not shown.
+1
View File
@@ -8,3 +8,4 @@ services:
volumes:
- ./modules:/azerothcore/modules:ro
- ./lua_scripts:/azerothcore/lua_scripts
- ./client_patch/enUS/DBFilesClient/LFGDungeons.dbc:/azerothcore/env/dist/data/dbc/LFGDungeons.dbc:ro
+2
View File
@@ -108,6 +108,8 @@ namespace lfg
RANDOM_DUNGEON_HEROIC_WOTLK = 262
};
constexpr uint32 LFG_SOLO_RANDOM_DUNGEON_ID_OFFSET = 1000;
class Lfg5Guids;
typedef std::list<Lfg5Guids> Lfg5GuidsList;
+30 -3
View File
@@ -519,6 +519,7 @@ namespace lfg
LfgJoinResultData joinData;
LfgGuidSet players;
uint32 rDungeonId = 0;
bool soloQueue = false;
bool isContinue = grp && grp->isLFGGroup() && GetState(gguid) != LFG_STATE_FINISHED_DUNGEON;
if (grp && (grp->isBGGroup() || grp->isBFGroup()))
@@ -611,6 +612,10 @@ namespace lfg
}
}
soloQueue = rDungeonId && IsSoloRandomDungeon(rDungeonId);
if (soloQueue && grp)
joinData.result = LFG_JOIN_PARTY_NOT_MEET_REQS;
if (!isRaid && joinData.result == LFG_JOIN_OK)
{
// Check player or group member restrictions
@@ -775,7 +780,7 @@ namespace lfg
LfgRolesMap rolesMap;
rolesMap[guid] = roles;
LFGQueue& queue = GetQueue(guid);
queue.AddQueueData(guid, GameTime::GetGameTime().count(), dungeons, rolesMap);
queue.AddQueueData(guid, GameTime::GetGameTime().count(), dungeons, rolesMap, soloQueue);
if (!isContinue)
{
@@ -1671,7 +1676,11 @@ namespace lfg
}
// Xinef: Store amount of random players player grouped with
if (group)
LfgDungeonSet const& selectedDungeons = GetSelectedDungeons(pguid);
bool const soloRandom = !selectedDungeons.empty() && IsSoloRandomDungeon(*selectedDungeons.begin());
if (soloRandom)
SetRandomPlayersCount(pguid, 0);
else if (group)
{
SetRandomPlayersCount(pguid, group->GetMembersCount() >= MAXGROUPSIZE ? 0 : MAXGROUPSIZE - group->GetMembersCount());
oldGroupGUID = group->GetGUID();
@@ -2345,8 +2354,12 @@ namespace lfg
*/
LfgReward const* LFGMgr::GetRandomDungeonReward(uint32 dungeon, uint8 level)
{
uint32 dungeonId = dungeon & 0x00FFFFFF;
if (IsSoloRandomDungeon(dungeonId))
dungeonId -= LFG_SOLO_RANDOM_DUNGEON_ID_OFFSET;
LfgReward const* rew = nullptr;
LfgRewardContainerBounds bounds = RewardMapStore.equal_range(dungeon & 0x00FFFFFF);
LfgRewardContainerBounds bounds = RewardMapStore.equal_range(dungeonId);
for (LfgRewardContainer::const_iterator itr = bounds.first; itr != bounds.second; ++itr)
{
rew = itr->second;
@@ -2358,6 +2371,20 @@ namespace lfg
return rew;
}
bool LFGMgr::IsSoloRandomDungeon(uint32 dungeon)
{
uint32 dungeonId = dungeon & 0x00FFFFFF;
if (dungeonId < LFG_SOLO_RANDOM_DUNGEON_ID_OFFSET)
return false;
LFGDungeonData const* solo = GetLFGDungeon(dungeonId);
LFGDungeonData const* regular = GetLFGDungeon(dungeonId - LFG_SOLO_RANDOM_DUNGEON_ID_OFFSET);
return solo && regular &&
solo->type == LFG_TYPE_RANDOM &&
regular->type == LFG_TYPE_RANDOM &&
solo->group == regular->group;
}
/**
Given a Dungeon id returns the dungeon Type
+2
View File
@@ -525,6 +525,8 @@ namespace lfg
bool IsDungeonDisabled(uint32 mapId, Difficulty difficulty) const;
/// Gets the random dungeon reward corresponding to given dungeon and player level
LfgReward const* GetRandomDungeonReward(uint32 dungeon, uint8 level);
/// Checks whether an entry is a custom one-player random dungeon category
bool IsSoloRandomDungeon(uint32 dungeon);
/// Returns all random and seasonal dungeons for given level and expansion
LfgDungeonSet GetRandomAndSeasonalDungeons(uint8 level, uint8 expansion);
/// Teleport a player to/from selected dungeon
+28 -4
View File
@@ -104,10 +104,10 @@ namespace lfg
restoredAfterProposal.remove(guid);
}
void LFGQueue::AddQueueData(ObjectGuid guid, time_t joinTime, LfgDungeonSet const& dungeons, LfgRolesMap const& rolesMap)
void LFGQueue::AddQueueData(ObjectGuid guid, time_t joinTime, LfgDungeonSet const& dungeons, LfgRolesMap const& rolesMap, bool solo)
{
LOG_DEBUG("lfg", "JOINED AddQueueData: {}", guid.ToString());
QueueDataStore[guid] = LfgQueueData(joinTime, dungeons, rolesMap);
QueueDataStore[guid] = LfgQueueData(joinTime, dungeons, rolesMap, solo);
AddToQueue(guid);
}
@@ -263,6 +263,7 @@ namespace lfg
// Check if more than one LFG group and number of players joining
uint8 numPlayers = 0;
uint8 numLfgGroups = 0;
bool soloQueue = false;
ObjectGuid guid;
uint64 addToFoundMask = 0;
@@ -280,6 +281,7 @@ namespace lfg
for (LfgRolesMap::const_iterator it2 = itQueue->second.roles.begin(); it2 != itQueue->second.roles.end(); ++it2)
proposalGroups[it2->first] = itQueue->first.IsGroup() ? itQueue->first : ObjectGuid::Empty;
soloQueue = soloQueue || itQueue->second.solo;
numPlayers += itQueue->second.roles.size();
if (sLFGMgr->IsLfgGroup(guid))
@@ -293,8 +295,13 @@ namespace lfg
if (numLfgGroups > 1)
return LFG_INCOMPATIBLES_MULTIPLE_LFG_GROUPS;
// A solo RDF entry is a complete one-player match and must not absorb
// entries from the regular five-player queue.
if (soloQueue && (check.size() != 1 || numPlayers != 1))
return LFG_INCOMPATIBLES_WRONG_GROUP_SIZE;
// Group with less that MAXGROUPSIZE members always compatible
if (!sLFGMgr->IsTesting() && check.size() == 1 && numPlayers < MAXGROUPSIZE)
if (!sLFGMgr->IsTesting() && !soloQueue && check.size() == 1 && numPlayers < MAXGROUPSIZE)
{
LfgQueueDataContainer::iterator itQueue = QueueDataStore.find(check.front());
LfgRolesMap roles = itQueue->second.roles;
@@ -388,10 +395,27 @@ namespace lfg
proposalDungeons = queue.dungeons;
proposalRoles = queue.roles;
LFGMgr::CheckGroupRoles(proposalRoles); // assing new roles
// A one-player proposal still has to contain one of the three
// canonical combat roles. The stock queue never reaches a
// proposal with PLAYER_ROLE_NONE, while the solo queue can.
// Sending NONE (or a multi-role mask) makes the 3.3.5a client
// return "UNKNOWN" and LFDFrame.lua raises an error.
if (soloQueue)
{
uint8& role = proposalRoles.begin()->second;
role &= PLAYER_ROLE_TANK | PLAYER_ROLE_HEALER | PLAYER_ROLE_DAMAGE;
if (role & PLAYER_ROLE_TANK)
role = PLAYER_ROLE_TANK;
else if (role & PLAYER_ROLE_HEALER)
role = PLAYER_ROLE_HEALER;
else
role = PLAYER_ROLE_DAMAGE;
}
}
// Enough players?
if (!sLFGMgr->IsTesting() && numPlayers != MAXGROUPSIZE)
if (!sLFGMgr->IsTesting() && !soloQueue && numPlayers != MAXGROUPSIZE)
{
strGuids.addRoles(proposalRoles);
for (uint8 i = 0; i < 5 && check.guids[i]; ++i)
+4 -3
View File
@@ -40,9 +40,9 @@ namespace lfg
{
LfgQueueData();
LfgQueueData(time_t _joinTime, LfgDungeonSet _dungeons, LfgRolesMap _roles):
LfgQueueData(time_t _joinTime, LfgDungeonSet _dungeons, LfgRolesMap _roles, bool _solo):
joinTime(_joinTime), lastRefreshTime(_joinTime), tanks(LFG_TANKS_NEEDED), healers(LFG_HEALERS_NEEDED),
dps(LFG_DPS_NEEDED), dungeons(std::move(_dungeons)), roles(std::move(_roles))
dps(LFG_DPS_NEEDED), dungeons(std::move(_dungeons)), roles(std::move(_roles)), solo(_solo)
{ }
time_t joinTime; // Player queue join time (to calculate wait times)
@@ -52,6 +52,7 @@ namespace lfg
uint8 dps{LFG_DPS_NEEDED}; // Dps needed
LfgDungeonSet dungeons; // Selected Player/Group Dungeon/s
LfgRolesMap roles; // Selected Player Role/s
bool solo{false}; // Match as a one-player RDF group
Lfg5Guids bestCompatible; // Best compatible combination of people queued
};
@@ -75,7 +76,7 @@ namespace lfg
// Add/Remove from queue
void AddToQueue(ObjectGuid guid, bool failedProposal = false);
void RemoveFromQueue(ObjectGuid guid, bool partial = false); // xinef: partial remove, dont delete data from list!
void AddQueueData(ObjectGuid guid, time_t joinTime, LfgDungeonSet const& dungeons, LfgRolesMap const& rolesMap);
void AddQueueData(ObjectGuid guid, time_t joinTime, LfgDungeonSet const& dungeons, LfgRolesMap const& rolesMap, bool solo = false);
void RemoveQueueData(ObjectGuid guid);
// Update Timers (when proposal success)
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""Add native solo RDF categories to a WotLK 3.3.5a LFGDungeons.dbc."""
from __future__ import annotations
import argparse
import struct
from pathlib import Path
MAGIC = b"WDBC"
FIELD_COUNT = 49
RECORD_SIZE = FIELD_COUNT * 4
SOLO_ID_OFFSET = 1000
SOLO_NAMES = {
258: "Случайное одиночное подземелье: Классика",
259: "Случайное одиночное подземелье: Burning Crusade",
260: "Случайное одиночное героическое: Burning Crusade",
261: "Случайное одиночное подземелье Lich King",
262: "Случайное одиночное героическое подземелье Lich King",
}
SOLO_DESCRIPTION = (
"Случайное подземелье для одного игрока. "
"Существа и боссы масштабируются системой AutoBalance."
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Duplicate the five stock random dungeon rows as solo RDF categories."
)
parser.add_argument("source", type=Path, help="Source LFGDungeons.dbc")
parser.add_argument("output", type=Path, help="Patched LFGDungeons.dbc")
return parser.parse_args()
def add_string(string_block: bytearray, value: str) -> int:
encoded = value.encode("utf-8") + b"\0"
offset = len(string_block)
string_block.extend(encoded)
return offset
def main() -> None:
args = parse_args()
raw = args.source.read_bytes()
if len(raw) < 20 or raw[:4] != MAGIC:
raise SystemExit(f"{args.source}: not a WDBC file")
record_count, field_count, record_size, string_size = struct.unpack_from("<4I", raw, 4)
if field_count != FIELD_COUNT or record_size != RECORD_SIZE:
raise SystemExit(
f"{args.source}: unexpected layout "
f"(fields={field_count}, record_size={record_size})"
)
records_start = 20
strings_start = records_start + record_count * record_size
strings_end = strings_start + string_size
if strings_end != len(raw):
raise SystemExit(f"{args.source}: inconsistent WDBC size")
records: list[list[int]] = []
source_records: dict[int, list[int]] = {}
solo_ids = {base_id + SOLO_ID_OFFSET for base_id in SOLO_NAMES}
for index in range(record_count):
offset = records_start + index * record_size
record = list(struct.unpack_from(f"<{FIELD_COUNT}I", raw, offset))
if record[0] in SOLO_NAMES:
source_records[record[0]] = record
if record[0] not in solo_ids:
records.append(record)
missing = sorted(set(SOLO_NAMES) - set(source_records))
if missing:
raise SystemExit(f"{args.source}: missing stock random dungeon rows: {missing}")
string_block = bytearray(raw[strings_start:strings_end])
for base_id, name in SOLO_NAMES.items():
record = source_records[base_id].copy()
record[0] = base_id + SOLO_ID_OFFSET
name_offset = add_string(string_block, name)
description_offset = add_string(string_block, SOLO_DESCRIPTION)
# Fill every locale slot. This keeps the custom row readable regardless
# of which locale column the client selects.
record[1:17] = [name_offset] * 16
record[32:48] = [description_offset] * 16
records.append(record)
records.sort(key=lambda record: record[0])
output = bytearray()
output.extend(MAGIC)
output.extend(struct.pack("<4I", len(records), FIELD_COUNT, RECORD_SIZE, len(string_block)))
for record in records:
output.extend(struct.pack(f"<{FIELD_COUNT}I", *record))
output.extend(string_block)
args.output.parent.mkdir(parents=True, exist_ok=True)
temporary = args.output.with_suffix(args.output.suffix + ".tmp")
temporary.write_bytes(output)
temporary.replace(args.output)
print(f"Wrote {args.output} with solo RDF entries 1258-1262")
if __name__ == "__main__":
main()