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

206 lines
8.3 KiB
GDScript

extends Control
signal texture_changed
var state: Dictionary = {}
var textures: Dictionary = {}
var original_frame: Texture2D
var frame_image: Image
var frame_signature := ""
var frame_file := "hud_frame.png"
var use_original_frame := true
var source_rect := Rect2(0, 0, 1280, 720)
var base_path := ""
var font: Font = ThemeDB.fallback_font
var received_frames := 0
var capture_time_unix := 0.0
const NATIVE_SIZE := Vector2(1280, 720)
const RAW_HEADER_SIZE := 36
func set_state(value: Dictionary) -> void:
state = value
if original_frame == null:
request_redraw()
func request_redraw() -> void:
queue_redraw()
texture_changed.emit()
func poll_original_frame() -> void:
if not use_original_frame:
return
var path := base_path.path_join(frame_file)
var raw_path := path.get_basename() + ".rgba"
if FileAccess.file_exists(raw_path):
_poll_raw_frame(raw_path)
return
if not FileAccess.file_exists(path):
var had_frame := original_frame != null
original_frame = null
frame_image = null
frame_signature = ""
if had_frame:
request_redraw()
return
var source := FileAccess.open(path, FileAccess.READ)
if source == null:
return
var bytes := source.get_buffer(source.get_length())
source.close()
var digest := HashingContext.new()
digest.start(HashingContext.HASH_SHA256)
digest.update(bytes)
var signature := frame_file + digest.finish().hex_encode()
if signature != frame_signature:
var image := Image.new()
if image.load_png_from_buffer(bytes) == OK and not image.is_empty():
_accept_frame(image, signature)
func _poll_raw_frame(path: String) -> void:
var source := FileAccess.open(path, FileAccess.READ)
if source == null:
return
# Header-only polling avoids reading, hashing and decoding an unchanged image.
var header := source.get_buffer(RAW_HEADER_SIZE)
if header.size() != RAW_HEADER_SIZE or header.slice(0, 4).get_string_from_ascii() != "FVR1" or header.decode_u32(4) != 1:
source.close()
return
var width := int(header.decode_u32(8))
var height := int(header.decode_u32(12))
if width < 1 or height < 1 or width > 2560 or height > 1440 or header.decode_u32(16) != 4 or source.get_length() != RAW_HEADER_SIZE + width * height * 4:
source.close()
return
var signature := frame_file + header.hex_encode()
if signature == frame_signature:
source.close()
return
var bytes := source.get_buffer(width * height * 4)
source.close()
if bytes.size() != width * height * 4:
return
capture_time_unix = header.decode_double(28)
_accept_frame(Image.create_from_data(width, height, false, Image.FORMAT_RGBA8, bytes), signature)
func _accept_frame(image: Image, signature: String) -> void:
frame_image = image
if original_frame is ImageTexture and Vector2i(original_frame.get_size()) == image.get_size():
original_frame.update(image)
else:
original_frame = ImageTexture.create_from_image(image)
frame_signature = signature
received_frames += 1
request_redraw()
func native_alpha_at(pixel: Vector2i) -> float:
if frame_image == null:
return 0.0
var image_size := frame_image.get_size()
var scaled := Vector2(pixel) * Vector2(image_size) / NATIVE_SIZE
var sample := Vector2i(clampi(floori(scaled.x), 0, image_size.x - 1), clampi(floori(scaled.y), 0, image_size.y - 1))
return frame_image.get_pixelv(sample).a
func native_used_rect() -> Rect2i:
if frame_image == null:
return Rect2i()
var used := frame_image.get_used_rect()
var scale := NATIVE_SIZE / Vector2(frame_image.get_size())
var first := Vector2(used.position) * scale
var last := Vector2(used.end) * scale
return Rect2i(Vector2i(floori(first.x), floori(first.y)), Vector2i(ceili(last.x), ceili(last.y)) - Vector2i(floori(first.x), floori(first.y)))
func _texture(path: String) -> Texture2D:
if textures.has(path):
return textures[path]
var file := base_path.path_join("ui/img").path_join(path)
if not FileAccess.file_exists(file):
return null
var image := Image.load_from_file(file)
var texture := ImageTexture.create_from_image(image)
textures[path] = texture
return texture
func _art(path: String, point: Vector2) -> void:
var texture := _texture(path)
if texture != null:
draw_texture(texture, point)
func _text(value: String, point: Vector2, size_px: int = 17, color: Color = Color(0.92, 0.97, 0.87)) -> void:
draw_string(font, point, value, HORIZONTAL_ALIGNMENT_LEFT, -1.0, size_px, color)
func _draw() -> void:
if original_frame != null:
# The live bridge can provide the original game's transparent UI render target.
var transport_rect := Rect2(source_rect.position * original_frame.get_size() / NATIVE_SIZE, source_rect.size * original_frame.get_size() / NATIVE_SIZE)
draw_texture_rect_region(original_frame, Rect2(Vector2.ZERO, size), transport_rect)
return
_art("statusUI/top_hull.png", Vector2(5, 0))
_art("statusUI/top_hull_label.png", Vector2(5, 0))
_text("HULL", Vector2(12, 22), 18, Color(0.04, 0.12, 0.13))
var hull := int(state.get("hull", 30))
var hull_max := maxi(1, int(state.get("hull_max", 30)))
for i in range(30):
var color := Color(0.48, 1.0, 0.47) if i < roundi(30.0 * hull / hull_max) else Color(0.11, 0.18, 0.16)
draw_rect(Rect2(19 + i * 11.7, 32, 10, 14), color)
_art("statusUI/top_scrap.png", Vector2(393, 0))
_text(str(state.get("scrap", 52)), Vector2(444, 34))
_art("statusUI/top_shields4_on.png", Vector2(5, 57))
for i in range(clampi(int(state.get("shield", 2)), 0, 4)):
_art("statusUI/top_shieldsquare1_on.png", Vector2(31 + i * 23, 64))
for entry in [["fuel", 140, 17], ["missiles", 220, 8], ["drones", 300, 2]]:
_art("statusUI/top_%s_on.png" % entry[0], Vector2(entry[1], 57))
_text(str(state.get(entry[0], entry[2])), Vector2(entry[1] + 42, 84))
_art("statusUI/top_evade_oxygen.png", Vector2(5, 106))
_text("%d%%" % int(state.get("evasion", 25)), Vector2(48, 128), 15)
_text("%d%%" % int(state.get("oxygen", 100)), Vector2(48, 152), 15)
var crew: Array = state.get("crew", [{"name": "Crew 1", "health": 100}, {"name": "Crew 2", "health": 100}, {"name": "Crew 3", "health": 100}])
for i in range(crew.size()):
var y := 178 + i * 31
draw_rect(Rect2(8, y, 108, 26), Color(0.06, 0.13, 0.14, 0.9))
draw_rect(Rect2(8, y, 108, 26), Color(0.58, 0.68, 0.6), false)
draw_circle(Vector2(21, y + 10), 6, Color(0.6, 0.9, 0.34))
_text(str(crew[i].get("name", "Crew")), Vector2(34, y + 16), 14)
draw_rect(Rect2(13, y + 21, 94 * clampf(float(crew[i].get("health", 100)) / 100.0, 0, 1), 3), Color(0.36, 0.88, 0.33))
var reactor := int(state.get("reactor", 8))
for i in range(25):
draw_rect(Rect2(20, 682 - i * 6, 20, 4), Color(0.53, 0.97, 0.49) if i < reactor else Color(0.08, 0.1, 0.11))
draw_rect(Rect2(20, 682 - i * 6, 20, 4), Color(0.55, 0.61, 0.6), false)
var systems: Dictionary = state.get("systems", {"shields": 2, "engines": 2, "medbay": 1, "oxygen": 1, "weapons": 3, "drones": 0})
var system_order := ["shields", "engines", "medbay", "oxygen", "weapons", "drones"]
for i in range(system_order.size()):
var x := 62 + i * 43
var system_name: String = system_order[i]
_art("icons/s_%s_green1.png" % system_name, Vector2(x, 666))
_art("systemUI/button_default_base.png", Vector2(x, 592))
for cell in range(int(systems.get(system_name, 0))):
draw_rect(Rect2(x + 12, 651 - cell * 9, 17, 7), Color(0.4, 0.94, 0.35))
_art("box_weapons_bottom4.png", Vector2(341, 616))
_art("box_weapons_bottom_label.png", Vector2(341, 680))
_text("WEAPONS", Vector2(352, 705), 17, Color(0.07, 0.17, 0.17))
var weapons: Array = state.get("weapons", [{"name": "Burst Laser II", "charge": 1.0}, {"name": "Artemis", "charge": 1.0}])
for i in range(mini(4, weapons.size())):
var x := 350 + i * 99
draw_rect(Rect2(x, 624, 93, 46), Color(0.02, 0.08, 0.06, 0.88))
draw_rect(Rect2(x, 624, 93, 46), Color(0.75, 0.87, 0.78), false)
draw_rect(Rect2(x + 3, 628, 5, 37 * clampf(float(weapons[i].get("charge", 0)), 0, 1)), Color(0.38, 0.97, 0.38))
_text(str(weapons[i].get("name", "Weapon")), Vector2(x + 12, 647), 11)
_text(str(i + 1), Vector2(x + 12, 665), 13)
_art("box_weapons_autofire_base.png", Vector2(663, 678))
_text("AUTOFIRE", Vector2(676, 698), 12, Color(0.12, 0.2, 0.12))
_art("box_subsystems4.png", Vector2(1080, 678))
for i in range(3):
_art("icons/s_%s_green1.png" % ["pilot", "sensors", "doors"][i], Vector2(1083 + i * 43, 646))
_text("SUBSYSTEMS", Vector2(1089, 703), 15, Color(0.07, 0.17, 0.17))
if bool(state.get("paused", false)):
_art("Text_pause1.png", Vector2(536, 523))