The cast getters read live; a refused reagent line shows at its answer (#2818)
The cast state's getters read a copy a feed pushed once a frame, and a refused cast line that missed the item cache retried at frame time, so both lagged their packets. **Reference** - IsCurrentAction, IsAutoRepeatAction, SpellIsTargeting, SpellStopCasting and the targeting word's checks read the cast state at the call (`0x4e54c1`, `0x6e48e0`, `0xcecac0`). - A reagent or totem refusal that misses the cache registers `ShowItemCastFailure` (`0x6e1e82`-`0x6e1ea4`, `0x6e29b0`). The item's answer shows it, with UNKNOWN on a refusal (`0x6e2a49`). With no item word no line ever shows (`0x55be5d`). **Changed** - The getters read through the host, and `feed_cast_reports` is gone. - The refused line rides the item cache's callback list. - The "Missing reagent" fallback that 1.12.1 never shows is removed. **Tests:** the line between its neighbours for a record and for a refusal, the getters between packets, no line with no item word. Part of #2168 and #2217. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -613,6 +613,10 @@ pub(crate) enum RecordCallback {
|
||||
/// The action bar's, asked by an item slot's icon, count and spell lookups (`0x4e6b10`,
|
||||
/// `0x4e6d69`, `0x4e5b30` with `0x4e5b80`), deduped.
|
||||
ActionSlots,
|
||||
/// A refused cast's reagent or tool line whose item was not cached (`0x6e1ea4` with
|
||||
/// `ShowItemCastFailure 0x6e29b0` and the reason), one per refusal; the spell picks the
|
||||
/// line's message as its first pass would.
|
||||
CastFailure { spell: u32, reason: u8 },
|
||||
}
|
||||
|
||||
impl Items {
|
||||
@@ -864,9 +868,8 @@ impl Items {
|
||||
self.templates.answered(entry)
|
||||
}
|
||||
|
||||
/// Whether the server refused `entry`, not still pending; the cast-fail redisplay then shows
|
||||
/// the reference's `"UNKNOWN"` instead of waiting (`0x6e29b0` runs on the status-0 callback and
|
||||
/// finds no record, `0x6e2a49`).
|
||||
/// Whether the server refused `entry` and nothing has asked since, not still pending: the
|
||||
/// status-0 answer its waiters were told.
|
||||
pub(crate) fn template_answered_unknown(&self, entry: u32) -> bool {
|
||||
self.templates.answered_unknown(entry)
|
||||
}
|
||||
@@ -884,7 +887,7 @@ impl Items {
|
||||
self.templates.insert(entry, info);
|
||||
}
|
||||
// A refusal moves the generation too: "still asking" to "unknown" is a display
|
||||
// transition for the cast-fail redisplay.
|
||||
// transition.
|
||||
None => {
|
||||
if self.templates.refuse(entry) == crate::query_cache::Refused::Record {
|
||||
self.note_change(entry, false);
|
||||
@@ -939,8 +942,7 @@ impl Items {
|
||||
self.templates.clear();
|
||||
}
|
||||
|
||||
/// Bumped by every landed answer, for consumers besides the drain; the stand-in for the
|
||||
/// reference's `DBCACHECALLBACK` redisplay (`0x6e29b0`).
|
||||
/// Bumped by every landed answer, for consumers besides the drain.
|
||||
pub(crate) fn template_epoch(&self) -> u64 {
|
||||
self.templates.generation()
|
||||
}
|
||||
|
||||
@@ -459,3 +459,230 @@ fn a_failed_results_gcd_clear_reads_the_targeting_word_still_up() {
|
||||
"{seen:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// A reagent the refusal below names, whose record the item cache lacks until its answer.
|
||||
const SHARD: u32 = 6265;
|
||||
|
||||
/// The reagent line's strings, and a spy on the lines, each `UI_ERROR_MESSAGE` as `LINE:<text>`.
|
||||
fn spy_lines(c: &mut Client) {
|
||||
c.eval::<()>(
|
||||
false,
|
||||
r#"SPELL_FAILED_REAGENTS = "Missing reagent: %s"
|
||||
ERR_SPELL_FAILED_REAGENTS = "%s"
|
||||
local g = CreateFrame("Frame")
|
||||
g:RegisterEvent("UI_ERROR_MESSAGE")
|
||||
g:SetScript("OnEvent", function() table.insert(Seen, "LINE:" .. arg1) end)"#,
|
||||
);
|
||||
}
|
||||
|
||||
/// Our cast of `spell_id` refused for a missing reagent, its item word `arg`.
|
||||
fn refuse_for_a_reagent(c: &mut Client, spell_id: u32, arg: Option<u32>) {
|
||||
c.frame(vec![vec![SessionEvent::CastResult {
|
||||
spell_id,
|
||||
success: false,
|
||||
reason: Some(0x5c),
|
||||
arg,
|
||||
}]]);
|
||||
}
|
||||
|
||||
/// [`client`] with [`spy_lines`]; then our cast refused for [`SHARD`], which the cache lacks: the
|
||||
/// reporter's lookup asks for it and leaves past the line and its log line (`0x6e1ea4`,
|
||||
/// `0x6e1eab`).
|
||||
fn refused_for_a_reagent() -> Client {
|
||||
let mut c = client();
|
||||
spy_lines(&mut c);
|
||||
refuse_for_a_reagent(&mut c, HEAL, Some(SHARD));
|
||||
assert!(
|
||||
c.seen(&["LINE"]).is_empty(),
|
||||
"no line while the record is asked"
|
||||
);
|
||||
c
|
||||
}
|
||||
|
||||
/// The answer for [`SHARD`]: its record, or the server's refusal.
|
||||
fn shard_answer(name: Option<&str>) -> SessionEvent {
|
||||
SessionEvent::ItemTemplate {
|
||||
entry: SHARD,
|
||||
info: name.map(|n| Box::new(crate::items::test_template(n))),
|
||||
}
|
||||
}
|
||||
|
||||
/// The record's answer runs the refusal's callback (`ShowItemCastFailure 0x6e29b0`) inside its
|
||||
/// packet (`ItemCacheReceive 0x55bdb0`): the line fires between the power packets around it.
|
||||
#[test]
|
||||
fn a_reagent_line_shows_at_its_records_answer() {
|
||||
let mut c = refused_for_a_reagent();
|
||||
c.frame(vec![
|
||||
vec![values(ME, &[(MANA, 90)])],
|
||||
vec![shard_answer(Some("Soul Shard"))],
|
||||
vec![values(ME, &[(MANA, 80)])],
|
||||
]);
|
||||
assert_eq!(
|
||||
c.seen(&["UNIT_MANA", "LINE"]),
|
||||
[
|
||||
"UNIT_MANA:nil:nil",
|
||||
"LINE:Missing reagent: Soul Shard",
|
||||
"UNIT_MANA:nil:nil",
|
||||
]
|
||||
);
|
||||
c.frame(vec![]);
|
||||
assert!(c.seen(&["LINE"]).is_empty(), "the callback ran once");
|
||||
}
|
||||
|
||||
/// A refusal runs the callback too, whose lookup finds no record: `UNKNOWN` (`0x6e2a49`).
|
||||
#[test]
|
||||
fn a_refused_reagent_record_shows_unknown_at_its_answer() {
|
||||
let mut c = refused_for_a_reagent();
|
||||
c.frame(vec![
|
||||
vec![values(ME, &[(MANA, 90)])],
|
||||
vec![shard_answer(None)],
|
||||
vec![values(ME, &[(MANA, 80)])],
|
||||
]);
|
||||
assert_eq!(
|
||||
c.seen(&["UNIT_MANA", "LINE"]),
|
||||
[
|
||||
"UNIT_MANA:nil:nil",
|
||||
"LINE:Missing reagent: UNKNOWN",
|
||||
"UNIT_MANA:nil:nil",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// With no frame after them, the getters read through the host the cast state each packet left,
|
||||
/// inside the call (`0x4e53a0`, `0x6e6e80`): Heal in flight after its start, popped after its
|
||||
/// result, where the VM's copy, which no feed pushes, still holds it in flight; and
|
||||
/// `SpellStopCasting` finds nothing to stop.
|
||||
#[test]
|
||||
fn the_cast_getters_read_each_packets_state_at_the_call() {
|
||||
let mut c = client();
|
||||
cast_heal(&mut c);
|
||||
let chunk = "return IsCurrentAction(1), SpellIsTargeting()";
|
||||
crate::net::handlers::dispatch(
|
||||
c.app.world_mut(),
|
||||
vec![SessionEvent::SpellStart {
|
||||
caster: ME,
|
||||
spell_id: HEAL,
|
||||
cast_flags: 0,
|
||||
cast_time_ms: 2500,
|
||||
target: None,
|
||||
ammo_display_id: None,
|
||||
}],
|
||||
);
|
||||
assert_eq!(
|
||||
c.eval::<(Option<i64>, Option<i64>)>(true, chunk),
|
||||
(Some(1), None)
|
||||
);
|
||||
crate::net::handlers::dispatch(
|
||||
c.app.world_mut(),
|
||||
vec![SessionEvent::CastResult {
|
||||
spell_id: HEAL,
|
||||
success: true,
|
||||
reason: None,
|
||||
arg: None,
|
||||
}],
|
||||
);
|
||||
assert_eq!(
|
||||
c.eval::<(Option<i64>, Option<i64>)>(true, chunk),
|
||||
(None, None)
|
||||
);
|
||||
assert_eq!(
|
||||
c.eval::<(Option<i64>, Option<i64>)>(false, chunk),
|
||||
(Some(1), None),
|
||||
"the VM's copy"
|
||||
);
|
||||
c.press("Stopped = SpellStopCasting()");
|
||||
assert_eq!(c.eval::<Option<i64>>(false, "return Stopped"), None);
|
||||
}
|
||||
|
||||
/// The targeting word and the auto-repeat key read at the call too (`0x6e6cd0`, `0x6e9fd0`), each
|
||||
/// written with no push after it: the cursor up for Heal and Heal repeating. `SpellStopTargeting`
|
||||
/// tests the word at the call and ends it inside it (`0x6e6e30` → `0x6e4900`).
|
||||
#[test]
|
||||
fn the_targeting_word_and_the_auto_repeat_key_read_at_the_call() {
|
||||
let mut c = client();
|
||||
let world = c.app.world_mut();
|
||||
world.resource_mut::<crate::spell::SpellTargeting>().enter(
|
||||
HEAL,
|
||||
crate::spell::CastCommit::Spell,
|
||||
0x0002,
|
||||
);
|
||||
world.resource_mut::<crate::spell::AutoRepeatActive>().0 = Some(HEAL);
|
||||
let chunk = "return SpellIsTargeting(), IsAutoRepeatAction(1)";
|
||||
assert_eq!(
|
||||
c.eval::<(Option<i64>, Option<i64>)>(true, chunk),
|
||||
(Some(1), Some(1))
|
||||
);
|
||||
assert_eq!(
|
||||
c.eval::<(Option<i64>, Option<i64>)>(false, chunk),
|
||||
(None, None),
|
||||
"the VM's copy"
|
||||
);
|
||||
c.press("Stopped = SpellStopTargeting()");
|
||||
assert_eq!(c.eval::<Option<i64>>(false, "return Stopped"), Some(1));
|
||||
assert!(!c
|
||||
.app
|
||||
.world()
|
||||
.resource::<crate::spell::SpellTargeting>()
|
||||
.active());
|
||||
}
|
||||
|
||||
/// A refusal of ours with no item word reads -1 (`0x6e736e`), whose ask no answer ever matches
|
||||
/// (`0x55bdf7`, `0x55be5d`), and one with a word of 0 asks nothing (`0x55ba42`): neither shows a
|
||||
/// line, now or at an answer, though Heal's reagent is cached and missing from our bags.
|
||||
#[test]
|
||||
fn a_reagent_refusal_with_no_item_word_shows_no_line() {
|
||||
let mut c = client();
|
||||
let mut reagents = [(0, 0); 8];
|
||||
reagents[0] = (SHARD, 1);
|
||||
c.app.insert_resource(crate::ui_action::Spells {
|
||||
catalog: SpellCatalog::from_displays(HashMap::from([(
|
||||
HEAL,
|
||||
SpellDisplay {
|
||||
id: HEAL,
|
||||
name: "Heal".into(),
|
||||
reagents,
|
||||
..Default::default()
|
||||
},
|
||||
)])),
|
||||
..crate::ui_action::Spells::empty_for_tests()
|
||||
});
|
||||
c.app
|
||||
.world_mut()
|
||||
.resource_mut::<crate::items::Items>()
|
||||
.insert_template(SHARD, Some(crate::items::test_template("Soul Shard")));
|
||||
spy_lines(&mut c);
|
||||
for arg in [None, Some(0)] {
|
||||
refuse_for_a_reagent(&mut c, HEAL, arg);
|
||||
c.frame(vec![vec![shard_answer(Some("Soul Shard"))]]);
|
||||
assert!(c.seen(&["LINE"]).is_empty(), "{arg:?}");
|
||||
}
|
||||
assert!(
|
||||
c.app
|
||||
.world_mut()
|
||||
.resource_mut::<crate::items::Items>()
|
||||
.take_callbacks(0)
|
||||
.is_empty(),
|
||||
"a word of 0 keeps no callback"
|
||||
);
|
||||
}
|
||||
|
||||
/// The reagent arm reads the item word alone, never the spell record (`0x6e1e7f`): a refusal of
|
||||
/// a spell the catalog lacks shows its line at the record's answer under `0xd2`, which the
|
||||
/// callback picks off the reason (`0x6e29fe`), and with the record cached under the default id
|
||||
/// `0x2c`, as the reporter skips the record's chain (`0x6e1a7c`).
|
||||
#[test]
|
||||
fn a_reagent_line_shows_for_a_spell_with_no_record() {
|
||||
const UNLISTED: u32 = 99_999;
|
||||
let mut c = client();
|
||||
spy_lines(&mut c);
|
||||
c.eval::<()>(
|
||||
false,
|
||||
r#"ERR_SPELL_FAILED_S = "S:%s" ERR_SPELL_FAILED_REAGENTS = "R:%s""#,
|
||||
);
|
||||
refuse_for_a_reagent(&mut c, UNLISTED, Some(SHARD));
|
||||
assert!(c.seen(&["LINE"]).is_empty(), "the record is asked");
|
||||
c.frame(vec![vec![shard_answer(Some("Soul Shard"))]]);
|
||||
assert_eq!(c.seen(&["LINE"]), ["LINE:R:Missing reagent: Soul Shard"]);
|
||||
refuse_for_a_reagent(&mut c, UNLISTED, Some(SHARD));
|
||||
assert_eq!(c.seen(&["LINE"]), ["LINE:S:Missing reagent: Soul Shard"]);
|
||||
}
|
||||
|
||||
@@ -75,11 +75,10 @@ pub(super) const NOT_YET_ON_A_LANE: &[(Row, &str, &str)] = &[
|
||||
"::items::Items",
|
||||
"the record answers' callbacks still run off the frame's records rather than in the \
|
||||
answer's list (`ItemCacheReceive` `0x55bdb0`, `0x55bf12`-`0x55bf39`): the \
|
||||
dress-up's (`0x504ba0`), the cast failure's redisplay (`0x6e29b0`, `0x6e2a90`), the \
|
||||
equipment display's (`0x5ed880`), and the lock-key tooltip's (`0x52ad10`), which \
|
||||
`drive_mouseover_tooltip` rebuilds each frame; and the combat log's (`0x6294b0`) and the \
|
||||
auction lists' (`0x4cca10`, `0x4ccc60`, `0x4cceb0`), which `run_answer_tail` runs after \
|
||||
the answer's list rather than in it",
|
||||
dress-up's (`0x504ba0`), the equipment display's (`0x5ed880`), and the lock-key \
|
||||
tooltip's (`0x52ad10`), which `drive_mouseover_tooltip` rebuilds each frame; and the \
|
||||
combat log's (`0x6294b0`) and the auction lists' (`0x4cca10`, `0x4ccc60`, `0x4cceb0`), \
|
||||
which `run_answer_tail` runs after the answer's list rather than in it",
|
||||
),
|
||||
];
|
||||
|
||||
@@ -145,8 +144,8 @@ pub(super) const HOST_HELD_UNREAD: &[&str] = &[
|
||||
"::spell::cast_send::ScriptCastPermit",
|
||||
"::spell::cast_send::unit_caster::UnitAttackOwed",
|
||||
// Quiet: the cast in flight, which `SpellStopCasting` and the bar's tests read through the
|
||||
// host; `feed_cast_reports` pushes its view (its `READS_FRAME_FINAL` row), and the spell
|
||||
// packets' events fire on the cast lane at their handlers.
|
||||
// host, as of the drain's place, and the spell packets' events fire on the cast lane at their
|
||||
// handlers.
|
||||
"::spell::inflight::ActiveChannel",
|
||||
"::spell::inflight::AutoRepeatActive",
|
||||
"::spell::inflight::PendingCast",
|
||||
@@ -160,8 +159,8 @@ pub(super) const HOST_HELD_UNREAD: &[&str] = &[
|
||||
// call's before its next step; no feed reads it.
|
||||
"::spell::targeting::CursorPicks",
|
||||
// Quiet: the targeting word, which `SpellIsTargeting` (`0x6e6cd0`) reads through the host;
|
||||
// `feed_cast_reports` pushes its view and the feed publishes the world pick's words for the
|
||||
// next frame's input, and no feed fires off it.
|
||||
// the feed publishes the world pick's words for the next frame's input, and no feed fires off
|
||||
// it.
|
||||
"::spell::targeting::SpellTargeting",
|
||||
// Quiet: every selection writer signals `PLAYER_TARGET_CHANGED` in place (`0x49389c`), and the
|
||||
// getters' `"target"` reads it through the host (`0x515a86`); the feeds that read it push
|
||||
@@ -360,10 +359,6 @@ pub(super) const READS_FRAME_FINAL: &[(&str, &[&str])] = &[
|
||||
("::ui_aura::feed_unit_guids", &["net::GuidIndex", "net::ObjectStore", "target::Selection",
|
||||
"ui_party::GroupState"]),
|
||||
("::ui_macro::load_macros", &["ui_action::ActionSlots"]),
|
||||
("::ui_cast::feed_cast_reports", &["items::Countdowns", "items::Items", "net::GuidIndex",
|
||||
"net::ObjectStore", "spell::inflight::AutoRepeatActive", "spell::inflight::PendingCast",
|
||||
"spell::inflight::QueuedMeleeSpell", "spell::targeting::SpellTargeting",
|
||||
"ui_action::cast_lines::CastFailRedisplays"]),
|
||||
// The locks of the objects that went, forgotten as the frame left the index.
|
||||
("::ui_items::locks::forget_gone_item_locks", &["net::GuidIndex"]),
|
||||
("::ui_items::feed::feed_item_stats", &["items::Items", "net::HomeBind", "net::ObjectStore",
|
||||
@@ -1783,8 +1778,7 @@ pub(super) const LANE_READS_FRAME_FINAL: &[(&str, &[&str])] = &[
|
||||
// tokens' live reads; it reads the index and the selection as the frame left them (#2217).
|
||||
("unit views", &["net::GuidIndex", "net::ObjectStore", "target::Selection",
|
||||
"ui_party::GroupState"]),
|
||||
("cast reports", &["items::Countdowns", "items::Items", "net::GuidIndex", "net::ObjectStore",
|
||||
"ui_action::cast_lines::CastFailRedisplays"]),
|
||||
("cast reports", &["items::Items", "net::ObjectStore"]),
|
||||
("item watchers", &["items::Countdowns", "items::Items", "names::NameCache", "net::GuidIndex",
|
||||
"net::ObjectStore", "ui_trade::TradeSession"]),
|
||||
("auction house", &["items::Items", "names::NameCache", "net::SelfGuid",
|
||||
|
||||
@@ -43,8 +43,8 @@ use crate::game_plugins::schedule_tests::{census, type_names_available, SyncPoin
|
||||
|
||||
/// The most rows [`super::guard_tests::READS_FRAME_FINAL`] may hold, and the most inputs across
|
||||
/// them: each only falls, so lower it when a row or an input goes.
|
||||
const FRAME_FINAL_FEEDS: usize = 9;
|
||||
const FRAME_FINAL_INPUTS: usize = 43;
|
||||
const FRAME_FINAL_FEEDS: usize = 8;
|
||||
const FRAME_FINAL_INPUTS: usize = 34;
|
||||
|
||||
/// The systems that may run after the dispatch (`apply_net_updates`) and run ahead of the drain
|
||||
/// (`fire_in_order`), by a suffix of the system's name, with what each writes of the state a
|
||||
@@ -72,11 +72,6 @@ const BETWEEN_THE_DISPATCH_AND_THE_DRAIN: &[(&str, &[&str], &str)] = &[
|
||||
"the arrival snap `0x7c6420` (`0x618dfc`) is written inside the packet"),
|
||||
("::transport::compose_riders", &["Transform", "net::motion::remote::RemoteMotion"],
|
||||
"the arrival snap `0x7c6420` (`0x618dfc`) is written inside the packet"),
|
||||
("::ui_cast::feed_cast_reports", &["ui_action::cast_lines::CastFailRedisplays"],
|
||||
"holds the refusals that wait on their item's record, which it tests to queue the cast \
|
||||
lane's retry behind the frame's packets, where the reference shows each in the record's \
|
||||
callback (`ShowItemCastFailure` `0x6e29b0`), inside `ItemCacheReceive` `0x55bdb0` in the \
|
||||
answer's handler"),
|
||||
("::ui_char::feed_char", &["UiScript", "ui_char::AmmoRecordHits"],
|
||||
"a field event's listeners read its push, and it takes the ammo getters' record hits, a \
|
||||
`UNIT_INVENTORY_CHANGED` each behind it, where the reference's hit registers no callback \
|
||||
@@ -140,6 +135,9 @@ const AFTER_THE_PACKETS_IN_THE_REFERENCE: &[(&str, &[&str], &str)] = &[
|
||||
&["ui_pet::PetBar"],
|
||||
"drops the pet's cooldown log, run only while no in-game interface \
|
||||
is up, so no listener exists; it goes with the log"),
|
||||
("::ui_cast::views::drop_cast_views", &["ui_cast::views::CastViews"],
|
||||
"drops the cast views' log, run only while no in-game interface \
|
||||
is up, so no listener exists; it goes with the log"),
|
||||
("::ui_chat::language::load_language_words", &["ui_chat::language::ChatLanguages"],
|
||||
"loads `LanguageWords.dbc` once, the first frame the install is open, ahead of every \
|
||||
world packet, as the reference loads it at startup (`StaticDBLoadAll` `0x53f8b0` → \
|
||||
|
||||
@@ -519,15 +519,12 @@ fn cast_stops(_: In<SessionEvent>, mut reports: ResMut<crate::ui_cast::CastRepor
|
||||
reports.bar(crate::ui_cast::CastBarEdge::Stop);
|
||||
}
|
||||
|
||||
/// A client with the cast reports' lane too, and what their readying reads.
|
||||
/// A client with the cast reports' lane too.
|
||||
fn cast_client(handlers: impl FnOnce(&mut App)) -> App {
|
||||
let mut app = client(|app| {
|
||||
client(|app| {
|
||||
app.add_plugins(crate::ui_cast::UiCastPlugin);
|
||||
handlers(app);
|
||||
});
|
||||
app.world_mut()
|
||||
.init_resource::<crate::ui_action::CastFailRedisplays>();
|
||||
app
|
||||
})
|
||||
}
|
||||
|
||||
/// A cast's report fires inside its packet's handler (`SPELLCAST_STOP` from `SMSG_CAST_RESULT`,
|
||||
|
||||
@@ -317,6 +317,13 @@ fn run_callback(
|
||||
}
|
||||
Cb::CooldownQuery => {}
|
||||
Cb::Stationery => run_answer_step(world, paper_answered, entry),
|
||||
// `ShowItemCastFailure 0x6e29b0`, for a record and a refusal alike: the line again,
|
||||
// on the cast lane here.
|
||||
Cb::CastFailure { spell, reason } => {
|
||||
if let Some(mut reports) = world.get_resource_mut::<crate::ui_cast::CastReports>() {
|
||||
reports.redisplay(spell, reason, entry);
|
||||
}
|
||||
}
|
||||
// `QUEST_LOG_UPDATE` for a ready record only (`0x4e0838`), held and parked here.
|
||||
Cb::QuestLog if answered => crate::ui_items::feed::quest_log_answered(world),
|
||||
// The tooltips' rebuilds for a found record only (`0x52e551`, `0x52f826`).
|
||||
@@ -416,6 +423,7 @@ fn run_record_callback(
|
||||
| Cb::ItemCooldown
|
||||
| Cb::CooldownQuery
|
||||
| Cb::Stationery
|
||||
| Cb::CastFailure { .. }
|
||||
| Cb::QuestLog
|
||||
| Cb::Tooltip
|
||||
| Cb::TradeSkill
|
||||
|
||||
@@ -4938,10 +4938,11 @@ mod tests {
|
||||
crate::dev::add_every_instrument,
|
||||
);
|
||||
let (writers, unhosted) = vm_writers_and_the_unhosted(&mut app);
|
||||
// The census reads the client's systems: ninety writers at least in either build, and
|
||||
// where the build carries type names, a feed and two frame polls among them.
|
||||
// The census reads the client's systems: eighty writers at least in either build, a floor
|
||||
// the feeds' going live lowers toward, and where the build carries type names, a feed and
|
||||
// two frame polls among them.
|
||||
assert!(
|
||||
writers.len() >= 90,
|
||||
writers.len() >= 80,
|
||||
"the census reads the client's VM writers: {} read",
|
||||
writers.len()
|
||||
);
|
||||
|
||||
@@ -171,6 +171,9 @@ live_reads! {
|
||||
/// The targeting word's halves and `SpellCanTargetUnit`'s unit leg; absent in a harness
|
||||
/// without the unit checks' inputs.
|
||||
targeting: Option<crate::spell::targeting::TargetingLive<'static, 'static>>,
|
||||
/// Our cast's state: the in-flight slot, the targeting word, the auto-repeat key and the
|
||||
/// attack flag; absent in a harness without the cast state.
|
||||
cast: Option<crate::ui_cast::CastLive<'static, 'static>>,
|
||||
#[cfg(test)]
|
||||
probe: tests::ProbeLive<'static>,
|
||||
}
|
||||
|
||||
@@ -223,7 +223,8 @@ pub(crate) fn reaches_tail(
|
||||
fail.arg,
|
||||
spells.and_then(|s| s.catalog.get(fail.spell_id)),
|
||||
spells.map(|s| &s.forms),
|
||||
// The lookup with the callback `0x6e29b0` (`0x6e1ea4`).
|
||||
// The lookup (`0x6e1ea4`), asking on a miss; the line's own registers the callback
|
||||
// `0x6e29b0` ([`crate::ui_action::CastFailLines`]).
|
||||
|item| items.template(item, 0, Ask::Event(net)).is_some(),
|
||||
)
|
||||
}
|
||||
@@ -2336,7 +2337,6 @@ mod tests {
|
||||
crate::ui_cast::CastReport::TradeSkillDisarm => "DISARM",
|
||||
crate::ui_cast::CastReport::AutoRepeat(true) => "START_AUTOREPEAT",
|
||||
crate::ui_cast::CastReport::AutoRepeat(false) => "STOP_AUTOREPEAT",
|
||||
crate::ui_cast::CastReport::Redisplay => "REDISPLAY",
|
||||
crate::ui_cast::CastReport::Line(e) => e.key,
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -830,7 +830,6 @@ mod tests {
|
||||
.add_message::<crate::net::FieldChanged>()
|
||||
.init_resource::<CastReports>()
|
||||
.init_resource::<crate::items::Items>()
|
||||
.init_resource::<crate::ui_action::CastFailRedisplays>()
|
||||
.init_resource::<crate::ui_chat::ChatLog>()
|
||||
.init_resource::<crate::sound::MessageSounds>()
|
||||
.init_resource::<PendingCast>()
|
||||
@@ -899,22 +898,39 @@ mod tests {
|
||||
crate::creature_anim::apply_unit_edits(app.world_mut());
|
||||
}
|
||||
|
||||
/// `SpellStopCasting()`, its answer, with the cast's cancel lent as the host, which applies
|
||||
/// `SpellStopCasting()`, its answer, with the world lent as the host: its test reads the
|
||||
/// cast state at the call ([`crate::ui_cast::CastLive`]), and the cast's cancel applies
|
||||
/// the call inside the verb as `crate::script_calls` does.
|
||||
fn stop_casting(app: &mut App) -> Option<i64> {
|
||||
app.world_mut()
|
||||
.run_system_once(|mut script: NonSendMut<UiScript>, mut cancel: SelfCancel| {
|
||||
let mut host = crate::script_calls::FnHost(|_: &mut UiScript, call| {
|
||||
if call == ScriptCall::SpellStopCasting {
|
||||
cancel.stop_casting();
|
||||
}
|
||||
});
|
||||
script.hosted(&mut host, |s| {
|
||||
s.eval::<Option<i64>>("return SpellStopCasting()")
|
||||
})
|
||||
})
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
use crate::script_calls::live::LiveFamily;
|
||||
use benilla_ui::script::{CallHost, CallTail};
|
||||
use bevy::ecs::system::SystemState;
|
||||
|
||||
struct Host<'a>(&'a mut World);
|
||||
|
||||
impl CallHost for Host<'_> {
|
||||
fn apply(&mut self, _: &mut UiScript, call: ScriptCall) -> Option<CallTail> {
|
||||
if call == ScriptCall::SpellStopCasting {
|
||||
let mut cancel = SystemState::<SelfCancel>::new(self.0);
|
||||
cancel.get_mut(self.0).stop_casting();
|
||||
cancel.apply(self.0);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn read_live(&mut self, ask: &mut benilla_ui::script::live::LiveAsk<'_>) -> bool {
|
||||
let mut live = SystemState::<crate::ui_cast::CastLive>::new(self.0);
|
||||
live.get(self.0).answer(ask)
|
||||
}
|
||||
}
|
||||
|
||||
let world = app.world_mut();
|
||||
let mut script = world.remove_non_send_resource::<UiScript>().expect("a VM");
|
||||
let answer = script.hosted(&mut Host(world), |s| {
|
||||
s.eval::<Option<i64>>("return SpellStopCasting()")
|
||||
});
|
||||
world.insert_non_send_resource(script);
|
||||
answer.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1146,7 +1146,8 @@ fn cast_result(
|
||||
arg,
|
||||
spells.and_then(|s| s.catalog.get(spell_id)),
|
||||
spells.map(|s| &s.forms),
|
||||
// The packet's lookup with the callback `0x6e29b0` (`0x6e1ea4`).
|
||||
// The packet's lookup (`0x6e1ea4`), asking on a miss; the line's own registers the
|
||||
// callback `0x6e29b0` ([`crate::ui_action::CastFailLines`]).
|
||||
|item| items.is_some_and(|i| i.template(item, 0, Ask::Event(net)).is_some()),
|
||||
);
|
||||
// The line, then the tail: the spell in flight popped with its failed flag and no packet
|
||||
|
||||
@@ -400,7 +400,6 @@ mod tests {
|
||||
CastReport::TradeSkillDisarm => "DISARM".into(),
|
||||
CastReport::AutoRepeat(true) => "START_AUTOREPEAT".into(),
|
||||
CastReport::AutoRepeat(false) => "STOP_AUTOREPEAT".into(),
|
||||
CastReport::Redisplay => "REDISPLAY".into(),
|
||||
CastReport::Line(e) => e.key.into(),
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -436,6 +436,16 @@ pub(super) fn cast_fail_text(
|
||||
// player's chain and its `_PET` flag are skipped (`0x6e1a7c`), and the pet's handler leaves
|
||||
// (`0x6e8f04`).
|
||||
let (mut error, pet_text) = match (caster, spell) {
|
||||
// The item-cache callback `0x6e29b0` picks its id off the reason alone, `0xd2` for `0x5c`
|
||||
// and `0xd1` for `0x78`, reading no spell record (`0x6e29fe`-`0x6e2a0d`).
|
||||
(Caster::Player, _) if args.redisplay => (
|
||||
match reason {
|
||||
0x5C => Some("ERR_SPELL_FAILED_REAGENTS"),
|
||||
0x78 => Some("ERR_SPELL_FAILED_TOTEMS"),
|
||||
_ => None,
|
||||
},
|
||||
false,
|
||||
),
|
||||
(Caster::Player, Some(d)) => (
|
||||
player_error_key(reason, d),
|
||||
!args.redisplay && trains_pet(d),
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
//! table, and the combat-log line beside it. `ReportCastFailure 0x6e1a00` builds the text, shows it
|
||||
//! (`DisplayError`, `0x6e21dd`) and logs it (`0x62c360`, `0x6e21fc`) inside the cast, so the line
|
||||
//! is resolved where the report is signalled: inside the script call that raised it
|
||||
//! ([`crate::script_calls`]), or by the frame's report feed ([`crate::ui_cast`]). The text layers
|
||||
//! are [`super::cast_fail`]'s.
|
||||
//! ([`crate::script_calls`]), or at its place on the cast lane ([`crate::ui_cast`]). The text
|
||||
//! layers are [`super::cast_fail`]'s.
|
||||
|
||||
use bevy::ecs::system::SystemParam;
|
||||
use bevy::prelude::*;
|
||||
@@ -16,15 +16,8 @@ use crate::query_cache::Ask;
|
||||
use crate::ui_chat::combat::PendingCombat;
|
||||
|
||||
use super::cast_fail::{self, CastFailLine};
|
||||
use super::errors::{first_missing_totem, first_short_reagent};
|
||||
use super::{CastFail, Caster, Shown, Spells};
|
||||
|
||||
/// The refusals whose item had no cached record when their line was due: the reference's first
|
||||
/// pass exits past the line (`0x6e1eab`), and the cache callback `0x6e29b0` shows it once the
|
||||
/// record answers, with no log line.
|
||||
#[derive(Resource, Default)]
|
||||
pub(crate) struct CastFailRedisplays(pub(crate) Vec<CastFail>);
|
||||
|
||||
/// The DBC name tables the cast-fail argument arms read; each is absent without game data. The
|
||||
/// shapeshift forms ride [`Spells`].
|
||||
#[derive(SystemParam)]
|
||||
@@ -65,10 +58,8 @@ pub(crate) struct CastFailLines<'w, 's> {
|
||||
sub_classes: Option<Res<'w, crate::ui_items::ItemSubClasses>>,
|
||||
names: FailNameTables<'w>,
|
||||
self_q: Query<'w, 's, &'static ObjectStore, With<SelfPlayer>>,
|
||||
objects: crate::net::Objects<'w, 's>,
|
||||
items: Res<'w, Items>,
|
||||
commands: Res<'w, NetCommands>,
|
||||
waiting: ResMut<'w, CastFailRedisplays>,
|
||||
}
|
||||
|
||||
impl CastFailLines<'_, '_> {
|
||||
@@ -77,14 +68,9 @@ impl CastFailLines<'_, '_> {
|
||||
self.spells.as_deref()
|
||||
}
|
||||
|
||||
/// Whether a line waits on its item's record.
|
||||
pub(crate) fn any_waiting(&self) -> bool {
|
||||
!self.waiting.0.is_empty()
|
||||
}
|
||||
|
||||
/// `fail`'s line and its log line, `None` where the reference leaves before both now; an empty
|
||||
/// line shows nothing and still logs. A line whose item record is not cached waits in
|
||||
/// [`CastFailRedisplays`].
|
||||
/// line shows nothing and still logs. A line whose item record is not cached waits on the
|
||||
/// record's answer ([`crate::items::RecordCallback::CastFailure`]).
|
||||
pub(crate) fn resolve(&mut self, script: &UiScript, fail: CastFail) -> Option<ResolvedFail> {
|
||||
let line = self.line(script, fail)?;
|
||||
// The reason, its wire word and both resolved buffers, so a declined arm is readable
|
||||
@@ -100,15 +86,6 @@ impl CastFailLines<'_, '_> {
|
||||
})
|
||||
}
|
||||
|
||||
/// The waiting lines whose item record has answered, each shown with no log line; the rest
|
||||
/// keep waiting.
|
||||
pub(crate) fn redisplays(&mut self, script: &UiScript) -> Vec<Shown> {
|
||||
std::mem::take(&mut self.waiting.0)
|
||||
.into_iter()
|
||||
.filter_map(|fail| self.resolve(script, fail).map(|r| r.shown))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The displayed line first, the log reads from it: the reference's order.
|
||||
fn line(&mut self, script: &UiScript, fail: CastFail) -> Option<CastFailLine> {
|
||||
let CastFail {
|
||||
@@ -148,41 +125,35 @@ impl CastFailLines<'_, '_> {
|
||||
.requirement_display_name(ITEM_CLASS_PROJECTILE, 1u32 << (arg & 31))
|
||||
}),
|
||||
// `0x78` and `0x5c` (`0x6e1e7f`), the player's arms only (the pet's table sends both to
|
||||
// its generic arm): the name of argument 1's item, the entry `0x6e4000` found; a
|
||||
// word-less refusal names the failing slot as the pre-send check finds it.
|
||||
// its generic arm): the name of argument 1's item, the entry `0x6e4000` found, read
|
||||
// with no spell record. The server's refusal carries no word for either (vmangos
|
||||
// `Spell.cpp:4407-4431`), which reads -1 (`0x6e736e`): its lookup asks for an entry
|
||||
// whose refusal no node matches (`0x55bdf7`, `0x55be5d`), so neither line nor log ever
|
||||
// shows; a word of 0 asks nothing and shows nothing (`0x55ba42`).
|
||||
0x78 | 0x5c if !pet => {
|
||||
let d = d?;
|
||||
let self_store = self.self_q.iter().next();
|
||||
let failing = fail.arg.or_else(|| {
|
||||
if reason == 0x78 {
|
||||
self_store
|
||||
.and_then(|s| first_missing_totem(d, s, &self.objects))
|
||||
// No self store yet: name the first tool.
|
||||
.or_else(|| d.totems.iter().copied().find(|&t| t != 0))
|
||||
} else {
|
||||
self_store
|
||||
.and_then(|s| first_short_reagent(d, s, &self.objects))
|
||||
.or_else(|| d.reagents.iter().map(|&(id, _)| id).find(|&id| id != 0))
|
||||
}
|
||||
})?;
|
||||
// The failure handler's lookup with the callback `0x6e29b0` (`0x6e1ea4`) on a
|
||||
// first pass; its redisplay only reads.
|
||||
let ask = if fail.redisplay {
|
||||
Ask::Read
|
||||
let failing = fail.arg.filter(|&entry| entry != 0)?;
|
||||
// The callback `0x6e29b0` looks the record up again with no callback of its own
|
||||
// (`0x6e2a27`), a refusal's finding none: the literal `UNKNOWN` (`0x6e2a49`).
|
||||
if fail.redisplay {
|
||||
let cached = self.items.template(failing, 0, Ask::Read);
|
||||
Some(cached.map_or_else(|| "UNKNOWN".to_string(), |i| i.name.clone()))
|
||||
} else {
|
||||
Ask::Event(&self.commands)
|
||||
};
|
||||
let cached = self.items.template(failing, 0, ask).map(|i| i.name.clone());
|
||||
match cached {
|
||||
Some(name) => Some(name),
|
||||
// Answered unknown: the reference's callback literal (`0x838044`).
|
||||
None if self.items.template_answered_unknown(failing) => {
|
||||
Some("UNKNOWN".to_string())
|
||||
}
|
||||
None => {
|
||||
// Pending: shown as a redisplay once it answers, never logged.
|
||||
self.waiting.0.push(fail.requeued());
|
||||
return None;
|
||||
// The failure handler's lookup with the callback `0x6e29b0` and the reason, no
|
||||
// dedupe (`0x6e1e82`-`0x6e1ea4`): a miss leaves past the line and its log
|
||||
// (`0x6e1eab`), and the record's answer shows it ([`crate::ui_cast`]).
|
||||
let cached = self.items.template(failing, 0, Ask::Event(&self.commands));
|
||||
match cached {
|
||||
Some(info) => Some(info.name.clone()),
|
||||
None => {
|
||||
self.items.wait(
|
||||
failing,
|
||||
crate::items::RecordCallback::CastFailure {
|
||||
spell: spell_id,
|
||||
reason,
|
||||
},
|
||||
);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,11 +83,12 @@ impl CastFail {
|
||||
}
|
||||
}
|
||||
|
||||
/// The same entry, marked as a [`Self::redisplay`].
|
||||
pub(crate) const fn requeued(self) -> Self {
|
||||
/// The item-cache callback's retry of our refused line whose item had no record
|
||||
/// (`ShowItemCastFailure 0x6e29b0`), naming the item, marked as a [`Self::redisplay`].
|
||||
pub(crate) const fn item_redisplay(spell_id: u32, reason: u8, item: u32) -> Self {
|
||||
Self {
|
||||
redisplay: true,
|
||||
..self
|
||||
..Self::local_arg(spell_id, reason, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,8 +188,8 @@ fn a_notice_under_the_loading_cover_fires_at_the_reveal() {
|
||||
assert!(app.world().resource::<UiErrorKeys>().texts().is_empty());
|
||||
}
|
||||
|
||||
/// The cast reports' feed ([`crate::ui_cast::feed_cast_reports`]) over a VM, with `reports`
|
||||
/// queued and `spells` as the catalog.
|
||||
/// The cast reports' lane ([`crate::ui_cast::signal_reports`]) over a VM, with `reports` queued
|
||||
/// and `spells` as the catalog.
|
||||
fn report_feed(spells: crate::ui_action::Spells, reports: crate::ui_cast::CastReports) -> App {
|
||||
let (tx, _rx) = crossbeam_channel::unbounded();
|
||||
let mut app = App::new();
|
||||
@@ -198,7 +198,6 @@ fn report_feed(spells: crate::ui_action::Spells, reports: crate::ui_cast::CastRe
|
||||
.insert_resource(spells)
|
||||
.init_resource::<Items>()
|
||||
.init_resource::<crate::net::GuidIndex>()
|
||||
.init_resource::<super::CastFailRedisplays>()
|
||||
.init_resource::<crate::ui_chat::ChatLog>()
|
||||
.init_resource::<crate::sound::MessageSounds>()
|
||||
.init_resource::<crate::spell::PendingCast>()
|
||||
|
||||
@@ -15,7 +15,7 @@ use benilla_assets::{AssetSet, LockRecover, WorldAssets};
|
||||
|
||||
mod cast_fail;
|
||||
mod cast_lines;
|
||||
pub(crate) use cast_lines::{CastFailLines, CastFailRedisplays};
|
||||
pub(crate) use cast_lines::CastFailLines;
|
||||
mod drain;
|
||||
// A `UseAction` press, applied in call order by `crate::script_calls`.
|
||||
pub(crate) use drain::{
|
||||
@@ -522,7 +522,6 @@ impl Plugin for UiActionPlugin {
|
||||
.init_resource::<ReactiveSpells>()
|
||||
.init_resource::<ActionSlots>()
|
||||
.init_resource::<LearnedAbilities>()
|
||||
.init_resource::<CastFailRedisplays>()
|
||||
.init_resource::<ClickCasts>()
|
||||
.add_systems(Startup, load_spells.after(AssetSet::Open))
|
||||
.add_systems(Startup, load_spell_mechanics.after(AssetSet::Open))
|
||||
|
||||
@@ -2,20 +2,18 @@
|
||||
//! (`ReportCastFailure 0x6e1a00`), the wire edges that become the `SPELLCAST_*` events stock
|
||||
//! `CastingBarFrame.lua:6-13` registers for, named from `Spell.dbc`, and the tradeskill repeat's
|
||||
//! disarm. A script call's reports are signalled inside the call ([`crate::script_calls`]), the
|
||||
//! rest from their lane in the packets' order ([`fire_cast_reports`]), after the frame's feed has
|
||||
//! pushed the VM's cast state ([`feed_cast_reports`]). The cast lifecycle is `crate::spell`'s.
|
||||
//! rest from their lane in the packets' order ([`fire_cast_reports`]), where the getters read the
|
||||
//! cast state each packet left ([`CastLive`]). The cast lifecycle is `crate::spell`'s.
|
||||
|
||||
use bevy::ecs::system::SystemParam;
|
||||
use bevy::prelude::*;
|
||||
|
||||
use benilla_ui::script::{ScriptValue, UiScript};
|
||||
|
||||
use crate::spell::CastState;
|
||||
use crate::ui_action::{CastFail, CastFailLines, MessageSink, Shown, Spells};
|
||||
use crate::ui_unit::UnitFeed;
|
||||
|
||||
mod views;
|
||||
pub(crate) use views::{slot_moves_here, CastViews};
|
||||
pub(crate) use views::{slot_moves_here, CastLive, CastViews};
|
||||
|
||||
/// One edge of our own cast, queued by the spell's packet handlers, its reports and its cancels.
|
||||
pub(crate) enum CastBarEdge {
|
||||
@@ -62,8 +60,6 @@ pub(crate) enum CastReport {
|
||||
/// send stores it (`0x6e594c`, `0x6e5952`), `STOP_AUTOREPEAT_SPELL` (436) as
|
||||
/// `StopAutoRepeatSpell` clears it (`0x6ea16a`, `0x6ea170`).
|
||||
AutoRepeat(bool),
|
||||
/// The frame's retry of the refused lines that wait on an item's record (`0x6e29b0`).
|
||||
Redisplay,
|
||||
/// A line the cast validator shows itself through `CGGameUI::DisplayError 0x496720`, outside
|
||||
/// `ReportCastFailure`: a learn rung's, or the tame rung's ahead of its report.
|
||||
Line(crate::ui_action::UiError),
|
||||
@@ -97,6 +93,13 @@ impl CastReports {
|
||||
self.push(CastReport::Line(line));
|
||||
}
|
||||
|
||||
/// `ShowItemCastFailure 0x6e29b0`, run by the item answer of a refused line that waited on
|
||||
/// it ([`crate::items::RecordCallback::CastFailure`]): the line again, under the plain key,
|
||||
/// with no log line and no event.
|
||||
pub(crate) fn redisplay(&mut self, spell_id: u32, reason: u8, item: u32) {
|
||||
self.refused(CastFail::item_redisplay(spell_id, reason, item));
|
||||
}
|
||||
|
||||
/// `SMSG_PET_CAST_FAILED` (`0x6e8eb0`): one message, no log line and no event.
|
||||
pub(crate) fn pet_refused(&mut self, spell_id: u32, reason: u8) {
|
||||
self.refused(CastFail::pet(spell_id, reason));
|
||||
@@ -399,7 +402,6 @@ impl CastReporter<'_, '_> {
|
||||
CastReport::AutoRepeat(false) => {
|
||||
out.push(Ready::Event("STOP_AUTOREPEAT_SPELL", vec![]))
|
||||
}
|
||||
CastReport::Redisplay => out.extend(self.redisplays(script)),
|
||||
CastReport::Line(e) => {
|
||||
let get = |key: &str| script.lua().globals().get::<String>(key).ok();
|
||||
if let Some(text) = crate::ui_action::ui_error_text(&e, &get) {
|
||||
@@ -411,15 +413,6 @@ impl CastReporter<'_, '_> {
|
||||
out
|
||||
}
|
||||
|
||||
/// The refused lines whose item record has answered since (`0x6e29b0`).
|
||||
pub(crate) fn redisplays(&mut self, script: &UiScript) -> Vec<Ready> {
|
||||
let shown = self.lines.redisplays(script);
|
||||
shown
|
||||
.into_iter()
|
||||
.filter_map(|s| self.line(script, s))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A line's world half done, its event readied: a chat line's too, which `DisplayError`
|
||||
/// writes in place (`0x49a870`).
|
||||
pub(crate) fn line(&mut self, script: &UiScript, shown: Shown) -> Option<Ready> {
|
||||
@@ -476,34 +469,6 @@ impl CallReports<'_, '_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Push the cast state the frame's packets and cancels left ([`CastState`]), which
|
||||
/// `SpellStopCasting` (`0x6e6e80`) and the bar's checked tests read: a queued strike and the spell
|
||||
/// awaiting its click count as in flight, so the first Esc stops it and `ToggleGameMenu` skips
|
||||
/// `ClearTarget()` (`UIParent.lua:1489-1492`). The reports made outside any script call fire from
|
||||
/// their lane after every feed ([`fire_cast_reports`]), each listener reading the view its own
|
||||
/// packet left, which the views lane pushes at its place ([`views`]); a refused line that waits
|
||||
/// on its item's record is retried there, behind the frame's packets.
|
||||
#[allow(clippy::type_complexity)] // a Bevy system's full input set
|
||||
pub(crate) fn feed_cast_reports(
|
||||
// The reports and the cast state, which the host's casts write, and the views' log.
|
||||
mut vm: crate::script_calls::VmWith<(
|
||||
ResMut<'static, CastReports>,
|
||||
CastReporter<'static, 'static>,
|
||||
CastState<'static, 'static>,
|
||||
Option<ResMut<'static, CastViews>>,
|
||||
)>,
|
||||
) {
|
||||
vm.turn(|script, (mut reports, reporter, cast_state, mut views)| {
|
||||
let Some(script) = script else {
|
||||
return;
|
||||
};
|
||||
views::feed_live(script, views.as_deref_mut(), cast_state.view());
|
||||
if reporter.lines.any_waiting() {
|
||||
reports.push(CastReport::Redisplay);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// The cast lane's `fire_until` ([`crate::lanes`]): each report the lane holds while `until`
|
||||
/// admits it, readied and signalled at its place with the host lent, as the spell handlers signal
|
||||
/// inside their packets (`0x6e1a00`, `0x6e7a2d`, `0x6e7550`); the lane stops after a report whose
|
||||
@@ -537,24 +502,15 @@ fn fire_cast_reports(
|
||||
}
|
||||
}
|
||||
|
||||
/// The frame's cast-state push, then every report queued fired alone, for a test that drives the
|
||||
/// reports without the other lanes.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn signal_reports(world: &mut World) {
|
||||
use bevy::ecs::system::RunSystemOnce;
|
||||
world.run_system_once(feed_cast_reports).unwrap();
|
||||
fire_cast_lane(world);
|
||||
}
|
||||
|
||||
/// The session's end drops the reports no VM will hear, as the chat lane drops its lines: the
|
||||
/// Lua state they were for is replaced at the session end.
|
||||
fn end_session_reports(mut reports: ResMut<CastReports>) {
|
||||
reports.clear();
|
||||
}
|
||||
|
||||
/// The reports queued, fired alone, for a test that drives them without the other lanes.
|
||||
/// Every report queued, fired alone, for a test that drives the reports without the other lanes.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn fire_cast_lane(world: &mut World) {
|
||||
pub(crate) fn signal_reports(world: &mut World) {
|
||||
use bevy::ecs::system::RunSystemOnce;
|
||||
let until = crate::lanes::Until::all();
|
||||
while world
|
||||
@@ -568,8 +524,7 @@ pub(crate) fn fire_cast_lane(world: &mut World) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The cast-state push, before the VM ticks so an edge and its first `OnUpdate` share a frame, and
|
||||
/// after the local cancel so a move or Esc kills the bar on the next tick; and the cast lane.
|
||||
/// The cast lane and the cast views' log.
|
||||
pub(crate) struct UiCastPlugin;
|
||||
|
||||
impl Plugin for UiCastPlugin {
|
||||
@@ -583,13 +538,6 @@ impl Plugin for UiCastPlugin {
|
||||
fire_cast_reports,
|
||||
None,
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
// After the spell's local cancel, whose state this pushes the same frame.
|
||||
feed_cast_reports
|
||||
.in_set(UnitFeed)
|
||||
.after(crate::spell::LocalCancel),
|
||||
)
|
||||
.add_systems(
|
||||
OnExit(crate::client_state::ClientState::InWorld),
|
||||
end_session_reports.in_set(crate::ui_script::SessionEnd::Clears),
|
||||
|
||||
@@ -7,17 +7,19 @@
|
||||
//! `0x6e9fd0`, `SpellIsTargeting` `0x6e6cd0`, `SpellStopCasting` `0x6e6e80`), so a listener reads
|
||||
//! its own packet's. Here every handler of the frame runs before any listener, so the view each
|
||||
//! packet left is logged, stamped ahead of what the packet raised ([`CastViews`]); the views lane
|
||||
//! pushes the frame's start, then each packet's view at its place among the other lanes' items.
|
||||
//! A listener's call that moves the state moves it live, after every handler, so what it moved
|
||||
//! is the live state's from then on: each later view carries the call's fields as they stand
|
||||
//! live ([`Moved`]), and the rest as its packet left them.
|
||||
//! replays the frame's start, then each packet's view at its place among the other lanes' items,
|
||||
//! and the getters read it there through the host ([`CastLive`]). A listener's call that moves the
|
||||
//! state moves it live, after every handler, so what it moved is the live state's from then on:
|
||||
//! each later view carries the call's fields as they stand live ([`Moved`]), and the rest as its
|
||||
//! packet left them. The VM's copy follows the same views, for a read with no host to ask.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use bevy::ecs::system::SystemState;
|
||||
use bevy::ecs::system::{SystemParam, SystemState};
|
||||
use bevy::prelude::*;
|
||||
|
||||
use benilla_ui::script::{CastView, UiScript};
|
||||
use benilla_ui::script::live::LiveAsk;
|
||||
use benilla_ui::script::{CastView, CastViewNow, UiScript};
|
||||
|
||||
use crate::lanes::{Stamp, Until};
|
||||
use crate::spell::CastState;
|
||||
@@ -33,8 +35,10 @@ pub(crate) struct CastViews {
|
||||
last: Option<CastView>,
|
||||
/// The drain has pushed the start.
|
||||
started: bool,
|
||||
/// The live view the VM last took, from the frame's feed or a call: a call that leaves it as
|
||||
/// it stands pushes nothing, so the drain's view stays its listener's packet's.
|
||||
/// The packet's view the drain last replayed: where its listeners stand.
|
||||
at: Option<CastView>,
|
||||
/// The live view as the last packet or call left it: a call that leaves it as it stands
|
||||
/// pushes nothing, so the drain's view stays its listener's packet's.
|
||||
live: Option<CastView>,
|
||||
/// The view the VM holds.
|
||||
shown: Option<CastView>,
|
||||
@@ -152,15 +156,40 @@ impl CastViews {
|
||||
|
||||
/// A logged `view` into the VM, with the fields the drain's calls moved as they stand live.
|
||||
fn replay(&mut self, script: &mut UiScript, view: CastView) {
|
||||
self.at = Some(view);
|
||||
let view = self.called.over(view, self.live.unwrap_or(view));
|
||||
self.show(script, view);
|
||||
}
|
||||
|
||||
/// The view a getter reads at the call, `live` being the state as it stands: inside the
|
||||
/// drain, the packet's view the lane last replayed, with the fields the drain's calls moved as
|
||||
/// they stand live; else the live state.
|
||||
pub(crate) fn here(&self, live: CastView) -> CastView {
|
||||
match self.at.filter(|_| self.started) {
|
||||
Some(at) => self.called.over(at, live),
|
||||
None => live,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `view` into the VM with no log kept, in a harness without one.
|
||||
fn push(script: &mut UiScript, view: CastView) {
|
||||
script.set_cast_view(view);
|
||||
script.set_spell_targeting(view.targeting.is_some());
|
||||
/// Our cast's state as the getters read it inside the call (`IsCurrentAction` `0x4e53a0`,
|
||||
/// `IsAutoRepeatAction` `0x4e55b0` → `0x6e9fd0`, `SpellIsTargeting` `0x6e6cd0`, `SpellStopCasting`
|
||||
/// `0x6e6e80`): the live state, as of the drain's place while the views lane replays the frame's
|
||||
/// packets ([`CastViews::here`]).
|
||||
#[derive(SystemParam)]
|
||||
pub(crate) struct CastLive<'w, 's> {
|
||||
state: CastState<'w, 's>,
|
||||
/// The views' log; absent in a harness without the cast lanes, which reads the live state.
|
||||
views: Option<Res<'w, CastViews>>,
|
||||
}
|
||||
|
||||
impl crate::script_calls::live::LiveFamily for CastLive<'_, '_> {
|
||||
fn answer(&mut self, ask: &mut LiveAsk<'_>) -> bool {
|
||||
ask.answer(|_: &CastViewNow| {
|
||||
let live = self.state.view();
|
||||
Some(self.views.as_deref().map_or(live, |v| v.here(live)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The live view off the world, through a reader built once.
|
||||
@@ -202,7 +231,8 @@ fn packets_begin(world: &mut World) {
|
||||
|
||||
/// A packet handled: its view logged at `at`, the stamp taken ahead of its handler, when it
|
||||
/// differs from the one before it; with its slot at the stamp its handler moved it, when it
|
||||
/// named one ([`slot_moves_here`]).
|
||||
/// named one ([`slot_moves_here`]). The view it left is the live one a call after it is told
|
||||
/// against.
|
||||
fn packet_end(world: &mut World, at: Stamp) {
|
||||
let moves = SLOT_MOVES.take().filter(|s| *s > at);
|
||||
let Some(view) = live_view(world) else {
|
||||
@@ -211,6 +241,7 @@ fn packet_end(world: &mut World, at: Stamp) {
|
||||
let Some(mut views) = world.get_resource_mut::<CastViews>() else {
|
||||
return;
|
||||
};
|
||||
views.live = Some(view);
|
||||
let Some(last) = views.last else {
|
||||
return;
|
||||
};
|
||||
@@ -274,6 +305,7 @@ fn end_cast_views(world: &mut World) {
|
||||
views.entries.clear();
|
||||
views.start = None;
|
||||
views.started = false;
|
||||
views.at = None;
|
||||
views.called = Moved::default();
|
||||
let Some(view) = view else {
|
||||
return;
|
||||
@@ -295,18 +327,6 @@ fn drop_cast_views(mut views: ResMut<CastViews>) {
|
||||
};
|
||||
}
|
||||
|
||||
/// The frame's feed of the live view, which the drain's start replaces while the frame logged
|
||||
/// any ([`super::feed_cast_reports`]).
|
||||
pub(super) fn feed_live(script: &mut UiScript, views: Option<&mut CastViews>, view: CastView) {
|
||||
match views {
|
||||
Some(views) => {
|
||||
views.show(script, view);
|
||||
views.live = Some(view);
|
||||
}
|
||||
None => push(script, view),
|
||||
}
|
||||
}
|
||||
|
||||
/// The log, its packet hooks, the lane, its end and the drop under the exact complement of the
|
||||
/// drain's gate.
|
||||
pub(super) fn add_cast_views_lane(app: &mut App) {
|
||||
|
||||
@@ -1037,11 +1037,8 @@ impl Plugin for UiItemsPlugin {
|
||||
.in_set(crate::ui_script::UiFeed)
|
||||
.before(crate::ui_char::feed_char),
|
||||
// The item-tooltip store answers in the feed, so the next frame's input
|
||||
// records, a re-hover among them, see the answer, and seats a new VM before its world entry; a
|
||||
// refused cast's item name reads the store it leaves.
|
||||
feed_item_stats
|
||||
.in_set(UnitFeed)
|
||||
.before(crate::ui_cast::feed_cast_reports),
|
||||
// records, a re-hover among them, see the answer, and seats a new VM before its world entry.
|
||||
feed_item_stats.in_set(UnitFeed),
|
||||
// The equip callbacks a lookup ran on a record the VM's copy lacked, after
|
||||
// the record feed has pushed it; an answer runs its own at its packet.
|
||||
crate::ui_bind_confirm::replay_equip_records.in_set(crate::ui_script::UiDrain),
|
||||
|
||||
@@ -320,10 +320,10 @@ impl ActionState {
|
||||
}
|
||||
|
||||
/// The player's cast state as the reference's globals hold it, which the action bar's checked
|
||||
/// tests (`0x4e53a0`, `0x4e55b0`) and `SpellStopCasting` (`0x6e6e80`) read live. The app pushes it
|
||||
/// each frame, as each packet left it where that packet's events fire, and after every script call
|
||||
/// that moved it, so a listener reads its own packet's and a script's next line what the call
|
||||
/// left.
|
||||
/// tests (`0x4e53a0`, `0x4e55b0`) and `SpellStopCasting` (`0x6e6e80`) read live, through the lent
|
||||
/// host ([`CastViewNow`]), so a listener reads its own packet's and a script's next line what the
|
||||
/// call left. The app also pushes it, as each packet left it and after every script call that
|
||||
/// moved it, for a read with no host to ask.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct CastView {
|
||||
/// The spell in the in-flight slot (`0xceca88`, `0x6e3d10`): a cast from its attempt to its
|
||||
@@ -344,6 +344,38 @@ pub struct CastView {
|
||||
pub item: Option<u32>,
|
||||
}
|
||||
|
||||
/// The cast state's question to the lent host: our cast's globals as they stand where the getter
|
||||
/// runs ([`CastView`]), which `IsCurrentAction`, `IsAutoRepeatAction`, `SpellStopCasting` and the
|
||||
/// targeting word's tests read inside the call.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct CastViewNow;
|
||||
|
||||
impl super::live::LiveQuery for CastViewNow {
|
||||
type Answer = CastView;
|
||||
}
|
||||
|
||||
/// The cast state at the call ([`CastViewNow`]), else as the app last pushed it.
|
||||
pub(crate) fn cast_view(lua: &Lua) -> CastView {
|
||||
super::live::ask(lua, &CastViewNow).unwrap_or_else(|| {
|
||||
lua.app_data_ref::<Model>()
|
||||
.expect("model app_data")
|
||||
.cast_view
|
||||
})
|
||||
}
|
||||
|
||||
/// `IsTargeting 0x6e48a0` at the call: the targeting word is up, as [`cast_view`] holds its spell,
|
||||
/// else as the app last pushed it.
|
||||
pub(crate) fn spell_targeting(lua: &Lua) -> bool {
|
||||
super::live::ask(lua, &CastViewNow).map_or_else(
|
||||
|| {
|
||||
lua.app_data_ref::<Model>()
|
||||
.expect("model app_data")
|
||||
.spell_targeting
|
||||
},
|
||||
|view| view.targeting.is_some(),
|
||||
)
|
||||
}
|
||||
|
||||
impl CastView {
|
||||
/// What `SpellStopCasting` (`0x6e6e80`) can stop: the auto-repeat (`0x6e9fd0`), else a spell
|
||||
/// in flight (`IsCastInFlight 0x6e3d30`), never a channel, whose spell is not in flight, nor a
|
||||
@@ -454,7 +486,7 @@ impl super::UiScript {
|
||||
}
|
||||
|
||||
/// Push the player's cast state ([`CastView`]), which the checked tests and `SpellStopCasting`
|
||||
/// read.
|
||||
/// read with no host to ask ([`CastViewNow`]).
|
||||
pub fn set_cast_view(&mut self, view: CastView) {
|
||||
self.model_mut().cast_view = view;
|
||||
}
|
||||
@@ -692,12 +724,11 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 1 or nil for a slot's test against the live cast state.
|
||||
/// 1 or nil for a slot's test against the cast state at the call.
|
||||
fn live_flag(lua: &Lua, action: u32, pick: impl Fn(&ActionState, &CastView) -> bool) -> Value {
|
||||
let state = state_of(lua, action);
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
match state {
|
||||
Some(s) if pick(&s, &model.cast_view) => Value::Integer(1),
|
||||
Some(s) if pick(&s, &cast_view(lua)) => Value::Integer(1),
|
||||
_ => Value::Nil,
|
||||
}
|
||||
}
|
||||
@@ -756,12 +787,14 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// The form arm reads the active player's form byte at the call (`0x4e558b`).
|
||||
let form = super::stat_read::read_self(lua, |own| own.shapeshift_form());
|
||||
let state = state_of(lua, action);
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
let open = model.open_professions();
|
||||
let view = CastView {
|
||||
form,
|
||||
..model.cast_view
|
||||
..cast_view(lua)
|
||||
};
|
||||
let open = lua
|
||||
.app_data_ref::<Model>()
|
||||
.expect("model app_data")
|
||||
.open_professions();
|
||||
Ok(match state {
|
||||
Some(s) if s.is_current(&view, open) => Value::Integer(1),
|
||||
_ => Value::Nil,
|
||||
|
||||
@@ -1177,7 +1177,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
lua.create_function(|lua, (bag, slot): (Value, Value)| {
|
||||
// The targeting and base-mode gates run before either argument is read
|
||||
// (`0x4fa469`-`0x4fa47a`).
|
||||
if super::merchant::sell_cursor_shut(&lua.app_data_ref::<Model>().expect("model")) {
|
||||
if super::merchant::sell_cursor_shut(lua) {
|
||||
return Ok(());
|
||||
}
|
||||
// `is-number` on args 1 and 2 else `Usage: ShowContainerSellCursor(index, slot)`
|
||||
|
||||
@@ -831,12 +831,9 @@ pub(crate) fn split_container_item(
|
||||
equip_slots: s.equip_slots.clone(),
|
||||
}
|
||||
};
|
||||
let targeting = verb(lua, |model| {
|
||||
clear_cursor(model);
|
||||
model.spell_targeting
|
||||
});
|
||||
verb(lua, clear_cursor);
|
||||
// `StopTargeting` ends an armed spell's targeting, as `SpellStopTargeting` does.
|
||||
if targeting {
|
||||
if super::action::spell_targeting(lua) {
|
||||
super::calls::submit(lua, super::ScriptCall::SpellStopTargeting)?;
|
||||
}
|
||||
pick_up_item(lua, item);
|
||||
|
||||
@@ -43,9 +43,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
g.set(
|
||||
"ShowInventorySellCursor",
|
||||
lua.create_function(|lua, slot: Value| {
|
||||
if super::merchant::sell_cursor_shut(
|
||||
&lua.app_data_ref::<Model>().expect("model app_data"),
|
||||
) {
|
||||
if super::merchant::sell_cursor_shut(lua) {
|
||||
return Ok(());
|
||||
}
|
||||
let slot0 = inventory_slot_arg(
|
||||
|
||||
@@ -730,7 +730,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// its events running first.
|
||||
{
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
if model.spell_targeting || !head(lua, &model).can_repair {
|
||||
if super::action::spell_targeting(lua) || !head(lua, &model).can_repair {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -771,7 +771,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"ShowMerchantSellCursor",
|
||||
lua.create_function(|lua, index: Value| {
|
||||
// The targeting and base-mode gates come first (`0x4fbab7`-`0x4fbacc`).
|
||||
if sell_cursor_shut(&lua.app_data_ref::<Model>().expect("model app_data")) {
|
||||
if sell_cursor_shut(lua) {
|
||||
return Ok(());
|
||||
}
|
||||
// `is-number` else `Usage: ShowMerchantSellCursor(index)` (`0x84dd14`, `0x4fbadf`).
|
||||
@@ -786,7 +786,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"ShowBuybackSellCursor",
|
||||
lua.create_function(|lua, index: Value| {
|
||||
// The targeting and base-mode gates come first (`0x4fbbb9`-`0x4fbbce`).
|
||||
if sell_cursor_shut(&lua.app_data_ref::<Model>().expect("model app_data")) {
|
||||
if sell_cursor_shut(lua) {
|
||||
return Ok(());
|
||||
}
|
||||
// `is-number` else `Usage: ShowBuybackSellCursor(index)` (`0x84dd3c`, `0x4fbbe1`).
|
||||
@@ -804,8 +804,12 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
/// The gates every sell-cursor verb opens with, ahead of its argument reads: `IsSpellTargeting`
|
||||
/// (`0x6e48a0`), then the base cursor mode (`0x523d10`) must be 1, which the repair cursor moves
|
||||
/// (`0x4fbce3`). Either failing returns quietly; each is sticky, so a Buy would stamp it out.
|
||||
pub(super) fn sell_cursor_shut(model: &Model) -> bool {
|
||||
model.spell_targeting || model.repair_mode
|
||||
pub(super) fn sell_cursor_shut(lua: &Lua) -> bool {
|
||||
super::action::spell_targeting(lua)
|
||||
|| lua
|
||||
.app_data_ref::<Model>()
|
||||
.expect("model app_data")
|
||||
.repair_mode
|
||||
}
|
||||
|
||||
/// Arm the vendor hover cursor from a row's price, read at the call: Buy(3) if the player's coinage
|
||||
|
||||
@@ -477,8 +477,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
}
|
||||
let (world, ground) = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
let ground =
|
||||
model.spell_targeting && super::cursor::targeting_wants(lua, &model).location;
|
||||
let ground = super::action::spell_targeting(lua)
|
||||
&& super::cursor::targeting_wants(lua, &model).location;
|
||||
let d = 2.0 * f64::from(model.minimap.radius_yd());
|
||||
// Each fraction times the diameter is stored as a float (`0x4eee2e`, `0x4eee3d`).
|
||||
let (second, first) = (
|
||||
|
||||
@@ -236,7 +236,7 @@ mod zone_text;
|
||||
pub use action::{
|
||||
macro_slot_id, macro_slot_word, ActionAttackFaceNow, ActionBarCall, ActionConsumableNow,
|
||||
ActionCountNow, ActionSlot, ActionState, ActionStateNow, ActionTextureNow, ActionUsableNow,
|
||||
ActionUse, CastView, OpenProfessions, Profession, WearsItem,
|
||||
ActionUse, CastView, CastViewNow, OpenProfessions, Profession, WearsItem,
|
||||
};
|
||||
pub use addon::{addon_load_context, AddOnInfo, AddonLoad, AddonReply, AddonSecurity};
|
||||
pub use addon_enable::{AddonStateFiles, EnableHash, EnableStore};
|
||||
|
||||
@@ -257,8 +257,9 @@ impl super::UiScript {
|
||||
.is_empty()
|
||||
}
|
||||
|
||||
/// Whether the spell-targeting cursor is up, for `SpellIsTargeting()` and
|
||||
/// `SpellStopTargeting()`; pushed each frame before the input pass runs the ESC chain. Arming
|
||||
/// Whether the spell-targeting cursor is up, for `SpellIsTargeting()`, `SpellStopTargeting()`
|
||||
/// and the other tests of the word with no host to ask ([`super::CastViewNow`]); pushed each
|
||||
/// frame before the input pass runs the ESC chain. Arming
|
||||
/// ends repair mode, as `HideRepairCursor` does: the targeting arm writes the Cast base mode
|
||||
/// over Repair (`0x6e50b0`), and its end restores Point (`0x6e49f5`, `0x6e554c`), never Repair.
|
||||
pub fn set_spell_targeting(&mut self, targeting: bool) {
|
||||
@@ -827,11 +828,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let may_stop = protected::gate(lua, Tag::CAST);
|
||||
// `0x6e6e80` reads only the auto-repeat key `0xceac30` and the in-flight id
|
||||
// `0xceca88`, which is 0 mid-channel, as the call that cast last left them.
|
||||
let stoppable = lua
|
||||
.app_data_ref::<Model>()
|
||||
.expect("model app_data")
|
||||
.cast_view
|
||||
.stoppable();
|
||||
let stoppable = super::action::cast_view(lua).stoppable();
|
||||
if may_stop && stoppable {
|
||||
super::calls::submit(lua, ScriptCall::SpellStopCasting)?;
|
||||
Ok(Value::Integer(1))
|
||||
@@ -844,10 +841,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// SpellIsTargeting() (`0x6e6cd0`): 1 while the targeting cursor is up, else nil.
|
||||
g.set(
|
||||
"SpellIsTargeting",
|
||||
lua.create_function(|lua, ()| {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
Ok(flag(model.spell_targeting))
|
||||
})?,
|
||||
lua.create_function(|lua, ()| Ok(flag(super::action::spell_targeting(lua))))?,
|
||||
)?;
|
||||
|
||||
// SpellCanTargetUnit("unit") (`0x6e6d00`): the argument is a string or a number (`0x6e6d0e`,
|
||||
@@ -885,11 +879,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
lua.create_function(|lua, ()| {
|
||||
// The gate's tag 2 opens the function (`0x6e6e38`); a refusal answers nil (`0x6e6e69`).
|
||||
let may_stop = protected::gate(lua, Tag::CAST);
|
||||
let targeting = lua
|
||||
.app_data_ref::<Model>()
|
||||
.expect("model app_data")
|
||||
.spell_targeting;
|
||||
if may_stop && targeting {
|
||||
if may_stop && super::action::spell_targeting(lua) {
|
||||
super::calls::submit(lua, ScriptCall::SpellStopTargeting)?;
|
||||
Ok(Value::Integer(1))
|
||||
} else {
|
||||
|
||||
@@ -1138,11 +1138,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
if !crate::script::protected::gate(lua, crate::script::protected::Tag::CAST) {
|
||||
return Ok(());
|
||||
}
|
||||
if !lua
|
||||
.app_data_ref::<Model>()
|
||||
.expect("model app_data")
|
||||
.spell_targeting
|
||||
{
|
||||
if !super::super::action::spell_targeting(lua) {
|
||||
return Ok(());
|
||||
}
|
||||
check_unit_token(&Some(token.clone()))?;
|
||||
|
||||
+6
-6
@@ -278,7 +278,7 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it
|
||||
#### spell
|
||||
|
||||
- An out-of-range byte drops the packet, because the reference's unchecked write overruns the table (`(64, 19)` lands on the class family at `0xcecaac`). ([`src/spell/mods.rs:131`](../crates/benilla-app/src/spell/mods.rs#L131))
|
||||
- The fallback is the packet's target guid, started only when the validator keeps it (`on_spell_go`), and with none there is no start, never the nearest-hostile acquire, because that would attack something the player never named. ([`src/spell/net.rs:1478`](../crates/benilla-app/src/spell/net.rs#L1478))
|
||||
- The fallback is the packet's target guid, started only when the validator keeps it (`on_spell_go`), and with none there is no start, never the nearest-hostile acquire, because that would attack something the player never named. ([`src/spell/net.rs:1479`](../crates/benilla-app/src/spell/net.rs#L1479))
|
||||
- A range failure leaves targeting armed and working. The reference's report cancels the cast (`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. ([`src/spell/targeting/mod.rs:658`](../crates/benilla-app/src/spell/targeting/mod.rs#L658))
|
||||
|
||||
#### target
|
||||
@@ -299,8 +299,8 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it
|
||||
|
||||
- The player's `0x5e` with no word falls back to the spell's own `RequiresSpellFocus`, which the reference never reads here, because it is the id vmangos sends (`Spell.cpp:4426`). `0x5d` has no such stand-in: vmangos takes its area from its own `spell_area` table (`Spell.cpp:4429`), which `Spell.dbc` does not carry. ([`src/ui_action/cast_fail.rs:311`](../crates/benilla-app/src/ui_action/cast_fail.rs#L311))
|
||||
- A reason past the table prints its hex code, so an unknown reason is visible. ([`src/ui_action/cast_fail.rs:415`](../crates/benilla-app/src/ui_action/cast_fail.rs#L415))
|
||||
- A declined arm's template loses its tokens ("Missing reagent: %s" reads "Missing reagent") where the reference shows the literal `%s`, because a raw token reads as broken. ([`src/ui_action/cast_fail.rs:524`](../crates/benilla-app/src/ui_action/cast_fail.rs#L524))
|
||||
- An empty chat line is dropped, where the reference adds a blank one (`0x49a870` refuses only a null text, `0x49a881`), because a blank line is worse than none; only a string an addon emptied, or a chain missing a stock key, reaches it. ([`src/ui_action/errors.rs:587`](../crates/benilla-app/src/ui_action/errors.rs#L587))
|
||||
- A declined arm's template loses its tokens ("Missing reagent: %s" reads "Missing reagent") where the reference shows the literal `%s`, because a raw token reads as broken. ([`src/ui_action/cast_fail.rs:534`](../crates/benilla-app/src/ui_action/cast_fail.rs#L534))
|
||||
- An empty chat line is dropped, where the reference adds a blank one (`0x49a870` refuses only a null text, `0x49a881`), because a blank line is worse than none; only a string an addon emptied, or a chain missing a stock key, reaches it. ([`src/ui_action/errors.rs:588`](../crates/benilla-app/src/ui_action/errors.rs#L588))
|
||||
- The book's and the bar's login packets run this over every staged spell word, where the reference re-points slots only at `SMSG_SUPERCEDED_SPELL`, old rank to new (`0x4e5e60`, `super::spell_slots`), because vmangos never rewrites stored slots on a rank-up (`Player::AddSpell`), drops superseded ranks from the book (`Player.cpp:3463`) and sends no supersede while loading (`Player.cpp:3704`), so a stored slot can name a spell the character no longer knows. ([`src/ui_action/ranks.rs:3`](../crates/benilla-app/src/ui_action/ranks.rs#L3))
|
||||
|
||||
#### ui_aura
|
||||
@@ -723,7 +723,7 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it
|
||||
|
||||
#### script
|
||||
|
||||
- An out-of-range slot answers nil, because the reference indexes `0xbc6980` with no bounds check and reads the memory beside its 120 slots. ([`src/script/action.rs:589`](../crates/benilla-ui/src/script/action.rs#L589))
|
||||
- An out-of-range slot answers nil, because the reference indexes `0xbc6980` with no bounds check and reads the memory beside its 120 slots. ([`src/script/action.rs:621`](../crates/benilla-ui/src/script/action.rs#L621))
|
||||
- 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. ([`src/script/addon.rs:207`](../crates/benilla-ui/src/script/addon.rs#L207))
|
||||
- Holding the file, so the shutdown write keeps it. ([`src/script/addon.rs:787`](../crates/benilla-ui/src/script/addon.rs#L787))
|
||||
- The reference drops a send that resolves to PARTY while party slot 0's guid (`0xbc6f48`) is zero (`0x49fa9f`-`0x49faab`); benilla sends it, because `PartyState` is empty until the first `SMSG_GROUP_LIST` and the gate would swallow a real broadcast. vmangos drops a group-less party line anyway (`ChatHandler.cpp:476`). ([`src/script/addon_message.rs:7`](../crates/benilla-ui/src/script/addon_message.rs#L7))
|
||||
@@ -750,8 +750,8 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it
|
||||
- A last `spell_id` key breaks the remaining ties (Charge's six ranks all have `spellLevel` 0), because the reference leaves them to its qsort over an array benilla does not reproduce; ascending id is ascending rank in every 1.12 pet-ability family. ([`src/script/craft.rs:255`](../crates/benilla-ui/src/script/craft.rs#L255))
|
||||
- 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. ([`src/script/craft.rs:926`](../crates/benilla-ui/src/script/craft.rs#L926))
|
||||
- On `Terrain` the reference keeps a spell, action, macro or pet action on the left button; we clear them, because otherwise a spell has no left-click way off the cursor. ([`src/script/cursor.rs:744`](../crates/benilla-ui/src/script/cursor.rs#L744))
|
||||
- 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 copy. ([`src/script/cursor.rs:953`](../crates/benilla-ui/src/script/cursor.rs#L953))
|
||||
- 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. ([`src/script/cursor.rs:1249`](../crates/benilla-ui/src/script/cursor.rs#L1249))
|
||||
- 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 copy. ([`src/script/cursor.rs:950`](../crates/benilla-ui/src/script/cursor.rs#L950))
|
||||
- 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. ([`src/script/cursor.rs:1246`](../crates/benilla-ui/src/script/cursor.rs#L1246))
|
||||
- An auto-store into a bank item or keyring slot sends nothing, because no container id names it and no stock caller passes one; the reference sends it and the server refuses. ([`src/script/cursor/bag_verbs.rs:26`](../crates/benilla-ui/src/script/cursor/bag_verbs.rs#L26))
|
||||
- A row index the list no longer holds buys nothing and drops, where this site reads through a null row (`0x4c7e9a`, `0x4c7ea5`). ([`src/script/cursor/bag_verbs.rs:132`](../crates/benilla-ui/src/script/cursor/bag_verbs.rs#L132))
|
||||
- Slot 11 is refused, because the reference's gate (`cmp esi,0xa; jbe`) admits it; `0x4bce00` checks its bound (`0x4bce14`) but `0x4bc9a0` does not, so it writes one dword past the array (`0x4bcad9`), and the autocast toggle (`0x4bcbb0`) reads that word (`[0xb71438+0x28]`) with no bounds check. ([`src/script/cursor/pet.rs:20`](../crates/benilla-ui/src/script/cursor/pet.rs#L20))
|
||||
|
||||
Reference in New Issue
Block a user