1use std::collections::HashMap;
10use std::io::Read;
11use yog_sync::{Arc, Mutex, OnceLock};
12
13use serde_json::Value;
14
15use super::assets::{Image, Resources, decode_png};
16
17pub struct Glyph {
19 pub advance: f32,
21 pub shadow_offset: f32,
22 pub bold_offset: f32,
23 pub bitmap: Option<GlyphBitmap>,
24}
25
26pub struct GlyphBitmap {
29 pub w: u32,
30 pub h: u32,
31 pub rgba: Vec<u8>,
32 pub oversample: f32,
33 pub top: f32,
34}
35
36struct BitmapProvider {
37 image: Arc<Image>,
38 cell_w: u32,
39 cell_h: u32,
40 scale: f32,
41 ascent: i32,
42 chars: HashMap<char, (u32, u32)>,
43}
44
45struct UnihexProvider {
46 glyphs: Arc<HashMap<u32, ([u32; 16], u32)>>,
48 overrides: Vec<(u32, u32, i32, i32)>,
49}
50
51struct TtfProvider {
54 font: fontdue::Font,
55 size: f32,
56 oversample: f32,
57 shift: (f32, f32),
58 skip: Vec<char>,
59}
60
61enum Provider {
62 Space(HashMap<char, f32>),
63 Bitmap(BitmapProvider),
64 Unihex(Arc<UnihexProvider>),
65 Ttf(Arc<TtfProvider>),
66 Yog(Arc<super::font_yog::YogProvider>),
68}
69
70pub struct Font {
71 providers: Vec<Provider>,
72 cache: Mutex<HashMap<char, Arc<Glyph>>>,
73}
74
75fn lenient_json(bytes: &[u8]) -> Option<Value> {
78 if let Ok(v) = serde_json::from_slice(bytes) {
79 return Some(v);
80 }
81 let text = std::str::from_utf8(bytes).ok()?;
82 let mut out = String::with_capacity(text.len());
83 let (mut in_str, mut escaped) = (false, false);
84 let chars: Vec<char> = text.chars().collect();
85 for (i, &c) in chars.iter().enumerate() {
86 if in_str {
87 out.push(c);
88 if escaped {
89 escaped = false;
90 } else if c == '\\' {
91 escaped = true;
92 } else if c == '"' {
93 in_str = false;
94 }
95 continue;
96 }
97 if c == '"' {
98 in_str = true;
99 } else if c == ','
100 && chars[i + 1..]
101 .iter()
102 .find(|c| !c.is_whitespace())
103 .is_some_and(|&n| n == ']' || n == '}')
104 {
105 continue;
106 }
107 out.push(c);
108 }
109 serde_json::from_str(&out).ok()
110}
111
112fn json(res: &Resources, ns: &str, path: &str) -> Vec<Value> {
113 let mut out = Vec::new();
114 if let Some(v) = &res.vanilla {
115 out.extend(
116 v.read_all(&format!("{ns}/{path}"))
117 .iter()
118 .filter_map(|b| lenient_json(b)),
119 );
120 }
121 out
122}
123
124fn filter_passes(p: &Value) -> bool {
127 p.get("filter")
128 .and_then(Value::as_object)
129 .is_none_or(|f| f.values().all(|v| v.as_bool() == Some(false)))
130}
131
132fn unihex(res: &Resources, file: &str, overrides: &[Value]) -> Option<Arc<UnihexProvider>> {
134 type Cache = Mutex<HashMap<String, Arc<HashMap<u32, ([u32; 16], u32)>>>>;
135 static CACHE: OnceLock<Cache> = OnceLock::new();
136 let cache = CACHE.get_or_init(Default::default);
137 let glyphs = if let Some(hit) = cache.lock().get(file).cloned() {
138 hit
139 } else {
140 let (ns, path) = yog_ids::split(file);
141 let bytes = res.read(ns, path)?;
142 let mut zip = zip::ZipArchive::new(std::io::Cursor::new(bytes)).ok()?;
143 let mut glyphs = HashMap::new();
144 for i in 0..zip.len() {
145 let Ok(mut entry) = zip.by_index(i) else {
146 continue;
147 };
148 if !entry.name().is_ok_and(|n| n.ends_with(".hex")) {
149 continue;
150 }
151 let mut text = String::new();
152 if entry.read_to_string(&mut text).is_err() {
153 continue;
154 }
155 for line in text.lines() {
156 let Some((cp, data)) = line.split_once(':') else {
157 continue;
158 };
159 let Ok(cp) = u32::from_str_radix(cp.trim(), 16) else {
160 continue;
161 };
162 let data = data.trim();
163 let row_digits = data.len() / 16;
164 if !matches!(row_digits, 2 | 4 | 6 | 8) || data.len() % 16 != 0 {
165 continue;
166 }
167 let mut rows = [0u32; 16];
168 let mut ok = true;
169 for (r, row) in rows.iter_mut().enumerate() {
170 match u32::from_str_radix(&data[r * row_digits..(r + 1) * row_digits], 16) {
171 Ok(v) => *row = v << (32 - row_digits * 4),
172 Err(_) => ok = false,
173 }
174 }
175 if ok {
176 glyphs.insert(cp, (rows, (row_digits * 4) as u32));
177 }
178 }
179 }
180 let glyphs = Arc::new(glyphs);
181 let mut c = cache.lock();
182 c.insert(file.to_string(), glyphs.clone());
183 glyphs
184 };
185 let overrides = overrides
186 .iter()
187 .filter_map(|o| {
188 let ch = |k: &str| o.get(k)?.as_str()?.chars().next().map(|c| c as u32);
189 Some((
190 ch("from")?,
191 ch("to")?,
192 o.get("left")?.as_i64()? as i32,
193 o.get("right")?.as_i64()? as i32,
194 ))
195 })
196 .collect();
197 Some(Arc::new(UnihexProvider { glyphs, overrides }))
198}
199
200fn load_providers(res: &Resources, id: &str, depth: usize, out: &mut Vec<Provider>) {
201 if depth > 8 {
202 return;
203 }
204 let (ns, path) = yog_ids::split(id);
205 for def in json(res, ns, &format!("font/{path}.json")) {
206 let Some(list) = def.get("providers").and_then(Value::as_array) else {
207 continue;
208 };
209 for p in list {
210 if !filter_passes(p) {
211 continue;
212 }
213 match p.get("type").and_then(Value::as_str) {
214 Some("reference") => {
215 if let Some(id) = p.get("id").and_then(Value::as_str) {
216 load_providers(res, id, depth + 1, out);
217 }
218 }
219 Some("space") => {
220 let advances = p
221 .get("advances")
222 .and_then(Value::as_object)
223 .map(|a| {
224 a.iter()
225 .filter_map(|(k, v)| Some((k.chars().next()?, v.as_f64()? as f32)))
226 .collect()
227 })
228 .unwrap_or_default();
229 out.push(Provider::Space(advances));
230 }
231 Some("bitmap") => {
232 if let Some(b) = bitmap(res, p) {
233 out.push(Provider::Bitmap(b));
234 }
235 }
236 Some("unihex") => {
237 let file = p
238 .get("hex_file")
239 .and_then(Value::as_str)
240 .unwrap_or_default();
241 let overrides = p
242 .get("size_overrides")
243 .and_then(Value::as_array)
244 .cloned()
245 .unwrap_or_default();
246 if let Some(u) = unihex(res, file, &overrides) {
247 out.push(Provider::Unihex(u));
248 }
249 }
250 Some("ttf") => {
251 if let Some(t) = ttf(res, p) {
252 out.push(Provider::Ttf(Arc::new(t)));
253 }
254 }
255 _ => {} }
257 }
258 }
259}
260
261const MIN_OVERSAMPLE: f64 = 0.1;
263const TTF_SIZE: f64 = 11.0;
267const BITMAP_HEIGHT: f64 = 8.0;
268const BASELINE: f32 = 7.0;
269
270fn ttf(res: &Resources, p: &Value) -> Option<TtfProvider> {
271 let file = p.get("file")?.as_str()?;
272 let (ns, path) = yog_ids::split(file);
273 let bytes = res.read(ns, &format!("font/{path}"))?;
274 let size = p.get("size").and_then(Value::as_f64).unwrap_or(TTF_SIZE) as f32;
275 let oversample = p
276 .get("oversample")
277 .and_then(Value::as_f64)
278 .unwrap_or(1.0)
279 .max(MIN_OVERSAMPLE) as f32;
280 let font = fontdue::Font::from_bytes(
281 bytes,
282 fontdue::FontSettings {
283 scale: size * oversample,
284 ..Default::default()
285 },
286 )
287 .ok()?;
288 let shift = p
289 .get("shift")
290 .and_then(Value::as_array)
291 .map(|a| {
292 (
293 a.first().and_then(Value::as_f64).unwrap_or(0.0) as f32,
294 a.get(1).and_then(Value::as_f64).unwrap_or(0.0) as f32,
295 )
296 })
297 .unwrap_or((0.0, 0.0));
298 let skip = match p.get("skip") {
299 Some(Value::String(s)) => s.chars().collect(),
300 Some(Value::Array(a)) => a
301 .iter()
302 .filter_map(Value::as_str)
303 .flat_map(str::chars)
304 .collect(),
305 _ => Vec::new(),
306 };
307 Some(TtfProvider {
308 font,
309 size,
310 oversample,
311 shift,
312 skip,
313 })
314}
315
316impl TtfProvider {
317 fn glyph(&self, ch: char) -> Option<Glyph> {
318 if self.skip.contains(&ch) || self.font.lookup_glyph_index(ch) == 0 {
319 return None;
320 }
321 let px = self.size * self.oversample;
322 let (m, coverage) = self.font.rasterize(ch, px);
323 let advance = m.advance_width / self.oversample;
324 if m.width == 0 || m.height == 0 {
325 return Some(Glyph {
326 advance,
327 shadow_offset: 1.0,
328 bold_offset: 1.0,
329 bitmap: None,
330 });
331 }
332 let pad = (m.xmin.max(0) as f32 + self.shift.0 * self.oversample)
334 .round()
335 .max(0.0) as usize;
336 let w = m.width + pad;
337 let mut rgba = vec![0u8; w * m.height * 4];
338 for y in 0..m.height {
339 for x in 0..m.width {
340 let a = coverage[y * m.width + x];
341 let i = (y * w + x + pad) * 4;
342 rgba[i..i + 4].copy_from_slice(&[255, 255, 255, a]);
343 }
344 }
345 let top = BASELINE - (m.ymin as f32 + m.height as f32) / self.oversample + self.shift.1;
347 Some(Glyph {
348 advance,
349 shadow_offset: 1.0,
350 bold_offset: 1.0,
351 bitmap: Some(GlyphBitmap {
352 w: w as u32,
353 h: m.height as u32,
354 rgba,
355 oversample: self.oversample,
356 top,
357 }),
358 })
359 }
360}
361
362fn bitmap(res: &Resources, p: &Value) -> Option<BitmapProvider> {
363 let file = p.get("file")?.as_str()?;
364 let (ns, path) = yog_ids::split(file);
365 let image = Arc::new(decode_png(
366 &res.read(ns, &format!("{}{path}", yog_ids::res::TEXTURES))?,
367 )?);
368 let rows: Vec<Vec<char>> = p
369 .get("chars")?
370 .as_array()?
371 .iter()
372 .filter_map(|r| Some(r.as_str()?.chars().collect()))
373 .collect();
374 let cols = rows.first()?.len() as u32;
375 if cols == 0 || rows.is_empty() {
376 return None;
377 }
378 let cell_w = image.width / cols;
379 let cell_h = image.height / rows.len() as u32;
380 let height = p
381 .get("height")
382 .and_then(Value::as_f64)
383 .unwrap_or(BITMAP_HEIGHT) as f32;
384 let mut chars = HashMap::new();
385 for (r, row) in rows.iter().enumerate() {
386 for (c, &ch) in row.iter().enumerate() {
387 if ch != '\0' {
388 chars.entry(ch).or_insert((c as u32, r as u32));
389 }
390 }
391 }
392 Some(BitmapProvider {
393 image,
394 cell_w,
395 cell_h,
396 scale: height / cell_h as f32,
397 ascent: p.get("ascent").and_then(Value::as_i64).unwrap_or(7) as i32,
398 chars,
399 })
400}
401
402impl BitmapProvider {
403 fn glyph(&self, ch: char) -> Option<Glyph> {
404 let &(col, row) = self.chars.get(&ch)?;
405 let (x0, y0) = (col * self.cell_w, row * self.cell_h);
406 let actual = (0..self.cell_w)
408 .rev()
409 .find(|&x| (0..self.cell_h).any(|y| self.image.pixel(x0 + x, y0 + y).a() != 0))
410 .map_or(0, |x| x + 1);
411 let mut rgba = Vec::with_capacity((self.cell_w * self.cell_h * 4) as usize);
412 for y in 0..self.cell_h {
413 for x in 0..self.cell_w {
414 rgba.extend_from_slice(&self.image.pixel(x0 + x, y0 + y).to_rgba8());
415 }
416 }
417 Some(Glyph {
418 advance: (0.5 + actual as f32 * self.scale).floor() + 1.0,
419 shadow_offset: 1.0,
420 bold_offset: 1.0,
421 bitmap: Some(GlyphBitmap {
422 w: self.cell_w,
423 h: self.cell_h,
424 rgba,
425 oversample: 1.0 / self.scale,
426 top: (7 - self.ascent) as f32,
427 }),
428 })
429 }
430}
431
432impl UnihexProvider {
433 fn glyph(&self, ch: char) -> Option<Glyph> {
434 let cp = ch as u32;
435 let (rows, bit_width) = self.glyphs.get(&cp)?;
436 let (left, right) = match self.overrides.iter().find(|o| (o.0..=o.1).contains(&cp)) {
437 Some(&(_, _, l, r)) => (l, r),
438 None => {
439 let all = rows.iter().fold(0u32, |a, r| a | r);
440 if all == 0 {
441 (0, *bit_width as i32 - 1)
442 } else {
443 (all.leading_zeros() as i32, 31 - all.trailing_zeros() as i32)
444 }
445 }
446 };
447 let width = (right - left + 1).max(0) as u32;
448 let mut rgba = Vec::with_capacity((width * 16 * 4) as usize);
449 for row in rows {
450 for x in 0..width {
451 let bit = left + x as i32;
452 let on = (0..32).contains(&bit) && row & (1u32 << (31 - bit)) != 0;
453 rgba.extend_from_slice(if on { &[0xFF; 4] } else { &[0; 4] });
454 }
455 }
456 Some(Glyph {
457 advance: (width / 2 + 1) as f32,
458 shadow_offset: 0.5,
459 bold_offset: 0.5,
460 bitmap: (width > 0).then_some(GlyphBitmap {
461 w: width,
462 h: 16,
463 rgba,
464 oversample: 2.0,
465 top: 0.0,
466 }),
467 })
468 }
469}
470
471fn missing_glyph() -> Glyph {
473 let mut rgba = Vec::with_capacity(5 * 8 * 4);
474 for y in 0..8 {
475 for x in 0..5 {
476 let edge = x == 0 || x == 4 || y == 0 || y == 7;
477 rgba.extend_from_slice(if edge { &[0xFF; 4] } else { &[0; 4] });
478 }
479 }
480 Glyph {
481 advance: yog_ui::text::CHAR_W,
482 shadow_offset: 1.0,
483 bold_offset: 1.0,
484 bitmap: Some(GlyphBitmap {
485 w: 5,
486 h: 8,
487 rgba,
488 oversample: 1.0,
489 top: 0.0,
490 }),
491 }
492}
493
494impl Font {
495 pub fn load(res: &Resources, font_id: &str) -> Arc<Font> {
498 type Cache = Mutex<
500 HashMap<
501 (
502 Option<std::path::PathBuf>,
503 String,
504 Option<std::time::SystemTime>,
505 ),
506 Arc<Font>,
507 >,
508 >;
509 static CACHE: OnceLock<Cache> = OnceLock::new();
510 let named = font_id.trim();
511 let game = yog_ids::game_font_of(named);
512 let id = match &game {
513 Some(game) => game.as_str(),
514 None if named.is_empty() => yog_ids::game::DEFAULT_FONT,
515 None => named,
516 };
517 let key = (
518 res.mod_root.clone(),
519 id.to_string(),
520 super::font_yog::changed(res, id),
521 );
522 let cache = CACHE.get_or_init(Default::default);
523 if let Some(f) = cache.lock().get(&key).cloned() {
524 return f;
525 }
526 let mut providers = Vec::new();
527 match super::font_yog::load(res, id) {
528 Some(own) => providers.push(Provider::Yog(own)),
529 None => load_providers(res, id, 0, &mut providers),
530 }
531 if providers.is_empty() && id != yog_ids::game::DEFAULT_FONT {
532 load_providers(res, yog_ids::game::DEFAULT_FONT, 0, &mut providers);
534 }
535 let font = Arc::new(Font {
536 providers,
537 cache: Mutex::new(HashMap::new()),
538 });
539 let mut c = cache.lock();
540 c.insert(key, font.clone());
541 font
542 }
543
544 pub fn chars(&self, unihex: bool) -> Vec<char> {
547 let mut out: Vec<char> = self
548 .providers
549 .iter()
550 .flat_map(|p| -> Vec<char> {
551 match p {
552 Provider::Space(a) => a.keys().copied().collect(),
553 Provider::Bitmap(b) => b.chars.keys().copied().collect(),
554 Provider::Unihex(u) if unihex => {
555 u.glyphs.keys().filter_map(|&c| char::from_u32(c)).collect()
556 }
557 Provider::Unihex(_) => Vec::new(),
558 Provider::Ttf(t) => t
559 .font
560 .chars()
561 .keys()
562 .filter(|c| !t.skip.contains(c))
563 .copied()
564 .collect(),
565 Provider::Yog(y) => y.chars().collect(),
566 }
567 })
568 .collect();
569 out.sort_unstable();
570 out.dedup();
571 out
572 }
573
574 pub const BASELINE: f32 = BASELINE;
576
577 pub fn is_real(&self) -> bool {
579 !self.providers.is_empty()
580 }
581
582 pub fn glyph(&self, ch: char) -> Arc<Glyph> {
583 if let Some(g) = self.cache.lock().get(&ch).cloned() {
584 return g;
585 }
586 let g = Arc::new(
587 self.providers
588 .iter()
589 .find_map(|p| match p {
590 Provider::Space(a) => a.get(&ch).map(|&advance| Glyph {
591 advance,
592 shadow_offset: 1.0,
593 bold_offset: 1.0,
594 bitmap: None,
595 }),
596 Provider::Bitmap(b) => b.glyph(ch),
597 Provider::Unihex(u) => u.glyph(ch),
598 Provider::Ttf(t) => t.glyph(ch),
599 Provider::Yog(y) => y.glyph(ch, BASELINE),
600 })
601 .unwrap_or_else(missing_glyph),
602 );
603 let mut c = self.cache.lock();
604 c.insert(ch, g.clone());
605 g
606 }
607
608 pub fn width(&self, text: &str) -> f32 {
610 styled(text, yog_gfx::Color::WHITE.0)
611 .iter()
612 .map(|s| {
613 self.glyph(s.ch).advance
614 + if s.bold {
615 self.glyph(s.ch).bold_offset
616 } else {
617 0.0
618 }
619 })
620 .sum()
621 }
622}
623
624#[derive(Clone, Copy)]
626pub struct Styled {
627 pub ch: char,
628 pub color: u32,
629 pub bold: bool,
630 pub underline: bool,
631 pub strike: bool,
632}
633
634pub fn chat_color(code: char) -> Option<u32> {
636 code.to_digit(CODE_RADIX)
637 .map(|i| yog_gfx::Color::rgb(CODES[i as usize]).0)
638}
639
640const CODE_RADIX: u32 = 16;
642
643const CODES: [u32; 16] = [
645 0x000000, 0x0000AA, 0x00AA00, 0x00AAAA, 0xAA0000, 0xAA00AA, 0xFFAA00, 0xAAAAAA, 0x555555,
646 0x5555FF, 0x55FF55, 0x55FFFF, 0xFF5555, 0xFF55FF, 0xFFFF55, 0xFFFFFF,
647];
648
649fn with_alpha_of(color: u32, rgb: u32) -> u32 {
653 yog_gfx::Color::rgb(rgb)
654 .with_alpha(yog_gfx::Color(color).a())
655 .0
656}
657
658pub fn styled(text: &str, color: u32) -> Vec<Styled> {
659 let mut out = Vec::new();
662 for (run, st) in yog_ui::rich::runs(text) {
663 let base = Styled {
664 ch: ' ',
665 color: st.color.map_or(color, |rgb| with_alpha_of(color, rgb)),
666 bold: st.bold,
667 underline: st.underline,
668 strike: st.strike,
669 };
670 let mut cur = base;
671 let mut it = run.chars();
672 while let Some(c) = it.next() {
673 if c == '§' {
674 let Some(code) = it.next() else { break };
675 let code = code.to_ascii_lowercase();
676 if let Some(i) = code.to_digit(16) {
677 cur = Styled {
678 color: with_alpha_of(color, CODES[i as usize]),
679 ..base
680 };
681 } else {
682 match code {
683 'l' => cur.bold = true,
684 'n' => cur.underline = true,
685 'm' => cur.strike = true,
686 'r' => cur = base,
687 _ => {} }
689 }
690 continue;
691 }
692 out.push(Styled { ch: c, ..cur });
693 }
694 }
695 out
696}
697
698#[cfg(test)]
699mod tests {
700 use super::*;
701
702 #[test]
703 fn json_is_read_leniently_like_gson() {
704 let v = lenient_json(br#"{"a": [1, 2,], "s": "x,]",}"#).unwrap();
705 assert_eq!(v["a"][1], 2);
706 assert_eq!(v["s"], "x,]");
707 }
708
709 #[test]
710 fn styled_text_follows_rich_spans() {
711 let s = styled("a{b #102030|b}c", 0xFF_FFFFFF);
712 assert_eq!(s.iter().map(|c| c.ch).collect::<String>(), "abc");
713 assert!(s[1].bold && s[1].color == 0xFF_102030 && !s[2].bold);
714 }
715
716 #[test]
717 fn styled_text_follows_section_codes() {
718 let s = styled("a§cb§lc§rd", 0xFF_FFFFFF);
719 assert_eq!(s.iter().map(|c| c.ch).collect::<String>(), "abcd");
720 assert_eq!(s[1].color, 0xFF_FF5555);
721 assert!(s[2].bold && !s[3].bold);
722 assert_eq!(s[3].color, 0xFF_FFFFFF);
723 }
724
725 #[test]
726 fn without_game_files_text_measures_with_the_missing_glyph() {
727 let f = Font::load(
728 &Resources {
729 mod_root: None,
730 vanilla: None,
731 },
732 yog_ids::game::DEFAULT_FONT,
733 );
734 assert!(!f.is_real());
735 assert_eq!(f.width("ab"), 12.0);
736 }
737}