refactor: split long functions and fix AGENTS.md violations
All checks were successful
Build plugin / build (push) Successful in 1m8s
All checks were successful
Build plugin / build (push) Successful in 1m8s
- Rename goos/goarch to os_name/arch_name (no domain jargon) - Split _install_ffmpeg into _detect_os, _detect_arch, _build_ffmpeg_url, _download_url_bytes, _extract_ffmpeg - Split _build_animated_gif_texture into _extract_gif_frames and _assemble_animated_texture - Split _parse_gif_frame_delays into _skip_gif_header, _parse_gif_extension, _skip_gif_image_descriptor - Replace _download_gif_bytes with _download_url_bytes (shared helper) - All functions now under 50 lines
This commit is contained in:
parent
208dc58f28
commit
3f5c45a99d
1 changed files with 106 additions and 114 deletions
|
|
@ -260,32 +260,52 @@ func _ensure_ffmpeg() -> String:
|
|||
## the same way the itch plugin's butler installer does, then fetches
|
||||
## the matching BtbN pre-built binary.
|
||||
func _install_ffmpeg() -> bool:
|
||||
var goos := "linux"
|
||||
var goarch := "64"
|
||||
var os_name := _detect_os()
|
||||
var arch_name := _detect_arch()
|
||||
var archive_url := _build_ffmpeg_url(os_name, arch_name)
|
||||
var body := await _download_url_bytes(archive_url)
|
||||
if body.is_empty():
|
||||
return false
|
||||
return _extract_ffmpeg(body, os_name)
|
||||
|
||||
|
||||
## Detects the current OS as a short name for ffmpeg builds.
|
||||
func _detect_os() -> String:
|
||||
if OS.get_name() == "Windows":
|
||||
goos = "win"
|
||||
return "win"
|
||||
if OS.get_name() == "macOS":
|
||||
goos = "macos"
|
||||
return "macos"
|
||||
return "linux"
|
||||
|
||||
|
||||
## Detects the CPU architecture as a short name for ffmpeg builds.
|
||||
func _detect_arch() -> String:
|
||||
if Engine.has_method("get_architecture_name"):
|
||||
var arch: String = Engine.get_architecture_name()
|
||||
if "arm64" in arch or "aarch64" in arch:
|
||||
goarch = "arm64"
|
||||
return "arm64"
|
||||
return "64"
|
||||
|
||||
var platform_slug := goos + goarch
|
||||
|
||||
## Builds the download URL for the ffmpeg archive matching the given platform.
|
||||
func _build_ffmpeg_url(os_name: String, arch_name: String) -> String:
|
||||
var platform_slug := os_name + arch_name
|
||||
var archive_name := "ffmpeg-master-latest-%s-gpl" % platform_slug
|
||||
var is_win: bool = goos == "win"
|
||||
var archive_url := "%s/%s.%s" % [FFMPEG_BUILDS_URL, archive_name,
|
||||
"zip" if is_win else "tar.xz"]
|
||||
var ext := "zip" if os_name == "win" else "tar.xz"
|
||||
return "%s/%s.%s" % [FFMPEG_BUILDS_URL, archive_name, ext]
|
||||
|
||||
|
||||
## Downloads bytes from a URL. Returns empty PackedByteArray on failure.
|
||||
func _download_url_bytes(url: String) -> PackedByteArray:
|
||||
var http := HTTPRequest.new()
|
||||
add_child.call_deferred(http)
|
||||
await http.ready
|
||||
|
||||
if http.request(archive_url) != OK:
|
||||
logger.error("Error requesting ffmpeg archive: " + archive_url)
|
||||
if http.request(url) != OK:
|
||||
logger.error("Error requesting: " + url)
|
||||
remove_child(http)
|
||||
http.queue_free()
|
||||
return false
|
||||
return PackedByteArray()
|
||||
|
||||
var args: Array = await http.request_completed
|
||||
var result: int = args[0]
|
||||
|
|
@ -295,11 +315,16 @@ func _install_ffmpeg() -> bool:
|
|||
http.queue_free()
|
||||
|
||||
if result != HTTPRequest.RESULT_SUCCESS or response_code != 200:
|
||||
logger.error("ffmpeg download failed: HTTP %d for %s" % [response_code, archive_url])
|
||||
return false
|
||||
logger.error("Download failed: HTTP %d for %s" % [response_code, url])
|
||||
return PackedByteArray()
|
||||
return body
|
||||
|
||||
|
||||
## Extracts the ffmpeg binary from the downloaded archive.
|
||||
func _extract_ffmpeg(body: PackedByteArray, os_name: String) -> bool:
|
||||
var globalized_dir := ProjectSettings.globalize_path(BOXART_DIR)
|
||||
DirAccess.make_dir_recursive_absolute(globalized_dir)
|
||||
var is_win: bool = os_name == "win"
|
||||
var ext := "zip" if is_win else "tar.xz"
|
||||
var archive_path := globalized_dir + "/ffmpeg." + ext
|
||||
var file := FileAccess.open(archive_path, FileAccess.WRITE)
|
||||
|
|
@ -388,53 +413,7 @@ func _fetch_gif_as_texture(url: String, cache_flags: int) -> Texture2D:
|
|||
|
||||
## Downloads GIF bytes from the given URL. Returns empty PackedByteArray on failure.
|
||||
func _download_gif_bytes(url: String) -> PackedByteArray:
|
||||
var http := HTTPClient.new()
|
||||
var parts := url.split("/")
|
||||
var use_tls := parts[0] == "https:"
|
||||
var err := http.connect_to_host(parts[2], 443 if use_tls else 80, TLSOptions.client() if use_tls else null)
|
||||
if err != OK:
|
||||
logger.warn("GIF fetch: HTTP connect failed for %s: %d" % [url.left(LOG_URL_MAX_LEN), err])
|
||||
return PackedByteArray()
|
||||
var deadline := Time.get_ticks_msec() + HTTP_TIMEOUT_MS
|
||||
while http.get_status() == HTTPClient.STATUS_CONNECTING or http.get_status() == HTTPClient.STATUS_RESOLVING:
|
||||
http.poll()
|
||||
if Time.get_ticks_msec() > deadline:
|
||||
http.close()
|
||||
logger.warn("GIF fetch: connect timed out")
|
||||
return PackedByteArray()
|
||||
OS.delay_msec(CONNECT_POLL_DELAY_MS)
|
||||
if http.get_status() != HTTPClient.STATUS_CONNECTED:
|
||||
logger.warn("GIF fetch: not connected (status=%d)" % http.get_status())
|
||||
http.close()
|
||||
return PackedByteArray()
|
||||
|
||||
http.request(HTTPClient.METHOD_GET, "/" + "/".join(parts.slice(3)), PackedStringArray())
|
||||
deadline = Time.get_ticks_msec() + HTTP_TIMEOUT_MS
|
||||
while http.get_status() == HTTPClient.STATUS_REQUESTING:
|
||||
http.poll()
|
||||
if Time.get_ticks_msec() > deadline:
|
||||
http.close()
|
||||
logger.warn("GIF fetch: request timed out")
|
||||
return PackedByteArray()
|
||||
OS.delay_msec(READ_POLL_DELAY_MS)
|
||||
|
||||
var body := PackedByteArray()
|
||||
while http.get_status() == HTTPClient.STATUS_BODY:
|
||||
http.poll()
|
||||
var chunk: PackedByteArray = http.read_response_body_chunk()
|
||||
if chunk.is_empty():
|
||||
if Time.get_ticks_msec() > deadline:
|
||||
http.close()
|
||||
logger.warn("GIF fetch: body read timed out")
|
||||
return PackedByteArray()
|
||||
OS.delay_msec(READ_POLL_DELAY_MS)
|
||||
continue
|
||||
body.append_array(chunk)
|
||||
http.close()
|
||||
|
||||
if body.is_empty():
|
||||
logger.warn("GIF fetch: empty response for %s" % url.left(LOG_URL_MAX_LEN))
|
||||
return body
|
||||
return await _download_url_bytes(url)
|
||||
|
||||
|
||||
## Extracts the first frame of a GIF using ffmpeg. Returns a static ImageTexture.
|
||||
|
|
@ -465,10 +444,15 @@ func _build_static_gif_texture(ffmpeg_bin: String, gif_path: String, png_path: S
|
|||
## Extracts all frames from a GIF using ffmpeg and builds an AnimatedTexture.
|
||||
## The animation is paused by default — the focus handler unpauses it.
|
||||
func _build_animated_gif_texture(ffmpeg_bin: String, gif_path: String, frames_dir: String, gif_bytes: PackedByteArray, url: String) -> AnimatedTexture:
|
||||
# Parse frame delays from the GIF binary.
|
||||
var delays: Array = _parse_gif_frame_delays(gif_bytes)
|
||||
var frame_files: PackedStringArray = _extract_gif_frames(ffmpeg_bin, gif_path, frames_dir)
|
||||
if frame_files.is_empty():
|
||||
return null
|
||||
return _assemble_animated_texture(frame_files, frames_dir, delays, url)
|
||||
|
||||
# Extract all frames with ffmpeg.
|
||||
|
||||
## Extracts all frames from a GIF file using ffmpeg. Returns sorted frame filenames.
|
||||
func _extract_gif_frames(ffmpeg_bin: String, gif_path: String, frames_dir: String) -> PackedStringArray:
|
||||
DirAccess.make_dir_recursive_absolute(frames_dir)
|
||||
var ffmpeg_args: PackedStringArray = [
|
||||
"-y", "-i", gif_path,
|
||||
|
|
@ -478,11 +462,10 @@ func _build_animated_gif_texture(ffmpeg_bin: String, gif_path: String, frames_di
|
|||
var ret: Array = []
|
||||
OS.execute(ffmpeg_bin, ffmpeg_args, ret)
|
||||
|
||||
# Collect extracted frame files.
|
||||
var dir := DirAccess.open(frames_dir)
|
||||
if dir == null:
|
||||
logger.warn("GIF fetch: cannot open frames dir")
|
||||
return null
|
||||
return PackedStringArray()
|
||||
dir.list_dir_begin()
|
||||
var frame_files: PackedStringArray = []
|
||||
var fname := dir.get_next()
|
||||
|
|
@ -492,13 +475,11 @@ func _build_animated_gif_texture(ffmpeg_bin: String, gif_path: String, frames_di
|
|||
fname = dir.get_next()
|
||||
dir.list_dir_end()
|
||||
frame_files.sort()
|
||||
return frame_files
|
||||
|
||||
if frame_files.is_empty():
|
||||
logger.warn("GIF fetch: no frames extracted for %s" % url.left(LOG_URL_MAX_LEN))
|
||||
DirAccess.remove_absolute(frames_dir)
|
||||
return null
|
||||
|
||||
# Build AnimatedTexture.
|
||||
## Loads extracted frame images and assembles them into an AnimatedTexture.
|
||||
func _assemble_animated_texture(frame_files: PackedStringArray, frames_dir: String, delays: Array, url: String) -> AnimatedTexture:
|
||||
var anim_tex := AnimatedTexture.new()
|
||||
anim_tex.frames_count = frame_files.size()
|
||||
anim_tex.pause = true # Start paused — focus handler unpauses.
|
||||
|
|
@ -511,7 +492,6 @@ func _build_animated_gif_texture(ffmpeg_bin: String, gif_path: String, frames_di
|
|||
logger.warn("GIF fetch: failed to load frame %d" % i)
|
||||
continue
|
||||
anim_tex.set_frame_texture(i, ImageTexture.create_from_image(image))
|
||||
# Use parsed delay if available, otherwise default.
|
||||
var delay_ms: int = delays[i] if i < delays.size() else GIF_DEFAULT_FRAME_DELAY_MS
|
||||
anim_tex.set_frame_delay(i, delay_ms / 1000.0) # AnimatedTexture uses seconds.
|
||||
|
||||
|
|
@ -528,56 +508,68 @@ func _parse_gif_frame_delays(data: PackedByteArray) -> Array:
|
|||
if data.size() < 13:
|
||||
return delays
|
||||
|
||||
# Skip header (6) + Logical Screen Descriptor (7).
|
||||
var pos := 13
|
||||
# Skip Global Color Table if present.
|
||||
var pos := _skip_gif_header(data)
|
||||
while pos < data.size() - 1:
|
||||
var byte: int = data[pos]
|
||||
if byte == 0x21:
|
||||
var result: Array = _parse_gif_extension(data, pos)
|
||||
if result[0] >= 0:
|
||||
delays.append(result[0])
|
||||
pos = result[1]
|
||||
elif byte == 0x2C:
|
||||
pos = _skip_gif_image_descriptor(data, pos)
|
||||
elif byte == 0x3B:
|
||||
break
|
||||
else:
|
||||
pos += 1
|
||||
|
||||
return delays
|
||||
|
||||
|
||||
## Skips the GIF header, Logical Screen Descriptor, and Global Color Table.
|
||||
## Returns the byte offset past all header data.
|
||||
func _skip_gif_header(data: PackedByteArray) -> int:
|
||||
var pos := 13 # Header (6) + Logical Screen Descriptor (7).
|
||||
var packed_byte: int = data[10]
|
||||
if packed_byte & 0x80 != 0:
|
||||
var gct_size := 3 * (1 << ((packed_byte & 0x07) + 1))
|
||||
pos += gct_size
|
||||
return pos
|
||||
|
||||
while pos < data.size() - 1:
|
||||
var byte: int = data[pos]
|
||||
if byte == 0x21:
|
||||
# Extension block.
|
||||
var label: int = data[pos + 1]
|
||||
if label == 0xF9:
|
||||
# Graphics Control Extension — contains frame delay.
|
||||
var delay_cs: int = data[pos + 4] | (data[pos + 5] << 8) # Centiseconds.
|
||||
var delay_ms: int = delay_cs * 10 if delay_cs > 0 else GIF_DEFAULT_FRAME_DELAY_MS
|
||||
delays.append(delay_ms)
|
||||
# Skip extension block.
|
||||
pos += 2
|
||||
while pos < data.size():
|
||||
var block_size: int = data[pos]
|
||||
pos += 1
|
||||
if block_size == 0:
|
||||
break
|
||||
pos += block_size
|
||||
elif byte == 0x2C:
|
||||
# Image descriptor — skip it.
|
||||
pos += 10
|
||||
# Skip Local Color Table if present.
|
||||
var img_packed: int = data[pos - 1]
|
||||
if img_packed & 0x80 != 0:
|
||||
var lct_size := 3 * (1 << ((img_packed & 0x07) + 1))
|
||||
pos += lct_size
|
||||
# Skip LZW Minimum Code Size + sub-blocks.
|
||||
pos += 1 # LZW min code size.
|
||||
while pos < data.size():
|
||||
var block_size: int = data[pos]
|
||||
pos += 1
|
||||
if block_size == 0:
|
||||
break
|
||||
pos += block_size
|
||||
elif byte == 0x3B:
|
||||
# Trailer.
|
||||
|
||||
## Parses an extension block starting at `pos`. Returns [delay_ms, new_pos].
|
||||
## If the extension is not a Graphics Control Extension, returns [-1, new_pos].
|
||||
func _parse_gif_extension(data: PackedByteArray, pos: int) -> Array:
|
||||
var label: int = data[pos + 1]
|
||||
var delay_ms := -1
|
||||
if label == 0xF9:
|
||||
var delay_cs: int = data[pos + 4] | (data[pos + 5] << 8)
|
||||
delay_ms = delay_cs * 10 if delay_cs > 0 else GIF_DEFAULT_FRAME_DELAY_MS
|
||||
pos += 2
|
||||
while pos < data.size():
|
||||
var block_size: int = data[pos]
|
||||
pos += 1
|
||||
if block_size == 0:
|
||||
break
|
||||
else:
|
||||
# Unknown block — skip.
|
||||
pos += 1
|
||||
pos += block_size
|
||||
return [delay_ms, pos]
|
||||
|
||||
return delays
|
||||
|
||||
## Skips an image descriptor and its associated data at `pos`.
|
||||
func _skip_gif_image_descriptor(data: PackedByteArray, pos: int) -> int:
|
||||
pos += 10
|
||||
var img_packed: int = data[pos - 1]
|
||||
if img_packed & 0x80 != 0:
|
||||
var lct_size := 3 * (1 << ((img_packed & 0x07) + 1))
|
||||
pos += lct_size
|
||||
pos += 1 # LZW Minimum Code Size.
|
||||
while pos < data.size():
|
||||
var block_size: int = data[pos]
|
||||
pos += 1
|
||||
if block_size == 0:
|
||||
break
|
||||
pos += block_size
|
||||
return pos
|
||||
|
||||
|
||||
## Combines two textures side-by-side into a single banner image.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue