Every deviation names its reason, and a gate keeps it so (#2868)

`docs/METHOD.md` asks each deviation to name the reference fact and why benilla differs. About 130 `Deviation:` comments gave no reason, and the Warden comment lost its reason when the refusal became a read-past.

- `scripts/genmap.sh --check`, now a gate, fails a `Deviation:` comment without "because" and a `Deviates` row without a Why.
- `scripts/deviation-why.allow` lists the 43 comments whose change recorded no reason. It only shrinks: an entry that gains its reason, or is new against main's copy, fails.
- About 190 comments that gave their reason in other words now say "because", with their meaning kept.
- Nine reasons, the Warden one among them, come back from the commits and pull requests that made them.

Tested by the gate on the full tree, and by its failure on a comment with no reason and on a stale entry.

Fixes #2856
Fixes #2857

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-10-31 06:23:34 +07:00
committed by GitHub
parent f55684b8ee
commit d5e37dd975
144 changed files with 743 additions and 602 deletions
@@ -6,11 +6,12 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/">
<Script><![CDATA[
-- ══ The two bag verbs benilla answers differently ═════════════════════════════════════
-- Deviation: `OpenBackpack` opens every equipped bag, not only the backpack, so the vendor
-- and mailbox windows that call it (MerchantFrame.lua:35, MailFrame.lua:39) bring up every
-- bag to sell or mail from. The was-open memory is keyed by bag id: `ContainerFrame1..12`
-- are recycled across bags. The stock `CloseAllBags` (ContainerFrame.lua:702-707) runs
-- unchanged: it calls this `CloseBackpack`, then closes bags 1 and up.
-- Deviation: `OpenBackpack` opens every equipped bag, not only the backpack, because that
-- way the vendor and mailbox windows that call it (MerchantFrame.lua:35, MailFrame.lua:39)
-- bring up every bag to sell or mail from. The was-open memory is keyed by bag id:
-- `ContainerFrame1..12` are recycled across bags. The stock `CloseAllBags`
-- (ContainerFrame.lua:702-707) runs unchanged: it calls this `CloseBackpack`, then closes
-- bags 1 and up.
BENILLA_BAG_WAS_OPEN = {}
-- The reference's `backpackWasOpen`, one entry per bag: CloseBackpack leaves those open.
@@ -7,7 +7,7 @@
-- oldAlpha even under a live hover, so leaving and re-entering within
-- CHAT_FRAME_FADE_TIME (a flick to the scroll buttons, outside the hover box, does it)
-- stops the plate fading in (l.873) for the rest of the session; this keeps oldAlpha while
-- hover is set.
-- hover is set because the stock one nils it even under a live hover.
if FCF_ChatTabFadeFinished then
function FCF_ChatTabFadeFinished(chatTab, chatFrame)
chatTab:Hide()
@@ -18,9 +18,9 @@
end
-- Deviation: re-runs UIParent_ManageFramePositions after DurabilityFrame's own OnEvent. The
-- pass seats the frame 20 further in while a side glyph shows (UIParent.lua:1759-1768),
-- but the stock frame runs it only from OnShow/OnHide, so a glyph appearing while the frame
-- is shown leaves the shield's overhang off the screen edge. The stock handler runs first,
-- pass seats the frame 20 further in while a side glyph shows (UIParent.lua:1759-1768); a
-- glyph appearing while the frame is shown leaves the shield's overhang off the screen edge
-- because the stock frame runs it only from OnShow/OnHide. The stock handler runs first,
-- unchanged, on the this/event/arg1 already set for the dispatch.
if DurabilityFrame then
local refOnEvent = DurabilityFrame:GetScript("OnEvent")
@@ -30,9 +30,9 @@
end)
end
-- Deviation: friendly plates the player turned on come back at every world entry. The
-- stock UpdateNameplates (UIOptionsFrame.lua:768-780) shows them and hides them again at
-- once (l.775-776), and FRIENDNAMEPLATES_ON is never saved, since l.182 registers
-- Deviation: friendly plates the player turned on come back at every world entry; the stock
-- UpdateNameplates (UIOptionsFrame.lua:768-780) shows them and hides them again
-- at once (l.775-776), and FRIENDNAMEPLATES_ON is never saved because l.182 registers
-- NAMEPLATES_ON a second time.
function UpdateNameplates()
if ( NAMEPLATES_ON ) then
@@ -49,7 +49,7 @@
RegisterForSave("FRIENDNAMEPLATES_ON")
-- Deviation: a button whose key moved away keeps the old key's label while it holds an
-- action with no range (a stance, a self buff). The stock ActionButton_UpdateHotkeys
-- action with no range (a stance, a self buff) because the stock ActionButton_UpdateHotkeys
-- (ActionButton.lua:129-140) writes no text in that one unbound case; this clears it. The
-- stock function runs first, unchanged.
local refUpdateHotkeys = ActionButton_UpdateHotkeys
+17 -16
View File
@@ -1,10 +1,11 @@
<Ui>
<!--
The Options window the game menu opens. Deviation: the 1.15 era client's single settings
window replaces the 1.12 four-panel family, which is much worse to use; the 1.12 frames
still load, hidden, for addons. Every art byte is 1.12-native or a colour fill, so era
names below cite geometry only. Hiding discards pending edits and Apply commits outright.
A slider shows the 1.12 range; an out-of-range value displays clamped, not written back.
window replaces the 1.12 four-panel family because that is much worse to use; the 1.12
frames still load, hidden, for addons. Every art byte is 1.12-native or a colour fill, so
era names below cite geometry only. Hiding discards pending edits and Apply commits
outright. A slider shows the 1.12 range; an out-of-range value displays clamped, not written
back.
-->
<!-- The era's 14 and 20 white fonts, outside Fonts.xml's 1.12 set; MasterFont's shadow. -->
@@ -28,8 +29,8 @@
<Script><![CDATA[
-- UIOptionsFrame.xml loads first and owns the uvars; each row captures its default at
-- OnLoad.
-- Deviation: AUTO_QUEST_WATCH is the string "1". 1.12 assigns the number 1 (l.122) but
-- every consumer compares against "1" (QuestLogFrame.lua:64/68); 1 ~= "1" in Lua 5.0.
-- Deviation: AUTO_QUEST_WATCH is the string "1" because 1.12 assigns the number 1 (l.122)
-- but every consumer compares against "1" (QuestLogFrame.lua:64/68); 1 ~= "1" in Lua 5.0.
AUTO_QUEST_WATCH = "1"
RegisterForSave("AUTO_QUEST_WATCH")
@@ -495,8 +496,8 @@
"RowGuildMemberAlert", "RowRemoveChatDelay", "RowChatBubbles", "RowLootSpam",
"RowPartyChatBubbles", "RowSpamFilter",
},
-- Deviation: the bar toggles precede the lock, which 1.12's box leads with, so what is
-- on screen reads first.
-- Deviation: the bar toggles precede the lock, which 1.12's box leads with, because
-- that way what is on screen reads first.
ActionBars = {
"RowMultiBar1", "RowMultiBar2", "RowMultiBar3", "RowMultiBar4",
"RowAlwaysShowMultibars", "RowLockActionBar",
@@ -628,8 +629,8 @@
row.fmt = fmt
-- Kept for the "travel" readout, a share of the row's own range.
row.minv, row.maxv = min, max
-- Deviation: a named ladder reads out its stop; 1.12's slider labels only its ends, and
-- a bare index would mean nothing to a player.
-- Deviation: a named ladder reads out its stop because 1.12's slider labels only its
-- ends, and a bare index would mean nothing to a player.
row.stops = stops
row.refreshing = 1
slider:SetMinMaxValues(min, max)
@@ -743,8 +744,8 @@
GameTooltip:SetOwner(getglobal(row:GetName() .. "Tip"), "ANCHOR_RIGHT", -10, 0)
GameTooltip:SetText(text, nil, nil, nil, nil, 1)
else
-- Deviation: 1.12 returns early here; a mute row hides the last plate, so a
-- neighbour's text cannot stand when its OnLeave was missed.
-- Deviation: 1.12 returns early here; a mute row hides the last plate because
-- otherwise a neighbour's text stands when its OnLeave was missed.
GameTooltip:Hide()
end
end
@@ -1695,8 +1696,8 @@
</OnLoad></Scripts>
</Frame>
<!-- UIOptionsFrameSliders row 2. Deviation: no partner
write of cameraPitchSmoothSpeed, which has no reader:
the follow here has one rate. -->
write of cameraPitchSmoothSpeed because it has no
reader: the follow here has one rate. -->
<Frame name="$parentRowAutoFollowSpeed" inherits="BenillaOptionsSliderRowTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentRowHeadBob" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
@@ -1981,7 +1982,7 @@
</Anchors>
<Scripts><OnLoad>
-- Deviation: modern Classic's two-entry Display
-- Mode, as there is no exclusive fullscreen;
-- Mode, because there is no exclusive fullscreen;
-- gxWindow "0" is the borderless window.
BenillaOptionsRow_OnLoad(this, "gxWindow", "Display Mode",
"BENILLA_TOOLTIP_DISPLAY_MODE")
@@ -2010,7 +2011,7 @@
</Anchors>
<Scripts><OnLoad>
-- Deviation: our own row (the reference draws to
-- the backbuffer), and no Multisampling, which
-- the backbuffer), and no Multisampling because it
-- misses alpha-tested cutouts. Deferred: a write
-- rebuilds the render target.
BenillaOptionsRow_OnLoad(this, "renderScale", "Render Scale",
@@ -379,7 +379,8 @@
declares SlashCmdList. -->
<Script><![CDATA[
-- Deviation: 1.12 has no `/reload` (later clients added it; 1.12 reloads through
-- `/console reloadui`); kept so a player can reload the interface after toggling addons.
-- `/console reloadui`); kept because it lets a player reload the interface after toggling
-- addons.
SLASH_BENILLA_RELOAD1 = "/reload"
SlashCmdList["BENILLA_RELOAD"] = function()
ReloadUI()
@@ -1,7 +1,7 @@
<Ui>
<!--
Deviation: the pet page's autocast widgets take the pet bar button's proportions, so the
shine and the corner brackets sit on the button's edge and share its centre. Stock
Deviation: the pet page's autocast widgets take the pet bar button's proportions because
that seats the shine and the corner brackets on the button's edge, sharing its centre. Stock
SpellBookFrame.xml has the shine 36x36 at CENTER (1,1), scale 1.22, the brackets 60; 71.53
is 37 x 58/30 (the art fills 33/64 of UI-AutoCastableOverlay.blp).
-->
+6 -6
View File
@@ -16,10 +16,10 @@
//! - what the bodies call reaches the engine through the Lua binding functions
//! ([`benilla_ui::script::BindingInput`]) as [`BindingsState`]'s [`Input`]s.
//!
//! Deviation: a press with Super (Cmd) held runs no binding, so a system chord such as Cmd+Tab or
//! Cmd+Q never acts in game. The reference has no such modifier and runs the bare key's binding:
//! the Mac enqueues the key with Command held and skips only its text (`0x8a519`), and Windows
//! hands `VK_LWIN`/`VK_RWIN` to the layout arm (`0x42da39`) like any key.
//! Deviation: a press with Super (Cmd) held runs no binding because that way a system chord such as
//! Cmd+Tab or Cmd+Q never acts in game. The reference has no such modifier and runs the bare key's
//! binding: the Mac enqueues the key with Command held and skips only its text (`0x8a519`), and
//! Windows hands `VK_LWIN`/`VK_RWIN` to the layout arm (`0x42da39`) like any key.
//!
//! Persistence: `benilla-config/bindings/account.txt` and `<Realm>-<Char>.txt` ([`store`]); the
//! character file's existence is the character-set state, as in the reference.
@@ -636,8 +636,8 @@ pub(crate) fn world_takes_wheel(dispatch: Option<&BindingDispatch>, mods: Mods,
///
/// Deviation: a trackpad's `Pixel` deltas are carried until they make a line, and step at most
/// once per event ([`crate::ui_script::Record::Wheel`]). The reference steps on every
/// `WM_MOUSEWHEEL`, whatever its delta (`0x76c1a9`); carried, a trackpad does not zoom or scroll
/// at its event rate.
/// `WM_MOUSEWHEEL`, whatever its delta (`0x76c1a9`); the carry is kept because a carried trackpad
/// does not zoom or scroll at its event rate.
///
/// For consumers that act per notch off the sign alone: the UI's `OnMouseWheel` (stock
/// `ScrollFrameTemplate_OnMouseWheel` moves half a pane per call, `UIPanelTemplates.lua:150`) and
+2 -1
View File
@@ -163,7 +163,8 @@ fn fixed_name(k: KeyCode) -> Option<KeyName> {
F13 => Word("PRINTSCREEN"),
// `IsValidBindingKeyString` accepts `F` + any digits (`0x846c04`) and the Mac table maps
// keycode `0x6A` to `F16` (`0x30d`). Deviation: the Windows key table stops at `VK_F12`
// and drops later F-keys; they are named here so a stored `F13`-`F24` can be pressed.
// and drops later F-keys; they are named here because that way a stored `F13`-`F24` can be
// pressed.
#[cfg(not(target_os = "macos"))]
F13 => Word("F13"),
// On a Mac F14 and F15 are ScrollLock and Pause: keycodes `0x6B` and `0x71` map to
+9 -8
View File
@@ -213,10 +213,10 @@ fn one_char(key: &winit::keyboard::Key) -> Option<char> {
/// The reference's name for the virtual key `vk`, given `MapVirtualKeyExW(vk, MAPVK_VK_TO_CHAR)`;
/// `None` for a key `0x42d800`'s fixed table names.
///
/// Deviation: a character above U+00FF names the key by itself. The reference's `MapVirtualKeyA`
/// returns an ANSI code-page byte, which `0x42da4b` drops past `0xff` and the namer's UTF-8
/// encoder (`0x41abb0`) otherwise reads as Latin-1, so such a key is mislabelled or dropped; named
/// by what it types, it stays bindable and readable.
/// Deviation: a character above U+00FF names the key by itself because the reference's
/// `MapVirtualKeyA` returns an ANSI code-page byte, which `0x42da4b` drops past `0xff` and the
/// namer's UTF-8 encoder (`0x41abb0`) otherwise reads as Latin-1, so such a key is mislabelled or
/// dropped; named by what it types, it stays bindable and readable.
#[cfg(any(target_os = "windows", test))]
fn windows_name(vk: u32, to_char: u32) -> Option<LayoutName> {
match vk {
@@ -288,11 +288,12 @@ mod windows {
/// `KeyTranslate` gives the reference, or none.
///
/// Deviation: a layout that is not ASCII-capable (Hebrew, Greek, Russian, Arabic, Thai) names its
/// keys through the ASCII-capable layout macOS pairs with it, as macOS names its own shortcuts. The
/// reference reads a non-Roman `KCHR`'s byte as MacRoman (`0x8a40f`), which names no real key.
/// keys through the ASCII-capable layout macOS pairs with it, as macOS names its own shortcuts,
/// because the reference reads a non-Roman `KCHR`'s byte as MacRoman (`0x8a40f`), which names no
/// real key.
///
/// Deviation: a character above U+00FF (Czech `ě`, Polish `ł`, Turkish `ş`) names the key by
/// itself. The reference reads `KeyTranslate`'s byte as MacRoman (`0x8a40f`) and keeps it only if
/// Deviation: a character above U+00FF (Czech `ě`, Polish `ł`, Turkish `ş`) names the key by itself
/// because the reference reads `KeyTranslate`'s byte as MacRoman (`0x8a40f`) and keeps it only if
/// it converts to Latin-1 (`CFStringGetBytes`, `0x8a46b`, stored at `0x8a482`), else leaves the
/// slot `-1`, which `0x8a4aa` drops, so such a key is mislabelled or dropped; named by what it
/// types, it stays bindable and readable.
+3 -3
View File
@@ -287,9 +287,9 @@ fn fire_shakes(
/// Fire the shake group a spell-visual kit's field 14 names, once per kit play, with no stage test
/// and no cancel (`0x620e11`, or `0x60f4e6` when the play created no effect node).
///
/// Deviation: the shake spawns at the kit play's position. The reference passes `&node+0x48` to
/// the shake (`0x620e0e`) before `0x62101d` fills it from the attachment, so a bone-attached first
/// node (`0x61fdd0`, which never writes it) shakes at the world origin its constructor left
/// Deviation: the shake spawns at the kit play's position because the reference passes `&node+0x48`
/// to the shake (`0x620e0e`) before `0x62101d` fills it from the attachment, so a bone-attached
/// first node (`0x61fdd0`, which never writes it) shakes at the world origin its constructor left
/// (`0x61f4c1`) and is culled beyond 80 yards of it, silencing 26 of the 58 shipped shake kits;
/// that is a read before the write, not a behaviour.
fn fire_kit_shakes(
+1 -1
View File
@@ -22,7 +22,7 @@
//! but the wire-type remap (`0x49a870`) that would admit other kinds is untraced.
//!
//! Deviation: sizes ride the plates' damped diagonal basis ([`plate_basis`]), not the unbounded
//! reference law, so bubble text and plate text keep one em at every window size.
//! reference law, because that way bubble text and plate text keep one em at every window size.
//!
//! Not built: the reference also ends a bubble whose speaker's model data is not resident
//! (`0x7103d0`); here a speaker with no model yet only counts as not drawn.
+2 -2
View File
@@ -184,8 +184,8 @@ impl Cinematic {
}
/// One of the two letterbox bars, full-width black, top or bottom.
/// Deviation: Bevy UI nodes computed per frame, not `CinematicFrame.xml`'s textures, so a window
/// resized mid-cinematic stays letterboxed. `CinematicFrame` still shows and owns ESC.
/// Deviation: Bevy UI nodes computed per frame, not `CinematicFrame.xml`'s textures, because that
/// way a window resized mid-cinematic stays letterboxed. `CinematicFrame` still shows and owns ESC.
#[derive(Component)]
struct LetterboxBar;
+1 -1
View File
@@ -271,7 +271,7 @@ pub(crate) fn float_combat_text(
let size_value = scale_value(t.category, life);
let target_px = text_px(size_value, viewport);
// Deviation: the reference stretches a raster of at most 32 px; each whole-pixel size is
// rasterized here, so the crit pop stays crisp.
// rasterized here because that way the crit pop stays crisp.
let mut e = atlas.lock();
let mut glyphs = layout_text_quads(
&mut e,
+8 -7
View File
@@ -465,15 +465,16 @@ const HELP_COMMAND_LINE: usize = 0xc4;
/// - Anything else prints nothing.
///
/// Deviation: a line of eight names is not left with its trailing `, ` and a list of a multiple
/// of eight names is not followed by `NONE`, two slips of the reference's loop (`0x63ffed`,
/// `0x64004d`). The commands list in registration order, then the CVars in the registry's list
/// order, where the reference's one table interleaves them as start-up registers them.
/// of eight names is not followed by `NONE`, because those are two slips of the reference's loop
/// (`0x63ffed`, `0x64004d`). The commands list in registration order, then the CVars in the
/// registry's list order, where the reference's one table interleaves them as start-up registers
/// them.
///
/// Deviation: a CVar lists under its registration's category with its registration's help, even
/// when `config.toml` held it at boot. In the reference such a record was made by the config
/// line's `set` (`0x63d569`-`0x63d582`), which registered its command in category 5 with the help
/// `""` (`0x63ddc9`), and the later registration finds the record and returns without registering
/// again (`0x63dc1a`-`0x63dc87`), so what `help` says would hang on what the file held.
/// when `config.toml` held it at boot, because in the reference such a record was made by the
/// config line's `set` (`0x63d569`-`0x63d582`), which registered its command in category 5 with the
/// help `""` (`0x63ddc9`), and the later registration finds the record and returns without
/// registering again (`0x63dc1a`-`0x63dc87`), so what `help` says would hang on what the file held.
fn help(steps: &mut ConsoleSteps, args: &str) -> Vec<String> {
if args.is_empty() {
return vec![
@@ -1349,9 +1349,9 @@ pub(crate) fn arm_aura_state_fx(
// (`0x604e7b`-`0x604e85`), so the highest new slot's effect ends at the head.
//
// Deviation: a spell's state kit and body procs are reaped only once no slot holds the
// spell. The reference's removal of one slot (`0x612320` → `0x5ff290`) stops every
// spell because the reference's removal of one slot (`0x612320` → `0x5ff290`) stops every
// effect of that spell on the unit (`0x614150(spell, 1)` at `0x5ff2d2`, and its procs
// through `0x60d150` and `0x5ff320`), even while another slot holds the same spell: it
// through `0x60d150` and `0x5ff320`), even while another slot holds the same spell, which
// drops a visual the server says still holds.
let mut cur: Vec<u32> = Vec::new();
for a in store.0.unit_auras() {
+2 -1
View File
@@ -1169,7 +1169,8 @@ const HEADER: &str = "\
/// Dirty and one quiet second since the last change: save, so a crash loses at most that second.
/// Deviation: the reference's `SaveConfig` (`0x63d980`) writes only at world entry (`0x46b6f6`),
/// at the leave-world tail (`0x490c35`-`0x490c64`) and at shutdown (`0x63daf0`), so a crash or a
/// kill loses every change since; here a player's settings survive one.
/// kill loses every change since; here a player's settings survive one because the config saves a
/// quiet second after a change.
fn save_config_when_quiet(mut cvars: ResMut<Cvars>) {
if cvars.dirty && cvars.last_change.is_none_or(|t| t.elapsed() >= SAVE_QUIET) {
write_config(&mut cvars);
+3 -2
View File
@@ -306,8 +306,9 @@ pub(crate) fn poll_corpse(mut vm: crate::script_calls::VmWith<CorpsePoll>) {
let corpse = death_net.corpse.as_ref();
let in_range = corpse.is_some_and(|cp| {
// Deviation: `0x492130` compares the positions whatever map we stand on, so a
// ghost on another map at the corpse's coordinates reads in range for a reclaim
// the server refuses; here only the corpse's display map counts.
// ghost on another map at the corpse's coordinates reads in range for a reclaim the
// server refuses; here only the corpse's display map counts because the server refuses
// that reclaim.
let on_display_map = map
.as_ref()
.is_some_and(|m| m.0 == u32::try_from(cp.display_map).unwrap_or(u32::MAX));
@@ -51,8 +51,9 @@ const PERP_EPSILON: f32 = 0.001;
/// (`0x7afc10`/`0x7afc38`), which makes the per-frame re-roll crawl rather than strobe.
const ADVECT_KEEP: f32 = 0.75;
/// Deviation: a cap on one hop's sub-segment count, which the reference lacks, so a vertex run is
/// never sized off table data alone; shipped rows peak at 12 (30 yd at `avgSegLen` 2.78).
/// Deviation: a cap on one hop's sub-segment count, which the reference lacks, because that way a
/// vertex run is never sized off table data alone; shipped rows peak at 12 (30 yd at `avgSegLen`
/// 2.78).
const MAX_SUBDIVISIONS: usize = 256;
/// The caster's chain-target hop array, the reference's growable `unit+0xd44`: guids in wire
+4 -3
View File
@@ -65,9 +65,10 @@ fn conform_rotation(mode: u8, n_local: Vec3) -> Quat {
/// `CMovement+0x24`; the two agree on a uniform slope), smoothed on the up-vector.
///
/// Deviation: the reference updates every unit every frame; here only units showing a flagged
/// visual are tracked, and a parked rig skips its ray and re-seeds at the slope on waking, to bound
/// the cost at one ray per flagged unit on screen. Nothing shows: an unflagged unit's vector is
/// never read, and a first-sight seed at the slope seats a mount-up at once, as the reference does.
/// visual are tracked, and a parked rig skips its ray and re-seeds at the slope on waking, because
/// that bounds the cost at one ray per flagged unit on screen. Nothing shows: an unflagged unit's
/// vector is never read, and a first-sight seed at the slope seats a mount-up at once, as the
/// reference does.
#[allow(clippy::type_complexity)] // one Bevy system's full input set
pub(super) fn conform_units(
time: Res<Time>,
@@ -178,8 +178,9 @@ pub(crate) struct DressKey {
pub(crate) display_id: Option<u32>,
/// Mainhand, offhand, ranged: the display id, model kind and folded enchant ids.
pub(crate) held: [Option<(u32, ItemModelKind, i32)>; HELD_SLOTS],
/// Every placed weapon's model has built parts. Deviation: the reference copies a finished
/// model, ours stream in, so while false the pane keeps re-taking and a late weapon is baked.
/// Every placed weapon's model has built parts. Deviation: while false the pane keeps re-taking
/// and a late weapon is baked, because the reference copies a finished model and ours stream
/// in.
pub(crate) held_ready: bool,
}
+2 -2
View File
@@ -430,8 +430,8 @@ pub(crate) fn shots_path() -> Option<PathBuf> {
/// `benilla-config/Screenshots/`: where the print-screen key writes, each image once, not through
/// [`write_atomic`].
///
/// Deviation: the reference writes `Screenshots\\` inside the install, which benilla never writes
/// to; the folder keeps the reference's name.
/// Deviation: the reference writes `Screenshots\\` inside the install; benilla keeps the folder in
/// its own state because it never writes to the install; the folder keeps the reference's name.
pub(crate) fn screenshots_dir() -> Option<PathBuf> {
home().map(|h| h.join("Screenshots"))
}
+3 -2
View File
@@ -539,8 +539,9 @@ pub(crate) fn drive_nameplates(
ghost_visual: store.is_some_and(|s| s.0.unit_is_ghost_visual()),
is_self,
// No name over a fully faded first-person avatar. Deviation: `ShouldShowName`'s
// first leg is `IsSelectable` (`0x60be60`), which passes our own unit; what hides
// the own name in first person in 1.12.1 is untraced, so the avatar's fade stands in.
// first leg is `IsSelectable` (`0x60be60`), which passes our own unit; the avatar's
// fade stands in because what hides the own name in first person in 1.12.1 is
// untraced.
self_faded: rig
.as_deref()
.map_or(1.0, crate::player::CameraControl::self_fade)
+2 -1
View File
@@ -10,7 +10,8 @@
//! Deviation: the reference loads the world first and logs in after: the entry's first frame
//! starts the load at the record's map and position (`0x401570`, called at `0x46c236`), and a
//! later one, once its file reader is idle (`0x46c247`), sends the login (`0x46c272`). Here the two
//! overlap, so entry is a round trip faster, and a refusal ends the cycle as a logout does.
//! overlap because that way entry is a round trip faster; a refusal ends the cycle as a logout
//! does.
//!
//! A logout or a later `SMSG_CHARACTER_LOGIN_FAILED` ends the world session, announced as a
//! [`SessionEvent::Disconnected`] with [`SessionEnd::LoggedOut`], and parks at character select
+2 -2
View File
@@ -93,8 +93,8 @@ pub(in crate::net) enum RelayOutcome {
Seed,
/// A server-authored pose for our own mover, handed to the controller as a
/// [`crate::net::SelfMoveMessage`] at arrival. Deviation: the reference paces a self move
/// through the same replay chain as a remote's, which would only delay a correction to our own
/// pose.
/// through the same replay chain as a remote's; benilla hands it over at arrival because pacing
/// it would only delay a correction to our own pose.
SelfMover,
/// No entity for this guid: the packet changes nothing.
Unknown,
+3 -2
View File
@@ -1086,7 +1086,7 @@ fn on_update_end(
// Deviation: a first-pass values block the clear header byte left unwritten re-fires,
// for a live object, its last write's edges against the watch copy (`0x465570`,
// `0x466686`); benilla compares against this packet's copies, which hold none for it,
// so a stale edge never fires twice.
// because that way a stale edge never fires twice.
Notify::Values { guid, sent } => {
let moved = copies.get(&guid).unwrap_or(&none);
let block = Notified::new(Sent::Fields(&sent), moved);
@@ -1099,7 +1099,8 @@ fn on_update_end(
// `0x465d79`). For a first-pass create the clear header byte left unwritten, an
// object not live crashes the reference, and a live one runs the all-fields watch
// (`0x465d8e`) against its stale watch copy, re-firing its last write's edges as the
// values block above does; benilla's [`notify`] fires nothing for either.
// values block above does; benilla's [`notify`] fires nothing for either, because the
// faults the reference's passes have on malformed packets are recorded, not copied.
Notify::Create { guid, kind } => {
if unshown.remove(&guid) {
create_hook(&mut sc, guid, kind, script.as_deref(), &mut arrival);
+3 -3
View File
@@ -308,9 +308,9 @@ fn on_disconnected(
// so the selection ends as at the world's teardown, with nothing sent or signalled.
//
// Deviation: on benilla's unattended reconnect, which the reference lacks, the world's VM
// survives the loss, so the clear signals its `PLAYER_TARGET_CHANGED` and usable refresh
// as a release's teardown does (`0x493a69`, `0x493a73`), still sending nothing, and the
// surviving target frame lets the lost unit go.
// survives the loss; because of that the clear signals its `PLAYER_TARGET_CHANGED` and
// usable refresh as a release's teardown does (`0x493a69`, `0x493a73`), still sending
// nothing, and the surviving target frame lets the lost unit go.
let LostSelection { selection, owed } = lost;
if let Some(mut selection) = selection.filter(|s| s.guid.is_some()) {
let before = selection.guid;
+8 -7
View File
@@ -417,8 +417,9 @@ impl AutoMove<'_, '_> {
/// Deviation: the reference re-projects the point's height onto the first walkable facet a
/// zero-size box gather finds at the enemy (`0x60fe25`-`0x60fe51`); here it keeps the enemy's
/// height. The snap radius is the stop, so the two differ only while swimming or on a steep facet.
/// Deviation: the reference stores the world point unconverted and reads it back as deck-local
/// (`0x60fe6c`, `0x610e95`), so on a transport it walks astray; here it rides the deck.
/// Deviation: here it rides the deck, because the reference stores the world point unconverted
/// and reads it back as deck-local (`0x60fe6c`, `0x610e95`) and so, on a transport, walks
/// astray.
pub(crate) fn walk_into_melee(
&mut self,
at: Vec3,
@@ -446,9 +447,9 @@ impl AutoMove<'_, '_> {
self.arm(Toward::FacingTurn(facing), at, 0.0, stand).is_ok()
}
/// Deviation: the reference converts back through the transport the player stands on each
/// frame (`0x610e6b`), so stepping off mid-walk sends the point astray; here the deck is fixed
/// at the arm.
/// Deviation: here the deck is fixed at the arm, because the reference converts back through
/// the transport the player stands on each frame (`0x610e6b`), so stepping off mid-walk sends
/// the point astray.
fn anchor(&self, at: Vec3) -> Anchor {
let frame = self.player.ride.as_ref().map(|r| r.entity);
match frame.and_then(|e| self.transports.get(e).ok().map(|tf| (e, tf))) {
@@ -662,8 +663,8 @@ pub(super) fn steer_approach(
if d2 <= arrive * arrive {
info!("approach: arrived at {:.2} yd", d2.sqrt());
// The canceller faces the armed guid (`0x60fbe8`): the object, and none for the ground.
// Deviation: the melee arm names the player's own guid (`0x60fe5f`), so the reference
// turns to face itself, a fixed heading; here the melee walk does not turn.
// Deviation: here the melee walk does not turn, because the reference's melee arm names
// the player's own guid (`0x60fe5f`), so it turns to face itself, a fixed heading.
if let Toward::Object { verb, guid } = toward {
if delta.x * delta.x + delta.z * delta.z > f32::EPSILON {
player.face_yaw = bearing;
+1 -1
View File
@@ -10,7 +10,7 @@ use super::camera::CameraControl;
/// head, and the avatar fades out. Deviation: the starting zoom, [`CAM_DIST_DEFAULT`], is 15 yd
/// where the reference starts at 5.55, the third-person-A row of its default table `0x84f488`
/// that `cameraView` 1 picks (`0x50c17a`, `0x50c18b`; the `cameraDistance` CVar itself registers
/// "0.0"), for a wider view.
/// "0.0"), because that gives a wider view.
pub(super) const CAM_DIST_MIN: f32 = 0.0;
/// The zoom ceiling, yd (`ds:0x8089a4` = 50.0): the cap on [`ZoomLimit`] (`0x511309`) and the top
/// of the distance validator `0x50b310`; also the clamp for a distance read back off disk.
+3 -2
View File
@@ -118,8 +118,9 @@ pub(super) const SKIN_WIDTH: f32 = 0.02;
/// deadline on while the world is stale or any load counter moves, so only a dead stream hits it.
/// Terrain that never arrives still holds the body, by the world query gate ([`super::edge`]),
/// so what the release can hand over early is a placement that never spawned. Deviation: the
/// reference's blocking load (`0x694920`) waits out its reads before the frame goes on and has
/// no such release; without one, a model that never loads would freeze our body for good.
/// reference's blocking load (`0x694920`) waits out its reads before the frame goes on and has no
/// such release; the timeout exists because without one a model that never loads would freeze our
/// body for good.
pub(crate) const SETTLE_TIMEOUT: f32 = 6.0;
/// The player body's collision capsule, swept by avian's `MoveAndSlide`: origin at its centre, the
+3 -3
View File
@@ -51,9 +51,9 @@ pub(crate) fn on_app_exit(app: &mut App, systems: ScheduleConfigs<ScheduleSystem
/// to `DefWindowProc`, whose `SC_CLOSE` becomes the kind-5 event (`0x42d287`, as `WM_CLOSE` posts at
/// `0x42d292`) and closes at once: the running client's callback `0x63c9a0` (installed from
/// `0x402840`, asked at `0x423a21`) answers 0 and tears the context down (`0x41f9b0`), with no
/// logout timer, at the login screen and in game alike. A tiling window manager (i3,
/// sway, Hyprland) binds nothing to it, so benilla reads the chord itself and asks for the same
/// close, which the shutdown tail above already serves. Not on macOS: `Cmd+Q` is
/// logout timer, at the login screen and in game alike. benilla reads the chord itself and asks for
/// the same close, which the shutdown tail above already serves, because a tiling window manager
/// (i3, sway, Hyprland) binds nothing to Alt+F4. Not on macOS: `Cmd+Q` is
/// [`benilla_world::mac_quit`].
pub(crate) struct AltF4Plugin;
+1 -1
View File
@@ -48,7 +48,7 @@ impl SmartBucket {
}
/// The seat without the claim. Deviation: the plate caller snaps the result to device
/// pixels before [`Self::claim`], so its border art blits 1:1.
/// pixels before [`Self::claim`], because that way its border art blits 1:1.
pub(crate) fn resolve(&self, desired: Rect, viewport: Vec2) -> Rect {
let (w, h) = (desired.width(), desired.height());
let (half_w, half_h) = (w * 0.5, h * 0.5);
+3 -3
View File
@@ -51,9 +51,9 @@ struct Record {
struct Entry {
/// `[+0xE00]`, the SoundEntries id; 0 is free.
id: u32,
/// Deviation: the reference marks an unresolvable kit by negating the id (`[+0xE00] = -id`),
/// so the owner's next `$DSL` re-registers into a fresh entry every cycle; a flag keeps it in
/// place without the churn. Both are silent.
/// Deviation: a flag keeps an unresolvable kit in place without the churn, because the
/// reference marks it by negating the id (`[+0xE00] = -id`), so the owner's next `$DSL`
/// re-registers into a fresh entry every cycle. Both are silent.
failed: bool,
/// `[+0xC00]`'s active set, in claim order.
records: Vec<Record>,
+2 -2
View File
@@ -237,8 +237,8 @@ impl Bus {
const BUS_CAP: [u32; 13] = [0x7fff_ffff, 1, 2, 2, 1, 1, 2, 2, 1, 6, 4, 1, 2];
/// How many copies of one kit may sound at once, for kits without the `NO_DUPLICATES` flag.
/// Deviation: the reference leaves those kits uncapped; sample-aligned copies of one file sum
/// coherently (five are one sound +14 dB), so a mass buff distorts.
/// Deviation: the reference leaves those kits uncapped; here they are capped because sample-aligned
/// copies of one file sum coherently (five are one sound +14 dB), so a mass buff distorts.
const SAME_KIT_MAX: usize = 2;
/// The reference's duplicate suppressor: kit flag 0x20 and a channel of the same file still live
+5 -5
View File
@@ -102,8 +102,8 @@ pub(crate) struct SoundConfig {
/// preset (`0x45a75b`) and the per-channel wet send (`0x458f13`). Read by
/// [`reverb::zone_reverb`].
///
/// Deviation: default off where the reference registers "1". Its reverb is FMOD 3's EAX API,
/// which renders only with hardware 3D mixing, so off is what the reference is heard to
/// Deviation: default off where the reference registers "1", because its reverb is FMOD 3's EAX
/// API, which renders only with hardware 3D mixing, so off is what the reference is heard to
/// produce (inferred: `fmod.dll`'s render side is untraced).
pub reverb: bool,
/// Not a CVar: set while the loading cover is up ([`feed_world_hold`]). No new sound starts
@@ -120,9 +120,9 @@ pub(crate) struct SoundConfig {
/// it would save music off for a player who quits mid-cinematic. So the reference's restore
/// latch (`[0xb4e278]`), needed there because its flag is the CVar itself, is not carried.
pub music_suppressed: bool,
/// Deviation: `SoundOutputLimiter`, not a 1.12 CVar, default on, so overlapping full-scale
/// kits are limited rather than clipped. The reference has no headroom mechanism and clips at
/// full scale, as kira's hard clamp would (see [`limiter`]).
/// Deviation: `SoundOutputLimiter`, not a 1.12 CVar, default on, because that way overlapping
/// full-scale kits are limited rather than clipped. The reference has no headroom mechanism and
/// clips at full scale, as kira's hard clamp would (see [`limiter`]).
pub limiter: bool,
/// `SoundListenerAtCharacter` (default "1", `0x457890`), read once per frame at `0x483125`.
/// `1` puts the listener on the mover with the character's facing, `0` at the camera eye with
+9 -10
View File
@@ -704,17 +704,16 @@ fn bind_target_unit(
/// range." or "Target too close") and binds nothing; otherwise the emptied word commits (`SendCast
/// 0x6e54f0`).
///
/// Deviation: a range failure leaves targeting armed and working. For a cast in flight the
/// Deviation: a range failure leaves targeting armed and working because, for a cast in flight, the
/// reference's report cancels it (`0x6e2218` → `0x6e4940`): its line, `SPELLCAST_FAILED` and the
/// tradeskill disarm,
/// which benilla keeps; with the word the bind emptied it also sends `CMSG_CANCEL_CAST` for the
/// unsent spell (`0x6e4970`) and clears the caster, then restores the pre-click word (`0x6e6096`),
/// so every later click fails the caster test (`0x6e5b93`) and the cursor is stranded until a
/// cancel. That is a defect: benilla's cancel finds the cursor up, so it drops the word, with its
/// signal (`0x6e495c`), instead of sending, and the cursor is re-armed as its attempt left it,
/// its caster and its hold on the slot included, which is what a player can use. A record never
/// pushed is not in flight, and its report cancels nothing: the word and the caster stand, as in
/// the reference.
/// tradeskill disarm, which benilla keeps; with the word the bind emptied it also sends
/// `CMSG_CANCEL_CAST` for the unsent spell (`0x6e4970`) and clears the caster, then restores the
/// pre-click word (`0x6e6096`), so every later click fails the caster test (`0x6e5b93`) and the
/// cursor is stranded until a cancel. That is a defect: benilla's cancel finds the cursor up, so it
/// drops the word, with its signal (`0x6e495c`), instead of sending, and the cursor is re-armed as
/// its attempt left it, its caster and its hold on the slot included, which is what a player can
/// use. A record never pushed is not in flight, and its report cancels nothing: the word and the
/// caster stand, as in the reference.
fn merge(
ladder: &mut crate::spell::CastLadder,
spell_id: u32,
+1 -1
View File
@@ -485,7 +485,7 @@ impl Relations<'_> {
/// forced. The lookup `0x4d6490` returns the stored rank verbatim (`0x4d64b3`).
///
/// Deviation: the rank is held to 7, the top of the ring scale's eight entries (`0..=7`), where the
/// reference keeps any value; a server rank past 7 would index outside our palette.
/// reference keeps any value, because a server rank past 7 would index outside our palette.
pub(crate) fn forced_rank(reputations: &Reputations, faction: u32) -> Option<u8> {
reputations
.forced_rank(faction)
+7 -5
View File
@@ -191,9 +191,10 @@ pub(super) fn load_creature_types(mut commands: Commands, world_assets: Option<R
/// Everything the scan reads, shared by TAB, the attack acquire and the pet bar's Attack.
///
/// Deviation: our own body is never a candidate. The reference's friendly scan picks the player at
/// some facings (`CanAssist(P, P)` is true, `0x6061e0`; the cone test `0x47f220`), but our scoring
/// always puts the player dead centre at distance 0, so CTRL-TAB would pick us on every press.
/// Deviation: our own body is never a candidate because our scoring always puts the player dead
/// centre at distance 0, so CTRL-TAB would pick us on every press. The reference's friendly scan
/// picks the player at some facings (`CanAssist(P, P)` is true, `0x6061e0`; the cone test
/// `0x47f220`).
#[derive(SystemParam)]
#[allow(clippy::type_complexity)] // one bundled system param, the app's convention
pub(crate) struct TargetScan<'w, 's> {
@@ -1125,8 +1126,9 @@ pub(crate) struct LastEnemy(pub(crate) Option<u64>);
/// alive was stamped the frame it was selected, so its death's deselect (`super::died`, in a later
/// frame's packet) leaves it remembered.
///
/// Deviation: sampled each frame, not stamped in `SetSelection`, so every selection writer is
/// covered; a selected unit that turns hostile is remembered, where the reference's is not.
/// Deviation: sampled each frame, not stamped in `SetSelection`, because that way every selection
/// writer is covered; a selected unit that turns hostile is remembered, where the reference's is
/// not.
pub(super) fn remember_last_enemy(
selection: Res<Selection>,
stores: Query<&ObjectStore>,
@@ -412,8 +412,8 @@ impl CastFailLine {
/// The line for a failed cast, `None` where the reference leaves before `DisplayError` and the
/// log; `get` is the VM's GlobalStrings lookup. An empty [`CastFailLine::text`] shows nothing and
/// still logs. Deviation: a reason past the table prints its hex code, so an unknown reason is
/// visible.
/// still logs. Deviation: a reason past the table prints its hex code, because that way an unknown
/// reason is visible.
pub(super) fn cast_fail_text(
caster: Caster,
reason: u8,
+2 -1
View File
@@ -1009,7 +1009,8 @@ fn join_duration(stamp: Option<&DurationStamp>, stamp_now: f64, script_now: f64)
/// The session's end: drops the lists and stamps, so nothing reaches the next character's bar.
/// Deviation: the reference clears its expiry array (`0xbc5f68`) at every game entry, in
/// `ResetBuffCache` (`0x4e40c0`, called from `0x48f5a9`); benilla clears it at exit instead.
/// `ResetBuffCache` (`0x4e40c0`, called from `0x48f5a9`); benilla clears it at exit instead,
/// because that way a buff or debuff keeps its duration timer through a teleport.
fn end_session_aura_state(
mut hosted_vm: crate::script_calls::HostedVm,
mut cache: ResMut<PlayerAuraCache>,
+2 -2
View File
@@ -263,8 +263,8 @@ pub(crate) fn add_chat_lane(app: &mut App) {
/// lines. The Lua state is itself replaced at the session end, as the reference destroys its own
/// at logout.
///
/// Deviation: not run on benilla's same-character reconnect, which the reference lacks (it
/// returns to the login screen), so the window keeps its scrollback there.
/// Deviation: not run on benilla's same-character reconnect, which the reference lacks (it returns
/// to the login screen), because that way the window keeps its scrollback there.
fn end_session_chat(
mut hosted_vm: crate::script_calls::HostedVm,
log: Res<ChatLog>,
+1 -1
View File
@@ -395,7 +395,7 @@ impl ChatLog {
///
/// Deviation: the reference holds every line that waits on its `$`-macro subject's name in
/// the name cache's callback list (`0x49daea` → `0x55f080`) until the name answers, with no
/// bound; here at most [`MAX_HELD_SUBJECT_LINES`] wait, the oldest dropped past that, as a
/// bound; here at most [`MAX_HELD_SUBJECT_LINES`] wait, the oldest dropped past that, because a
/// subject no player owns never answers.
fn hold(&self, item: Pending) {
let mut q = self.q();
+2 -2
View File
@@ -303,8 +303,8 @@ impl ChatSender<'_, '_> {
/// `0x508680` over `text` for drunk byte `drunk` (`[[player+0xe68]+0x1d]`, `0x49f61a`): its
/// strength is the byte cut to 100 and scaled by 0.01 (`0x49f62c`-`0x49f64e`), its buffer 256
/// bytes (`0x49f651`). Deviation: a line of 256 bytes or more, which reaches here only through
/// `chat_send`'s deviation for `0x49f604`, is slurred whole instead of cut to that buffer, so
/// it reaches the server whole for the refusal that deviation leaves to it.
/// `chat_send`'s deviation for `0x49f604`, is slurred whole instead of cut to that buffer,
/// because that way it reaches the server whole for the refusal that deviation leaves to it.
fn slur(&self, script: &UiScript, text: &str, drunk: u8) -> String {
let cap = if text.len() < 0x100 {
0x100
+1 -1
View File
@@ -625,7 +625,7 @@ fn write(script: &UiScript, channels: &super::edit::ChannelState, path: &std::pa
/// teardown (`0x499a80` from `0x490c55`) with no dirty check (`[0xb6e5c4]` is never read); host
/// state such as the zone mask never marks the file dirty.
/// - Deviation: the debounce also writes Lua's changes (`dirty`) after the quiet time, where the
/// reference writes only at teardown, so a crash loses at most a second of changes.
/// reference writes only at teardown, because that way a crash loses at most a second of changes.
///
/// A held reader has restored nothing, so neither writes, as `0x5afed0` skips a pending section
/// (`0x5afee0`).
+1 -1
View File
@@ -92,7 +92,7 @@ impl DuelState {
/// `SMSG_DUEL_COUNTDOWN` (`0x4d4ae0`): the seconds stored and `ProcessCountdown`'s body run
/// before its timer is armed, so the first line is the packet's; the line it owes.
/// Deviation: zero arms nothing, where the reference prints 0 and re-arms forever on the
/// Deviation: zero arms nothing, because the reference prints 0 and re-arms forever on the
/// `dec`'s wraparound, a bug vmangos never reaches (it sends 3000).
fn apply_countdown(&mut self, seconds: u32) -> Option<OwedLine> {
self.countdown = (seconds > 0).then(|| Countdown {
+8 -8
View File
@@ -53,20 +53,20 @@ impl Default for UiGammaLane {
/// `__ftol(pow(i / 255, gamma) · 65535)` (`0x591680`); there is no other whole-frame grade and
/// no `Brightness` or `Contrast` CVar.
///
/// Deviation: the reference skips that upload when windowed (`byte[dev+0x20b]` is `gxWindow`), and
/// every benilla mode is windowed, so the same curve applies in the compositor, where the slider
/// can move pixels: [`UiGammaNode`] raises the gamma byte the RAMDAC would have read to `gamma`
/// before its decode, the continuous form of the 256-entry ramp.
/// Deviation: the same curve applies in the compositor, where the slider can move pixels, because
/// the reference skips that upload when windowed (`byte[dev+0x20b]` is `gxWindow`) and every
/// benilla mode is windowed: [`UiGammaNode`] raises the gamma byte the RAMDAC would have read to
/// `gamma` before its decode, the continuous form of the 256-entry ramp.
#[derive(Resource, Debug, Clone, Copy, PartialEq)]
pub(crate) struct DisplayGamma(pub(crate) f32);
/// The registered `"1.0"`: the identity ramp, at which the pass changes no byte.
pub(crate) const DEFAULT_GAMMA: f32 = 1.0;
/// Deviation: the reference never clamps (`SetGamma` `0x4891f0` writes `-4.000000` for 5), but its
/// ramp is a fullscreen OS call and ours is the image, where an extreme exponent blacks out the
/// panel that would undo it. The range spans the stock slider's `[0.5, 1.5]` four times over; the
/// CVar keeps the value written and only its consumers clamp.
/// Deviation: the reference never clamps (`SetGamma` `0x4891f0` writes `-4.000000` for 5), but here
/// the consumers clamp because its ramp is a fullscreen OS call and ours is the image, where an
/// extreme exponent blacks out the panel that would undo it. The range spans the stock slider's
/// `[0.5, 1.5]` four times over; the CVar keeps the value written and only its consumers clamp.
pub(crate) const GAMMA_RANGE: std::ops::RangeInclusive<f32> = 0.25..=4.0;
impl Default for DisplayGamma {
+1 -1
View File
@@ -166,7 +166,7 @@ fn update_ticket_args(ticket: Option<&benilla_protocol::messages::GmTicket>) ->
/// (`0x5e7945`) the saved ebp, the return address into the dispatcher and the handler's first
/// argument.
///
/// Deviation: those three are stack residue, so benilla sends 0 for each.
/// Deviation: benilla sends 0 for each of those three because they are stack residue.
fn no_ticket_args() -> Vec<ScriptValue> {
vec![
ScriptValue::Int(0),
+2 -2
View File
@@ -37,8 +37,8 @@ impl InspectHonor {
///
/// Deviation: a new target zeroes the figures too, and dropping a held reply signals
/// `INSPECT_HONOR_UPDATE`, where `0x4c6f70` keeps the figures and clears only has-data
/// (`0xb71fc8`) and pending (`0xb71fcc`): a server that never answers the ask, as vmangos
/// often does not, would leave the last player's figures shown under the new one.
/// (`0xb71fc8`) and pending (`0xb71fcc`), because a server that never answers the ask, as
/// vmangos often does not, would leave the last player's figures shown under the new one.
pub(crate) fn set_target(&mut self, guid: u64) -> bool {
if guid == self.target {
return false;
+2 -2
View File
@@ -509,8 +509,8 @@ fn open_tail(world: &mut World, place: Place) {
/// (`0x4e35de`). In 1.12.1 a turn to a page not yet cached closes the book, which stays closed, and
/// a letter whose author's name is not cached shows without its From line, then closes when the
/// name lands. `0x4930d0`'s same-target test (`0x4930eb`-`0x4930f3`) only skips a unit's greeting,
/// so this is a bug. benilla keeps the book open and shows it again ([`show_at`]): the page or the
/// From line appears when it lands.
/// so this is a bug, and benilla keeps the book open and shows it again ([`show_at`]) because of
/// that: the page or the From line appears when it lands.
fn answered(world: &mut World, wait: Wait, place: Place) {
let Some(sess) = world
.resource_mut::<ItemTextOpen>()
+2 -2
View File
@@ -86,8 +86,8 @@ pub(crate) enum Layout {
/// with no name, belong to no frame and are dropped, as the reference applies them to a null frame
/// (`0x4905e6`); a name no frame has drops its lines at the restore the same way.
///
/// Deviation: a record's fields start empty. The reference's parser clears X, Y, W and the level
/// at each `Frame:` (`ebx`, `edi`, `esi`, `[ebp-0x4]` at `0x49045b`–`0x490469`) but not H
/// Deviation: a record's fields start empty because the reference's parser clears X, Y, W and the
/// level at each `Frame:` (`ebx`, `edi`, `esi`, `[ebp-0x4]` at `0x49045b`–`0x490469`) but not H
/// (`[ebp-0xc]`), so a record with no `H:` takes the one before's; benilla writes `H:` whenever it
/// writes `W:`, so only a hand-edited file reaches the slip, which would size a window from
/// another window's row.
+7 -5
View File
@@ -330,7 +330,8 @@ pub(crate) struct LootState {
/// signal's handlers (`0x4c2917`, `0x4c21dc`), cleared by the confirmed send (`0x4c281a`) and
/// by the next response (`0x4c1df5`), not by a close.
/// Deviation: it is the display row, where the reference's is the item index plus one
/// (`0x4c2885`), so an addon's `GetLootSlotInfo(arg1)` reads the row the dialog is for.
/// (`0x4c2885`), because that way an addon's `GetLootSlotInfo(arg1)` reads the row the dialog
/// is for.
pending_bind_confirm: Option<u32>,
/// The candidates `GetMasterLootCandidate` looked up and the name cache missed, each a
/// registration of the callback `0x4c2fb0` (dedupe flag 0, `0x4c2f61`), spent by the name's
@@ -861,9 +862,9 @@ impl LootState {
/// `0xb71b44` above 0: a row's item record is still being fetched, counted from the copier
/// (`0x4c1e77`) until its callback (`0x4c2ac0`); a negative answer releases the row, as
/// [`Window::records_pending`] has it. Deviation: a row cleared meanwhile stops counting, where
/// `ClearLootRow` (`0x4c1f00`) cancels its callback (`0x4c1f52`) and never lowers the count
/// (its writers are `0x4c1def`, `0x4c1e94` and `0x4c2af6` alone), so in 1.12.1 a group
/// [`Window::records_pending`] has it. Deviation: a row cleared meanwhile stops counting,
/// because `ClearLootRow` (`0x4c1f00`) cancels its callback (`0x4c1f52`) and never lowers the
/// count (its writers are `0x4c1def`, `0x4c1e94` and `0x4c2af6` alone), so in 1.12.1 a group
/// member's take of an uncached item before the answer leaves that corpse's window shut until
/// the next loot response resets the count (`0x4c1def`), and ours opens.
fn records_pending(&self, items: &Items) -> bool {
@@ -920,7 +921,8 @@ impl Default for LootConfig {
/// admits vmangos' type-1 answer for the item.
///
/// Deviation: our close paths clear it guid-matched, where `CloseInteraction` clears it
/// unconditionally (`0x48f2c9`), so closing an old window keeps a newer `CMSG_LOOT`'s latch.
/// unconditionally (`0x48f2c9`), because that way closing an old window keeps a newer `CMSG_LOOT`'s
/// latch.
#[derive(Resource, Default)]
pub(crate) struct LootLatch(pub(crate) Option<u64>);
+6 -6
View File
@@ -19,18 +19,18 @@
//! free (`0x61b760`). Stock `GroupLootFrame_OpenNewFrame` has four frames and drops a fifth
//! silently, so an unqueued fifth could never be voted.
//!
//! Deviation: the open count is the number of rolls announced and not closed. The reference keeps
//! a counter that its server-close paths decrement for any roll whose closed byte is clear
//! Deviation: the open count is the number of rolls announced and not closed, because the reference
//! keeps a counter that its server-close paths decrement for any roll whose closed byte is clear
//! (`0x61b6f8`, `0x61bc17`), a deferred roll that never counted up included, so it under-counts
//! and lets a fifth frame open; here a deferred roll's close changes nothing.
//!
//! A result's line, its `CANCEL_LOOT_ROLL` and the promotion after it wait for the result's item
//! record and, for a win, the winner's name (`0x61b96a` → `0x61b9a0` → `0x61b9bf` → `0x61b9e0`,
//! `0x61b5c0` → `0x61b640`); until then the roll stays listed. Deviation: a refused record or name
//! closes the roll unspoken. In 1.12.1 the failure callbacks (`0x61bc80`, `0x61bd50`, `0x61b7d0`)
//! free the record with no `CANCEL_LOOT_ROLL` and leave the open count unlowered, so its frame
//! stays up, empty and dead, until a `/reload`, and holds one of the four frames, so a later
//! fourth roll never shows; benilla closes the roll instead.
//! closes the roll unspoken because in 1.12.1 the failure callbacks (`0x61bc80`, `0x61bd50`,
//! `0x61b7d0`) free the record with no `CANCEL_LOOT_ROLL` and leave the open count unlowered, so
//! its frame stays up, empty and dead, until a `/reload`, and holds one of the four frames, so a
//! later fourth roll never shows; benilla closes the roll instead.
//!
//! A promoted roll's `arg2` is still the wire's countdown (`0x61b430` and `0x61b79a` push
//! `node+0x3c`, which nothing rewrites); only the bar is short, as `GetLootRollTimeLeft` counts
+2 -2
View File
@@ -19,8 +19,8 @@ const LINE_BYTES: usize = 0x100;
const NAME_DELIMS: &[u8] = b" \"\r\n";
/// A token as its buffer kept it. Deviation: the reference's 255-byte cut (`0x4f0677`,
/// `0x4f0b93`) can end inside a character and keep its stray bytes; a `String` must stay UTF-8,
/// so the cut backs off to the character's start.
/// `0x4f0b93`) can end inside a character and keep its stray bytes; here the cut backs off to the
/// character's start because a `String` must stay UTF-8.
fn token_text(bytes: Vec<u8>) -> String {
match String::from_utf8(bytes) {
Ok(text) => text,
+1 -1
View File
@@ -231,7 +231,7 @@ fn on_session_end(
///
/// Deviation: with no usable paper the reference reads the first record of an empty list through
/// a null pointer (`0x4ad561`-`0x4ad56d`, `mov eax,[eax]` with `eax` 0) and faults; benilla keeps
/// the row's wire id, so the inbox still opens.
/// the row's wire id, because that way the inbox still opens.
pub(super) fn mail_list(
mails: Vec<MailListEntry>,
mail: &mut MailOpen,
+4 -4
View File
@@ -263,8 +263,8 @@ const ADDONS_PREFIX: &str = "Interface/AddOns/";
/// The addon folder, `<benilla-config>/AddOns/`, or `None` (always under `$WOW_CAPTURE`); addon
/// textures and fonts resolve onto it too. Deviation: the reference reads the install's
/// `Interface\AddOns\`; benilla reads an install and does not live in one, so its addons live in
/// its own state folder, and only there.
/// `Interface\AddOns\`; its addons live in its own state folder, and only there, because benilla
/// reads an install and does not live in one.
pub(crate) fn root() -> Option<PathBuf> {
root_from(crate::local_state::home())
}
@@ -681,8 +681,8 @@ pub(super) fn save_enable_state(script: &UiScript, identity: Option<&(String, St
/// file, before `AddOns.txt`). No autosave and no dirty bit, as in the reference, whose write gate
/// is the record's loaded byte: an addon that never loaded is not written. A scope declaring no
/// names loses its file (`0x51f859`, `0x51f9ac`) when it ran cleanly this session.
/// Deviation: that file is renamed to `<Name>.lua.bak`, where the reference deletes it, so an addon
/// update that drops its toc line costs the player nothing they cannot get back.
/// Deviation: that file is renamed to `<Name>.lua.bak`, where the reference deletes it, because
/// that way an addon update that drops its toc line costs the player nothing they cannot get back.
pub(super) fn save_addon_variables(script: &mut UiScript, identity: Option<&(String, String)>) {
let account = crate::local_state::addon_saved_account_dir();
let character = identity.and_then(|(r, c)| crate::local_state::addon_saved_character_dir(r, c));
@@ -1461,8 +1461,8 @@ fn convert_entry(
);
}
// Deviation: the V-plate border is resampled sharp to the quad's physical
// size rather than GPU-magnified, so its 1 px bevel stays crisp; keyed by
// that size, so only a resize re-rasterises it.
// size rather than GPU-magnified, because that way its 1 px bevel stays
// crisp; keyed by that size, so only a resize re-rasterises it.
let px = |v: f32| (v * dpi).round().max(1.0) as u32;
a.resampled_sprite(
p,
+3 -2
View File
@@ -346,8 +346,9 @@ pub(crate) fn watch_glue_aspect(
/// Deviation: the reference's size callback only re-scales `GlueParent` and re-lays it out
/// (`0x46ac51`, `0x46ac56`), as its root keeps `gxResolution`'s aspect until a rebuild (`0x41ad10`
/// at `0x46a828`) and its window keeps that shape while dragged (`0x5989d7`); here the root is the
/// window's, which takes any shape, so the box follows it on every resize, where a rebuild would
/// reset the screen (the create screen's character and name, the typed password, the selection).
/// window's, which takes any shape, so the box follows it on every resize, without a rebuild
/// because that would reset the screen (the create screen's character and name, the typed password,
/// the selection).
pub(super) fn on_glue_screen_resized(script: &UiScript) {
if let Err(e) = script.run(GLUE_PARENT_RESEAT) {
error!("ui_script: GlueParent re-seat: {e}");
+3 -3
View File
@@ -1519,9 +1519,9 @@ impl GlueMgr {
///
/// Deviation: the reference's handler completes whichever operation is current; here only the
/// character list's does, in state 3, and a reply of another cause (a roster nothing of the
/// screen asked for) or of another link (before a realm change) completes nothing. Those
/// replies cannot be told from the operation they would be applied to in the reference, which
/// has one link and one request in flight; here the IO thread's park serves several.
/// screen asked for) or of another link (before a realm change) completes nothing, because the
/// reference has one link and one request in flight and so cannot tell those replies from the
/// operation they would be applied to, while here the IO thread's park serves several.
fn on_roster(&mut self, roster: Vec<Character>, cause: RosterCause, epoch: u64) {
if cause != RosterCause::Asked
|| epoch != self.link
+5 -5
View File
@@ -760,11 +760,11 @@ pub(crate) fn feed_key(
// The char walk gates no binding (`0x765df0` reaches no keybinding), so whether a
// frame took the character is not recorded: the key-down already decided that.
//
// Deviation: the character is the platform's Unicode text, so a player can type any
// language on any platform.
// The reference's window is ANSI (`RegisterClassExA`, `DefWindowProcA`), so `WM_CHAR`
// carries a byte of the system code page, which `0x42d185`-`0x42d1f5` maps through a
// table `GetACP` picks; a character outside that code page never arrives.
// Deviation: the character is the platform's Unicode text, because that way a player can
// type any language on any platform. The reference's window is ANSI (`RegisterClassExA`,
// `DefWindowProcA`), so `WM_CHAR` carries a byte of the system code page, which
// `0x42d185`-`0x42d1f5` maps through a table `GetACP` picks; a character outside that code
// page never arrives.
if let Some(text) = &ev.text {
script.char_input(text);
}
@@ -177,7 +177,8 @@ fn new_vm<E: std::fmt::Display>(
channels.mirror_into(&mut script);
}
// The reference seeds `rand` with the clock before `WinMain` (`0x5b800b`). Deviation: a
// capture run (`scenario_active`) keeps the CRT's default seed, 1, so its frames reproduce.
// capture run (`scenario_active`) keeps the CRT's default seed, 1, because that way its frames
// reproduce.
if crate::run_mode::scenario_active() {
script.lua().srand(1);
}
+1 -1
View File
@@ -2,7 +2,7 @@
//! line at a time as the reference's log layer writes one (`0x65ac20`): the stamp
//! `"%u/%u %02u:%02u:%02u.%03u "` (`0x866aa0`), the text, then `"\r\n"` (`0x82edfc`). The glue
//! build writes its own `Logs\GlueXML.log` (`0x837024`, opened at `0x46a8ce`) the same way.
//! Deviation: the files are under `benilla-config/Logs/`, since the install is read-only.
//! Deviation: the files are under `benilla-config/Logs/`, because the install is read-only.
use std::io::Write as _;
@@ -20,10 +20,10 @@
//! client sets neither redirect flag (`0x4000`, `0x8000`); what WinINet does with an
//! http-to-https redirect is the OS's, not in the client's bytes, and benilla does not follow one.
//!
//! Deviation: the body is read with 0x4000-byte reads filled to the end of the body, so what a
//! read returns does not depend on how the network cut the packets; and the reading stops once
//! the buffer is full, since no later byte can change it.
//! Deviation: the connection is direct. The reference opens its session with access type 0
//! Deviation: the body is read with 0x4000-byte reads filled to the end of the body, because that
//! way what a read returns does not depend on how the network cut the packets; and the reading
//! stops once the buffer is full, because no later byte can change it.
//! Deviation: the connection is direct because the reference opens its session with access type 0
//! (`InternetOpenA`, `0x7a6d97`), the system's proxy settings, which benilla does not read.
use std::io::{self, BufRead, BufReader, Read, Write};
+4 -4
View File
@@ -75,7 +75,7 @@ pub(crate) fn parse(
.collect();
// Deviation: a filter of 127 bytes or more puts the copy's NUL in the name buffer's first byte
// (`0x5aec1a`), so a name an earlier tag set is scanned as more filter. benilla ends the copy
// at its own NUL: the run-on reads one stack buffer through another.
// at its own NUL because the run-on reads one stack buffer through another.
copy.push(b' ');
let mut builder = Builder {
tags,
@@ -161,9 +161,9 @@ fn is_space(c: u32) -> bool {
match c as u16 {
0xffff => false,
w @ 0..=0xff => matches!(w, 0x09..=0x0d | 0x20),
// Deviation: `C1_SPACE` from `GetStringTypeW` is the Windows system's own table, which
// no file of the client holds; benilla answers Unicode's `White_Space` for those units,
// and nothing for a lone surrogate.
// Deviation: benilla answers Unicode's `White_Space` for those units, and nothing for a
// lone surrogate, because `C1_SPACE` from `GetStringTypeW` is the Windows system's own
// table, which no file of the client holds.
w => char::from_u32(u32::from(w)).is_some_and(char::is_whitespace),
}
}
+3 -3
View File
@@ -15,9 +15,9 @@
//! - `ConfirmSummon()` (`0x48b770`) checks no deadline, so two Accepts are two packets; vmangos
//! ignores the guid and checks its own `m_summon_expire`.
//!
//! Deviation: the latch is dropped at session end and an Accept with nothing latched sends
//! nothing, so no stale or zero guid reaches the server. The reference never clears the guid or
//! zone, only the deadline (`0x49014a`), so its `ConfirmSummon()` sends whatever is left.
//! Deviation: the latch is dropped at session end and an Accept with nothing latched sends nothing,
//! because that way no stale or zero guid reaches the server. The reference never clears the guid
//! or zone, only the deadline (`0x49014a`), so its `ConfirmSummon()` sends whatever is left.
use benilla_ui::script::SummonConfirmUiState;
use bevy::prelude::*;
+2 -2
View File
@@ -360,8 +360,8 @@ impl TextEngine {
/// [`Self::ensure_str`] without the raster: shape what is missing, so a string that is measured
/// and never drawn never touches the sheet. Deviation: the reference's measure kernels
/// (`0x5c6940`, `0x5c6b70`, `0x5c6c50`, `0x5c7300`, `0x5c7470`) rasterize each character, but
/// the step law reads only the advance, so skipping the bitmap changes no width.
/// (`0x5c6940`, `0x5c6b70`, `0x5c6c50`, `0x5c7300`, `0x5c7470`) rasterize each character; here
/// the bitmap is skipped, which changes no width because the step law reads only the advance.
pub(super) fn ensure_metrics(&mut self, face: usize, ppem: u16, text: &str) {
for ch in text.chars() {
self.ensure_char(face, ppem, None, ch);
+3 -2
View File
@@ -57,8 +57,9 @@ fn fill_gray(coverage: u8) -> u8 {
/// 4. Pack: the coverage as a 4-bit gray ([`fill_gray`]), so the pure ring is black and the fill
/// ramps toward it.
///
/// Deviation: `r × floor(dpi)` passes, so the ring keeps its logical weight on a whole-number
/// high-DPI raster; a fractional scale such as 1.5 rounds down to the reference's one texel a pass.
/// Deviation: `r × floor(dpi)` passes, because that way the ring keeps its logical weight on a
/// whole-number high-DPI raster; a fractional scale such as 1.5 rounds down to the reference's one
/// texel a pass.
///
/// It draws as one quad per glyph (`0x5ccbe0`) whose vertex colour tints the fill and leaves the
/// ring black. Returns `(rgba, out_w, out_h, pad)`: the cell grows `pad` texels each side (the
+2 -2
View File
@@ -19,8 +19,8 @@
//! or `AutoVsync` (a maximize is a resize). The fault is in the driver's own DXGI present
//! (`d3d11.dll`): it stays with `VK_LAYER_NV_present` disabled, and the validation layer reports
//! nothing before it. `Mailbox`, `AutoNoVsync` and DX12 survive, so there
//! "vsync on" is `AutoNoVsync` paced to the monitor's refresh rate ([`present_choice`]), which
//! keeps the synced look without the crash. `$WOW_PRESENT=fifo` restores plain `Fifo`.
//! "vsync on" is `AutoNoVsync` paced to the monitor's refresh rate ([`present_choice`]) because
//! that keeps the synced look without the crash. `$WOW_PRESENT=fifo` restores plain `Fifo`.
use benilla_ui::script::{ScreenResolution, UiScript};
use bevy::prelude::*;
+8 -7
View File
@@ -16,7 +16,7 @@
//! large and soft at high resolutions.
//! - Highlight: the mouseover or the target (`0x606f20` into `0x607080`) lights the plate.
//! Deviation: the bar fill brightens by `LIT_BOOST` in place of the reference's ADD
//! `Nameplate-Glow` rim, which reads as hard edge lines under benilla's linear blending.
//! `Nameplate-Glow` rim, because that reads as hard edge lines under benilla's linear blending.
//! Hovering a plate makes its unit the mouseover (`0x7cb850`) and turns the name yellow; a
//! click on it selects.
//! - With a target, every other plate dims ([`DIM_ALPHA`]).
@@ -191,14 +191,15 @@ fn con_color(pl_level: u32, unit_level: u32) -> [f32; 4] {
/// Snap a logical-pixel coordinate onto the device pixel grid, so the sharp-resampled border
/// blits 1:1 at any scale factor. Deviation: the reference draws at fractional device
/// coordinates; the snap keeps the magnified border crisp. Shared with the chat bubble.
/// coordinates; here the position snaps because that keeps the magnified border crisp. Shared with
/// the chat bubble.
pub(crate) fn device_snap(v: f32, scale: f32) -> f32 {
(v * scale).round() / scale
}
/// Deviation: the reference's plates grow with the diagonal without limit (`0x7705b0`), which
/// reads too big and soft; past this diagonal, where the frame is the border art's native
/// 128x32 px, the basis grows at [`PLATE_GROWTH_DAMP`] of that rate.
/// Deviation: the reference's plates grow with the diagonal without limit (`0x7705b0`); past this
/// diagonal, where the frame is the border art's native 128x32 px, the basis grows at
/// [`PLATE_GROWTH_DAMP`] of that rate, because unlimited growth reads too big and soft.
const PLATE_DIAG_KNEE: f32 = 1280.0;
/// 0 pins the native size, 1 is the reference's rate.
const PLATE_GROWTH_DAMP: f32 = 0.5;
@@ -668,8 +669,8 @@ fn drive_vplates(
name_height: text_px(NAME_H, basis) / seam,
level_height: text_px(LEVEL_H, basis) / seam,
// Deviation: a constant one-logical-pixel shadow, not the reference's 0.001 gx,
// which draws 2 px thick and too heavy at larger windows. Divided by the seam,
// which the shared text arm would otherwise scale it by.
// because that draws 2 px thick and too heavy at larger windows. Divided by the
// seam, which the shared text arm would otherwise scale it by.
shadow_offset: 1.0 / seam,
},
states,
+2 -1
View File
@@ -343,7 +343,8 @@ struct Stores<'w> {
///
/// Deviation: the reference keeps a saved store's ask "pending" for the rest of the process
/// (`0x555740` clears only the stores built with persistence off), so a query a lost link dropped
/// is never sent again until restart; benilla's reconnect asks it again.
/// is never sent again until restart; benilla's reconnect asks it again because the name or text it
/// was for would otherwise stay blank until restart.
fn clear_saved_asks(In(ev): In<SessionEvent>, mut stores: Stores) {
if !matches!(ev, SessionEvent::Disconnected { .. }) {
return;
+1 -1
View File
@@ -214,7 +214,7 @@ fn water_ripples(world: &mut World, args: &str) -> Vec<String> {
/// (`0x867c50`), a NaN passing both; else `__ftol(v · 255)` of each, the first in the top byte,
/// goes to the shadow colour setter `0x672590`. Prints nothing when it takes them.
///
/// Deviation: a float the scan leaves unread is 0.0, where the reference reads whatever its
/// Deviation: a float the scan leaves unread is 0.0, because the reference reads whatever its
/// uninitialised stack slot holds.
fn set_shadow(world: &mut World, args: &str) -> Vec<String> {
let mut v = [0.0f32; 4];
+1 -1
View File
@@ -37,7 +37,7 @@ impl AnimRng {
}
/// The reference's `srand(GetTickCount())`: a wall-clock seed, once per process. Deviation: a
/// capture run (`deterministic`) keeps the default seed, so its frames reproduce.
/// capture run (`deterministic`) keeps the default seed, because that way its frames reproduce.
pub fn seed_for_session(&mut self, deterministic: bool) {
if deterministic {
return;
+1 -1
View File
@@ -232,7 +232,7 @@ const LISTFILE_LINE_MAX: usize = 0x103;
/// `EnumerateArchiveListfile` (`0x648fb0`) over one listfile, with `0x4014f0`'s prefix test: a line
/// ends at a CR or LF (`0x649022`, `0x649026`); an empty one, and one whose text from its last dot
/// is `.md5` without case (`0x865bd8`, `0x64905f`), is passed over. Deviation: a last line with no
/// CR or LF after it is passed too, where `0x648fb0` ends at the buffer's end without a flush
/// CR or LF after it is passed too, because `0x648fb0` ends at the buffer's end without a flush
/// (`0x64907f`) and drops it, a bug that loses a listfile's last name when it lacks a newline.
fn listfile_lines(listfile: &[u8], prefix: &str, out: &mut Vec<String>) {
let mut line: Vec<u8> = Vec::with_capacity(LISTFILE_LINE_MAX);
+1 -1
View File
@@ -60,7 +60,7 @@ fn schema() -> Schema {
}
/// Read `TransportAnimation.dbc` off the patch chain. Deviation: a path that cannot drive a cycle
/// is dropped and its GO stays at its spawn point, where the reference faults or hangs: a last
/// is dropped and its GO stays at its spawn point, because the reference faults or hangs: a last
/// frame at 0 divides by zero (`0x5f62a4`), and a first frame past 0, one frame among them, leaves
/// a time before it in no bracket, which `0x5f62bc`-`0x5f62e7` seek forever.
pub fn load_elevator_paths(chain: &mut Chain) -> Result<ElevatorPaths> {
+1 -1
View File
@@ -111,7 +111,7 @@ impl EmoteTextCatalog {
}
/// `SStrPrintf` (`0x64a7f0`) reduced to the `%s` these templates use. Deviation: a surplus `%s`
/// stays verbatim, where the reference would print stack garbage; no shipped row has one.
/// stays verbatim, because the reference would print stack garbage; no shipped row has one.
fn fill(template: &str, args: &[&str]) -> String {
let mut out = String::with_capacity(template.len() + 16);
let mut rest = template;
+3 -2
View File
@@ -30,8 +30,9 @@ pub enum LooseOpen {
///
/// Deviation: a folder is listed the first time a key reaches it, where the reference lists every
/// folder at the first open (`0x646ea0`), so a file added to a folder no key has reached is found
/// before the index empties; the whole walk takes minutes and gigabytes where the install folder
/// holds or links a large tree, as a Wine prefix's `z:` drive links `/`.
/// before the index empties; folders are listed lazily because the whole walk takes minutes and
/// gigabytes where the install folder holds or links a large tree, as a Wine prefix's `z:` drive
/// links `/`.
#[derive(Debug, Default)]
pub struct LooseIndex {
/// The base folder's walk, then `Data\`'s.
@@ -127,8 +127,9 @@ impl<V: Lerp> KeyAnim<V> {
/// The reference's track read at `t_ms` over the key window `[lo, hi]` (`0x713d50`, interp
/// dispatch `0x71af20`): `keys[lo]` for a collapsed window (`lo >= hi`), else the last window key
/// at or before `t_ms`, held for a step track or past the last key, else lerped toward the next.
/// Deviation: the fraction is clamped to `[0, 1]`; the reference extrapolates, which for a window
/// whose brackets miss the band can push a fix16 factor negative and cull the batch on a quirk.
/// Deviation: the fraction is clamped to `[0, 1]` because the reference extrapolates, which for a
/// window whose brackets miss the band can push a fix16 factor negative and cull the batch on a
/// quirk.
pub(super) fn sample_window<V: Lerp>(
keys: &[(u32, V)],
step: bool,
+2 -1
View File
@@ -380,7 +380,8 @@ pub struct TextureStage {
/// A flat ground-plane quad: four vertices on one horizontal plane at or just above z = 0 (hover
/// ≤ [`GROUND_HOVER_MAX`]), an axis-aligned XY rectangle wholly on one bone, as ground-ring spells
/// author them (Battle Shout's crescents). The reference draws them as geometry slopes bury.
/// Deviation: the fx lane redraws them as projected decals (`ground_fx`) so they drape the terrain.
/// Deviation: the fx lane redraws them as projected decals (`ground_fx`) because that way they
/// drape the terrain.
#[derive(Debug, Clone, Copy)]
pub struct GroundQuad {
/// Posed through the joint's matrix × the bone's inverse bind pose, as a skinned vertex.
+3 -3
View File
@@ -687,9 +687,9 @@ pub fn substitute_parts(
};
match token_value(letter, slot, target, ctx, scale, at) {
Some((sub, val)) => {
// Deviation: every token keys the `$l` plural. The reference's `$a`, `$d`, `$t`,
// `$e`, `$c`, `$p`, `$f`, `$F` and `$z` arms never write `[0xbe0b84]`, so its `$l`
// keys on the number before them, and Blizzard's "$s1 … every $t1
// Deviation: every token keys the `$l` plural because the reference's `$a`, `$d`,
// `$t`, `$e`, `$c`, `$p`, `$f`, `$F` and `$z` arms never write `[0xbe0b84]`, so its
// `$l` keys on the number before them, and Blizzard's "$s1 … every $t1
// $lsecond:seconds;" reads "every 1 seconds" in 1.12.1. We print the grammar the
// text means.
last_value = val;
+9 -8
View File
@@ -6,21 +6,22 @@
//! Deviation: grayscale types 3 and 11 decode where the reference refuses them, because no shipped
//! or addon file is one and removing working code would buy nothing.
//!
//! Deviation: colour maps of 15, 16 and 32 bits decode at the entry width the file declares, where
//! the reference has no defined result to copy for 15 and 16 bits and only a garble for 32. It
//! sizes an entry as `(min(bits / 3, 8) · 3) >> 3` bytes (`0x5a3b40`): 3 for 24 and 32 bits, 1 for
//! 15 and 16. A 16-bit map is read short and copied one byte a pixel (`0x5a3890`-`0x5a38fa`) into
//! the three-byte-a-pixel buffer `0x5a3820` allocates and never zeroes, so the rest of the colours
//! are heap garbage; a 32-bit map is read a byte an entry short and the pixels start inside it.
//! Deviation: colour maps of 15, 16 and 32 bits decode at the entry width the file declares,
//! because the reference has no defined result to copy for 15 and 16 bits and only a garble for 32.
//! It sizes an entry as `(min(bits / 3, 8) · 3) >> 3` bytes (`0x5a3b40`): 3 for 24 and 32 bits, 1
//! for 15 and 16. A 16-bit map is read short and copied one byte a pixel (`0x5a3890`-`0x5a38fa`)
//! into the three-byte-a-pixel buffer `0x5a3820` allocates and never zeroes, so the rest of the
//! colours are heap garbage; a 32-bit map is read a byte an entry short and the pixels start inside
//! it.
//!
//! Deviation: a colour-mapped image whose descriptor has alpha bits decodes as its picture, where
//! Deviation: a colour-mapped image whose descriptor has alpha bits decodes as its picture, because
//! the reference has no defined result to copy. `0x5a3820` sizes each pixel as 24 plus the alpha
//! bits (`0x5a382b`-`0x5a383d`, stored at `0x5a3973`) but copies 3-byte entries (`0x5a38f3`), then
//! `0x5a3b80` moves `(bpp + 7) >> 3` bytes a pixel (`0x5a3c00`-`0x5a3c13`): each pixel after the
//! first takes its bytes from its neighbours, and the last read heap that was never written.
//!
//! Deviation: a truecolour image whose depth less its alpha bits is 24, but whose depth is not 24
//! or 32, is refused where the reference crashes. The check `0x5a37e0` admits it (25 to 31 bits
//! or 32, is refused because the reference crashes. The check `0x5a37e0` admits it (25 to 31 bits
//! with 1 to 7 alpha bits, 33 to 39 with 9 to 15), then `0x5a41f0` returns 0 for a depth that is
//! not 32, and the copy at `0x4492c4` reads through that null pointer: there is no result to match.
//!
+4 -4
View File
@@ -79,10 +79,10 @@ impl TransportTimetable {
/// `accelRate`, each made a float through `fild` of the zero-extended value. `None` for no
/// node or a period of 0, which the reference ticks with a modulus of 1 (`0x5f50d5`) and no
/// leg ending after 0, so it never moves the ship (`0x5f511c`), as a ship with no timetable.
/// Deviation: `None` too where the reference's build overruns its stack or reads memory it
/// never wrote (`transports#path-faults`), so the ship stays at its spawn: more than 1024
/// nodes in a leg (`0x5f9530`), a leg of 3 nodes or fewer (`0x4532e3`) and a stop on a leg's
/// last node (`0x5f91a1`). No shipped path reaches one.
/// Deviation: `None` too, so the ship stays at its spawn, because there the reference's build
/// overruns its stack or reads memory it never wrote (`transports#path-faults`): more than
/// 1024 nodes in a leg (`0x5f9530`), a leg of 3 nodes or fewer (`0x4532e3`) and a stop on a
/// leg's last node (`0x5f91a1`). No shipped path reaches one.
pub fn build(nodes: &[TaxiPathNode], move_speed: u32, accel_rate: u32) -> Option<Self> {
let speed = f64::from(move_speed) as f32;
let accel = f64::from(accel_rate) as f32;
+2 -2
View File
@@ -30,8 +30,8 @@ pub fn c_int(n: f64) -> i32 {
/// The longest string the libraries build, `INT32_MAX` bytes: more than the 1.12 client's 32-bit
/// address space could hold. Deviation: the C build grows a string while `realloc` succeeds, and
/// a failure is the catchable `not enough memory`; Rust's allocator aborts the process instead,
/// and Linux grants far more than it can fill, so past this bound the libraries raise C's message.
/// a failure is the catchable `not enough memory`; past this bound the libraries raise C's message
/// because Rust's allocator aborts the process instead, and Linux grants far more than it can fill.
pub const MAX_STRING: usize = i32::MAX as usize;
/// 5.1's `LUAI_MAXCSTACK`: the most values a native's frame may hold.
+2 -1
View File
@@ -229,7 +229,8 @@ fn max(vm: &mut Vm, nargs: usize) -> R<usize> {
/// `RAND_MAX` is 32767. The client's state is the main thread's, seeded with the millisecond
/// clock before `WinMain` (`0x5b800b` → `0x5d1c70`, `srand` at `0x5d1c8b`). Deviation: the
/// client's one process-wide stream is shared with its engine draws; here each VM holds its own,
/// seeded from the wall clock when it is built, so a fork or a clone draws its own sequence.
/// seeded from the wall clock when it is built, because that way a fork or a clone draws its own
/// sequence.
#[derive(Clone)]
pub(crate) struct CrtRand(u32);
+4 -4
View File
@@ -95,8 +95,8 @@ fn upper(vm: &mut Vm, nargs: usize) -> R<usize> {
/// `str_lower`'s loop: decode a code point, map it, encode it, advance by the bytes the decoder
/// read (`0x7fbe8a`). A string of ASCII without a NUL maps byte by byte, as the loop would.
///
/// Deviation: the decoder reads a NUL as no bytes (`0x7fbeda`), so the client's loop never
/// passes one and the call never returns; here the result ends there.
/// Deviation: here the result ends there, because the decoder reads a NUL as no bytes (`0x7fbeda`),
/// so the client's loop never passes one and the call never returns.
fn case_map(vm: &mut Vm, nargs: usize, map: fn(u32) -> u32) -> R<usize> {
let s = vm.check_str(nargs, 1)?;
let r = vm.heap.intern_with(|h, b| {
@@ -233,8 +233,8 @@ fn byte(vm: &mut Vm, nargs: usize) -> R<usize> {
/// function with no upvalues (`0x6f4383`, `0x6f438a`), else `unable to dump given function`
/// (`0x882284`). Deviation: the
/// client writes 5.0 bytecode (`luaU_dump`, `0x6fb600`); benilla writes 5.1's header and the
/// function's source name. The client loads no binary chunk (`0x6f6a15`), so the bytes are only
/// data, and matching them would need 5.0's code generator.
/// function's source name because the client loads no binary chunk (`0x6f6a15`), so the bytes are
/// only data, and matching them would need 5.0's code generator.
fn dump(vm: &mut Vm, nargs: usize) -> R<usize> {
vm.check_function(nargs, 1)?;
let source = match vm.arg(nargs, nargs - 1).unpack() {
+3 -3
View File
@@ -32,9 +32,9 @@ pub fn open(vm: &mut Vm) {
}
/// The most elements one `insert` or `remove` may shift. Deviation: the client's loops have no
/// bound (`0x7fb728`, `0x7fb7e0`), but a remembered size is whatever an addon wrote, and the
/// instruction-budget hook never sees a native's loop, so one over two billion slots would hang
/// the client.
/// bound (`0x7fb728`, `0x7fb7e0`); here they are bounded because a remembered size is whatever an
/// addon wrote, and the instruction-budget hook never sees a native's loop, so one over two billion
/// slots would hang the client.
const MAX_SHIFT: i64 = 1_000_000;
/// `luaB_getn` (`0x7fb630`).
@@ -6,7 +6,7 @@
//!
//! With an install directory it also derives that install's own digest the way the 1.12 client
//! does, HMAC-SHA1 keyed by the challenge over five binaries, and says whether it is the stock
//! one. Deviation: benilla always sends the published constant instead, since an install without
//! one. Deviation: benilla always sends the published constant instead, because an install without
//! those binaries could never answer for itself.
//!
//! Usage: `cargo run -p benilla-protocol --example version_check_probe -- <host[:port]> <user>
+1 -1
View File
@@ -375,7 +375,7 @@ pub fn read_proof_reply(r: &mut impl Read) -> Result<Option<[u8; 20]>> {
let server_proof = read_array::<20>(r)?;
// Deviation: the survey id is read and dropped, and no survey runs, where the reference keeps
// it on the link (`0x5bad53`) for the `Survey.dll` it downloads and calls (`GetSurveyId
// 0x5ab620`, called at `0x474064`). benilla runs no code a server sends
// 0x5ab620`, called at `0x474064`). benilla does this because it runs no code a server sends
// ([`AuthReject::reply`] gives the rule).
let _hardware_survey_id = read_u32_le(r)?;
Ok(Some(server_proof))
+3 -2
View File
@@ -1149,7 +1149,8 @@ fn decode_objects(first_pass: bool, objects: Vec<Object>) -> Vec<SessionEvent> {
// Deviation: an out-of-range list past the first block reads no body in the write
// passes (`0x466076`), which misread every block behind it, while the notify pass
// reads past it (`0x465fd0`) and stays in step; benilla reads the list and drops it,
// keeping the blocks behind it whole.
// keeping the blocks behind it whole, because the faults the reference's passes have on
// malformed packets are recorded, not copied.
Object::OutOfRange { guids } => {
if at == 0 {
first.push(SessionEvent::ObjectsRemoved(guids));
@@ -1166,7 +1167,7 @@ fn decode_objects(first_pass: bool, objects: Vec<Object>) -> Vec<SessionEvent> {
// Deviation: the flagged pass reads a movement block's guid as 8 raw bytes
// (`0x467140`, `0x46714d`) where the wire packs it, so unless the packed guid happens
// to be 8 bytes the reference misreads the rest of that pass; benilla reads the block
// once, packed, as pass 0 does (`0x467080`).
// once, packed, as pass 0 does (`0x467080`), because of the misreading just described.
Object::Movement { guid, movement } => {
if let Some((position, orientation)) = movement.position {
second.push(SessionEvent::ObjectMove {
@@ -185,7 +185,7 @@ pub struct BattlefieldPositions {
pub carrier: Option<BattlefieldPosition>,
}
/// The client's 40-entry store. Deviation: a larger count keeps the first 40, where the
/// The client's 40-entry store. Deviation: a larger count keeps the first 40, because the
/// reference's handler writes past the store.
pub const BATTLEFIELD_POSITIONS_MAX: usize = 40;
@@ -3,9 +3,9 @@
//!
//! Deviation: string reads are unbounded. The reference `0x4191b0` caps each field, counting the
//! NUL (roster name `0x30`, notes `0x80`, MOTD `0x200`, info `0x7d0`, guild name `0x60`, rank name
//! `0x40`), and on overflow empties it and no-ops every later read in the packet. Unbounded reads
//! cannot poison a packet, and every cap is far above the server's own length limits, so a
//! legitimate packet reads the same.
//! `0x40`), and on overflow empties it and no-ops every later read in the packet. Reads stay
//! unbounded because unbounded reads cannot poison a packet, and every cap is far above the
//! server's own length limits, so a legitimate packet reads the same.
use std::io::{self, Read};
+2 -2
View File
@@ -69,7 +69,7 @@ impl PetActionEntry {
((self.packed >> 24) & 0x3F) as u8
}
/// Client types 1-5 take the spell branch. Deviation: a type outside 1-7 reads as nothing
/// rather than the reference's default arm, which under-pushes its returns.
/// rather than the reference's default arm, because that arm under-pushes its returns.
pub fn is_spell(self) -> bool {
(PET_TYPE_SPELL_FIRST..=PET_TYPE_SPELL_LAST).contains(&self.kind())
}
@@ -189,7 +189,7 @@ pub(super) fn read_pet_spells(r: &mut &[u8]) -> io::Result<PetSpells> {
/// The trailing cooldown block. The client reads `u8 count` then 12-byte entries
/// `{u16 spell, u16 category, u32, u32}` (`0x4bda58`); vmangos writes `u16 count` then 14-byte
/// entries with a `u32` spell (`Unit.cpp:11245-11278`). Deviation: both are read, told apart
/// exactly by the tail length (`12n` vs `1 + 14n`), since the 1.12 client mis-parses vmangos's.
/// exactly by the tail length (`12n` vs `1 + 14n`), because the 1.12 client mis-parses vmangos's.
fn read_cooldown_block(r: &mut &[u8]) -> io::Result<Vec<PetSpellCooldown>> {
let count = usize::from(read_u8(r)?);
// At `count == 0` vmangos still leaves its `u16` count's high byte; it is consumed so the
+11 -8
View File
@@ -20,7 +20,8 @@ use super::{recv_packet, send_packet, LinkTraffic};
const HANDSHAKE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
/// Read timeout in the login queue, where updates can be minutes apart. Deviation: the reference
/// waits forever; a bound tells a server that died mid-queue from a long wait.
/// waits forever; here the wait is bounded because that tells a server that died mid-queue from a
/// long wait.
const QUEUE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300);
/// The world server refused the session. Its code is from `messages`' `AUTH_*` block, not
@@ -63,7 +64,7 @@ pub(super) struct LinkFacts {
/// Roster guid to race, so [`WorldSession::player_login`] can pick the chat language.
roster_races: std::collections::HashMap<u64, u8>,
/// The billing group the last `SMSG_AUTH_RESPONSE` to carry one gave. Deviation: zeros until
/// one does, where the reference's words are never initialised (the session is allocated
/// one does, because the reference's words are never initialised (the session is allocated
/// without zero-fill, `ui/billing` in benilla-re).
billing: messages::Billing,
/// `SMSG_ADDON_INFO`'s per-record status bytes in order; `None` until the server answers.
@@ -241,12 +242,14 @@ impl WorldSession {
// `SMSG_ADDON_INFO` can reach any of the handshake's read loops, so it is caught here.
match &packet {
ServerPacket::AddonInfo { records } => self.link.addon_info = Some(records.clone()),
// Deviation: benilla runs no Warden module and answers nothing, where the reference
// hands `SMSG_WARDEN_DATA` to the module its connect loads (`0x6ca2f0`, `0x6ca5c0`)
// and sends that module's answer as `CMSG_WARDEN_DATA` (`0x6ca6a0`). The message is
// read past like any stray and the login goes on; what follows is the server's policy
// (vmangos kicks after `Warden.ClientResponseDelay`, `Warden.cpp:686-691`). benilla
// runs no code a server sends ([`crate::auth::AuthReject::reply`] gives the rule).
// Deviation: benilla runs no Warden module and answers nothing, because the answers
// come from native Win32 x86 code the server sends, which the client maps, links and
// runs in its own process (`0x7a79d0`), and benilla runs no such code on any platform
// ([`crate::auth::AuthReject::reply`] gives the rule). The reference hands
// `SMSG_WARDEN_DATA` to the module its connect loads (`0x6ca2f0`, `0x6ca5c0`) and sends
// that module's answer as `CMSG_WARDEN_DATA` (`0x6ca6a0`); here the message is read
// past like any stray and the login goes on. What follows is the server's
// policy (vmangos kicks after `Warden.ClientResponseDelay`, `Warden.cpp:686-691`).
ServerPacket::Other {
opcode: opcode::SMSG_WARDEN_DATA,
} => self.link.warden_asked = true,
+2 -2
View File
@@ -84,8 +84,8 @@ pub fn local(secs: i64) -> (Civil, bool) {
/// `0x740825`), then adds `_dstbias` only when `isdst` is set (`0x740811`, `0x740836`). The sum is
/// a 32-bit `time_t`'s, so a wrap past it is negative and refused.
///
/// Deviation: a `_dstbias` that takes the time below 0 faults the client, which copies the second
/// `localtime`'s null answer unchecked (`0x740842` → `0x740860`); here it is refused.
/// Deviation: a `_dstbias` that takes the time below 0 is refused here because it faults the
/// client, which copies the second `localtime`'s null answer unchecked (`0x740842` → `0x740860`).
pub fn local_seconds(utc: i32, isdst: bool) -> Option<i32> {
let (zone, bias) = zone().biases;
let mut t = utc.wrapping_add(zone);
+6 -5
View File
@@ -435,8 +435,9 @@ impl Loader<'_> {
}
}
TopLevel::Script(ScriptRef::Inline { body, line }) => {
// Deviation: padded to the block's line, so a raise reports a line a reader can
// find in the file; the reference counts from the body's first line.
// Deviation: padded to the block's line, because that way a raise reports a
// line a reader can find in the file; the reference counts from the body's
// first line.
let mut chunk = "\n".repeat(line.saturating_sub(1) as usize).into_bytes();
chunk.extend_from_slice(body.as_bytes());
// The reference names an inline body `"%s:<Scripts>"` (`0x871074`, formatted at
@@ -544,7 +545,7 @@ impl Loader<'_> {
/// `0x76fe60`).
///
/// Deviation: a cycle (`A` inherits `B`, `B` inherits `A`) stops at the repeated name with a
/// warning. The reference has no guard: its `parent=` walk (`0x6ee390`-`0x6ee3c2`) spins
/// warning because the reference has no guard: its `parent=` walk (`0x6ee390`-`0x6ee3c2`) spins
/// forever round a cycle with no `parent=` on it, and where one stops that walk, the level
/// recursion (`0x76985c`, and `0x76a092` for the `<Frames>`) overflows the stack.
pub(super) fn levels<'a>(&mut self, raw: &'a Element) -> Levels<'a> {
@@ -721,8 +722,8 @@ impl Loader<'_> {
}
/// Link font `id` to `to` for an XML attribute. Deviation: a link that would close a loop is
/// refused and reported; the reference's XML legs (`0x783ce6`, `0x783d22`) check nothing, and
/// the next push down such a loop would never end.
/// refused and reported because the reference's XML legs (`0x783ce6`, `0x783d22`) check
/// nothing, and the next push down such a loop would never end.
fn link_font_node(
&mut self,
id: crate::script::font::FontId,
+3 -3
View File
@@ -213,9 +213,9 @@ impl Loader<'_> {
.find(|(t, _)| is(t))
{
// Each colour element (`0x6f23d0`) is its font's text colour. Deviation:
// `<HighlightColor>` and `<DisabledColor>` colour their own fonts; 1.12.1 sends
// both to the normal colour setter (`0x778aa4`, `0x778ada` call `0x778e20`), so
// neither could reach its state.
// `<HighlightColor>` and `<DisabledColor>` colour their own fonts because 1.12.1
// sends both to the normal colour setter (`0x778aa4`, `0x778ada` call `0x778e20`),
// so neither could reach its state.
self.call(wrapper, method, color_of(child).fractions(), dbg);
} else if is("PushedTextOffset") {
// `0x778aec`: the dimension read (`0x6f1eb0`), stored as `SetPushedTextOffset`
+4 -4
View File
@@ -541,10 +541,10 @@ fn emit_frame(
/// The scroll child `sf`'s callback draws when it is shown (`0x787267`): a ScrollFrame's live
/// child still in the scroll draw.
///
/// Deviation: a child that left the scroll draw is drawn once, by the flat pass, where the client's
/// callback, which reads only the child's visibility, draws it a second time, clipped, at the
/// ScrollFrame's place. A `SetParent` of the child, of the ScrollFrame or of any ancestor of the
/// child to an unflagged parent clears `0x2000` on the whole moved subtree (`0x76abcb` →
/// Deviation: a child that left the scroll draw is drawn once, by the flat pass, because the
/// client's callback, which reads only the child's visibility, draws it a second time, clipped, at
/// the ScrollFrame's place. A `SetParent` of the child, of the ScrollFrame or of any ancestor of
/// the child to an unflagged parent clears `0x2000` on the whole moved subtree (`0x76abcb` →
/// `0x76a400`), so each of those reaches it.
fn scroll_child(arena: &WidgetArena, sf: FrameHandle) -> Option<FrameHandle> {
let KindState::Scroll(s) = &arena.frame(sf)?.kind_state else {
+4 -4
View File
@@ -205,8 +205,8 @@ fn rebuild_index(model: &mut Model) {
/// check, whose answer 3 makes it SECURE (`0x51dbfb`).
///
/// Deviation: the files the handler writes beside the install (`<Name>.pub` with the key,
/// `<Name>.url`; `0x51dce0`, `0x51dde0`) are not written, as the install is read-only; the key is
/// never read either, as there is no signature to verify with it.
/// `<Name>.url`; `0x51dce0`, `0x51dde0`) are not written, because the install is read-only; the key
/// is never read either, because there is no signature to verify with it.
fn apply_reply(addons: &mut [AddOnInfo], reply: &[AddonReply]) {
for record in reply {
let Some(a) = addons
@@ -784,8 +784,8 @@ fn load_saved_variables(lua: &Lua, i: usize) {
Err(e) => {
// The reference runs the file under the Lua error handler (`0x704ae0`,
// `[0x8722c8]`, `0x704b68`), which stock sets to show the error
// (`BasicControls.xml:16`). Deviation: holding the file, so the shutdown write
// keeps it.
// (`BasicControls.xml:16`). Deviation: holding the file, because that way the
// shutdown write keeps it.
log_error(lua, &format!("{}: {e}", path.display()));
hold(path);
}
+1 -1
View File
@@ -729,7 +729,7 @@ fn run_held(lua: &Lua, held: Held) {
/// such a chain always ends inside its frame.
///
/// Deviation: the reference's only bound is `luaD_call`'s count of 200 nested calls (`0x6f65ae`),
/// past which it raises `C stack overflow` (`0x6f65bf`); eight here, as each level keeps an
/// past which it raises `C stack overflow` (`0x6f65bf`); eight here, because each level keeps an
/// apply's frame on the Rust stack.
pub(crate) const MAX_NESTING: u8 = 8;
+2 -2
View File
@@ -480,8 +480,8 @@ fn drive(
// The seat (`0x4b14a0`): the text's BOTTOM on the `WorldFrame`'s BOTTOMLEFT, re-anchored only
// when the snapped point moved. Deviation: the frame's bottom-left origin is snapped onto the
// device grid so the border art blits 1:1; the reference ceils the anchor point itself onto
// it (`0x7666a0`, `0x766750`).
// device grid because that way the border art blits 1:1; the reference ceils the anchor point
// itself onto it (`0x7666a0`, `0x766750`).
let width = model
.region_data
.get(&text)
+3 -2
View File
@@ -62,8 +62,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
let g = lua.globals();
// GetTime() (`0x515ea0`): one number, `GetTickCount` times the f64 0.001 at `0x801608`.
// Deviation: the reference reads the clock live at each call (`0x42c010`); ours is the session
// clock the tick advances by the paced frame step, one value per frame, so `GetTime()` deltas
// and the `elapsed` handed to OnUpdate agree, as FrameXML (CastingBarFrame and kin) assumes.
// clock the tick advances by the paced frame step, one value per frame, because that way
// `GetTime()` deltas and the `elapsed` handed to OnUpdate agree, as FrameXML (CastingBarFrame
// and kin) assumes.
g.set("GetTime", lua.create_function(|lua, ()| Ok(now(lua)))?)?;
// GetGameTime() (`0x515ee0`): two numbers, the hour `[0xce853c]` then the minute `[0xce8538]`,
// the clock the server's time messages set, read at the call through the host; minute
+1 -1
View File
@@ -751,7 +751,7 @@ fn pickup_container_item(
// `[0xb4d900] == 5`): `CMSG_BUY_ITEM_IN_SLOT` (0x1a3) with count 1, and the cursor clears
// either way, the item the row names now; with no vendor open the row stays held
// (`0x4f9f0f`). Deviation: a row index the list no longer holds buys nothing and drops,
// where this path reads through a null row (`0x4f9f35`).
// because this path reads through a null row in the reference (`0x4f9f35`).
Some(CursorPayload::Merchant(_)) if !vendor.open => ContainerClick::Done,
Some(CursorPayload::Merchant(_)) => {
if let Some(entry) = vendor.held_entry {
+3 -3
View File
@@ -923,9 +923,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
// table at `0x84d0f4`, the global the stock window labels its button with
// (`Blizzard_CraftUI.lua:106`). A close zeroes the type, so a closed window answers `USE`.
// Type 4 reads the zero word after the table, which pushes nil.
// Deviation: the reference reads past the table unbounded (`0x4f6c90`), so type 5 would answer
// "CloseCraft"; no shipped opener carries a type past 3, and benilla answers nil rather than
// read past it.
// Deviation: benilla answers nil rather than read past the table, because the reference reads
// past it unbounded (`0x4f6c90`), so type 5 would answer "CloseCraft"; no shipped opener
// carries a type past 3.
g.set(
"GetCraftButtonToken",
lua.create_function(|lua, ()| {
+7 -7
View File
@@ -947,10 +947,10 @@ impl super::UiScript {
/// The unlock-all (`0x495460`), on the revive edge (`0x5fefae`): the player array's items
/// unlock, then one `ITEM_LOCK_CHANGED` (`0x49557d`).
///
/// Deviation: every lock lifts here, bag contents included. The reference's walk of a bag's
/// contents indexes the bag by its own player-array index (`0x495528`, `0x495535`), at least
/// 19, past a bag's slots in every case but the biggest bags, so it normally unlocks none of
/// them; a lock it strands lasts until the item's seat or stack moves, which this does not
/// Deviation: every lock lifts here, bag contents included, because the reference's walk of a
/// bag's contents indexes the bag by its own player-array index (`0x495528`, `0x495535`), at
/// least 19, past a bag's slots in every case but the biggest bags, so it normally unlocks none
/// of them; a lock it strands lasts until the item's seat or stack moves, which this does not
/// copy.
pub fn unlock_all_items(&mut self) {
for guid in self.item_locks() {
@@ -1251,9 +1251,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
Ok(flag(matches!(model.cursor, Some(CursorPayload::Spell(_)))))
})?,
)?;
// Deviation: returns nothing. The handler ends `mov eax,1` with nothing pushed (`0x4895bc`), so
// Lua 5.0 hands back the stack top, the last argument or the function itself: an accident of
// the handler, not an API, and copying a stack-top leak would copy a bug.
// Deviation: returns nothing because the handler ends `mov eax,1` with nothing pushed
// (`0x4895bc`), so Lua 5.0 hands back the stack top, the last argument or the function itself:
// an accident of the handler, not an API, and copying a stack-top leak would copy a bug.
g.set(
"ClearCursor",
lua.create_function(|lua, ()| {

Some files were not shown because too many files have changed in this diff Show More