- the player picks the realm: benilla connected to whichever realm the auth server listed first — not a policy but the absence of one — with the reference's Change Realm button drawn permanently disabled and the character screen's banner reading "Connecting…", a string 1.12 does not have, invented because the realm only ever arrived as a passenger on the character list (`CharacterSelect.lua` l.66 ends that ladder with `CharSelectRealmName:Hide()` — no name, no banner); `read_realm_list` had read the flags byte, the category byte and the realm id into `_` locals and stringified the population on arrival, and all four are load-bearing; the IO thread gains a third park — logon → which realm? → world handshake → which character — publishing `SessionEvent::RealmList` and blocking on `RealmRequest::{Enter, Refresh, Abandon}` exactly as at the other two, `Enter` carrying the realm's name and not its index since the list is re-requested every 5 s (`RealmList.lua`'s `REALM_LIST_REFRESH_TIME`) off a realmd socket `Logon` now keeps; Change Realm hands the logon forward (`HeldLogon`) so a different realm costs a world dial, not a re-authentication, and a deliberate ask arms `Realms::forced` for exactly one list, because the realm remembered from last time is precisely the realm you are standing on and asking to leave — without it the button re-entered the same realm and appeared to do nothing; the auto-answer ladder is a pure `auto_answer` — `WOW_REALM`, then the remembered `realmName` CVar (which `set_realm_name` had poked into its slot in place, never reaching `cvar_changes`, so the persisted CVar whose whole job is "the last realm connected to" was absent from disk forever; it writes through `set_from_engine` now), then an unattended run takes the first realm that is up, every arm requiring the realm to be up — and the screen is `RealmList.xml` rebuilt on the same 640×512 HelpFrame plate the AddOns list uses, eighteen rows at the authored 20 px pitch, four sortable columns; the Population word is a BAND, not the float: `realm_load_stats` (0x46e510) takes the mean and `sqrt(Σ(pop − mean)² / (n − 1)) · 0.6`, `realm_load_classify` (0x46ec60) reads the sentinels first (0x20 → Recommended, 0x40 → New, 0x80 → Full) then strict `pop < mean − stddev` → Low, `pop > mean + stddev` → High, else Medium — so the same realm reads High on a quiet list and Medium on a busy one, and a lone realm is banded against the `n ≤ 1` short circuit's literal 1.0, so a local vmangos realm at 0.06 reads Low, not Medium; then wow-re's cross-check of `RealmList.cpp [0x46e3bf, 0x46fa05)` confirmed 0x01 = invalid / 0x02 = offline and that the real client does auto-connect by `realmName` without raising the list (`0x5ab800` resolves the stored name against the fresh list, its success leg calling no `SetStatus`), and corrected four things the first cut shipped: the statistics are GLOBAL — `0x46e510` walks the flat all-categories array unconditionally (`0x46e5ee fadd [esi+0x12c]`), the per-category bucketing in the same loop a side effect and never a bound, so scoping them to the tab made the same realm read a different word per tab; Recommended/New/Full never travel as flag bits at all — the parser recognises magic populations by bit pattern and rewrites them, 600.0 → 0.0 + 0x20, 200.0 → 0.001 + 0x40, 400.0 → 8.0 + 0x80, and it has to happen in `read_realm_list` before anything averages, since one realm advertised Recommended arriving as 600.0 drags the global mean past every other row; pvp/rp is a `Cfg_Configs.dbc` join on the type dword at `[realm+0x04]`, and the shipped rows make types 3 and 5 PvP and 7 RP where the 1/6/8 match had all three Normal; and the default sort is `[characters, load, name, mode]`, all ascending, written once per process by `0x46e300` — a four-key fallthrough where "ascending" characters means higher count first, a column click being move-to-front (`0x46e9b0`) that keeps the column's direction unless it was already in front — with the reference's non-antisymmetric characters comparator (its tie-breaking `strcmp` result clobbered one instruction later at `0x46e846`) deliberately not reproduced; `RealmWizard`, the `REALM_IS_FULL` confirm and the category tab strip are deliberately not built, each for a stated reason (2056, 2062)
- a re-played effect REPLACES the same record at the same attach point: the director's report that the reference draws far fewer impact effects when many mobs beat on one warrior — and named the star burst on both hands on every crit — is not a limiter but a replace, recorded in wow-re for weeks: `CEffect::AddEffect` (0x61fdd0), the path every kit slot and the melee blood spurt take, opens with `0x6208e0(owner, rec, tag)`, which walks the owner's `+0xb4` list and destroys every node carrying the same `SpellVisualEffectName` record at the same attach tag (bounded `[0x6208e0, 0x62092c]`), skipping `tag == -1`; benilla replaced only persistent instances, on the narrower `(spell_id, class)` key of the spell-id reap `0x614150`, and pushed every one-shot unconditionally — a gap seen and scoped to one caller four times (0850, 0927, 1149, 1518) without being generalised — so five attackers landing swings on one body held five blood spurts at `(UnitBlood, 0xf)` where the reference holds one, linear in the attacker count on additively-blended emitters where the reference's is flat, and near-identical in a 1v1, which is why it survived; the sharpest case is Flurry (12966–12970), applied on every critical strike, whose visual 2759 hangs record 416 — `Spells\Strike_Impact_Chest.mdx`, the gold chest-impact flare reused at the hand tags 0x15/0x16 — on both hands via cast kit 3010 over a persistent `BloodLust_State_Hand` glow from state kit 3009, and every crit stacked another pair; `spell_fx::replace_same_slot` now runs per slot at every Begin for persistent and self-terminating instances alike, keyed on the RECORD id threaded through the fx lane (`FxSlot { tag, effect, path }`, the hardcoded map becoming name → (id, path)) and not the path, because two rows may name one .mdx and the reference lets those coexist, with both exclusions kept — the world plant and an already-decaying node — and four tests pinning the key's two halves and both; the other half of the report, that the burst is too large and too long, is a separate mechanism and is not touched — and the instrument that would have measured it was lying: `m2part` read a burst emitter's `peak_rate()` as its particle count, while the reference fires on the rising edge of `enabled != 0 && rate > 0` sampled on one clock, and `Strike_Impact_Chest.m2`'s gold flare steps its gate 1 → 0 on the very keyframe its rate steps 0 → 50, so the two are never true together and that emitter emits NOTHING; the tool printed "burst of ~50 particles, life 0.50s" and a diagnosis started 2.7× over on count and 1.7× over on duration off that one line; `EmitTiming::first_burst` walks the slot's keyed span at 60 Hz and returns the first instant the predicate holds or `None`, the dump prints the real count and time or `NEVER FIRES`, and a real-asset test pins that model's emitter #0 as never and #1 as exactly 30 (2057)
- a harmful spell's impact flinches its victim: benilla had one spell-side wound path, `play_kit`'s `[8,10]` branch — the kit player's own `0x60f3ad` test — carrying the KIT's anim id, so a spell whose impact kit names no wound anim, which is nearly every one (Fireball, Frostbolt, Shadow Bolt, Sunder Armor, a mob's Strike), twitched nothing, and a kit naming CombatCritical played 10 where the client plays 9 or 8; `0x60ea70` has six callers, not the three an earlier sweep found (`grep "call 0x60ea70"` over the full objdump was the one command it never ran): the missile hand-off `0x61dc50` lays the flinch before the impact kit for every living target (`0x61dc74`, HEALTH > 0; the already-dead arm `0x60ea30` plays Death/SwimDeath instead), the instant-hit ImpactKit loop `0x6e8bf0` lays it after the kit when `0x6ea280(SpellRec) == 2` (`0x6e8c89`), the reflect kit `0x6e8cb0` does the same on the caster, and every spell-side call pushes severity 0, so the id is always `0x60ea70`'s own `(severity, engaged)` pick — CombatWound(9) engaged, StandWound(8) otherwise — never the kit column; `0x6ea280` is the spell's hostility class and `== 2` ⇔ the spell targets enemies: Targets bit `0x100` → never, `0x80` → yes, else `EffectImplicitTargetA[i]` then `B[i]` for effects 0..3 indexed into the byte tables `0x6ea338`/`0x6ea378`, whose case-0 ids `{2, 6, 15, 16, 24, 28, 53, 54}` are exactly the modern enum's `*_ENEMY` set; so `WoundAnim { entity }` loses its id and the driver resolves every spell edge as `wound_anim(0, engaged)`, `play_impact` takes an `ImpactPlay` with `flinch` — the instant hits pass `SpellDisplay::is_harmful()`, a missile arrival passes true — `SpellDisplay` gains `effect_implicit_target_a/b` (columns 82–84 / 85–87) and `is_harmful()`, and the flinch is laid as the same secondary-blend overlay as a melee wound, over a cast, a swing or a run, never replacing them; pinned against the real 5875 `Spell.dbc` — Fireball, Charge and Sunder Armor harmful through A, Frost Nova through B alone, Renew, Healing Touch, Arcane Intellect and Battle Shout not — and the driver never picks CombatCritical for a spell edge; `WOW_MOVE_TRACE` gains `fct wound trigger/evict/expire` lines, since the whole flinch lifecycle was invisible in a trace; and the report this was chased under — the warrior's swings reading as cut short by incoming impacts in a many-mob brawl — stays open, with one reference gate found on the way that benilla does not model: `0x60ea70` aborts while any attached CEffect on the victim carries `+0x2c & 0x4` (`0x60eab6`), and which kit-spawned nodes carry that bit is the question dispatched to wow-re (2058)
- other players' roots, levitation and blinks now relay to onlookers: a report that a nearby player getting rooted, water-walking or Blinking slid or stood in the wrong place until their next movement packet was exactly right — a movement mode has three legs and benilla carried two, the mover's own acked handshake (`SMSG_FORCE_MOVE_ROOT → CMSG_FORCE_MOVE_ROOT_ACK`, 0866) and the server-driven `SMSG_SPLINE_MOVE_*` twelve (1780), while the third, what observers are told once the mover acks (vmangos `MovementPacketSender.h:30-60`, broadcast by `SendMovementFlagChangeToObservers` / `SendTeleportToObservers`), fell through `parse_server` to `ServerPacket::Other` and was dropped — so a rooted player kept sliding for the ~500 ms to their next heartbeat, a Levitate on someone else was never rendered, and a Blink left them standing at the old spot; six opcodes — `MSG_MOVE_ROOT`/`UNROOT` (236/237), `MSG_MOVE_HOVER` (247), `MSG_MOVE_FEATHER_FALL` (688), `MSG_MOVE_WATER_WALK` (689), `MSG_MOVE_TELEPORT` (197) — all the ordinary `[packed guid][MovementInfo]` shape and all sharing the client's one relay handler `0x603bb0` with the 22 already carried (wow-re's opcode-handler table has thirty rows on it; the remaining two are cheat opcodes vmangos never constructs), so admitting them to `is_movement_relay` is the whole fix for five: apply/unapply rides the flags word rather than the opcode, since vmangos runs `SetHoverReal` &co. before the broadcast and the word on the wire already carries the bit's new state, and `apply_move` already commits it wholesale into the union the ground resolve reads for hover's offset and the water-walk plane — the pose the packet carries beside the mode is half the value, a rooted player's stopping place being news too, and the root arrives already post-wipe with `MOVEFLAG_ROOT` forced back on, so no wipe of ours is invented; the teleport is a relay in shape and a discontinuity in meaning, so `RelayMove::reconciles()` excludes it from the pre-fire reconcile beside the heartbeat, for the opposite reason — the heartbeat because the dead-reckon has already converged, the teleport because there is nothing to converge to, and lerping toward a Blink's destination would drag the mover across the twenty yards the Blink exists to skip, sweeping the shared capsule through every wall between — while it still rides the unit's relay chain rather than snapping at arrival, because replay is paced against the sender's own stamps and applying it early would put the player at the destination while their walk to the launch point was still replaying; a `tp` tag joins `hb`/`tr` on the `rly` trace line; and parse and decode of all six are pinned against golden bodies asserted four bytes shorter than the counter-carrying `MSG_MOVE_TELEPORT_ACK` form, so a counter is provably not being read (2061)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
259 lines
11 KiB
Rust
259 lines
11 KiB
Rust
//! The select screen's live refresh — everything that follows the roster or the pointer after
|
||
//! [`super::screen`] spawns the tree: the row texts + visibility, the row highlight (hover, and
|
||
//! LOCKED on the selected row — the ref's `LockHighlight`), the realm banner, the selected name
|
||
//! over the model, the buttons' enabled states, and the glue-booth feed (scene by the selected
|
||
//! character's race, the geared look).
|
||
|
||
use bevy::prelude::*;
|
||
|
||
use crate::area::AreaTableRes;
|
||
use crate::glue::widgets::{GlueDisabled, Hilight};
|
||
use crate::glue_strings::GlueStrings;
|
||
use crate::net::NetStatus;
|
||
use crate::portrait::{GlueLook, GluePreview, SelectLook};
|
||
|
||
use super::screen::{RealmBanner, RowText, SelectAction, SelectedName, MAX_ROWS};
|
||
use super::{class_name, Roster};
|
||
|
||
/// Refill the row texts + visibility, the selected name, and the realm banner whenever the roster
|
||
/// changes (a fresh enum, a selection move) — or when the screen was just (re)spawned (returning
|
||
/// from the create screen finds an unchanged roster; the fresh, empty tree must still fill).
|
||
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
|
||
pub(super) fn refresh_list(
|
||
roster: Res<Roster>,
|
||
areas: Option<Res<AreaTableRes>>,
|
||
strings: Option<Res<GlueStrings>>,
|
||
status: Res<NetStatus>,
|
||
mut rows: Query<(&SelectAction, &mut Visibility), (With<Button>, Without<Hilight>)>,
|
||
mut texts: Query<(&RowText, &mut Text), Without<SelectedName>>,
|
||
mut name: Query<&mut Text, (With<SelectedName>, Without<RealmBanner>, Without<RowText>)>,
|
||
mut banner: Query<&mut Text, (With<RealmBanner>, Without<SelectedName>, Without<RowText>)>,
|
||
spawned: Query<Ref<SelectAction>>,
|
||
) {
|
||
let fresh = spawned.iter().any(|r| r.is_added());
|
||
if !roster.is_changed() && !fresh && !status.is_changed() {
|
||
return;
|
||
}
|
||
let empty = GlueStrings::default();
|
||
let strings = strings.as_deref().unwrap_or(&empty);
|
||
|
||
// Row visibility: rows with a character show; the rest hide (the ref's enumerate-then-hide).
|
||
for (action, mut vis) in &mut rows {
|
||
if let SelectAction::Row(i) = action {
|
||
*vis = if *i < roster.chars.len() {
|
||
Visibility::Inherited
|
||
} else {
|
||
Visibility::Hidden
|
||
};
|
||
}
|
||
}
|
||
// Row texts: Name; Info = `CHARACTER_SELECT_INFO` "Level %d %s" (class only — the ghost
|
||
// variant appends "(Ghost)"); Location = the zone name off AreaTable.
|
||
for (text, mut t) in &mut texts {
|
||
let (i, kind) = match text {
|
||
RowText::Name(i) => (*i, 0),
|
||
RowText::Info(i) => (*i, 1),
|
||
RowText::Location(i) => (*i, 2),
|
||
};
|
||
let new = match roster.chars.get(i) {
|
||
None => String::new(),
|
||
Some(c) => match kind {
|
||
0 => c.name.clone(),
|
||
1 => {
|
||
let key = if c.flags & benilla_protocol::CHARACTER_FLAG_GHOST != 0 {
|
||
("CHARACTER_SELECT_INFO_GHOST", "Level %d %s (Ghost)")
|
||
} else {
|
||
("CHARACTER_SELECT_INFO", "Level %d %s")
|
||
};
|
||
strings
|
||
.text(key.0, key.1)
|
||
.replacen("%d", &c.level.to_string(), 1)
|
||
.replacen("%s", class_name(c.class), 1)
|
||
}
|
||
_ => areas
|
||
.as_deref()
|
||
.and_then(|a| a.0.name(c.zone))
|
||
.unwrap_or_default()
|
||
.to_string(),
|
||
},
|
||
};
|
||
if t.0 != new {
|
||
t.0 = new;
|
||
}
|
||
}
|
||
// The selected character's name over the model (empty list → empty, the ref's
|
||
// UPDATE_SELECTED_CHARACTER arg 0).
|
||
if let Ok(mut t) = name.single_mut() {
|
||
let new = roster
|
||
.selected_char()
|
||
.map(|c| c.name.clone())
|
||
.unwrap_or_default();
|
||
if t.0 != new {
|
||
t.0 = new;
|
||
}
|
||
}
|
||
if let Ok(mut t) = banner.single_mut() {
|
||
let new = match &roster.realm {
|
||
Some(realm) => {
|
||
// `GetServerName`'s own `isPVP, isRP` pair, from the one table that owns it.
|
||
// The banner's normal-realm arm is EMPTY, unlike the realm list's `Normal` — the
|
||
// reference leaves `serverType = ""` when neither flag is set.
|
||
let suffix = match crate::realm_select::pvp_rp(realm.realm_type) {
|
||
(true, true) => strings.text("RPPVP_PARENTHESES", "(RPPVP)"),
|
||
(false, true) => strings.text("RP_PARENTHESES", "(RP)"),
|
||
(true, false) => strings.text("PVP_PARENTHESES", "(PVP)"),
|
||
(false, false) => "",
|
||
};
|
||
let down = (status.last_reason.is_some() && roster.pending_pick.is_none())
|
||
.then(|| strings.text("SERVER_DOWN", "Server down"));
|
||
realm_banner(&realm.name, suffix, down)
|
||
}
|
||
// **`CharSelectRealmName:Hide()`** (`CharacterSelect.lua` l.66): with no server name
|
||
// the reference hides the FontString outright — it has no string for this case, and
|
||
// ours used to invent one (`"Connecting…"`). Empty text is our Hide.
|
||
//
|
||
// It is also all but unreachable now. The realm is chosen *before* the world dial, so
|
||
// by the time this screen exists the session has one; what is left here is the
|
||
// no-network harness, which is exactly the case the reference draws nothing for.
|
||
None => String::new(),
|
||
};
|
||
if t.0 != new {
|
||
t.0 = new;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The realm banner, composed the way `CharacterSelect_OnShow` composes it (`CharacterSelect.lua`
|
||
/// l.48-62): the down note is appended to the **name**, and the realm-type suffix goes after that
|
||
/// whole thing.
|
||
///
|
||
/// ```text
|
||
/// serverName = serverName.."\n("..TEXT(SERVER_DOWN)..")"; -- only while disconnected
|
||
/// CharSelectRealmName:SetText(serverName.." "..serverType);
|
||
/// ```
|
||
///
|
||
/// A separate function because the order is exactly what drifted: ours put the suffix first and the
|
||
/// down note last, under a comment claiming the reference's shape. Nothing showed while the suffix
|
||
/// was `"(PVP)"` — parentheses read plausibly at either end. Decision 2052 made the suffix the enGB
|
||
/// patch's `"PVP"`, a bare word, and the order became legible. A comment could not fail; this can.
|
||
fn realm_banner(name: &str, suffix: &str, down: Option<&str>) -> String {
|
||
let name = match down {
|
||
Some(reason) => format!("{name}\n({reason})"),
|
||
None => name.to_string(),
|
||
};
|
||
// The reference appends `" "..serverType` unconditionally, leaving a trailing space on a realm
|
||
// with no type; we trim it, since ours is a laid-out node rather than a Lua string.
|
||
format!("{name} {suffix}").trim_end().to_string()
|
||
}
|
||
|
||
/// Per-frame interaction visuals + button states: the row highlight (hover ∪ selected — the ref's
|
||
/// `LockHighlight` on the selected row), and the enabled states — Enter World / Delete disable on
|
||
/// an empty list (the ref's `UpdateCharacterList`), Create hides at the 10-cap or disconnected,
|
||
/// Change Realm stays disabled (decision 0465 §6).
|
||
#[allow(clippy::type_complexity)]
|
||
pub(super) fn refresh_banner_and_buttons(
|
||
roster: Res<Roster>,
|
||
mut rows: Query<(&SelectAction, &Interaction, &Children), With<Button>>,
|
||
mut hilights: Query<&mut Visibility, With<Hilight>>,
|
||
mut disables: Query<(&SelectAction, &mut GlueDisabled)>,
|
||
mut create_vis: Query<
|
||
(&SelectAction, &mut Visibility),
|
||
(With<crate::glue::widgets::GlueBtn>, Without<Hilight>),
|
||
>,
|
||
) {
|
||
for (action, interaction, children) in &mut rows {
|
||
let SelectAction::Row(i) = action else {
|
||
continue;
|
||
};
|
||
let lit = roster.selected() == Some(*i) || *interaction != Interaction::None;
|
||
for child in children {
|
||
if let Ok(mut vis) = hilights.get_mut(*child) {
|
||
*vis = if lit {
|
||
Visibility::Inherited
|
||
} else {
|
||
Visibility::Hidden
|
||
};
|
||
}
|
||
}
|
||
}
|
||
let have_chars = !roster.chars.is_empty();
|
||
for (action, mut disabled) in &mut disables {
|
||
let want = match action {
|
||
SelectAction::EnterWorld | SelectAction::Delete => !have_chars,
|
||
_ => false,
|
||
};
|
||
if disabled.0 != want {
|
||
disabled.0 = want;
|
||
}
|
||
}
|
||
// Create New Character shows while the realm answered and a slot is free (the ref hides it
|
||
// disconnected or at MAX_CHARACTERS_PER_REALM = 10).
|
||
let show_create = roster.realm.is_some() && roster.chars.len() < MAX_ROWS;
|
||
for (action, mut vis) in &mut create_vis {
|
||
if matches!(action, SelectAction::CreateChar) {
|
||
*vis = if show_create {
|
||
Visibility::Inherited
|
||
} else {
|
||
Visibility::Hidden
|
||
};
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Feed the glue booth from the roster (decision 0465): the scene is the SELECTED character's
|
||
/// race's (the ref's `SetBackgroundModel` on the enum fileString — Orc before a list arrives or
|
||
/// with an empty account, the ref's OnLoad default), the look its geared enum record. Runs every
|
||
/// frame (cheap compares inside `GluePreview` writers — the builder keys on value change).
|
||
pub(super) fn feed_glue_preview(
|
||
roster: Res<Roster>,
|
||
mut preview: ResMut<GluePreview>,
|
||
mut showing: Local<Option<u64>>,
|
||
) {
|
||
// **The facing belongs to the displayed character, not to the screen** (1533): turning one
|
||
// character must not hand its angle to the next one you click, and a fresh character faces the
|
||
// stage's own forward until you turn it. `GluePreview::yaw` is one resource shared by all three
|
||
// glue screens, so the reset lives at the selection edge rather than in the resource.
|
||
//
|
||
// Keyed on the selection **counter**, not on who is shown: the ref's `SelectCharacter` zeroes
|
||
// the facing unconditionally (`0x472950`, above the already-built discriminator), so clicking
|
||
// the row you are already on re-squares the character too. See `Roster::select_seq`.
|
||
if *showing != Some(roster.select_seq) {
|
||
*showing = Some(roster.select_seq);
|
||
preview.yaw = 0.0;
|
||
}
|
||
let (race, look) = match roster.selected_char() {
|
||
Some(c) => (c.race, Some(GlueLook::Select(SelectLook::from(c)))),
|
||
None => (2, None), // UI_Orc — the ref's initial/empty-account scene
|
||
};
|
||
let scene = Some(crate::portrait::GlueScene::Race(race));
|
||
if preview.scene != scene {
|
||
preview.scene = scene;
|
||
}
|
||
if preview.look != look {
|
||
preview.look = look;
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
/// The composition order, pinned against `CharacterSelect.lua` l.48-62.
|
||
#[test]
|
||
fn the_down_note_hangs_off_the_name_and_the_type_suffix_follows_it() {
|
||
assert_eq!(realm_banner("Kalimdor", "PVP", None), "Kalimdor PVP");
|
||
assert_eq!(
|
||
realm_banner("Kalimdor", "PVP", Some("Server down")),
|
||
"Kalimdor\n(Server down) PVP",
|
||
"the suffix follows the whole name+note, not the name alone"
|
||
);
|
||
// No realm type: the reference's unconditional " " is all that would be left, and a banner
|
||
// does not end in whitespace.
|
||
assert_eq!(realm_banner("Kalimdor", "", None), "Kalimdor");
|
||
assert_eq!(
|
||
realm_banner("Kalimdor", "", Some("Server down")),
|
||
"Kalimdor\n(Server down)"
|
||
);
|
||
}
|
||
}
|