the cooldown pie survives a /reload, and Bagnon's bags keep their order
- the cooldown pie, the GCD sweep, minimap pings and the map arrow draw again after a logout, login or /reload; they went blank until a restart, which read as OmniCC killing the cooldowns (2113) - an addon's own font on a button label keeps its face, size and outline instead of being undone every frame, MikScrollingBattleText's reported look (2112) - an empty label reads as nothing rather than an empty string, so Cartographer's map label keeps its colour; a slider thumb with no authored size takes its art's own size, so Dewdrop's Look'n'Feel slider moves (2109, 2110) - Bagnon's saved bag list comes back in the order it was written, so the keyring stops jumping to the front after a restart; hovering a Bagnon bag costs a third of what it did (2111, 2114) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -367,6 +367,126 @@ fn the_cooldown_sweep_paints_over_the_buttons_icon_and_ring() {
|
||||
);
|
||||
}
|
||||
|
||||
/// **A cooldown-count addon's hook leaves the sweep exactly where it was.**
|
||||
///
|
||||
/// `!OmniCC` 6.8.30 — the cooldown addon on the director's screen — is a single wrap of the
|
||||
/// FrameXML global: it captures `CooldownFrame_SetTimer` in an upvalue, replaces the global with
|
||||
/// a function that calls through, and hangs a `Frame` + `FontString` off the button for its own
|
||||
/// countdown, keeping the handle on a field of the cooldown widget itself (`cd.textFrame`). The
|
||||
/// shape is transcribed here — the widget verbs it uses, not its source — because that shape
|
||||
/// touches everything the pie's paint depends on: the global the bar calls, the widget's own
|
||||
/// Lua fields (`start`/`duration`/`stopping`, which the stock `Cooldown.lua` writes and its
|
||||
/// `OnUpdateModel` reads back), and the button's frame-level stack.
|
||||
///
|
||||
/// It came in as "with `!OmniCC` installed the pie and the GCD sweep are gone", and this is the
|
||||
/// half that answers whether the addon's hook itself is what breaks them. It is not: the engine
|
||||
/// reports the same paint list, the same armed sequence and the same scrub with the wrap in
|
||||
/// place as without it. (What did break them is the tile renderer's per-VM state — enabling an
|
||||
/// addon costs a logout and a login, and the pane's model facts did not survive that; see
|
||||
/// `ui_models`' own tests.)
|
||||
#[test]
|
||||
fn a_cooldown_count_addons_hook_leaves_the_sweep_running() {
|
||||
use benilla_ui::script::ActionState;
|
||||
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_action_bar(&s);
|
||||
// The addon loads after FrameXML (`!` sorts it first among addons, all of which run after the
|
||||
// interface), so the global it captures is the stock one.
|
||||
s.run(
|
||||
r#"
|
||||
local original = CooldownFrame_SetTimer
|
||||
seen = 0
|
||||
CooldownFrame_SetTimer = function(cd, start, duration, enable)
|
||||
seen = seen + 1
|
||||
original(cd, start, duration, enable)
|
||||
if start > 0 and duration > 3 and enable > 0 then
|
||||
local count = cd.textFrame
|
||||
if not count then
|
||||
local icon = getglobal(cd:GetParent():GetName() .. "Icon")
|
||||
if icon then
|
||||
count = CreateFrame("Frame", nil, cd:GetParent())
|
||||
count:SetAllPoints(cd:GetParent())
|
||||
count:SetFrameLevel(count:GetFrameLevel() + 1)
|
||||
count.text = count:CreateFontString(nil, "OVERLAY")
|
||||
count.text:SetFontObject(GameFontNormal)
|
||||
count.text:SetPoint("CENTER", count, "CENTER", 0, 1)
|
||||
count.icon = icon
|
||||
count:SetScript("OnUpdate", function() end)
|
||||
cd.textFrame = count
|
||||
end
|
||||
end
|
||||
if count then
|
||||
count.start = start
|
||||
count.duration = duration
|
||||
count:Show()
|
||||
end
|
||||
elseif cd.textFrame then
|
||||
cd.textFrame:Hide()
|
||||
end
|
||||
end
|
||||
"#,
|
||||
)
|
||||
.expect("the addon's hook installs");
|
||||
|
||||
s.set_action(
|
||||
1,
|
||||
Some(ActionSlot {
|
||||
texture: Some("Interface\\Icons\\Spell_Fire_FlameBolt".into()),
|
||||
kind: 0x00,
|
||||
action: 133,
|
||||
count: 0,
|
||||
consumable: false,
|
||||
}),
|
||||
);
|
||||
s.fire_event("PLAYER_ENTERING_WORLD", vec![]);
|
||||
s.tick(10.0);
|
||||
s.set_action_state(
|
||||
1,
|
||||
Some(ActionState {
|
||||
usable: true,
|
||||
cooldown: Some((6_000, 10_000, true)),
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
s.fire_event("ACTIONBAR_UPDATE_COOLDOWN", vec![]);
|
||||
super::test_ui::cooldown_facts(&mut s);
|
||||
s.tick(0.0);
|
||||
s.resolve();
|
||||
|
||||
assert!(
|
||||
s.eval::<i64>("return seen").unwrap() > 0,
|
||||
"the bar must reach the addon's replacement, not a captured original"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>("return ActionButton1Cooldown.textFrame ~= nil")
|
||||
.unwrap(),
|
||||
"the addon hangs its countdown off the cooldown widget — a field the widget must accept"
|
||||
);
|
||||
assert_eq!(
|
||||
super::test_ui::cooldown_play(&s, "ActionButton1Cooldown"),
|
||||
Some((0, 400)),
|
||||
"the wrap calls through, so the pane is on the paint list with sequence 0 at 40 %"
|
||||
);
|
||||
// And it goes cold the reference's way when the cooldown is cleared.
|
||||
s.set_action_state(
|
||||
1,
|
||||
Some(ActionState {
|
||||
usable: true,
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
s.fire_event("ACTIONBAR_UPDATE_COOLDOWN", vec![]);
|
||||
s.tick(0.0);
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
super::test_ui::cooldown_play(&s, "ActionButton1Cooldown"),
|
||||
None,
|
||||
"an elapsed cooldown hides the pane through the wrap exactly as it does without it"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// An action button is a TWO-button button (decision 0908; director's report B200: "I can't right
|
||||
/// click food on my bar to eat it or right click spells"). The ref's `ActionButton_OnLoad`
|
||||
/// registers `("LeftButtonUp", "RightButtonUp")` (ActionButton.lua:109) and its OnClick body reads
|
||||
|
||||
@@ -2342,7 +2342,10 @@ mod tests {
|
||||
|
||||
/// The written bytes are the recorded grammar (decision 1128's, itself byte-verified against
|
||||
/// `0x7043f0`/`0x704480`): `NAME = value`, bracketed keys, TAB indent, a trailing comma on
|
||||
/// every entry, and the file split by scope.
|
||||
/// every entry, and the file split by scope. `["t"]`'s `[1] = 1,` is the reference's shape for
|
||||
/// a list too — its writer emits a bracketed key for every table shape and never a bare
|
||||
/// positional entry (wow-re `system/ui/scratch/lua-table-storage-and-next-order.md` §Q5) —
|
||||
/// and it reloads in index order because of what 1.12's *parser* does with it (decision 2111).
|
||||
///
|
||||
/// Asserted as bytes rather than by re-reading, because "it round-trips through our own
|
||||
/// loader" is exactly the check that passes for a private format. The reference's own client
|
||||
|
||||
@@ -250,8 +250,10 @@ fn the_list_rows_carry_the_queue_status() {
|
||||
"(In Queue)"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return BattlefieldZone1Status:GetText()")
|
||||
s.eval::<Option<String>>("return BattlefieldZone1Status:GetText()")
|
||||
.unwrap(),
|
||||
""
|
||||
None,
|
||||
"an un-queued row's status line is blank, and a blank FontString reads back NIL \
|
||||
(`FontString:GetText 0x79d690` substitutes — decision 2110)"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -200,13 +200,24 @@ pub(super) fn emit(
|
||||
let ink = crate::ui_text::measure_text(&mut atlas.lock(), draw_text, None, spec).0;
|
||||
format!(" caret={:.1} ink={ink:.1}", ui.caret_x * host.scale)
|
||||
});
|
||||
// **The FACE and the FLAGS are on this line for a reason.** It used to print the
|
||||
// requested height and nothing else about the font, so the one question a "the text
|
||||
// looks wrong" report actually asks — *which face, at what size, with what outline, did
|
||||
// this quad draw* — could not be answered from the probe at all; it took a live
|
||||
// `SetFont` probe, a control plate and a screenshot to establish for MSBT what these
|
||||
// four fields say directly (decisions 2103, 2112). `px` is the DRAWN logical height
|
||||
// (`drawn_px`: the requested one through the cap, the 768 seam and the frame scale),
|
||||
// which is the number that disagrees with `h` whenever a size looks wrong.
|
||||
info!(
|
||||
"text probe: [{:.0},{:.0} {:.0}x{:.0}] h={:?}{} {:?}",
|
||||
"text probe: [{:.0},{:.0} {:.0}x{:.0}] h={:?} px={:?} flags={:?} face={:?}{} {:?}",
|
||||
draw_rect.min.x,
|
||||
draw_rect.min.y,
|
||||
host.rect.width(),
|
||||
host.rect.height(),
|
||||
style.font_height,
|
||||
spec.height,
|
||||
style.outline.as_str(),
|
||||
spec.path.unwrap_or("<none — the fallback face>"),
|
||||
ebox_geom.unwrap_or_default(),
|
||||
&draw_text[..draw_text.len().min(60)]
|
||||
);
|
||||
|
||||
@@ -82,8 +82,10 @@ fn bar_color(s: &UiScript, frame: &str) -> (f64, f64, f64) {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn caption(s: &UiScript, frame: &str) -> String {
|
||||
s.eval::<String>(&format!("return {frame}Text:GetText()"))
|
||||
/// `Option`, not `String`: a blank caption reads back **nil**, because `FontString:GetText
|
||||
/// 0x79d690` substitutes nil for an empty string (decision 2110).
|
||||
fn caption(s: &UiScript, frame: &str) -> Option<String> {
|
||||
s.eval::<Option<String>>(&format!("return {frame}Text:GetText()"))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
@@ -126,7 +128,7 @@ fn breath_takes_the_first_bar_in_the_reference_blue() {
|
||||
|
||||
assert!(shown(&s, "MirrorTimer1"));
|
||||
assert!(!shown(&s, "MirrorTimer2"), "only one timer is running");
|
||||
assert_eq!(caption(&s, "MirrorTimer1"), "Breath");
|
||||
assert_eq!(caption(&s, "MirrorTimer1").as_deref(), Some("Breath"));
|
||||
// Milliseconds on the wire, seconds in the bar — the reference's `/1000` on both.
|
||||
assert_eq!(bar_value(&s, "MirrorTimer1"), 45.0);
|
||||
assert_eq!(bar_max(&s, "MirrorTimer1"), 60.0);
|
||||
@@ -147,7 +149,7 @@ fn fatigue_is_the_reference_yellow_under_its_own_caption() {
|
||||
start(&mut s, "EXHAUSTION", 60_000, 60_000, -1, "Fatigue");
|
||||
|
||||
assert!(shown(&s, "MirrorTimer1"));
|
||||
assert_eq!(caption(&s, "MirrorTimer1"), "Fatigue");
|
||||
assert_eq!(caption(&s, "MirrorTimer1").as_deref(), Some("Fatigue"));
|
||||
let (r, g, b) = bar_color(&s, "MirrorTimer1");
|
||||
assert!(
|
||||
(r - 1.0).abs() < 1e-6 && (g - 0.9).abs() < 1e-6 && (b - 0.0).abs() < 1e-6,
|
||||
@@ -253,8 +255,8 @@ fn two_timers_stack_and_restate_in_place() {
|
||||
|
||||
assert!(shown(&s, "MirrorTimer1") && shown(&s, "MirrorTimer2"));
|
||||
assert!(!shown(&s, "MirrorTimer3"));
|
||||
assert_eq!(caption(&s, "MirrorTimer1"), "Fatigue");
|
||||
assert_eq!(caption(&s, "MirrorTimer2"), "Breath");
|
||||
assert_eq!(caption(&s, "MirrorTimer1").as_deref(), Some("Fatigue"));
|
||||
assert_eq!(caption(&s, "MirrorTimer2").as_deref(), Some("Breath"));
|
||||
|
||||
// The server re-sends breath on every change; it must reuse frame 2, not take frame 3.
|
||||
start(&mut s, "BREATH", 30_000, 60_000, -1, "Breath");
|
||||
@@ -283,8 +285,8 @@ fn stop_hides_that_timer_and_frees_its_frame() {
|
||||
assert!(shown(&s, "MirrorTimer1"));
|
||||
assert_eq!(
|
||||
caption(&s, "MirrorTimer1"),
|
||||
"",
|
||||
"no FEIGNDEATH_LABEL exists in the 1.12 GlobalStrings"
|
||||
None,
|
||||
"no FEIGNDEATH_LABEL exists in the 1.12 GlobalStrings, and a blank caption reads nil"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -90,8 +90,10 @@ fn hunter_pet_stats() -> PetStats {
|
||||
}
|
||||
}
|
||||
|
||||
fn level_text(s: &UiScript) -> String {
|
||||
s.eval::<String>("return PetStableLevelText:GetText()")
|
||||
/// `Option`, not `String`: an emptied line reads back **nil**, because `FontString:GetText
|
||||
/// 0x79d690` substitutes nil for an empty string (decision 2110).
|
||||
fn level_text(s: &UiScript) -> Option<String> {
|
||||
s.eval::<Option<String>>("return PetStableLevelText:GetText()")
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
@@ -123,7 +125,7 @@ fn call_pet_repaints_the_closed_stable_with_unknownobject_while_the_name_is_in_f
|
||||
!s.eval::<bool>("return PetStableFrame:IsVisible()").unwrap(),
|
||||
"UNIT_PET must repaint the window without opening it"
|
||||
);
|
||||
assert_eq!(level_text(&s), "Unknown Level 58 Boar");
|
||||
assert_eq!(level_text(&s).as_deref(), Some("Unknown Level 58 Boar"));
|
||||
assert_eq!(
|
||||
s.eval::<String>("return PetStableCurrentPet.tooltip")
|
||||
.unwrap(),
|
||||
@@ -141,7 +143,7 @@ fn call_pet_repaints_the_closed_stable_with_unknownobject_while_the_name_is_in_f
|
||||
s.set_unit("pet", Some(pet(Some("Snarl"))));
|
||||
s.fire_event("PET_STABLE_UPDATE", vec![]);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
assert_eq!(level_text(&s), "Snarl Level 58 Boar");
|
||||
assert_eq!(level_text(&s).as_deref(), Some("Snarl Level 58 Boar"));
|
||||
assert_eq!(
|
||||
s.eval::<String>("return PetStableCurrentPet.tooltip")
|
||||
.unwrap(),
|
||||
@@ -164,7 +166,7 @@ fn dismissing_the_pet_empties_the_closed_stable_without_raising() {
|
||||
s.set_pet_stats(false, PetStats::default());
|
||||
s.fire_event("UNIT_PET", vec![ScriptValue::Str("player".into())]);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
assert_eq!(level_text(&s), "");
|
||||
assert_eq!(level_text(&s), None, "an emptied line reads back nil");
|
||||
assert_eq!(
|
||||
s.eval::<String>("return PetStableCurrentPet.tooltip")
|
||||
.unwrap(),
|
||||
|
||||
@@ -771,22 +771,28 @@ fn the_row_tag_is_its_own_right_flush_string_and_the_state_word_wins() {
|
||||
s.run("ToggleQuestLog()").unwrap();
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
|
||||
// `Option`: an untagged row's string is blank, and a blank FontString reads back **nil** —
|
||||
// `FontString:GetText 0x79d690` substitutes nil for an empty string (decision 2110).
|
||||
let tag = |s: &mut UiScript, i: u32| {
|
||||
s.eval::<String>(&format!("return QuestLogTitle{i}Tag:GetText()"))
|
||||
s.eval::<Option<String>>(&format!("return QuestLogTitle{i}Tag:GetText()"))
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(
|
||||
tag(&mut s, 1),
|
||||
"(Elite)",
|
||||
tag(&mut s, 1).as_deref(),
|
||||
Some("(Elite)"),
|
||||
"the engine's tag, in parentheses"
|
||||
);
|
||||
assert_eq!(
|
||||
tag(&mut s, 2),
|
||||
"(Complete)",
|
||||
tag(&mut s, 2).as_deref(),
|
||||
Some("(Complete)"),
|
||||
"COMPLETE overwrites the quest's own tag — ref l.190-192"
|
||||
);
|
||||
assert_eq!(tag(&mut s, 3), "(Failed)", "FAILED, same override");
|
||||
assert_eq!(tag(&mut s, 4), "", "a header carries no tag");
|
||||
assert_eq!(
|
||||
tag(&mut s, 3).as_deref(),
|
||||
Some("(Failed)"),
|
||||
"FAILED, same override"
|
||||
);
|
||||
assert_eq!(tag(&mut s, 4), None, "a header carries no tag");
|
||||
|
||||
// The title is the bare (indented) name — the state word lives on the tag string now, so
|
||||
// appending it here too would double it.
|
||||
|
||||
@@ -726,3 +726,84 @@ fn an_action_bar_hover_sweep_costs_no_graph_derivation() {
|
||||
its node like any other (decision 1630, extending 1625)."
|
||||
);
|
||||
}
|
||||
|
||||
/// **The bag-addon hover law** (decision 2114, ledger B06's third idiom): a hover that owns the
|
||||
/// tooltip WITH an anchor and then re-points it by hand must cost **zero** derivations.
|
||||
///
|
||||
/// The guard above drives `SetOwner(owner, "ANCHOR_NONE")` + `ClearAllPoints()`, and it has always
|
||||
/// passed — because ANCHOR_NONE already dropped the anchors, so the `ClearAllPoints()` after it
|
||||
/// finds an empty list and touches nothing at all. It is the third idiom that was never driven:
|
||||
///
|
||||
/// ```lua
|
||||
/// -- Bagnon_Core/core/Item.lua -> Bagnon_Core/core/Utility.lua
|
||||
/// ContainerFrameItemButton_OnEnter(item) -- SetOwner(item, "ANCHOR_RIGHT"): sets an anchor
|
||||
/// Bagnon_AnchorTooltip(item) -- ClearAllPoints(), then GetLeft(), then SetPoint
|
||||
/// ```
|
||||
///
|
||||
/// `ClearAllPoints()` with anchors present took the conservative touch, and the `GetLeft()` on the
|
||||
/// next line settles the layout right there — so the whole graph was re-derived INSIDE the handler,
|
||||
/// once per bag slot the cursor crossed. Measured live on `Probetwo` with the director's AddOns
|
||||
/// folder, `WOW_UI_HANDLERS=8` over a 60 Hz sweep of twelve `BagnonItem*` buttons: `OnEnter` self
|
||||
/// 1.75 ms/frame and `[layout-derive]` naming this site in three of its four samples, against
|
||||
/// 0.43 ms/frame of total `tick` for the same sweep over stock `ContainerFrame1Item*` with no
|
||||
/// addons. `derives/frame` read 0.88 with Bagnon and 0.00 without.
|
||||
#[test]
|
||||
fn a_bag_addon_hover_sweep_costs_no_graph_derivation() {
|
||||
let mut s = settled_default_ui();
|
||||
s.run(
|
||||
r#"
|
||||
for i = 1, 12 do
|
||||
local b = CreateFrame("Button", "BagOwner" .. i)
|
||||
b:SetPoint("CENTER", 0, 0); b:SetSize(37, 37)
|
||||
end
|
||||
bag_n = 0
|
||||
function bag_hover()
|
||||
bag_n = bag_n + 1
|
||||
local item = getglobal("BagOwner" .. (math.mod(bag_n, 12) + 1))
|
||||
-- ContainerFrameItemButton_OnEnter's arm: an ANCHORED SetOwner.
|
||||
GameTooltip:SetOwner(item, "ANCHOR_RIGHT")
|
||||
GameTooltip:AddLine("Item " .. bag_n, 1, 1, 1)
|
||||
if math.mod(bag_n, 2) == 0 then
|
||||
GameTooltip:AddLine("Use: restores health over 21 sec.", 0, 1, 0, 1)
|
||||
else
|
||||
GameTooltip:AddLine("Main Hand", 1, 1, 1)
|
||||
end
|
||||
GameTooltip:Show()
|
||||
-- …then Bagnon_AnchorTooltip: drop the anchors it just set, ASK A RESOLVED EDGE
|
||||
-- (which settles the layout on the spot), and re-point by hand.
|
||||
GameTooltip:ClearAllPoints()
|
||||
local left = item:GetLeft() or 0
|
||||
if left < (UIParent:GetRight() / 2) then
|
||||
GameTooltip:SetPoint("TOPLEFT", item, "BOTTOMRIGHT")
|
||||
else
|
||||
GameTooltip:SetPoint("TOPRIGHT", item, "BOTTOMLEFT")
|
||||
end
|
||||
end
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Positive control across the births and the line pool's growth — both structural.
|
||||
let born_at = s.layout_derivations();
|
||||
for _ in 0..40 {
|
||||
s.run("bag_hover()").unwrap();
|
||||
app_frame(&mut s);
|
||||
}
|
||||
assert!(
|
||||
s.layout_derivations() > born_at,
|
||||
"creating twelve slots must derive the graph — zero here makes the assertion vacuous."
|
||||
);
|
||||
|
||||
let derives_before = s.layout_derivations();
|
||||
for _ in 0..24 {
|
||||
s.run("bag_hover()").unwrap();
|
||||
app_frame(&mut s);
|
||||
}
|
||||
let derives = s.layout_derivations() - derives_before;
|
||||
assert_eq!(
|
||||
derives, 0,
|
||||
"24 bag-slot hovers derived the layout graph {derives} times — one per slot crossed. \
|
||||
`ClearAllPoints` drops a node's whole anchor-target set, which is a retarget onto the \
|
||||
EMPTY set and names its node like any other (decision 2114, completing 1625/1630)."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -173,7 +173,8 @@ fn the_poi_pool_grows_and_parks_its_tail() {
|
||||
|
||||
/// Hovering a POI names it in the map's area label; the description line beneath carries the
|
||||
/// landmark's status only when it has one — the guard's directions never do, a battleground
|
||||
/// node's "In Conflict" would — and is blanked otherwise (stock `WorldMapPOI_OnEnter`).
|
||||
/// node's "In Conflict" would — and is blanked otherwise (stock `WorldMapPOI_OnEnter`), which
|
||||
/// reads back as **nil**, not `""` (decision 2110).
|
||||
#[test]
|
||||
fn hovering_a_poi_names_it_and_adds_a_status_line_only_when_there_is_one() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
@@ -188,10 +189,12 @@ fn hovering_a_poi_names_it_and_adds_a_status_line_only_when_there_is_one() {
|
||||
"Lion's Pride Inn"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return WorldMapFrameAreaDescription:GetText()")
|
||||
s.eval::<Option<String>>("return WorldMapFrameAreaDescription:GetText()")
|
||||
.unwrap(),
|
||||
"",
|
||||
"no description → an empty description line"
|
||||
None,
|
||||
"no description → a blank line that reads back NIL: `FontString:GetText 0x79d690` \
|
||||
substitutes nil for an empty string (decision 2110), and Cartographer 2.02's world-map \
|
||||
hover reads exactly this as \"this POI has no status line\""
|
||||
);
|
||||
|
||||
let mut with_status = landmark("Stables", 6, (0.5, 0.5));
|
||||
|
||||
Reference in New Issue
Block a user