Files
benilla-octo/crates/benilla-app/src/text_reshape.rs
T
Sam SungandClaude Opus 5.5 1d1f0a2c03 Comments in benilla-app, the glue screens, harness, settings and the rest: one line of fact each, no history, no record numbers (#365)
Part of #41: the last of five `benilla-app` pieces. This one is everything the others did not cover: the addon harness, the settings registry, the glue screens (login, realm list, character select and create), key bindings, the minimap, nameplates, the loading screen, transports, the performance instruments, the smaller modules, the integration tests, and the UI asset files (`OptionsFrame.xml`, `benilla.toc` and the rest). Their comments are rewritten to the rule in `docs/METHOD.md`, which is what the code does and the reference fact behind it, in a line. 27,351 comment lines become 11,010.

Fixes #33. Fixes #103.

- **The mechanical strip.** The citations that were only a private record number go first, and each edit is shown to be a pure removal.
- **Wrong facts corrected.** The rewrite corrects the comments that stated wrong facts. Among them:
  - The weapon trail's emissive is 0 (`0x70ca30`): the trail is a type-5 render record, so its ambient-only light term is exact.
  - The camera-style dropdown writes "1", "2", "0" (`UIOptionsFrame.lua:525,536,547`); several register-site addresses in the settings table are corrected.
  - The graphics settings are held until `RestartGx`, as in 1.12; `Fonts.xml`'s early load no longer carries a false reason.
  - vmangos sends a player-name invalidation after a rename (`CharacterHandler.cpp:839`), and the cinematic camera's announce packet has an empty handler there (`MiscHandler.cpp:919`).
  - Stock FrameXML and GlueXML line citations are checked against the stock 1.12 files, and vmangos line citations against the pinned revision.
- **Deviation labels.** "Deviation:" now marks only a choice with its recorded reason. Declining to copy a reference defect is stated plainly, and so is an unbuilt behaviour or an untraced one.
- **Verification.** Every file strips to identical code, apart from test literals in `login/`, `death/` and `ui_script/death_tests.rs` that now use placeholder account and character names, and `cargo fmt --check` is clean. Six second readers compared every file old against new, restored the facts the rewrite had dropped, and checked each new citation against the code, the stock files, vmangos and the reference.

A second commit corrects one comment in `benilla-ui` (`unit_is_grey`: the reference's exclusion is players only). A third lowers `ui_script_wall`'s ceiling from 45 to 40: five gameplay files named `UiScript` only in comments the rewrite removed, and the ratchet asks for the number to follow.

Behaviour differences the rewrite surfaced are filed as their own issues (#339 to #353 among them). String literals are untouched; they are #92.

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 00:27:56 +07:00

290 lines
9.5 KiB
Rust

//! Re-shapes a `bevy_ui` text root whose last span was despawned, which upstream misses.
//!
//! Despawning the last `TextSpan` removes `Children` rather than changing it, so
//! `detect_text_needs_rerender` never fires and the root keeps a multi-run buffer and a span list
//! naming dead entities. The next re-layout without a re-shape, such as a window resize, indexes
//! `glyph_info` past the shortened list and panics (`bevy_text-0.18.1/src/pipeline.rs:398`).
//!
//! This is a net: the site that empties a node should still leave it coherent. It covers
//! `bevy_ui` text only; `Text2d` has the same hole, but this tree spawns none.
use bevy::prelude::*;
use bevy::text::detect_text_needs_rerender;
pub(crate) struct TextReshapePlugin;
impl Plugin for TextReshapePlugin {
fn build(&self, app: &mut App) {
app.add_systems(
PostUpdate,
// The mark must land before the detector reads it.
reshape_emptied_text_roots.before(detect_text_needs_rerender::<Text>),
);
}
}
/// Marks a `Text` root changed when it loses its `Children`.
fn reshape_emptied_text_roots(
mut emptied: RemovedComponents<Children>,
mut roots: Query<&mut Text>,
) {
for entity in emptied.read() {
if let Ok(mut text) = roots.get_mut(entity) {
text.set_changed();
}
}
}
/// The two `bevy_ui` pipeline calls around a text node, driven by hand, since `UiPlugin` needs a
/// render app, a camera and a window.
#[cfg(test)]
pub(crate) mod harness {
use bevy::ecs::system::RunSystemOnce;
use bevy::image::TextureAtlasLayout;
use bevy::prelude::*;
use bevy::text::{
detect_text_needs_rerender, ComputedTextBlock, CosmicFontSystem, Font, FontAtlasSet,
FontHinting, SwashCache, TextBounds, TextLayoutInfo, TextPipeline,
};
use bevy::ui::widget::TextUiReader;
/// Wide enough for one line; nothing depends on it.
const BOUNDS: TextBounds = TextBounds::new(400.0, 200.0);
/// `TextPlugin` with its embedded default font, and the glyph atlas's asset stores.
pub(crate) fn add_text_plugins(app: &mut App) {
app.add_plugins(bevy::text::TextPlugin);
app.init_asset::<TextureAtlasLayout>();
app.init_asset::<Image>();
}
/// Points the root and spans at the embedded default font. It bypasses change detection: a
/// `TextFont` change would request the re-shape the tests check is missing.
fn use_default_font(app: &mut App, root: Entity) {
let spans: Vec<Entity> = app
.world()
.get::<Children>(root)
.map(|c| c.to_vec())
.unwrap_or_default();
for entity in core::iter::once(root).chain(spans) {
if let Some(mut font) = app.world_mut().get_mut::<TextFont>(entity) {
font.bypass_change_detection().font = Handle::default();
}
}
}
/// Shapes the block as `widget::measure_text_system` does.
pub(crate) fn shape(app: &mut App, root: Entity) {
use_default_font(app, root);
app.world_mut()
.run_system_once_with(shape_one, root)
.expect("the shaping system runs");
}
fn shape_one(
In(root): In<Entity>,
mut pipeline: ResMut<TextPipeline>,
fonts: Res<Assets<Font>>,
mut font_system: ResMut<CosmicFontSystem>,
mut blocks: Query<(&TextLayout, &mut ComputedTextBlock, &FontHinting)>,
mut reader: TextUiReader,
) {
let (layout, mut computed, hinting) = blocks.get_mut(root).expect("a text root");
pipeline
.update_buffer(
&fonts,
reader.iter(root),
layout.linebreak,
layout.justify,
BOUNDS,
1.0,
&mut computed,
&mut font_system,
*hinting,
)
.expect("the embedded default font shapes headless");
}
/// A resize frame in `UiPlugin`'s order: the detector, the re-shape it asked for, then the
/// re-layout (`text_system`).
pub(crate) fn resize_frame(app: &mut App, root: Entity) {
app.update();
if app
.world()
.get::<ComputedTextBlock>(root)
.is_some_and(ComputedTextBlock::needs_rerender)
{
shape(app, root);
}
app.world_mut()
.run_system_once_with(relayout_one, root)
.expect("the relayout system runs");
}
fn relayout_one(
In(root): In<Entity>,
mut pipeline: ResMut<TextPipeline>,
mut atlas_set: ResMut<FontAtlasSet>,
mut atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
mut images: ResMut<Assets<Image>>,
mut font_system: ResMut<CosmicFontSystem>,
mut swash_cache: ResMut<SwashCache>,
text_font_query: Query<&TextFont>,
mut blocks: Query<(&TextLayout, &mut TextLayoutInfo, &mut ComputedTextBlock)>,
) {
let (layout, mut info, mut computed) = blocks.get_mut(root).expect("a text root");
pipeline
.update_text_layout_info(
&mut info,
text_font_query,
1.0,
&mut atlas_set,
&mut atlas_layouts,
&mut images,
&mut computed,
&mut font_system,
&mut swash_cache,
BOUNDS,
layout.justify,
)
.expect("the relayout succeeds");
}
/// The text stack and the real detector; `net` installs [`super::TextReshapePlugin`].
pub(crate) fn text_app(net: bool) -> App {
let mut app = App::new();
app.add_plugins((MinimalPlugins, bevy::asset::AssetPlugin::default()));
add_text_plugins(&mut app);
app.add_systems(PostUpdate, detect_text_needs_rerender::<Text>);
if net {
app.add_plugins(super::TextReshapePlugin);
}
app
}
}
#[cfg(test)]
mod tests {
use super::harness::{resize_frame, shape, text_app};
use bevy::prelude::*;
use bevy::text::ComputedTextBlock;
/// The root's text plus one `TextSpan`, the shape of every coloured string.
fn two_run_root(app: &mut App) -> Entity {
let root = app
.world_mut()
.spawn((
Text::new("Tip:"),
TextLayout::default(),
TextFont::default(),
TextColor::WHITE,
))
.with_children(|c| {
c.spawn((
TextSpan::new(" talk to the innkeeper."),
TextFont::default(),
));
})
.id();
app.update();
shape(app, root);
assert_eq!(
app.world()
.get::<ComputedTextBlock>(root)
.map(|b| b.entities().len()),
Some(2),
"the shaped block lists both runs"
);
root
}
/// Asserts the upstream hole on purpose: a bevy that closes it fails here and retires the net.
#[test]
fn upstream_misses_the_last_spans_despawn() {
let mut app = text_app(false);
let root = two_run_root(&mut app);
app.world_mut()
.entity_mut(root)
.despawn_related::<Children>();
app.update();
assert!(
app.world().get::<Children>(root).is_none(),
"the last child's despawn REMOVES `Children`, which is the whole mechanism"
);
assert!(
!app.world()
.get::<ComputedTextBlock>(root)
.expect("the block")
.needs_rerender(),
"upstream cannot see the removal — if this ever fails, bevy fixed it"
);
}
/// Without the net, `bevy_text` indexes `glyph_info[1]` of a one-entry list.
#[test]
#[should_panic(expected = "index out of bounds")]
fn without_the_net_the_resize_panics_inside_bevy_text() {
let mut app = text_app(false);
let root = two_run_root(&mut app);
app.world_mut()
.entity_mut(root)
.despawn_related::<Children>();
app.update();
resize_frame(&mut app, root);
}
#[test]
fn the_net_reshapes_an_emptied_root_before_the_next_resize() {
let mut app = text_app(true);
let root = two_run_root(&mut app);
app.world_mut()
.entity_mut(root)
.despawn_related::<Children>();
app.update();
assert!(
app.world()
.get::<ComputedTextBlock>(root)
.expect("the block")
.needs_rerender(),
"the net marks the emptied root for a re-shape"
);
resize_frame(&mut app, root);
assert_eq!(
app.world()
.get::<ComputedTextBlock>(root)
.map(|b| b.entities().len()),
Some(1),
"the re-shaped block lists the root alone"
);
}
#[test]
fn a_root_that_kept_its_span_is_not_reshaped() {
let mut app = text_app(true);
let root = two_run_root(&mut app);
app.update();
assert!(
!app.world()
.get::<ComputedTextBlock>(root)
.expect("the block")
.needs_rerender(),
"nothing changed, so nothing is asked to re-shape"
);
resize_frame(&mut app, root);
assert_eq!(
app.world()
.get::<ComputedTextBlock>(root)
.map(|b| b.entities().len()),
Some(2),
"both runs are still there"
);
}
}