перевод текстов
This commit is contained in:
@@ -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.
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user