The combat log fills deDE's numbered holes, so a German client prints its fights (#1994)
- The combat log fills deDE's numbered holes, so a German client prints its fights - The fill's comment cites the client's ordered printf Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Sam <samuv.eth@gmail.com> (cherry picked from commit b14793c294f6f33ad1c3491c99bf973011d21d28)
This commit is contained in:
@@ -723,31 +723,47 @@ enum Arg<'a> {
|
||||
Num(i64),
|
||||
}
|
||||
|
||||
/// `vsnprintf` over the `%s`/`%d` subset the combat-log templates use; `None` on any mismatch.
|
||||
/// The line's formatter over the `%s`/`%d` subset the combat-log templates use. `0x626850` formats
|
||||
/// through `SStrVPrintf 0x64a8b0`, which takes Storm's ordered printf (`0x6697d0`) once option
|
||||
/// `0xa` is set, as the client does at start (`0x402141`, `0x64ede9`); that printf reads `%N$`
|
||||
/// (`0x669af2`), so a numbered hole (`%2$d`, the deDE templates') names its argument in the list
|
||||
/// enUS fills in sequence (`Ihr trefft %1$s. Schaden: %2$d.`). `None` on any mismatch, an unused
|
||||
/// argument included.
|
||||
fn fill(template: &str, args: &[Arg<'_>]) -> Option<String> {
|
||||
let mut out = String::with_capacity(template.len() + 32);
|
||||
let mut next = args.iter();
|
||||
let mut used = vec![false; args.len()];
|
||||
let mut cursor = 0;
|
||||
let mut chars = template.chars().peekable();
|
||||
while let Some(c) = chars.next() {
|
||||
if c != '%' {
|
||||
out.push(c);
|
||||
continue;
|
||||
}
|
||||
match chars.next() {
|
||||
Some('%') => out.push('%'),
|
||||
Some('s') => match next.next()? {
|
||||
Arg::Str(s) => out.push_str(s),
|
||||
Arg::Num(_) => return None,
|
||||
},
|
||||
Some('d') => match next.next()? {
|
||||
Arg::Num(n) => out.push_str(&n.to_string()),
|
||||
Arg::Str(_) => return None,
|
||||
},
|
||||
if chars.next_if_eq(&'%').is_some() {
|
||||
out.push('%');
|
||||
continue;
|
||||
}
|
||||
let mut number = String::new();
|
||||
while let Some(d) = chars.next_if(char::is_ascii_digit) {
|
||||
number.push(d);
|
||||
}
|
||||
let index = if number.is_empty() {
|
||||
cursor += 1;
|
||||
cursor - 1
|
||||
} else {
|
||||
chars.next_if_eq(&'$')?;
|
||||
number.parse::<usize>().ok()?.checked_sub(1)?
|
||||
};
|
||||
let arg = args.get(index)?;
|
||||
used[index] = true;
|
||||
match (chars.next(), arg) {
|
||||
(Some('s'), Arg::Str(s)) => out.push_str(s),
|
||||
(Some('d'), Arg::Num(n)) => out.push_str(&n.to_string()),
|
||||
// Any other conversion is a mismatch, never a half-filled sentence.
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
next.next().is_none().then_some(out)
|
||||
used.iter().all(|&u| u).then_some(out)
|
||||
}
|
||||
|
||||
/// A GlobalString off the VM's globals, `None` when absent or empty: the two tests `0x703bf0`'s
|
||||
|
||||
@@ -3,24 +3,72 @@
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A template's `%s`/`%d` conversions in order; two templates with the same one take the same
|
||||
/// arguments.
|
||||
/// A template's `%s`/`%d` conversions by argument, a numbered hole (`%2$d`, deDE's) at its number;
|
||||
/// two templates with the same one take the same arguments.
|
||||
fn signature(template: &str) -> Vec<char> {
|
||||
let mut out = Vec::new();
|
||||
let mut cursor = 0;
|
||||
let mut chars = template.chars().peekable();
|
||||
while let Some(c) = chars.next() {
|
||||
if c != '%' {
|
||||
if c != '%' || chars.next_if_eq(&'%').is_some() {
|
||||
continue;
|
||||
}
|
||||
match chars.next() {
|
||||
Some('%') => {}
|
||||
Some(k) => out.push(k),
|
||||
None => {}
|
||||
let mut number = String::new();
|
||||
while let Some(d) = chars.next_if(char::is_ascii_digit) {
|
||||
number.push(d);
|
||||
}
|
||||
let index = if number.is_empty() {
|
||||
cursor += 1;
|
||||
cursor - 1
|
||||
} else {
|
||||
chars.next_if_eq(&'$');
|
||||
number
|
||||
.parse::<usize>()
|
||||
.map_or(usize::MAX, |n| n.saturating_sub(1))
|
||||
};
|
||||
let Some(k) = chars.next() else { break };
|
||||
if index >= out.len() {
|
||||
out.resize(index.saturating_add(1).min(64), '?');
|
||||
}
|
||||
if let Some(slot) = out.get_mut(index) {
|
||||
*slot = k;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// deDE numbers its holes in the argument order enUS fills in sequence, so both read one list; a
|
||||
/// hole past the list, a wrong type or an unused argument stays a mismatch.
|
||||
#[test]
|
||||
fn numbered_holes_fill_from_the_same_argument_list() {
|
||||
let hit = [Arg::Str("Ragnaros"), Arg::Num(1000)];
|
||||
assert_eq!(
|
||||
fill("You hit %s for %d.", &hit).as_deref(),
|
||||
Some("You hit Ragnaros for 1000.")
|
||||
);
|
||||
assert_eq!(
|
||||
fill("Ihr trefft %1$s. Schaden: %2$d.", &hit).as_deref(),
|
||||
Some("Ihr trefft Ragnaros. Schaden: 1000.")
|
||||
);
|
||||
let reflect = [Arg::Num(50), Arg::Str("Feuer"), Arg::Str("Ragnaros")];
|
||||
assert_eq!(
|
||||
fill("Ihr reflektiert %1$d %2$sschaden auf %3$s.", &reflect).as_deref(),
|
||||
Some("Ihr reflektiert 50 Feuerschaden auf Ragnaros.")
|
||||
);
|
||||
assert_eq!(
|
||||
fill("%2$s von %1$s", &[Arg::Str("A"), Arg::Str("B")]).as_deref(),
|
||||
Some("B von A")
|
||||
);
|
||||
assert_eq!(
|
||||
fill("100%% %1$s", &[Arg::Str("x")]).as_deref(),
|
||||
Some("100% x")
|
||||
);
|
||||
assert_eq!(fill("%3$s", &[Arg::Str("a")]), None);
|
||||
assert_eq!(fill("%1$d", &[Arg::Str("a")]), None);
|
||||
assert_eq!(fill("%1$s", &[Arg::Str("a"), Arg::Num(1)]), None);
|
||||
assert_eq!(fill("%0$s", &[Arg::Str("a")]), None);
|
||||
}
|
||||
|
||||
/// The signature a family's declared slots give for one variant; `Attacker` and `Victim` count
|
||||
/// only where the variant names them.
|
||||
fn declared_signature(family: Family, variant: Variant) -> Vec<char> {
|
||||
|
||||
Reference in New Issue
Block a user