Files
ElHombreAlex--FTL-Faster-Th…/scripts/ftl_ui_assets.gd
T
ElHombreAlex 35cdf71054 V0.1
First publishable version of the mod
2026-10-03 12:59:33 +02:00

123 lines
5.7 KiB
GDScript

extends RefCounted
# Owner-local SIL bitmap fonts. Preserve the original atlas, baseline, glyph
# bearings and 1/256-pixel advance; no editor import or installed font is needed.
const FONT_NAMES := {"body":"JustinFont10.font", "header":"c&c.font", "bold":"JustinFont11Bold.font"}
static var fonts: Dictionary = {}
static var textures: Dictionary = {}
static func data_path() -> String:
if OS.has_feature("editor") or OS.get_cmdline_user_args().has("--desktop"):
return ProjectSettings.globalize_path("res://local_game_data")
return OS.get_executable_path().get_base_dir().path_join("local_game_data")
static func font(role: String = "body") -> Font:
var name: String = FONT_NAMES.get(role,FONT_NAMES.body)
if not fonts.has(name):
var native := load_font(data_path().path_join("fonts").path_join(name))
fonts[name] = native if native != null else ThemeDB.fallback_font
return fonts[name]
static func texture(relative_path: String) -> Texture2D:
if not textures.has(relative_path):
var path := data_path().path_join("ui").path_join(relative_path)
var image := Image.load_from_file(path) if FileAccess.file_exists(path) else null
textures[relative_path] = ImageTexture.create_from_image(image) if image != null and not image.is_empty() else null
return textures[relative_path]
static func _be16(bytes: PackedByteArray, at: int) -> int:
return (int(bytes[at]) << 8) | int(bytes[at + 1])
static func _be32(bytes: PackedByteArray, at: int) -> int:
return (_be16(bytes,at) << 16) | _be16(bytes,at + 2)
static func _signed(value: int, bits: int) -> int:
return value - (1 << bits) if value & (1 << (bits - 1)) else value
static func load_font(path: String) -> FontFile:
if not FileAccess.file_exists(path): return null
var bytes := FileAccess.get_file_as_bytes(path)
if bytes.size() < 24 or bytes.slice(0,4).get_string_from_ascii() != "FONT" or bytes[4] != 1: return null
var height := int(bytes[5])
var baseline := int(bytes[6])
var info := _be32(bytes,8)
var count := _be16(bytes,12)
var stride := _be16(bytes,14)
var tex := _be32(bytes,16)
var tex_length := _be32(bytes,20)
if height < 1 or baseline > height or stride != 16 or count < 1 or info < 24 or info + count * stride > bytes.size(): return null
if tex < 24 or tex_length < 32 or tex + tex_length > bytes.size(): return null
# FTL's western fonts contain a SIL v2, uncompressed A8 texture (0x40).
if bytes.slice(tex,tex + 4).get_string_from_ascii() != "TEX\n" or bytes[tex + 4] != 2 or bytes[tex + 5] != 0x40: return null
var width := _be16(bytes,tex + 8)
var atlas_height := _be16(bytes,tex + 10)
var pixels := _be32(bytes,tex + 16)
var pixel_count := _be32(bytes,tex + 20)
if width < 1 or atlas_height < 1 or width > 2048 or atlas_height > 2048 or pixel_count != width * atlas_height or pixels < 32 or pixels + pixel_count > tex_length: return null
# Validate all glyph bounds before mutating the font cache.
for i in range(count):
var offset := info + i * stride
if _be32(bytes,offset) > 0x10ffff or _be16(bytes,offset + 4) + int(bytes[offset + 8]) > width or _be16(bytes,offset + 6) + int(bytes[offset + 9]) > atlas_height: return null
var rgba := PackedByteArray()
rgba.resize(pixel_count * 4)
for i in range(pixel_count):
rgba[i * 4] = 255
rgba[i * 4 + 1] = 255
rgba[i * 4 + 2] = 255
rgba[i * 4 + 3] = bytes[tex + pixels + i]
var source_image := Image.create_from_data(width,atlas_height,false,Image.FORMAT_RGBA8,rgba)
# Godot's 3D glyph quads need transparent margins around a narrow stroke.
# Repack original pixels without changing bearings/advances; a stroke at the
# right edge of a 2px I/i/l glyph otherwise disappears in Label3D sampling.
const PADDING := 2
var atlas_width := 512
var uv_rects: Array[Rect2i] = []
var cursor := Vector2i.ZERO
var row_height := 0
for i in range(count):
var offset := info + i * stride
var padded := Vector2i(int(bytes[offset + 8]),int(bytes[offset + 9])) + Vector2i.ONE * (PADDING * 2)
if cursor.x + padded.x > atlas_width:
cursor = Vector2i(0,cursor.y + row_height)
row_height = 0
uv_rects.append(Rect2i(cursor,padded))
cursor.x += padded.x
row_height = maxi(row_height,padded.y)
var padded_height := 1
while padded_height < cursor.y + row_height: padded_height <<= 1
var atlas := Image.create(atlas_width,padded_height,false,Image.FORMAT_RGBA8)
atlas.fill(Color(1,1,1,0))
for i in range(count):
var offset := info + i * stride
var original := Rect2i(_be16(bytes,offset + 4),_be16(bytes,offset + 6),int(bytes[offset + 8]),int(bytes[offset + 9]))
if original.size.x > 0 and original.size.y > 0:
atlas.blit_rect(source_image,original,uv_rects[i].position + Vector2i.ONE * PADDING)
var result := FontFile.new()
result.font_name = path.get_file().get_basename()
result.fixed_size = height
result.fixed_size_scale_mode = TextServer.FIXED_SIZE_SCALE_ENABLED
result.antialiasing = TextServer.FONT_ANTIALIASING_NONE
result.allow_system_fallback = false
result.set_cache_ascent(0,height,baseline)
result.set_cache_descent(0,height,height - baseline)
result.set_texture_image(0,Vector2i(height,0),0,atlas)
for i in range(count):
var offset := info + i * stride
var codepoint := _be32(bytes,offset)
var glyph_size := Vector2(int(bytes[offset + 8]),int(bytes[offset + 9]))
var pre := _signed(_be16(bytes,offset + 12),16) / 256.0
var post := _signed(_be16(bytes,offset + 14),16) / 256.0
result.set_glyph_advance(0,height,codepoint,Vector2(pre + glyph_size.x + post,0))
result.set_glyph_offset(0,Vector2i(height,0),codepoint,Vector2(pre - PADDING,-_signed(int(bytes[offset + 10]),8) - PADDING))
result.set_glyph_size(0,Vector2i(height,0),codepoint,glyph_size + Vector2.ONE * (PADDING * 2))
result.set_glyph_uv_rect(0,Vector2i(height,0),codepoint,Rect2(uv_rects[i]))
result.set_glyph_texture_idx(0,Vector2i(height,0),codepoint,0)
return result