Zone names stay uncoloured on PvE realms outside capitals, as in 1.12.1 (#825)

On a PvE realm, zone names were still coloured friendly, hostile or contested.

In 1.12.1, `GetZonePVPInfo` (`0x48d540`) answers a type only when its display gate is open (`0x48d5c2`/`0x48d5cc`). The gate opens on the realm's PvP flag or in a zone with Flags `0x10`, which is the six capitals. The realm flag is read from `Cfg_Configs.dbc` for the selected realm's type and stored once per world entry (`0x4015de`). A type with no row skips the store. Otherwise the type is nil, so the stock zone splash shows cream and the minimap zone text gold.

benilla now reads `Cfg_Configs.dbc` instead of a transcription that stopped at type 8, so realm type 10 lists as PvP. It latches the realm flag at world entry and applies the gate.

Tests cover the gate, the six capitals and every shipped Cfg_Configs row; each failed before the fix.

Fixes #816

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-10-08 08:53:16 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 63e917965d
commit ea7e0d087a
10 changed files with 418 additions and 58 deletions
+180 -22
View File
@@ -26,13 +26,13 @@
//! Deviation: the indoor dedup keys `(WMOID, MOGP uniqueID)`, not the client's raw per-WMO group
//! index, because the raw index can alias across adjacent buildings.
//!
//! The reference's PvP display gate `[0x88272c]` (a cached boolean, not the realm type) is
//! untraced; ours is always open.
//! `GetZonePVPInfo`'s type and faction show only on a PvP realm or in one of the six capitals
//! (zone Flags `0x10`); elsewhere they are nil and the stock zone texts stay uncoloured.
use bevy::ecs::system::NonSendMut;
use bevy::prelude::*;
use benilla_formats::AreaTableCatalog;
use benilla_formats::{AreaTableCatalog, AreaTableRow};
use benilla_ui::script::UiScript;
use crate::net::{ObjectStore, SelfPlayer};
@@ -107,6 +107,7 @@ fn feed_zone_events(
areas: Option<Res<AreaTableRes>>,
factions: Option<Res<Factions>>,
self_store: Query<&ObjectStore, With<SelfPlayer>>,
realm_pvp: Option<Res<RealmPvp>>,
mut cache: Local<crate::ui_script::VmMemo<ZoneCache>>,
) {
let (Some(mut script), Some(areas)) = (script, areas) else {
@@ -193,26 +194,23 @@ fn feed_zone_events(
return;
}
// GetZonePVPInfo (`0x48d540`): isArena is the leaf's Flags `0x80`; the type is the zone's
// FactionGroupMask against the player template's friend then enemy masks, "contested" when
// neither; never "arena"; nil only on structural failure. factionName is FactionGroup.dbc's
// Name for the zone's mask bit; the realm type never enters.
let is_arena = leaf_row.flags & 0x80 != 0;
let zone_mask = areas.0.get(zone).map_or(0, |r| r.faction_group_mask);
let pvp = factions
let zone_row = areas.0.get(zone);
let template = factions
.as_ref()
.zip(self_store.single().ok())
.and_then(|(f, store)| {
let tpl = f.catalog().template(store.0.unit_faction_template()?)?;
let ty = if zone_mask & tpl.friend_group_mask != 0 {
"friendly"
} else if zone_mask & tpl.enemy_group_mask != 0 {
"hostile"
} else {
"contested"
};
Some((ty, f.catalog().faction_group_name(zone_mask).unwrap_or("")))
});
.and_then(|(f, store)| f.catalog().template(store.0.unit_faction_template()?))
.map(|t| (t.friend_group_mask, t.enemy_group_mask));
let realm_pvp = realm_pvp.is_some_and(|r| r.0);
let (ty, is_arena) = zone_pvp_info(realm_pvp, leaf_row, zone_row, template);
// factionName is FactionGroup.dbc's Name for the zone's mask bit.
let pvp = ty.map(|ty| {
let mask = zone_row.map_or(0, |r| r.faction_group_mask);
let name = factions
.as_ref()
.and_then(|f| f.catalog().faction_group_name(mask))
.unwrap_or("");
(ty, name)
});
let pvp_type = pvp.map_or("", |(ty, _)| ty);
script.set_zone_texts(benilla_ui::script::ZoneTexts {
@@ -247,12 +245,70 @@ fn feed_zone_events(
cache.wmo_group = wmo_group;
}
/// `GetZonePVPInfo` (`0x48d540`): `(pvpType, isArena)`. isArena is the leaf's Flags `0x80`, read
/// before any bail. The type is nil with no zone row, no faction template, or the display gate
/// closed: it opens on a PvP realm ([`RealmPvp`]) or in a zone whose Flags carry `0x10`, the six
/// capitals (`0x48d5c2`/`0x48d5cc`). Open, it is the zone's FactionGroupMask against the template's
/// `(friend, enemy)` masks, "contested" when neither, never "arena".
fn zone_pvp_info(
realm_pvp: bool,
leaf: &AreaTableRow,
zone: Option<&AreaTableRow>,
template: Option<(u32, u32)>,
) -> (Option<&'static str>, bool) {
let is_arena = leaf.flags & 0x80 != 0;
let ty = zone
.filter(|z| realm_pvp || z.flags & 0x10 != 0)
.zip(template)
.map(|(z, (friend, enemy))| {
let mask = z.faction_group_mask;
if mask & friend != 0 {
"friendly"
} else if mask & enemy != 0 {
"hostile"
} else {
"contested"
}
});
(ty, is_arena)
}
/// `[0x88272c]`, the realm's PvP flag the display gate reads: the selected realm's
/// `PlayerKillingAllowed` in `Cfg_Configs.dbc`, stored once per world entry from the character
/// list (`0x4015de` in `0x401570`), so a `/reload` or a far teleport keeps it.
#[derive(Resource, Default)]
pub(crate) struct RealmPvp(bool);
/// Latch [`RealmPvp`] from the realm this session entered the world on.
fn latch_realm_pvp(
mut latch: ResMut<RealmPvp>,
roster: Option<Res<crate::char_select::Roster>>,
realms: Option<Res<crate::realm_select::Realms>>,
) {
let realm_type = roster.and_then(|r| r.realm.as_ref().map(|realm| realm.realm_type));
let row = realm_type
.zip(realms)
.and_then(|(t, realms)| realms.row_pvp(t));
latch.0 = latched_realm_pvp(latch.0, row);
}
/// A realm type with no `Cfg_Configs.dbc` row skips the store (`0x4015d2`) and keeps the last
/// world entry's value, which the process starts at 0 (`.data`).
fn latched_realm_pvp(previous: bool, row_pvp: Option<bool>) -> bool {
row_pvp.unwrap_or(previous)
}
/// The shared area catalog and the zone-event feed.
pub(crate) struct AreaPlugin;
impl Plugin for AreaPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Startup, load_area_table.after(AssetSet::Open))
app.init_resource::<RealmPvp>()
.add_systems(Startup, load_area_table.after(AssetSet::Open))
.add_systems(
OnEnter(crate::char_select::ClientState::InWorld),
latch_realm_pvp,
)
.add_systems(
Update,
// After the leaf authority: leaf, indoor bit and names must come from one frame,
@@ -329,6 +385,108 @@ mod tests {
);
}
fn row(flags: u32, faction_group_mask: u32) -> AreaTableRow {
AreaTableRow {
map_id: 0,
zone_id: 0,
explore_flag: 0,
flags,
faction_group_mask,
exploration_level: 0,
name: String::new(),
}
}
/// An Alliance player's template: friend Alliance (2), enemy Horde (4).
const ALLIANCE: Option<(u32, u32)> = Some((2, 4));
/// The display gate (`0x48d5c2`/`0x48d5cc`): a PvE realm outside a capital gets no type; a
/// capital's Flags `0x10` opens it there, and a PvP realm opens it everywhere.
#[test]
fn the_pvp_type_shows_on_a_pvp_realm_or_in_a_capital() {
let elwynn = row(0x40, 2);
let stormwind = row(0x138, 2);
let orgrimmar = row(0x138, 4);
let stranglethorn = row(0x40, 0);
let pvp =
|realm_pvp, zone: &AreaTableRow| zone_pvp_info(realm_pvp, zone, Some(zone), ALLIANCE).0;
assert_eq!(pvp(false, &elwynn), None, "PvE realm, outside a capital");
assert_eq!(
pvp(false, &stranglethorn),
None,
"PvE realm, ownerless zone"
);
assert_eq!(
pvp(false, &stormwind),
Some("friendly"),
"PvE realm, own capital"
);
assert_eq!(
pvp(false, &orgrimmar),
Some("hostile"),
"PvE realm, enemy capital"
);
assert_eq!(pvp(true, &elwynn), Some("friendly"));
assert_eq!(pvp(true, &stranglethorn), Some("contested"));
assert_eq!(pvp(true, &orgrimmar), Some("hostile"));
}
/// isArena is the leaf's `0x80`, read before any bail, so a closed gate keeps it.
#[test]
fn is_arena_survives_a_closed_gate() {
let battle_ring = row(0xd0, 0);
let stranglethorn = row(0x40, 0);
assert_eq!(
zone_pvp_info(false, &battle_ring, Some(&stranglethorn), ALLIANCE),
(None, true)
);
assert_eq!(
zone_pvp_info(true, &battle_ring, Some(&stranglethorn), ALLIANCE),
(Some("contested"), true)
);
assert_eq!(
zone_pvp_info(true, &battle_ring, Some(&stranglethorn), None),
(None, true),
"no faction template"
);
}
/// A realm type with no row keeps what the last world entry stored.
#[test]
fn a_realm_type_with_no_row_keeps_the_last_latch() {
assert!(!latched_realm_pvp(false, None));
assert!(latched_realm_pvp(true, None));
assert!(!latched_realm_pvp(true, Some(false)));
assert!(latched_realm_pvp(false, Some(true)));
}
/// No zone row is a bail, not an ownerless zone.
#[test]
fn a_missing_zone_row_has_no_pvp_type() {
assert_eq!(
zone_pvp_info(true, &row(0, 0), None, ALLIANCE),
(None, false)
);
}
/// On the install's AreaTable the gate opens in exactly the six capitals for a PvE realm.
#[test]
fn the_six_capitals_are_the_zones_a_pve_realm_colours() {
let data = benilla_formats::wow_data_or_skip!();
let mut chain = benilla_formats::open_chain(&data).expect("chain");
let cat = benilla_formats::load_area_table_catalog(&mut chain).expect("AreaTable");
let open: Vec<u32> = (0..=u32::from(u16::MAX))
.filter(|&id| {
cat.get(id).is_some_and(|r| {
r.zone_id == 0 && zone_pvp_info(false, r, Some(r), ALLIANCE).0.is_some()
})
})
.collect();
assert_eq!(open, [1497, 1519, 1537, 1637, 1638, 1657]);
}
/// The `0x67e670` override skip (abbey) and fire (inn) branches on the install's data.
#[test]
fn indoor_naming_matches_the_byte_law_on_real_data() {
@@ -18,6 +18,7 @@ use super::{class_name, Roster};
pub(super) fn refresh_list(
roster: Res<Roster>,
areas: Option<Res<AreaTableRes>>,
realms: Option<Res<crate::realm_select::Realms>>,
strings: Option<Res<GlueStrings>>,
status: Res<NetStatus>,
mut rows: Query<(&SelectAction, &mut Visibility), (With<Button>, Without<Hilight>)>,
@@ -90,7 +91,10 @@ pub(super) fn refresh_list(
Some(realm) => {
// `GetServerName`'s `isPVP, isRP` pair. A normal realm has no suffix, unlike
// the realm list's `Normal`: the reference leaves `serverType = ""`.
let suffix = match crate::realm_select::pvp_rp(realm.realm_type) {
let pvp_rp = realms
.as_deref()
.map_or((false, false), |r| r.pvp_rp(realm.realm_type));
let suffix = match pvp_rp {
(true, true) => strings.text("RPPVP_PARENTHESES", "(RPPVP)"),
(false, true) => strings.text("RP_PARENTHESES", "(RP)"),
(true, false) => strings.text("PVP_PARENTHESES", "(PVP)"),
+63 -27
View File
@@ -9,6 +9,8 @@
use bevy::prelude::*;
use benilla_formats::RealmConfigs;
// ── The six glue font colours (`GlueFonts.xml:4-9`) ─────────────────────────────────────────────
// `BLUE_FONT_COLOR` exists only in glue; FrameXML's `Fonts.xml` has the other five.
@@ -98,10 +100,9 @@ pub(super) fn load_column(down: bool, load: f32) -> (&'static str, Color) {
}
}
/// The Type column's key and colour; the Lua tests `pvp and rp`, `rp`, `pvp`, then neither, so
/// RP-PvP takes `NORMAL`, not the RP green.
pub(super) fn type_column(realm_type: u32) -> (&'static str, Color) {
let (pvp, rp) = pvp_rp(realm_type);
/// The Type column's key and colour for a realm's `(pvp, rp)`; the Lua tests `pvp and rp`, `rp`,
/// `pvp`, then neither, so RP-PvP takes `NORMAL`, not the RP green.
pub(super) fn type_column((pvp, rp): (bool, bool)) -> (&'static str, Color) {
match (pvp, rp) {
(true, true) => ("RPPVP_PARENTHESES", NORMAL),
(false, true) => ("RP_PARENTHESES", GREEN),
@@ -110,18 +111,38 @@ pub(super) fn type_column(realm_type: u32) -> (&'static str, Color) {
}
}
/// The realm type as `(pvp, rp)`, shared by `GetRealmInfo` and `GetServerName`. The reference
/// (`0x46efda`) scans `Cfg_Configs.dbc` for the row whose `RealmType` matches the realm's first
/// wire dword and reads `PlayerKillingAllowed` and `RoleplayingRealm`; the table is that DBC
/// transcribed (types 3 and 5 are PvP, 7 is RP), and a type with no row reads neither.
pub(crate) fn pvp_rp(realm_type: u32) -> (bool, bool) {
match realm_type {
// RealmType → (PlayerKillingAllowed, RoleplayingRealm)
0 | 2 | 4 => (false, false),
1 | 3 | 5 => (true, false),
6 | 7 => (false, true),
8 => (true, true),
_ => (false, false),
/// The realm type as `(pvp, rp)`, shared by `GetRealmInfo`, `GetServerName` and the world-entry
/// latch: each scans `Cfg_Configs.dbc` for the first row whose `RealmType` is the realm's wire
/// type and reads `PlayerKillingAllowed` and `RoleplayingRealm` (`0x46efda`); a type with no row
/// reads neither.
pub(super) fn pvp_rp(types: &RealmConfigs, realm_type: u32) -> (bool, bool) {
types
.get(realm_type)
.map_or((false, false), |c| (c.pvp, c.rp))
}
/// Read `Cfg_Configs.dbc` once, after the archive chain opens.
pub(super) fn load_realm_types(
mut realms: ResMut<super::Realms>,
assets: Option<Res<benilla_assets::WorldAssets>>,
) {
use benilla_assets::LockRecover;
let Some(assets) = assets else {
return;
};
let mut chain = assets.chain.lock_recover();
match benilla_formats::load_realm_configs(&mut chain) {
Ok(types) => {
info!(
"realm list: {} Cfg_Configs.dbc realm types",
types.rows().len()
);
realms.types = types;
}
Err(e) => warn!(
"realm list: Cfg_Configs.dbc failed to load, so every realm reads Normal and no realm \
is PvP: {e:#}"
),
}
}
@@ -236,22 +257,37 @@ mod tests {
assert_eq!(load_column(true, 2.0), ("REALM_DOWN", GRAY));
}
/// RP-PvP takes `NORMAL`: the Lua's `pvp and rp` arm sets `NORMAL_FONT_COLOR`.
/// The Type column through the install's `Cfg_Configs.dbc`: type 10 is PvP, and RP-PvP takes
/// `NORMAL`, as the Lua's `pvp and rp` arm sets `NORMAL_FONT_COLOR`.
#[test]
fn the_type_column_is_the_shipped_cfg_configs_rows() {
for t in [0, 2, 4] {
assert_eq!(type_column(t), ("GAMETYPE_NORMAL", NORMAL), "type {t}");
fn the_type_column_reads_the_shipped_cfg_configs() {
let data = benilla_formats::wow_data_or_skip!();
let mut chain = benilla_formats::open_chain(&data).expect("chain");
let types = benilla_formats::load_realm_configs(&mut chain).expect("Cfg_Configs.dbc");
let column = |t| type_column(pvp_rp(&types, t));
for t in [0, 2, 4, 9] {
assert_eq!(column(t), ("GAMETYPE_NORMAL", NORMAL), "type {t}");
}
// Cfg_Configs rows 3 and 5 are PvP too.
for t in [1, 3, 5] {
assert_eq!(type_column(t), ("PVP_PARENTHESES", RED), "type {t}");
for t in [1, 3, 5, 10] {
assert_eq!(column(t), ("PVP_PARENTHESES", RED), "type {t}");
}
for t in [6, 7] {
assert_eq!(type_column(t), ("RP_PARENTHESES", GREEN), "type {t}");
assert_eq!(column(t), ("RP_PARENTHESES", GREEN), "type {t}");
}
assert_eq!(type_column(8), ("RPPVP_PARENTHESES", NORMAL));
// No row: the scan reads neither column.
assert_eq!(type_column(77), ("GAMETYPE_NORMAL", NORMAL));
assert_eq!(column(8), ("RPPVP_PARENTHESES", NORMAL));
}
/// No row: the scan reads neither column, so the type is Normal, as it is with no table.
#[test]
fn a_type_with_no_row_is_normal() {
let types = RealmConfigs::from_rows(vec![benilla_formats::RealmConfig {
realm_type: 1,
pvp: true,
rp: false,
}]);
assert_eq!(pvp_rp(&types, 1), (true, false));
assert_eq!(pvp_rp(&types, 77), (false, false));
assert_eq!(pvp_rp(&RealmConfigs::default(), 1), (false, false));
}
/// The default gold is not `NORMAL_FONT_COLOR`.
+17 -5
View File
@@ -25,12 +25,10 @@ mod load;
mod screen;
mod smoke;
pub(crate) use load::pvp_rp;
use bevy::prelude::*;
use crate::net::{RealmChoice, RealmListMessage, RealmRequest};
use benilla_formats::RealmCategory;
use benilla_formats::{RealmCategory, RealmConfigs};
use benilla_protocol::RealmInfo;
/// The persisted 1.12 CVar naming the last realm connected to (`0x83f2d0`); the SavedVariables
@@ -51,6 +49,9 @@ pub(crate) struct Realms {
/// The client Region's categories ([`category`]), read once at startup; `None` until then,
/// and every realm lists untabbed.
pub(super) categories: Option<Vec<RealmCategory>>,
/// `Cfg_Configs.dbc`, the realm types ([`Self::pvp_rp`]), read once at startup; empty until
/// then, and every type reads Normal.
pub(super) types: RealmConfigs,
/// The tab in front, `RealmList.selectedCategory`: a 1-based ordinal over the categories
/// holding a realm.
pub(super) category: Option<usize>,
@@ -143,7 +144,7 @@ impl Realms {
la.total_cmp(&lb)
}
SortKey::Name => ra.name.to_lowercase().cmp(&rb.name.to_lowercase()),
SortKey::Type => load::pvp_rp(ra.realm_type).cmp(&load::pvp_rp(rb.realm_type)),
SortKey::Type => self.pvp_rp(ra.realm_type).cmp(&self.pvp_rp(rb.realm_type)),
};
if ord != std::cmp::Ordering::Equal {
return if descending { ord.reverse() } else { ord };
@@ -156,6 +157,16 @@ impl Realms {
rows
}
/// A realm type's `(pvp, rp)` in `Cfg_Configs.dbc`, `(false, false)` for a type with no row.
pub(crate) fn pvp_rp(&self, realm_type: u32) -> (bool, bool) {
load::pvp_rp(&self.types, realm_type)
}
/// A realm type's `PlayerKillingAllowed`, `None` for a type with no row.
pub(crate) fn row_pvp(&self, realm_type: u32) -> Option<bool> {
self.types.get(realm_type).map(|c| c.pvp)
}
/// The load distribution, over every realm, not the selected category: `0x46e510` walks the
/// flat all-categories array, so switching tabs moves no band.
pub(super) fn stats(&self) -> (f32, f32) {
@@ -240,7 +251,8 @@ impl Plugin for RealmSelectPlugin {
app.init_resource::<Realms>()
.add_systems(
Startup,
category::load_categories.after(benilla_assets::AssetSet::Open),
(category::load_categories, load::load_realm_types)
.after(benilla_assets::AssetSet::Open),
)
.add_systems(
Update,
@@ -728,7 +728,7 @@ pub(super) fn refresh_list(
}
// `RealmListUpdate` turns the selected row's type and load `HIGHLIGHT_FONT_COLOR`.
Column::Type => {
let (key, c) = load::type_column(realm.realm_type);
let (key, c) = load::type_column(realms.pvp_rp(realm.realm_type));
(text(key), if is_selected { load::HIGHLIGHT } else { c })
}
Column::Players => (load::players_text(realm.characters), load::HIGHLIGHT),
@@ -273,6 +273,27 @@ fn room_to_room_hop_splashes_the_room_name_alone() {
assert_eq!(text_of(&s, "SubZoneTextString"), "Library Wing");
}
/// A nil pvpType, a PvE realm outside a capital: no territory line, and the zone and subzone in
/// the stock else-arm's cream (`ZoneText.xml:32-33`), not a faction colour.
#[test]
fn a_nil_pvp_type_splashes_uncoloured_with_no_territory_line() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
set_area(&mut s, "Elwynn Forest", "Goldshire", "", "");
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
assert!(visible(&s, "ZoneTextFrame"));
assert_eq!(text_of(&s, "PVPInfoTextString"), "");
for region in ["ZoneTextString", "SubZoneTextString"] {
let (r, g, b) = s
.eval::<(f32, f32, f32)>(&format!("return {region}:GetTextColor()"))
.unwrap();
assert!(
(r - 1.0).abs() < 1e-3 && (g - 0.9294).abs() < 1e-3 && (b - 0.7607).abs() < 1e-3,
"{region} is ({r}, {g}, {b})"
);
}
}
/// An FFA pit: `GetZonePVPInfo`'s isArena, the leaf area's flag `0x80`.
#[test]
fn arena_pit_shows_the_ffa_line() {
+127
View File
@@ -0,0 +1,127 @@
//! `Cfg_Configs.dbc`, the realm types. A realm's wire type (its realm-list record's first dword)
//! is joined against `RealmType` by a linear scan that takes the first match, in the realm list
//! (`GetRealmInfo`, `0x46efda`), the character screen's `GetServerName` (`0x46d280`) and at world
//! entry (`0x4015b1`, the latch `GetZonePVPInfo` reads).
use anyhow::{Context, Result};
use benilla_dbc::{FieldType, Schema, SchemaField};
use crate::dbc::{parse, u32_at};
use crate::Chain;
const CFG_CONFIGS: &str = "DBFilesClient\\Cfg_Configs.dbc";
/// One realm type's row.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RealmConfig {
/// Column 1, the key a realm's wire type is matched against.
pub realm_type: u32,
/// Column 2, `PlayerKillingAllowed` (`+0x08`): `GetRealmInfo`'s `pvp`.
pub pvp: bool,
/// Column 3, `RoleplayingRealm` (`+0x0c`): `GetRealmInfo`'s `rp`.
pub rp: bool,
}
/// The realm types, in file order.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RealmConfigs(Vec<RealmConfig>);
impl RealmConfigs {
/// A table over rows given directly.
pub fn from_rows(rows: Vec<RealmConfig>) -> Self {
Self(rows)
}
/// The first row whose `RealmType` is `realm_type`, as every reader's scan stops there.
pub fn get(&self, realm_type: u32) -> Option<&RealmConfig> {
self.0.iter().find(|c| c.realm_type == realm_type)
}
/// The rows, in file order.
pub fn rows(&self) -> &[RealmConfig] {
&self.0
}
}
/// `ID`, `RealmType`, `PlayerKillingAllowed`, `RoleplayingRealm`: the 4 columns × 0x10 bytes the
/// loader (`0x5413f0`) gates on.
fn cfg_configs_schema() -> Schema {
let mut s = Schema::new("Cfg_Configs");
for name in [
"ID",
"RealmType",
"PlayerKillingAllowed",
"RoleplayingRealm",
] {
s.add_field(SchemaField::new(name, FieldType::UInt32));
}
s
}
/// Every row of `Cfg_Configs.dbc`.
pub fn load_realm_configs(chain: &mut Chain) -> Result<RealmConfigs> {
let bytes = chain
.read_file(CFG_CONFIGS)
.with_context(|| format!("reading {CFG_CONFIGS}"))?;
let rs = parse(&bytes, cfg_configs_schema(), "Cfg_Configs")?;
Ok(RealmConfigs(
rs.records()
.iter()
.filter_map(|r| {
Some(RealmConfig {
realm_type: u32_at(r, 1)?,
pvp: u32_at(r, 2)? != 0,
rp: u32_at(r, 3)? != 0,
})
})
.collect(),
))
}
#[cfg(test)]
mod tests {
use super::*;
/// The shipped table: 11 rows, types 0 to 10 in order; PvP on 1, 3, 5, 8 and 10, RP on 6, 7
/// and 8.
#[test]
fn the_shipped_table_is_eleven_types_with_pvp_on_1_3_5_8_10() {
let data = crate::wow_data_or_skip!();
let mut chain = crate::open_chain(&data).expect("open chain");
let configs = load_realm_configs(&mut chain).expect("Cfg_Configs.dbc");
let rows: Vec<(u32, bool, bool)> = configs
.rows()
.iter()
.map(|c| (c.realm_type, c.pvp, c.rp))
.collect();
assert_eq!(
rows,
[
(0, false, false),
(1, true, false),
(2, false, false),
(3, true, false),
(4, false, false),
(5, true, false),
(6, false, true),
(7, false, true),
(8, true, true),
(9, false, false),
(10, true, false),
]
);
assert!(configs.get(11).is_none(), "no row past type 10");
}
/// The scan stops at the first match.
#[test]
fn a_duplicated_type_reads_its_first_row() {
let row = |pvp| RealmConfig {
realm_type: 4,
pvp,
rp: false,
};
let configs = RealmConfigs::from_rows(vec![row(true), row(false)]);
assert!(configs.get(4).expect("row").pvp);
}
}
+2
View File
@@ -52,6 +52,8 @@ pub use gm_ticket_category::{
};
mod cfg_categories;
pub use cfg_categories::{load_realm_categories, RealmCategory};
mod cfg_configs;
pub use cfg_configs::{load_realm_configs, RealmConfig, RealmConfigs};
mod wow_ini;
pub use wow_ini::client_region;
mod itembagfamily;
+1 -1
View File
@@ -19,7 +19,7 @@ pub struct ZoneTexts {
/// `GetMinimapZoneText`: the subzone, else the zone.
pub minimap: String,
/// `GetZonePVPInfo`'s type, `"friendly"`, `"hostile"` or `"contested"`; `None` when the
/// player, the zone or its faction template is missing.
/// player, the zone or its faction template is missing, or on a PvE realm outside a capital.
pub pvp_type: Option<String>,
/// `GetZonePVPInfo`'s faction, `FactionGroup.dbc`'s name for the zone's mask bit; `None` for
/// none.
+1 -1
View File
@@ -104,7 +104,7 @@
- **benilla-adt** (lib.rs): combined_alpha
- **benilla-app** (lib.rs): addon_harness area area_poi area_trigger asset_churn aura_visual bindings blob_shadow bowstring camera_shake capture char_create char_select chat_bubble chr_classes cinematic combat_log combat_text console crash creature_anim creature_type cursor cvars death debug_panel dev doodad_events entities fishing_line footprints game_plugins game_tip glue glue_strings go_anim go_templates hover_log items loading_screen local_state login minimap name_persist nameplates names net npc_text opaque2d pending_item_ops perf pipe_warm player poi_marker portrait preflight probe_shield query_cache quest_markers raid_marks ranged_flex realm_select realmlist run_mode screen_fade screenshot script_calls shaders shutdown smart_rect sound spell swing_refusal target test_support text_filter text_reshape textinput transport tutorial ui_action ui_auction ui_aura ui_bank ui_battlefield ui_battlefield_positions ui_battlefield_score ui_bind_confirm ui_binder ui_cast ui_char ui_chat ui_craft ui_dialog_verbs ui_dressup ui_duel ui_follow ui_gamma ui_gm_ticket ui_gossip ui_guild ui_honor ui_inspect ui_instance ui_item_text ui_items ui_layout ui_logout ui_loot ui_loot_roll ui_macro ui_mail ui_merchant ui_mirror ui_models ui_net ui_party ui_pass ui_pet ui_pet_book ui_pet_doll ui_pet_stats ui_petition ui_quest ui_quest_log ui_quest_share ui_reputation ui_saved ui_script ui_session ui_shapeshift ui_social ui_spellbook ui_stable ui_summon ui_tabard ui_talent ui_talent_wipe ui_taxi ui_text ui_tooltip ui_trade ui_tradeskill ui_trainer ui_unit ui_world_map video vplates weapon_trail world_backdrop world_state world_state_ui
- **benilla-assets** (lib.rs): adt anim_rng blp column_grid coords gpu_blp m2 materials minimap_grid model spatial_cache terrain tex_filter trace wdt wmo world_assets
- **benilla-formats** (lib.rs): anim_data area_poi area_sound area_table area_trigger auction_house bank_bag_slot_prices camera_shakes cfg_categories chain characters chat_channels chr_classes cinematics creature_families creature_sound creature_types creatures dbc death_thud durability elevators emit_timing emote_text emotes environmental_damage exhaustion factions footsteps game_tips gameobjects garble gm_ticket_category ground_effects install item_random_properties item_sounds item_visuals itembagfamily itemclass items itemsets itemsubclass languages light liquid loading_screen lock lock_type macro_icons maps material minimap_translate models npc_greeting packages page_text_material particles pet_stats quest_headers quest_info race_creature_type race_pvp_team race_sound ribbons server_messages sheathe skill_lines sound_entries sound_provider sound_water spell_focus spell_mechanic spell_visual spells stable_slot_prices stationery talents taxi taxi_nodes taxi_path terrain text_filter_lists tga transport_period transports unit_blood value_track vocal_ui_sounds wdl weapon_impact weapon_swing wmo_area world_map_area world_map_continent world_map_overlay world_state_ui wow_ini zone_map
- **benilla-formats** (lib.rs): anim_data area_poi area_sound area_table area_trigger auction_house bank_bag_slot_prices camera_shakes cfg_categories cfg_configs chain characters chat_channels chr_classes cinematics creature_families creature_sound creature_types creatures dbc death_thud durability elevators emit_timing emote_text emotes environmental_damage exhaustion factions footsteps game_tips gameobjects garble gm_ticket_category ground_effects install item_random_properties item_sounds item_visuals itembagfamily itemclass items itemsets itemsubclass languages light liquid loading_screen lock lock_type macro_icons maps material minimap_translate models npc_greeting packages page_text_material particles pet_stats quest_headers quest_info race_creature_type race_pvp_team race_sound ribbons server_messages sheathe skill_lines sound_entries sound_provider sound_water spell_focus spell_mechanic spell_visual spells stable_slot_prices stationery talents taxi taxi_nodes taxi_path terrain text_filter_lists tga transport_period transports unit_blood value_track vocal_ui_sounds wdl weapon_impact weapon_swing wmo_area world_map_area world_map_continent world_map_overlay world_state_ui wow_ini zone_map
- **benilla-m2** (lib.rs): error model skin tests track
- **benilla-mpq** (lib.rs): crypto
- **benilla-protocol** (lib.rs): auth events guid messages wire world