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