a failed dial says which failure, and the sword reads the at-war bit
- "Unable to connect" now says which: an address that does not resolve reads the reference's "Invalid Login Server", a resolved address nothing answers on reads "Login Server Down", and each names the `host:port` actually dialed — resolution and connection are two operations now (a DNS failure surfaces as `Uncategorized` on macOS, so no error kind could tell them apart), and every address a name carries is tried, `localhost` → `::1` then `127.0.0.1` included - a typed wrong password shows the login dialog instead of exiting the client, and the two auth-result enums — realmd's and the world server's — are split so a code is read against the family that sent it, with the world server's own refusal text shown for its codes - the world cursor's fork is `CanInteract` and `CanAttack`, not a reaction-rank threshold: a Cenarion Circle NPC with no gossip showed the sword at neutral (and TAB acquired it, and right-click attacked it) because one predicate implemented twice read the standing where the binary reads the at-war bit in the player→unit direction; one byte-verified `CanAttack` is shared by the cursor, TAB, pet attack orders, `UnitCanAttack` and the action-button usable gate, the service ladder falls out to a plain pointer instead of the attack leg, and `/reaction` prints the at-war bit and the branch verdict Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -109,6 +109,33 @@ pub enum SessionEnd {
|
||||
Lost,
|
||||
}
|
||||
|
||||
/// **Who refused the login, and with which byte.**
|
||||
///
|
||||
/// A login crosses two servers and each has its own result enum. They overlap numerically and mean
|
||||
/// unrelated things — 0x0C is `AUTH_LOGON_FAILED_SUSPENDED` to realmd and `AUTH_OK` to the world
|
||||
/// server — so a bare `Option<u8>` could not say what it held, and the screen could not look up the
|
||||
/// right authored string from it. It used to be exactly that bare byte: the realmd code rode it and
|
||||
/// the world code was formatted into the reason string and lost, which is why every world-side
|
||||
/// refusal reached the player as a generic "Unable to connect".
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum LoginRefusal {
|
||||
/// realmd's logon-proof `AuthLogonResult` (`crate::AuthReject`) — bad password, banned, …
|
||||
Logon(u8),
|
||||
/// The world server's `SMSG_AUTH_RESPONSE` code (`crate::WorldAuthReject`, the
|
||||
/// `messages::AUTH_*` block) — session expired, server shutting down, …
|
||||
World(u8),
|
||||
}
|
||||
|
||||
impl LoginRefusal {
|
||||
/// The raw byte, for logging. Deliberately not a `From`/`Into`: the whole point of the type is
|
||||
/// that the byte alone is ambiguous, so getting it back should look like a narrowing.
|
||||
pub fn byte(self) -> u8 {
|
||||
match self {
|
||||
LoginRefusal::Logon(b) | LoginRefusal::World(b) => b,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One decoded event from the world stream. Carries only primitives + the coarse [`EntityKind`]
|
||||
/// classification — no wire types leak to the app, and no running state lives here.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -124,9 +151,14 @@ pub enum SessionEvent {
|
||||
/// the server is simply unusable by this client (e.g. [`crate::WardenRequired`]). The app must
|
||||
/// surface `reason` and stop, never fold it into the paced-resubmit path.
|
||||
LoginFailed {
|
||||
code: Option<u8>,
|
||||
refusal: Option<LoginRefusal>,
|
||||
reason: String,
|
||||
terminal: bool,
|
||||
/// Set when the failure was the **dial itself** — the socket never opened — carrying which
|
||||
/// of the two reasons it was and the address it was aimed at (decision 1667's follow-up).
|
||||
/// `None` for every other transport failure (a handshake that got a socket and then broke)
|
||||
/// and for every server refusal, which have a `code` instead.
|
||||
dial: Option<crate::DialFailure>,
|
||||
},
|
||||
/// The account's character roster (`SMSG_CHAR_ENUM`): the world socket is authenticated and
|
||||
/// parked at character select. The IO thread emits this each connection cycle, then waits for
|
||||
|
||||
@@ -14,7 +14,8 @@ pub mod wire;
|
||||
pub mod world;
|
||||
pub use auth::AuthReject;
|
||||
pub use events::{
|
||||
decode, CharAction, EntityKind, LoginStage, MoveSpeeds, Poll, SessionEnd, SessionEvent,
|
||||
decode, CharAction, EntityKind, LoginRefusal, LoginStage, MoveSpeeds, Poll, SessionEnd,
|
||||
SessionEvent,
|
||||
};
|
||||
pub use messages::{
|
||||
CharCreateReq, CharEnumItem, Character, CreateSpline, ItemInfo, JumpInfo, MonsterMoveFacing,
|
||||
@@ -22,7 +23,9 @@ pub use messages::{
|
||||
CHARACTER_FLAG_GHOST, CHARACTER_FLAG_HIDE_CLOAK, CHARACTER_FLAG_HIDE_HELM,
|
||||
CHARACTER_FLAG_RENAME,
|
||||
};
|
||||
pub use world::{WardenRequired, WorldReader, WorldSession, WorldWriter, WORLD_PORT};
|
||||
pub use world::{
|
||||
WardenRequired, WorldAuthReject, WorldReader, WorldSession, WorldWriter, WORLD_PORT,
|
||||
};
|
||||
|
||||
use std::net::TcpStream;
|
||||
|
||||
@@ -52,6 +55,72 @@ pub fn host_port(host: &str, default: u16) -> (&str, u16) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The dial never got a socket — **and which of the two reasons it was**.
|
||||
///
|
||||
/// This distinction only started mattering when the realmlist became something a player types
|
||||
/// (decision 1667). Before that the address was a constant and "Unable to connect" could only mean
|
||||
/// one thing; afterwards it means either *you typed a name that does not exist* or *the address is
|
||||
/// fine and nothing is answering on it*, and a player with no way to tell them apart edits a
|
||||
/// correct address over and over. That is exactly what the first live use of the editor produced.
|
||||
///
|
||||
/// The split is drawn where it is honest — resolution and connection are two separate operations,
|
||||
/// so we do them separately rather than reading tea leaves out of one `io::Error` (a DNS failure
|
||||
/// surfaces as `ErrorKind::Uncategorized` on macOS, so the kind cannot carry this).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DialFailure {
|
||||
/// What we tried to reach, as the player would recognise it (`host:port`).
|
||||
pub address: String,
|
||||
/// `true` = the host name resolved to nothing; `false` = it resolved and the connection failed.
|
||||
pub unresolved: bool,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DialFailure {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.unresolved {
|
||||
write!(f, "cannot find a server called {}", self.address)
|
||||
} else {
|
||||
write!(f, "nothing answered at {}", self.address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for DialFailure {}
|
||||
|
||||
/// Resolve `host:port` and open a socket to it, reporting the two failures apart ([`DialFailure`]).
|
||||
///
|
||||
/// `to_socket_addrs` is the resolution step on its own; an empty result is a name that resolved to
|
||||
/// no addresses, which is as much a "cannot find it" as an outright lookup error. Connecting to the
|
||||
/// resolved list (rather than to `(host, port)` again) also means the OS does not repeat the
|
||||
/// lookup, and that every address the name carries is tried — the `localhost` → `::1` then
|
||||
/// `127.0.0.1` case on a machine whose server binds only IPv4.
|
||||
fn dial(host: &str, port: u16) -> Result<TcpStream> {
|
||||
use std::net::ToSocketAddrs;
|
||||
let address = format!("{host}:{port}");
|
||||
let addrs: Vec<std::net::SocketAddr> = match (host, port).to_socket_addrs() {
|
||||
Ok(addrs) => addrs.collect(),
|
||||
Err(e) => {
|
||||
return Err(anyhow::Error::new(DialFailure {
|
||||
address,
|
||||
unresolved: true,
|
||||
})
|
||||
.context(format!("resolving {host}: {e}")))
|
||||
}
|
||||
};
|
||||
if addrs.is_empty() {
|
||||
return Err(anyhow::Error::new(DialFailure {
|
||||
address,
|
||||
unresolved: true,
|
||||
}));
|
||||
}
|
||||
TcpStream::connect(&addrs[..]).map_err(|e| {
|
||||
anyhow::Error::new(DialFailure {
|
||||
address,
|
||||
unresolved: false,
|
||||
})
|
||||
.context(format!("connecting: {e}"))
|
||||
})
|
||||
}
|
||||
|
||||
/// A realm as advertised by the auth server's realm list.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RealmInfo {
|
||||
@@ -96,8 +165,7 @@ pub fn logon(host: &str, username: &str, password: &str) -> Result<Logon> {
|
||||
let (mut stream, reply, server_public_key) = {
|
||||
let mut dialed = None;
|
||||
for _ in 0..MAX_CHALLENGE_DIALS {
|
||||
let mut stream = TcpStream::connect((host, port))
|
||||
.with_context(|| format!("connecting to {host}:{port}"))?;
|
||||
let mut stream = dial(host, port)?;
|
||||
auth::write_logon_challenge(&mut stream, &username.to_uppercase(), CLIENT_BUILD)
|
||||
.context("sending logon challenge")?;
|
||||
let reply =
|
||||
|
||||
@@ -222,8 +222,35 @@ pub use vendor::{
|
||||
};
|
||||
pub use world_state::InitWorldStates;
|
||||
|
||||
/// `SMSG_AUTH_RESPONSE` AuthOk result.
|
||||
/// The **world server's** `SMSG_AUTH_RESPONSE` result codes — a **different enum** from the
|
||||
/// realmd logon-proof's `AuthLogonResult` (`crate::AuthReject`, 0x00..=0x12), which is the trap
|
||||
/// this block exists to close: both are "the auth result byte", they overlap numerically, and they
|
||||
/// mean unrelated things. 0x0C is `AUTH_OK` here and `AUTH_LOGON_FAILED_SUSPENDED` there.
|
||||
///
|
||||
/// VERIFIED two ways. The client's own dispatch over exactly this enum is decompiled in wow-re
|
||||
/// `system/net/scratch/w2b-pack.c` — case 0x0c → `AUTH_OK`, 0x0d → `AUTH_FAILED`, 0x0e →
|
||||
/// `AUTH_REJECT`, … 0x15 → `AUTH_UNKNOWN_ACCOUNT`, 0x16 → `AUTH_INCORRECT_PASSWORD`, 0x1b →
|
||||
/// `AUTH_WAIT_QUEUE`, 0x22..=0x26 → the `REALM_LIST_*` strings — and the same numbering is
|
||||
/// `AuthResponseCodes` in cmangos `src/game/Globals/SharedDefines.h:1721+`. Each constant's name
|
||||
/// is the `GlueStrings` key the client shows for it, which is what makes the mapping in
|
||||
/// `crate::login`'s `world_refusal_text` a transcription rather than a judgement call.
|
||||
pub const AUTH_OK: u8 = 0x0C;
|
||||
pub const AUTH_FAILED: u8 = 0x0D;
|
||||
pub const AUTH_REJECT: u8 = 0x0E;
|
||||
pub const AUTH_BAD_SERVER_PROOF: u8 = 0x0F;
|
||||
pub const AUTH_UNAVAILABLE: u8 = 0x10;
|
||||
pub const AUTH_SYSTEM_ERROR: u8 = 0x11;
|
||||
pub const AUTH_BILLING_ERROR: u8 = 0x12;
|
||||
pub const AUTH_BILLING_EXPIRED: u8 = 0x13;
|
||||
pub const AUTH_VERSION_MISMATCH: u8 = 0x14;
|
||||
pub const AUTH_UNKNOWN_ACCOUNT: u8 = 0x15;
|
||||
pub const AUTH_INCORRECT_PASSWORD: u8 = 0x16;
|
||||
pub const AUTH_SESSION_EXPIRED: u8 = 0x17;
|
||||
pub const AUTH_SERVER_SHUTTING_DOWN: u8 = 0x18;
|
||||
pub const AUTH_ALREADY_LOGGING_IN: u8 = 0x19;
|
||||
pub const AUTH_LOGIN_SERVER_NOT_FOUND: u8 = 0x1A;
|
||||
/// The realm is full and we are **queued**, not refused — the one code here that is not an ending.
|
||||
pub const AUTH_WAIT_QUEUE: u8 = 0x1B;
|
||||
/// `LogoutResult::Success` (`SMSG_LOGOUT_RESPONSE`).
|
||||
pub const LOGOUT_SUCCESS: u32 = 0x0;
|
||||
/// Chat `Language` wire ids (VERIFIED vmangos `SharedDefines.h:256-261`): the faction tongues.
|
||||
|
||||
@@ -872,8 +872,8 @@ impl ObjectFields {
|
||||
}
|
||||
|
||||
/// The object's class from the type field (`OBJECT_FIELD_TYPE`), used for `Values`/`Movement`
|
||||
/// updates that carry no separate `TypeId`. Unused by the create path (which has a `TypeId`).
|
||||
#[allow(dead_code)]
|
||||
/// updates that carry no separate `TypeId`. Also the reference's own "is this a Player" test
|
||||
/// (`[obj->descriptorBlock[2] + 8] >> 4 & 1`), which is how `CanAttack`/`CanInteract` ask.
|
||||
pub fn object_type(&self) -> Option<ObjectType> {
|
||||
self.get_u32(FIELD_OBJECT_TYPE).map(|bits| {
|
||||
if bits & 0x10 != 0 {
|
||||
|
||||
@@ -28,7 +28,7 @@ mod session;
|
||||
mod writer;
|
||||
|
||||
pub use reader::WorldReader;
|
||||
pub use session::{WardenRequired, WorldSession};
|
||||
pub use session::{WardenRequired, WorldAuthReject, WorldSession};
|
||||
pub use writer::WorldWriter;
|
||||
|
||||
/// Default `mangosd` world-server port — the stock one, which our vmangos deploy maps straight
|
||||
|
||||
@@ -28,6 +28,35 @@ const HANDSHAKE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_se
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct WardenRequired;
|
||||
|
||||
/// The **world server** refused the session (`SMSG_AUTH_RESPONSE` with anything but
|
||||
/// [`messages::AUTH_OK`]).
|
||||
///
|
||||
/// Typed, and carrying its raw code, for the same reason [`crate::AuthReject`] is: the screen owes
|
||||
/// the player the client's own authored words for what happened, and it cannot look them up from a
|
||||
/// formatted string. Before this the code was interpolated into a `bail!` and thrown away, so every
|
||||
/// world-side refusal — expired session, server shutting down, already logging in — arrived at the
|
||||
/// login screen as the same generic "Unable to connect".
|
||||
///
|
||||
/// **Its codes are `messages`' `AUTH_*` block, NOT [`crate::AuthReject`]'s.** See that block for
|
||||
/// why the distinction is load-bearing: the two enums overlap numerically and mean unrelated
|
||||
/// things.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct WorldAuthReject {
|
||||
pub code: u8,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for WorldAuthReject {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"world server refused the session: result {:#04x}",
|
||||
self.code
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for WorldAuthReject {}
|
||||
|
||||
impl std::fmt::Display for WardenRequired {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
@@ -131,7 +160,7 @@ impl WorldSession {
|
||||
match session.recv()? {
|
||||
ServerPacket::AuthResponse { result } if result == messages::AUTH_OK => break,
|
||||
ServerPacket::AuthResponse { result } => {
|
||||
bail!("world auth rejected: result {result:#x}")
|
||||
return Err(WorldAuthReject { code: result }.into())
|
||||
}
|
||||
ServerPacket::Other {
|
||||
opcode: opcode::SMSG_WARDEN_DATA,
|
||||
|
||||
Reference in New Issue
Block a user