your own name survives the first login, and four cold pipelines warm
- your character's own name no longer goes missing on the first login of a fresh start, so KLHThreatMeter stops erroring: the four "player" verbs read the login record, as the reference does (2260, 2261, 2263) - four pipelines that compiled live mid-play now warm under the loading screen: the model tile atlas's orthographic view, the minimap composite's quad, the glow combine pair and the indoor pool's liquid layout (2262, 2264) - a hoverable sign seats its tooltip where its template says, at the cursor or in the corner, not by its object type: Brill's signpost still follows the cursor (2259) - three warnings that fired when nothing was wrong are quiet, and the lane-coverage gates walk every crate instead of one (2264) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -21,9 +21,36 @@ use super::super::binding_abi::flag;
|
||||
use super::super::Model;
|
||||
use super::{
|
||||
check_unit_token, classification_word, grey_band, level_reads_unknown, pick_unit_token,
|
||||
unit_predicate, unknownobject, with_unit, SelectionRequest,
|
||||
unit_predicate, unknownobject, with_unit, PlayerRecord, SelectionRequest,
|
||||
};
|
||||
|
||||
/// **The `"player"` fast path, shared by the four verbs that take it** (decision 2263).
|
||||
///
|
||||
/// `Some(x)` = this token is `"player"` and `x` is the record's answer; `None` = it is any other
|
||||
/// token and the caller falls through to the unit resolver. That is the reference's own shape —
|
||||
/// a full-string, case-insensitive compare of the token against `0x847894` (`b"player\0"`) whose
|
||||
/// **match is the fall-through** — and it is one function rather than four copies for the reason
|
||||
/// [`super::super::names::gated_rank`](crate)'s sibling comment gives about rank: four readers of
|
||||
/// one record, each testing the token itself, is how they drift apart. `UnitName`, `UnitRace`,
|
||||
/// `UnitClass` and `UnitSex` are the whole set; `UnitLevel` is **not** one of them.
|
||||
///
|
||||
/// The compare is a full string, not a prefix: `"playerfoo"` is a recognised-but-unresolvable
|
||||
/// token ([`super::token_recognised`]) and goes to the resolver, not here.
|
||||
fn player_record_arm<T>(
|
||||
lua: &Lua,
|
||||
token: &Option<String>,
|
||||
f: impl FnOnce(&PlayerRecord) -> T,
|
||||
) -> Option<T> {
|
||||
if !token
|
||||
.as_deref()
|
||||
.is_some_and(|t| t.eq_ignore_ascii_case("player"))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
Some(f(&model.player_record))
|
||||
}
|
||||
|
||||
/// The two class ids `GetComboPoints 0x51a190` accepts — the literals `4` and `0xb` it compares
|
||||
/// the class byte `[[player+0x110]+0x79]` against. That byte is `UNIT_FIELD_BYTES_0` byte 1, the
|
||||
/// same value [`PlayerReqState::class_id`](super::super::PlayerReqState) carries; `UnitClass`'s own
|
||||
@@ -180,8 +207,11 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
//
|
||||
// Value 1 — and the ONLY two nils a recognised token can produce (`0x517020`, wow-re
|
||||
// `ui/scratch/binding-shape-arity-law.md` §2.1): the `"player"` fast path reads the
|
||||
// local name buffer and pushes nil when it is empty (`0x517083` → `0x5abdc0`), and a
|
||||
// token that resolves to GUID 0 pushes nil (`0x5170c0`). EVERY other path ends in a
|
||||
// local name buffer and pushes nil when it is empty (`0x51708c` → `0x5abdc0`; the
|
||||
// `0x517083` this used to cite is the token's string *compare*, `call 0x64a4c0` —
|
||||
// corrected 2261), and a token that resolves to GUID 0 pushes nil (`0x5170c0`).
|
||||
// The empty-buffer nil is not an explicit push either: `0x517095` is
|
||||
// `lua_pushstring`, which falls through on NULL at `0x6f3895` into `lua_pushnil`. EVERY other path ends in a
|
||||
// string — the cached name, or `FrameScript_GetText("UNKNOWNOBJECT")`: `0x517220` for
|
||||
// a GUID with no object and no cache row, `0x609324` inside `CGUnit_C::GetUnitName`
|
||||
// for a unit whose name cache has not answered or is stale (a pet's is
|
||||
@@ -195,17 +225,30 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// named gap), so a not-selectable unit reads its name there with `UnitExists` nil; the
|
||||
// name resolver's own nil is GUID 0 and nothing else, and a feed that seats a token
|
||||
// has resolved it. Decision 2002.
|
||||
//
|
||||
// **The `"player"` arm is the BUFFER, not the snapshot** (decision 2261). `0x51708c`
|
||||
// reads `0x5abdc0` — the local name buffer at `0xc27d88` — and returns; it never
|
||||
// reaches the resolver, so no object, no descriptor and no name-query answer is
|
||||
// involved on this path at all. Modelling it off the snapshot (as this did until
|
||||
// 2261) put the one name the client always knows behind the one cache that can miss:
|
||||
// decision 2260's realm-cache load dropped our own guid, the feed pushed a nameless
|
||||
// player over the roster seat, and `UnitName("player")` read nil in the world.
|
||||
//
|
||||
// The buffer's emptiness is still the reference's own nil — it just means something
|
||||
// a snapshot cannot say, and something a client that is in the world never is:
|
||||
// "no Enter World has been committed in this process".
|
||||
if let Some(seeded) = player_record_arm(lua, &token, |r| r.name.clone()) {
|
||||
let name = if seeded.is_empty() {
|
||||
Value::Nil
|
||||
} else {
|
||||
Value::String(lua.create_string(&seeded)?)
|
||||
};
|
||||
return Ok((name, Value::Nil));
|
||||
}
|
||||
let name = with_unit(lua, &token, None, |u| Some(u.name.clone()))?;
|
||||
let name = match name {
|
||||
None => Value::Nil,
|
||||
Some(Some(n)) => Value::String(lua.create_string(&n)?),
|
||||
Some(None)
|
||||
if token
|
||||
.as_deref()
|
||||
.is_some_and(|t| t.eq_ignore_ascii_case("player")) =>
|
||||
{
|
||||
Value::Nil
|
||||
}
|
||||
Some(None) => Value::String(unknownobject(lua)?),
|
||||
};
|
||||
Ok((name, Value::Nil))
|
||||
@@ -862,6 +905,18 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
token,
|
||||
r#"Usage: UnitRace("unit")"#,
|
||||
)?);
|
||||
// The `"player"` arm is the RECORD, not the snapshot (decision 2263): `0x518269`
|
||||
// reads `0x5abdd0` — `0xc27e80`, the char-enum row's race byte — and resolves it
|
||||
// through `ChrRaces`, never reaching the unit resolver. See [`PlayerRecord`].
|
||||
if let Some(pair) = player_record_arm(lua, &token, |r| r.race.clone()) {
|
||||
return match pair {
|
||||
Some((loc, file)) => Ok((
|
||||
Value::String(lua.create_string(&loc)?),
|
||||
Value::String(lua.create_string(&file)?),
|
||||
)),
|
||||
None => Ok((Value::Nil, Value::Nil)),
|
||||
};
|
||||
}
|
||||
let pair = with_unit(lua, &token, None, |u| {
|
||||
u.race.clone().zip(u.race_file.clone())
|
||||
})?;
|
||||
@@ -882,6 +937,17 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
token,
|
||||
r#"Usage: UnitClass("unit")"#,
|
||||
)?);
|
||||
// The `"player"` arm is the RECORD (decision 2263): `0x5183b9` → `0x5abde0`,
|
||||
// `0xc27e81` through `ChrClasses`. Second return is the UPPERCASE token.
|
||||
if let Some(pair) = player_record_arm(lua, &token, |r| r.class.clone()) {
|
||||
return match pair {
|
||||
Some((loc, file)) => Ok((
|
||||
Value::String(lua.create_string(&loc)?),
|
||||
Value::String(lua.create_string(&file)?),
|
||||
)),
|
||||
None => Ok((Value::Nil, Value::Nil)),
|
||||
};
|
||||
}
|
||||
let pair = with_unit(lua, &token, None, |u| {
|
||||
u.class.clone().zip(u.class_file.clone())
|
||||
})?;
|
||||
@@ -945,7 +1011,14 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
token,
|
||||
r#"Usage: UnitSex("unit")"#,
|
||||
)?);
|
||||
let sex = with_unit(lua, &token, 0u8, |u| u.sex)?;
|
||||
// The `"player"` arm is the RECORD (decision 2263): `0x517ef9` → `0x5abdf0`,
|
||||
// `0xc27e82`. **No bounds check anywhere on that path** — the byte indexes
|
||||
// `fild [4*eax+0x808be4]` over `{2,3,1,6}` directly — so an unset record answers `2`
|
||||
// ("male") in the reference too, which is what our `0` maps to below.
|
||||
let sex = match player_record_arm(lua, &token, |r| r.sex) {
|
||||
Some(sex) => sex,
|
||||
None => with_unit(lua, &token, 0u8, |u| u.sex)?,
|
||||
};
|
||||
Ok(Value::Integer(i64::from(if sex == 0 { 2 } else { sex })))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -53,6 +53,59 @@ pub enum SelectionRequest {
|
||||
LastEnemy,
|
||||
}
|
||||
|
||||
/// **The local player record — `0xc27d80`, ours** (decision 2263).
|
||||
///
|
||||
/// A copy of ONE `SMSG_CHAR_ENUM` `CHARACTER_INFO` row, and the source the reference answers four
|
||||
/// `"player"` verbs from. Each opens with the identical full-string, case-insensitive compare of
|
||||
/// the token against `0x847894` (`b"player\0"`) whose **match is the fall-through**, and each is
|
||||
/// unconditional — there is no arm that re-routes `"player"` to the unit resolver, so the
|
||||
/// descriptor never supersedes this record even once the object exists:
|
||||
///
|
||||
/// | verb | fast path | accessor | byte |
|
||||
/// |---|---|---|---|
|
||||
/// | `UnitName` `0x517020` | `0x51708c` | `0x5abdc0` | `+0x08` name |
|
||||
/// | `UnitRace` `0x518200` | `0x518269` | `0x5abdd0` | `+0x100` → `ChrRaces` |
|
||||
/// | `UnitClass` `0x518350` | `0x5183b9` | `0x5abde0` | `+0x101` → `ChrClasses` |
|
||||
/// | `UnitSex` `0x517e90` | `0x517ef9` | `0x5abdf0` | `+0x102` gender |
|
||||
///
|
||||
/// One writer image-wide — `0x5abd9e` (`rep movsd`, 0x44 dwords) inside `CGlueMgr::EnterWorld`,
|
||||
/// at the character-select commit — and **no instruction anywhere clears it**. It is filled before
|
||||
/// FrameXML loads and before `CMSG_PLAYER_LOGIN` is sent, which is why the reference can run addon
|
||||
/// `OnUpdate` for many frames with no local player object and still answer all four (wow-re
|
||||
/// `ui/scratch/unitname-player-seed-window.md` §6).
|
||||
///
|
||||
/// **`UnitLevel` is deliberately NOT here.** The record carries the level at `+0x108` and the
|
||||
/// client ships an accessor for it, `0x5abe00`, that **nothing calls** — so `UnitLevel("player")`
|
||||
/// reads the descriptor and answers `0` in exactly the window the four answer in.
|
||||
///
|
||||
/// **Resolved strings, not the raw bytes**, because this crate has no DBC: the reference resolves
|
||||
/// `+0x100`/`+0x101` through `ChrRaces`/`ChrClasses` at call time, and the app does that same
|
||||
/// resolution once when it seeds the record (it is the identical lookup
|
||||
/// `ui_unit::race_names`/`class_names` already does for the unit snapshot, so the two cannot
|
||||
/// disagree about what a race byte means).
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct PlayerRecord {
|
||||
/// `+0x08` — the NUL-terminated name buffer at `0xc27d88`. Empty is the record's only unset
|
||||
/// state, and the reference's only `UnitName("player")` nil: `lua_pushstring(NULL)` at
|
||||
/// `0x517095` tail-jumps into `lua_pushnil`. Unreachable from Lua in the real client, because
|
||||
/// the verb is not registered until `UI_Init` and only an Enter World commit gets there.
|
||||
pub name: String,
|
||||
/// `+0x100` resolved — `UnitRace`'s `(localized, file token)` pair, e.g.
|
||||
/// `("Night Elf", "NightElf")`. `None` is the all-zero record: race 0 has no `ChrRaces` row,
|
||||
/// and the reference's bound/NULL-row arms push `nil, nil` rather than reaching the resolver.
|
||||
pub race: Option<(String, String)>,
|
||||
/// `+0x101` resolved — `UnitClass`'s `(localized, UPPERCASE token)` pair, e.g.
|
||||
/// `("Warrior", "WARRIOR")`. `None` for the all-zero record, same shape as [`Self::race`].
|
||||
pub class: Option<(String, String)>,
|
||||
/// `+0x102` on `UnitSex`'s scale (2 male, 3 female) — **not** the wire's 0/1.
|
||||
///
|
||||
/// `0` is our unset marker and reads as `2`, which is not a fudge: `0x517ef9`'s accessor feeds
|
||||
/// `fild [4*eax+0x808be4]` over `{2,3,1,6}` with **no bounds check at all**, so an all-zero
|
||||
/// record answers `2` ("male") in the reference too. It is the one of the four with no
|
||||
/// validity guard.
|
||||
pub sex: u8,
|
||||
}
|
||||
|
||||
/// One unit-token's game-state snapshot, pushed by the app each frame and read by the `Unit*`
|
||||
/// bindings. Plain data (no mlua handles, no ECS types) — the engine-free seam decision 0068 §3
|
||||
/// draws between the app's net/ECS feed and the Lua API.
|
||||
@@ -539,6 +592,33 @@ impl super::UiScript {
|
||||
// canonical lowercase token, so a feed that ever pushed `"Target"` could not create
|
||||
// a second, shadowing entry.
|
||||
Some(s) => {
|
||||
// **A `"player"` push keeps the record in step, field by field** (2261,
|
||||
// widened by 2263). The reference writes `0xc27d80` once per Enter World
|
||||
// commit and never clears it; our VM is rebuilt per login, so the entry load
|
||||
// seeds it — and this keeps it true for any later push that KNOWS a field,
|
||||
// without ever letting one that doesn't blank it.
|
||||
//
|
||||
// That asymmetry is the whole mechanism: 2260's nameless player snapshot is
|
||||
// exactly the push that must not be able to reach these four verbs. It is also
|
||||
// what keeps every fixture in the workspace correct without touching them — a
|
||||
// test that seats a full player is, by this rule, also seating the record.
|
||||
if token.eq_ignore_ascii_case("player") {
|
||||
let rec = &mut model.player_record;
|
||||
if let Some(name) = s.name.as_deref().filter(|n| !n.is_empty()) {
|
||||
if rec.name != name {
|
||||
rec.name = name.to_string();
|
||||
}
|
||||
}
|
||||
if let Some(race) = s.race.clone().zip(s.race_file.clone()) {
|
||||
rec.race = Some(race);
|
||||
}
|
||||
if let Some(class) = s.class.clone().zip(s.class_file.clone()) {
|
||||
rec.class = Some(class);
|
||||
}
|
||||
if s.sex != 0 {
|
||||
rec.sex = s.sex;
|
||||
}
|
||||
}
|
||||
model.units_by_lower.insert(token.to_ascii_lowercase(), s);
|
||||
}
|
||||
None => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! The `Unit*` binding tests (the parent module is the unit under test).
|
||||
|
||||
use crate::script::{PartyState, UiScript, UnitState};
|
||||
use crate::script::{PartyState, PlayerRecord, UiScript, UnitState};
|
||||
|
||||
fn player() -> UnitState {
|
||||
UnitState {
|
||||
@@ -1995,3 +1995,170 @@ fn unit_on_taxi_is_one_or_nil_and_raises_the_reference_usage() {
|
||||
"the gate's own message, not mlua's type error: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
/// **The four `"player"` verbs read the seeded record, and nothing can blank it** (2261/2263).
|
||||
///
|
||||
/// The reference's `"player"` arm never reaches the resolver: `0x51708c` reads `0xc27d88`, a copy
|
||||
/// of the char-enum row written once at the Enter World commit and never cleared anywhere in the
|
||||
/// image. So the name survives everything that can happen to the *snapshot* — which is the whole
|
||||
/// of decision 2260's bug: a name-cache miss for our own guid produced a nameless player push, it
|
||||
/// replaced the roster seat, and the verb went nil in the world. That push is modelled here
|
||||
/// exactly, and it must now be invisible to this verb.
|
||||
#[test]
|
||||
fn the_player_record_outlives_every_snapshot() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
|
||||
// Before any Enter World commit — the buffer's one empty state, and the reference's nil.
|
||||
assert!(s
|
||||
.eval::<bool>(r#"return UnitName("player") == nil"#)
|
||||
.unwrap());
|
||||
|
||||
// The world-entry load seeds it from the roster row, before a single addon file runs.
|
||||
s.set_player_record(PlayerRecord {
|
||||
name: "Nelprifour".into(),
|
||||
race: Some(("Night Elf".into(), "NightElf".into())),
|
||||
class: Some(("Priest".into(), "PRIEST".into())),
|
||||
sex: 2,
|
||||
});
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return UnitName("player")"#).unwrap(),
|
||||
"Nelprifour",
|
||||
"the buffer answers with no player snapshot at all — the reference runs addon OnUpdate \
|
||||
for many frames with no local player object and this verb still answers"
|
||||
);
|
||||
|
||||
// 2260's push, verbatim: the descriptor landed, the name cache missed for our own guid.
|
||||
let nameless = UnitState {
|
||||
exists: true,
|
||||
has_object: true,
|
||||
name: None,
|
||||
level: 5,
|
||||
..Default::default()
|
||||
};
|
||||
s.set_unit("player", Some(nameless));
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return UnitName("player")"#).unwrap(),
|
||||
"Nelprifour",
|
||||
"a nameless snapshot must not reach this verb — that push is the reported bug"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return UnitRace("player")"#).unwrap(),
|
||||
"Night Elf",
|
||||
"the same holds for the other three fields of the record (2263)"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"local _, t = UnitClass("player"); return t"#)
|
||||
.unwrap(),
|
||||
"PRIEST",
|
||||
"UnitClass's second return is the UPPERCASE token"
|
||||
);
|
||||
assert_eq!(s.eval::<i64>(r#"return UnitSex("player")"#).unwrap(), 2);
|
||||
// …and the snapshot is still the snapshot for everything else. `UnitLevel` is the boundary:
|
||||
// the record carries a level byte and the client's accessor for it (`0x5abe00`) is DEAD, so
|
||||
// this one reads the descriptor even for `"player"`.
|
||||
assert_eq!(s.eval::<i64>(r#"return UnitLevel("player")"#).unwrap(), 5);
|
||||
|
||||
// The token going away entirely (the logout despawn frames) does not empty the buffer either.
|
||||
s.set_unit("player", None);
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return UnitName("player")"#).unwrap(),
|
||||
"Nelprifour"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>(r#"return UnitExists("player") == nil"#)
|
||||
.unwrap(),
|
||||
"the name and the unit are different questions — which is exactly what the buffer buys"
|
||||
);
|
||||
|
||||
// An empty seed is refused, not stored: nothing in the reference ever empties this record.
|
||||
s.set_player_record(PlayerRecord::default());
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return UnitName("player")"#).unwrap(),
|
||||
"Nelprifour"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return UnitRace("player")"#).unwrap(),
|
||||
"Night Elf"
|
||||
);
|
||||
|
||||
// A NAMED push does keep it in step — a second login as somebody else (1290's per-login VM
|
||||
// is the usual route, but a push must never leave the two disagreeing).
|
||||
let mut other = player();
|
||||
other.name = Some("Onewarrior".into());
|
||||
s.set_unit("player", Some(other));
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return UnitName("player")"#).unwrap(),
|
||||
"Onewarrior"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"local _, t = UnitClass("player"); return t"#)
|
||||
.unwrap(),
|
||||
"WARRIOR",
|
||||
"a push that carries the field updates it; one that does not leaves it standing"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>(r#"return UnitSex("player")"#).unwrap(),
|
||||
3,
|
||||
"player() is female — the record follows the push"
|
||||
);
|
||||
|
||||
// Still the family's two returns, the second nil (1840).
|
||||
assert_eq!(
|
||||
s.eval::<i64>(r#"local t = {UnitName("player")}; return table.getn(t)"#)
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
// …and the case fold on the token holds on the fast path too.
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return UnitName("PLAYER")"#).unwrap(),
|
||||
"Onewarrior"
|
||||
);
|
||||
}
|
||||
|
||||
/// **The unset record's four answers, which are not uniform** (decision 2263).
|
||||
///
|
||||
/// The state is "no Enter World has been committed in this process" — unreachable from Lua in the
|
||||
/// reference, because the four verbs only exist inside `UI_Init`'s table and only an Enter World
|
||||
/// commit gets there. It is reachable *here* (a bare VM, a capture, a test world with no pick), so
|
||||
/// it is pinned to what the bytes do rather than left to whatever falls out:
|
||||
///
|
||||
/// - `UnitName` → nil, via `lua_pushstring(NULL)` at `0x517095` tail-jumping into `lua_pushnil`;
|
||||
/// - `UnitRace`/`UnitClass` → `nil, nil`, via the DBC bound and NULL-row arms — race 0 and class 0
|
||||
/// have no row, and those arms reach `pushnil` **without** falling back to the unit resolver;
|
||||
/// - `UnitSex` → the number **2**, because `0x517ef9`'s accessor feeds `fild [4*eax+0x808be4]`
|
||||
/// over `{2,3,1,6}` with **no bounds check at all**. It is the one of the four with no validity
|
||||
/// guard, and "male" is what an all-zero record means to it.
|
||||
#[test]
|
||||
fn an_unseeded_player_record_answers_the_references_four_ways() {
|
||||
let s = UiScript::new().unwrap();
|
||||
assert!(s
|
||||
.eval::<bool>(r#"return UnitName("player") == nil"#)
|
||||
.unwrap());
|
||||
assert!(s
|
||||
.eval::<bool>(r#"return UnitRace("player") == nil"#)
|
||||
.unwrap());
|
||||
assert!(s
|
||||
.eval::<bool>(r#"local _, t = UnitRace("player"); return t == nil"#)
|
||||
.unwrap());
|
||||
assert!(s
|
||||
.eval::<bool>(r#"return UnitClass("player") == nil"#)
|
||||
.unwrap());
|
||||
assert!(s
|
||||
.eval::<bool>(r#"local _, t = UnitClass("player"); return t == nil"#)
|
||||
.unwrap());
|
||||
assert_eq!(
|
||||
s.eval::<i64>(r#"return UnitSex("player")"#).unwrap(),
|
||||
2,
|
||||
"the one of the four with no bounds check — an all-zero record reads male, not nil"
|
||||
);
|
||||
// And the fast path is the TOKEN, not a prefix: `"playerfoo"` is recognised but resolves to
|
||||
// nothing, so it goes to the resolver and answers the resolver's nil.
|
||||
assert!(s
|
||||
.eval::<bool>(r#"return UnitName("playerfoo") == nil"#)
|
||||
.unwrap());
|
||||
assert_eq!(
|
||||
s.eval::<i64>(r#"return UnitSex("playerfoo")"#).unwrap(),
|
||||
2,
|
||||
"…which for UnitSex is the same 2, by the binding's own unresolved-token default"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user