reinstalling GDUnit from assetlib
Some checks failed
Create tag and build when new code gets to main / Export (push) Failing after 6m41s

This commit is contained in:
2026-01-26 09:05:55 +01:00
parent 4095f818f6
commit bdce8b969c
438 changed files with 22833 additions and 17 deletions

View File

@@ -0,0 +1,405 @@
@tool
extends RefCounted
const GdUnitUpdateClient = preload("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd")
const FONT_H1 := 22
const FONT_H2 := 20
const FONT_H3 := 18
const FONT_H4 := 16
const FONT_H5 := 14
const FONT_H6 := 12
const HORIZONTAL_RULE := "[img=4000x2]res://addons/gdUnit4/src/update/assets/horizontal-line2.png[/img]"
const HEADER_RULE := "[font_size=%d]$1[/font_size]"
const HEADER_CENTERED_RULE := "[font_size=%d][center]$1[/center][/font_size]"
const image_download_folder := "res://addons/gdUnit4/tmp-update/"
const exclude_font_size := "\b(?!(?:(font_size))\b)"
var md_replace_patterns := [
# comments
[regex("(?m)^\\n?\\s*<!--[\\s\\S]*?-->\\s*\\n?"), ""],
# horizontal rules
[regex("(?m)^[ ]{0,3}---$"), HORIZONTAL_RULE],
[regex("(?m)^[ ]{0,3}___$"), HORIZONTAL_RULE],
[regex("(?m)^[ ]{0,3}\\*\\*\\*$"), HORIZONTAL_RULE],
# headers
[regex("(?m)^###### (.*)"), HEADER_RULE % FONT_H6],
[regex("(?m)^##### (.*)"), HEADER_RULE % FONT_H5],
[regex("(?m)^#### (.*)"), HEADER_RULE % FONT_H4],
[regex("(?m)^### (.*)"), HEADER_RULE % FONT_H3],
[regex("(?m)^## (.*)"), (HEADER_RULE + HORIZONTAL_RULE) % FONT_H2],
[regex("(?m)^# (.*)"), (HEADER_RULE + HORIZONTAL_RULE) % FONT_H1],
[regex("(?m)^(.+)=={2,}$"), HEADER_RULE % FONT_H1],
[regex("(?m)^(.+)--{2,}$"), HEADER_RULE % FONT_H2],
# html headers
[regex("<h1>((.*?\\R?)+)<\\/h1>"), (HEADER_RULE + HORIZONTAL_RULE) % FONT_H1],
[regex("<h1[ ]*align[ ]*=[ ]*\"center\">((.*?\\R?)+)<\\/h1>"), (HEADER_CENTERED_RULE + HORIZONTAL_RULE) % FONT_H1],
[regex("<h2>((.*?\\R?)+)<\\/h2>"), (HEADER_RULE + HORIZONTAL_RULE) % FONT_H2],
[regex("<h2[ ]*align[ ]*=[ ]*\"center\">((.*?\\R?)+)<\\/h2>"), (HEADER_CENTERED_RULE + HORIZONTAL_RULE) % FONT_H1],
[regex("<h3>((.*?\\R?)+)<\\/h3>"), HEADER_RULE % FONT_H3],
[regex("<h3[ ]*align[ ]*=[ ]*\"center\">((.*?\\R?)+)<\\/h3>"), HEADER_CENTERED_RULE % FONT_H3],
[regex("<h4>((.*?\\R?)+)<\\/h4>"), HEADER_RULE % FONT_H4],
[regex("<h4[ ]*align[ ]*=[ ]*\"center\">((.*?\\R?)+)<\\/h4>"), HEADER_CENTERED_RULE % FONT_H4],
[regex("<h5>((.*?\\R?)+)<\\/h5>"), HEADER_RULE % FONT_H5],
[regex("<h5[ ]*align[ ]*=[ ]*\"center\">((.*?\\R?)+)<\\/h5>"), HEADER_CENTERED_RULE % FONT_H5],
[regex("<h6>((.*?\\R?)+)<\\/h6>"), HEADER_RULE % FONT_H6],
[regex("<h6[ ]*align[ ]*=[ ]*\"center\">((.*?\\R?)+)<\\/h6>"), HEADER_CENTERED_RULE % FONT_H6],
# asterics
#[regex("(\\*)"), "xxx$1xxx"],
# extract/compile image references
[regex("!\\[(.*?)\\]\\[(.*?)\\]"), process_image_references],
# extract images with path and optional tool tip
[regex("!\\[(.*?)\\]\\((.*?)(( )+(.*?))?\\)"), process_image],
# links
[regex("([!]|)\\[(.+)\\]\\(([^ ]+?)\\)"), "[url={\"url\":\"$3\"}]$2[/url]"],
# links with tool tip
[regex("([!]|)\\[(.+)\\]\\(([^ ]+?)( \"(.+)\")?\\)"), "[url={\"url\":\"$3\", \"tool_tip\":\"$5\"}]$2[/url]"],
# links to github, as shorted link
[regex("(https://github.*/?/(\\S+))"), '[url={"url":"$1", "tool_tip":"$1"}]#$2[/url]'],
# embeded text
[regex("(?m)^[ ]{0,3}>(.*?)$"), "[img=50x14]res://addons/gdUnit4/src/update/assets/embedded.png[/img][i]$1[/i]"],
# italic + bold font
[regex("[_]{3}(.*?)[_]{3}"), "[i][b]$1[/b][/i]"],
[regex("[\\*]{3}(.*?)[\\*]{3}"), "[i][b]$1[/b][/i]"],
# bold font
[regex("<b>(.*?)<\\/b>"), "[b]$1[/b]"],
[regex("[_]{2}(.*?)[_]{2}"), "[b]$1[/b]"],
[regex("[\\*]{2}(.*?)[\\*]{2}"), "[b]$1[/b]"],
# italic font
[regex("<i>(.*?)<\\/i>"), "[i]$1[/i]"],
[regex(exclude_font_size+"_(.*?)_"), "[i]$1[/i]"],
[regex("\\*(.*?)\\*"), "[i]$1[/i]"],
# strikethrough font
[regex("<s>(.*?)</s>"), "[s]$1[/s]"],
[regex("~~(.*?)~~"), "[s]$1[/s]"],
[regex("~(.*?)~"), "[s]$1[/s]"],
# handling lists
# using an image for dots
[regex("(?m)^[ ]{0,1}[*\\-+] (.*)$"), list_replace(0)],
[regex("(?m)^[ ]{2,3}[*\\-+] (.*)$"), list_replace(1)],
[regex("(?m)^[ ]{4,5}[*\\-+] (.*)$"), list_replace(2)],
[regex("(?m)^[ ]{6,7}[*\\-+] (.*)$"), list_replace(3)],
[regex("(?m)^[ ]{8,9}[*\\-+] (.*)$"), list_replace(4)],
# code
[regex("``([\\s\\S]*?)``"), code_block("$1")],
[regex("`([\\s\\S]*?)`{1,2}"), code_block("$1")],
]
var code_block_patterns := [
# code blocks, code blocks looks not like code blocks in richtext
[regex("```(javascript|python|shell|gdscript|gd)([\\s\\S]*?\n)```"), code_block("$2", true)],
]
var _img_replace_regex := RegEx.new()
var _image_urls := PackedStringArray()
var _on_table_tag := false
var _client: GdUnitUpdateClient
static func regex(pattern: String) -> RegEx:
var regex_ := RegEx.new()
var err := regex_.compile(pattern)
if err != OK:
push_error("error '%s' checked pattern '%s'" % [err, pattern])
return null
return regex_
func _init() -> void:
@warning_ignore("return_value_discarded")
_img_replace_regex.compile("\\[img\\]((.*?))\\[/img\\]")
func set_http_client(client: GdUnitUpdateClient) -> void:
_client = client
@warning_ignore("return_value_discarded")
func _notification(what: int) -> void:
if what == NOTIFICATION_PREDELETE:
# finally remove_at the downloaded images
for image in _image_urls:
DirAccess.remove_absolute(image)
DirAccess.remove_absolute(image + ".import")
func list_replace(indent: int) -> String:
var replace_pattern := "[img=12x12]res://addons/gdUnit4/src/update/assets/dot2.png[/img]" if indent %2 else "[img=12x12]res://addons/gdUnit4/src/update/assets/dot1.png[/img]"
replace_pattern += " $1"
for index in indent:
replace_pattern = replace_pattern.insert(0, " ")
return replace_pattern
func code_block(replace: String, border: bool = false) -> String:
if border:
return """
[img=1400x14]res://addons/gdUnit4/src/update/assets/border_top.png[/img]
[indent][color=GRAY][font_size=16]%s[/font_size][/color][/indent]
[img=1400x14]res://addons/gdUnit4/src/update/assets/border_bottom.png[/img]
""".dedent() % replace
return "[code][bgcolor=DARK_SLATE_GRAY][color=GRAY][font_size=16]%s[/font_size][/color][/bgcolor][/code]" % replace
func convert_text(input: String) -> String:
input = process_tables(input)
for pattern: Array in md_replace_patterns:
var regex_: RegEx = pattern[0]
var bb_replace: Variant = pattern[1]
if bb_replace is Callable:
@warning_ignore("unsafe_method_access")
input = await bb_replace.call(regex_, input)
else:
@warning_ignore("unsafe_cast")
input = regex_.sub(input, bb_replace as String, true)
return input
func convert_code_block(input: String) -> String:
for pattern: Array in code_block_patterns:
var regex_: RegEx = pattern[0]
var bb_replace: Variant = pattern[1]
if bb_replace is Callable:
@warning_ignore("unsafe_method_access")
input = await bb_replace.call(regex_, input)
else:
@warning_ignore("unsafe_cast")
input = regex_.sub(input, bb_replace as String, true)
return input
func to_bbcode(input: String) -> String:
var re := regex("(?m)```[\\s\\S]*?```")
var current_pos := 0
var as_bbcode := ""
# we split by code blocks to handle this blocks customized
for result in re.search_all(input):
# Add text before code block
if result.get_start() > current_pos:
as_bbcode += await convert_text(input.substr(current_pos, result.get_start() - current_pos))
# Add code block
as_bbcode += await convert_code_block(result.get_string())
current_pos = result.get_end()
# Add remaining text after last code block
if current_pos < input.length():
as_bbcode += await convert_text(input.substr(current_pos))
return as_bbcode
func process_tables(input: String) -> String:
var bbcode := PackedStringArray()
var lines: Array[String] = Array(input.split("\n") as Array, TYPE_STRING, "", null)
while not lines.is_empty():
if is_table(lines[0]):
bbcode.append_array(parse_table(lines))
continue
@warning_ignore("return_value_discarded", "unsafe_cast")
bbcode.append(lines.pop_front() as String)
return "\n".join(bbcode)
class GdUnitMDReaderTable:
var _columns: int
var _rows: Array[Row] = []
class Row:
var _cells := PackedStringArray()
func _init(cells: PackedStringArray, columns: int) -> void:
_cells = cells
for i in range(_cells.size(), columns):
@warning_ignore("return_value_discarded")
_cells.append("")
func to_bbcode(cell_sizes: PackedInt32Array, bold: bool) -> String:
var cells := PackedStringArray()
for cell_index in _cells.size():
var cell: String = _cells[cell_index]
if cell.strip_edges() == "--":
cell = create_line(cell_sizes[cell_index])
if bold:
cell = "[b]%s[/b]" % cell
@warning_ignore("return_value_discarded")
cells.append("[cell]%s[/cell]" % cell)
return "|".join(cells)
func create_line(length: int) -> String:
var line := ""
for i in length:
line += "-"
return line
func _init(columns: int) -> void:
_columns = columns
func parse_row(line :String) -> bool:
# is line containing cells?
if line.find("|") == -1:
return false
_rows.append(Row.new(line.split("|"), _columns))
return true
func calculate_max_cell_sizes() -> PackedInt32Array:
var cells_size := PackedInt32Array()
for column in _columns:
@warning_ignore("return_value_discarded")
cells_size.append(0)
for row_index in _rows.size():
var row: Row = _rows[row_index]
for cell_index in row._cells.size():
var cell_size: int = cells_size[cell_index]
var size := row._cells[cell_index].length()
if size > cell_size:
cells_size[cell_index] = size
return cells_size
@warning_ignore("return_value_discarded")
func to_bbcode() -> PackedStringArray:
var cell_sizes := calculate_max_cell_sizes()
var bb_code := PackedStringArray()
bb_code.append("[table=%d]" % _columns)
for row_index in _rows.size():
bb_code.append(_rows[row_index].to_bbcode(cell_sizes, row_index==0))
bb_code.append("[/table]\n")
return bb_code
func parse_table(lines: Array) -> PackedStringArray:
var line: String = lines[0]
var table := GdUnitMDReaderTable.new(line.count("|") + 1)
while not lines.is_empty():
line = lines.pop_front()
if not table.parse_row(line):
break
return table.to_bbcode()
func is_table(line: String) -> bool:
return line.find("|") != -1
func open_table(line: String) -> String:
_on_table_tag = true
return "[table=%d]" % (line.count("|") + 1)
func close_table() -> String:
_on_table_tag = false
return "[/table]"
func extract_cells(line: String, bold := false) -> String:
var cells := ""
for cell in line.split("|"):
if bold:
cell = "[b]%s[/b]" % cell
cells += "[cell]%s[/cell]" % cell
return cells
func process_image_references(p_regex: RegEx, p_input: String) -> String:
#return p_input
# exists references?
var matches := p_regex.search_all(p_input)
if matches.is_empty():
return p_input
# collect image references and remove_at it
var references := Dictionary()
var link_regex := regex("\\[(\\S+)\\]:(\\S+)([ ]\"(.*)\")?")
# create copy of original source to replace checked it
var input := p_input.replace("\r", "")
var extracted_references := p_input.replace("\r", "")
for reg_match in link_regex.search_all(input):
var line := reg_match.get_string(0) + "\n"
var ref := reg_match.get_string(1)
#var topl_tip = reg_match.get_string(4)
# collect reference and url
references[ref] = reg_match.get_string(2)
extracted_references = extracted_references.replace(line, "")
# replace image references by collected url's
for reference_key: String in references.keys():
var regex_key := regex("\\](\\[%s\\])" % reference_key)
for reg_match in regex_key.search_all(extracted_references):
var ref: String = reg_match.get_string(0)
var image_url: String = "](%s)" % references.get(reference_key)
extracted_references = extracted_references.replace(ref, image_url)
return extracted_references
@warning_ignore("return_value_discarded")
func process_image(p_regex: RegEx, p_input: String) -> String:
#return p_input
var to_replace := PackedStringArray()
var tool_tips := PackedStringArray()
# find all matches
var matches := p_regex.search_all(p_input)
if matches.is_empty():
return p_input
for reg_match in matches:
# grap the parts to replace and store temporay because a direct replace will distort the offsets
to_replace.append(p_input.substr(reg_match.get_start(0), reg_match.get_end(0)))
# grap optional tool tips
tool_tips.append(reg_match.get_string(5))
# finally replace all findings
for replace in to_replace:
var re := p_regex.sub(replace, "[img]$2[/img]")
p_input = p_input.replace(replace, re)
return await _process_external_image_resources(p_input)
func _process_external_image_resources(input: String) -> String:
@warning_ignore("return_value_discarded")
DirAccess.make_dir_recursive_absolute(image_download_folder)
# scan all img for external resources and download it
for value in _img_replace_regex.search_all(input):
if value.get_group_count() >= 1:
var image_url: String = value.get_string(1)
# if not a local resource we need to download it
if image_url.begins_with("http"):
if OS.is_stdout_verbose():
prints("download image:", image_url)
var response := await _client.request_image(image_url)
if response.status() == 200:
var image := Image.new()
var error := image.load_png_from_buffer(response.get_body())
if error != OK:
prints("Error creating image from response", error)
# replace characters where format characters
var new_url := image_download_folder + image_url.get_file().replace("_", "-")
if new_url.get_extension() != 'png':
new_url = new_url + '.png'
var err := image.save_png(new_url)
if err:
push_error("Can't save image to '%s'. Error: %s" % [new_url, error_string(err)])
@warning_ignore("return_value_discarded")
_image_urls.append(new_url)
input = input.replace(image_url, new_url)
return input

View File

@@ -0,0 +1 @@
uid://c2ii27n08li2k

View File

@@ -0,0 +1,20 @@
class_name GdUnitPatch
extends RefCounted
const PATCH_VERSION = "patch_version"
var _version :GdUnit4Version
func _init(version_ :GdUnit4Version) -> void:
_version = version_
func version() -> GdUnit4Version:
return _version
# this function needs to be implement
func execute() -> bool:
push_error("The function 'execute()' is not implemented at %s" % self)
return false

View File

@@ -0,0 +1 @@
uid://drpr6gj1mlhxl

View File

@@ -0,0 +1,75 @@
class_name GdUnitPatcher
extends RefCounted
const _base_dir := "res://addons/gdUnit4/src/update/patches/"
var _patches := Dictionary()
func scan(current :GdUnit4Version) -> void:
_scan(_base_dir, current)
func _scan(scan_path :String, current :GdUnit4Version) -> void:
_patches = Dictionary()
var patch_paths := _collect_patch_versions(scan_path, current)
for path in patch_paths:
prints("scan for patches checked '%s'" % path)
_patches[path] = _scan_patches(path)
func patch_count() -> int:
var count := 0
for key :String in _patches.keys():
@warning_ignore("unsafe_method_access")
count += _patches[key].size()
return count
func execute() -> void:
for key :String in _patches.keys():
for path :String in _patches[key]:
var patch :GdUnitPatch = (load(key + "/" + path) as GDScript).new()
if patch:
prints("execute patch", patch.version(), patch.get_script().resource_path)
if not patch.execute():
prints("error checked execution patch %s" % key + "/" + path)
func _collect_patch_versions(scan_path :String, current :GdUnit4Version) -> PackedStringArray:
if not DirAccess.dir_exists_absolute(scan_path):
return PackedStringArray()
var patches := Array()
var dir := DirAccess.open(scan_path)
if dir != null:
@warning_ignore("return_value_discarded")
dir.list_dir_begin() # TODO GODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547
var next := "."
while next != "":
next = dir.get_next()
if next.is_empty() or next == "." or next == "..":
continue
var version := GdUnit4Version.parse(next)
if version.is_greater(current):
patches.append(scan_path + next)
patches.sort()
return PackedStringArray(patches)
func _scan_patches(path :String) -> PackedStringArray:
var patches := Array()
var dir := DirAccess.open(path)
if dir != null:
@warning_ignore("return_value_discarded")
dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547
var next := "."
while next != "":
next = dir.get_next()
# step over directory links and .uid files
if next.is_empty() or next == "." or next == ".." or next.ends_with(".uid"):
continue
patches.append(next)
# make sorted from lowest to high version
patches.sort()
return PackedStringArray(patches)

View File

@@ -0,0 +1 @@
uid://cxv57lcra5lsd

View File

@@ -0,0 +1,305 @@
@tool
extends Container
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
const GdUnitUpdateClient := preload("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd")
const GDUNIT_TEMP := "user://tmp"
@onready var _progress_content: RichTextLabel = %message
@onready var _progress_bar: TextureProgressBar = %progress
@onready var _cancel_btn: Button = %cancel
@onready var _update_btn: Button = %update
@onready var _spinner_img := GdUnitUiTools.get_spinner()
var _debug_mode := false
var _update_client :GdUnitUpdateClient
var _download_url :String
func _ready() -> void:
init_progress(6)
func _process(_delta :float) -> void:
if _progress_content != null and _progress_content.is_visible_in_tree():
_progress_content.queue_redraw()
func init_progress(max_value: int) -> void:
_cancel_btn.disabled = false
_update_btn.disabled = false
_progress_bar.max_value = max_value
_progress_bar.value = 1
message_h4("Press [Update] to start.", Color.GREEN, false)
func setup(update_client: GdUnitUpdateClient, download_url: String) -> void:
_update_client = update_client
_download_url = download_url
func update_progress(message: String, color := Color.GREEN) -> void:
message_h4(message, color)
_progress_bar.value += 1
if _debug_mode:
await get_tree().create_timer(3).timeout
await get_tree().create_timer(.2).timeout
func _colored(message: String, color: Color) -> String:
return "[color=#%s]%s[/color]" % [color.to_html(), message]
func message_h4(message: String, color: Color, show_spinner := true) -> void:
_progress_content.clear()
if show_spinner:
_progress_content.add_image(_spinner_img)
_progress_content.append_text(" [font_size=16]%s[/font_size]" % _colored(message, color))
if _debug_mode:
prints(message)
@warning_ignore("return_value_discarded")
func run_update() -> void:
_cancel_btn.disabled = true
_update_btn.disabled = true
await update_progress("Downloading the update.")
await download_release()
await update_progress("Extracting")
var zip_file := temp_dir() + "/update.zip"
var tmp_path := create_temp_dir("update")
var result :Variant = extract_zip(zip_file, tmp_path)
if result == null:
await update_progress("Update failed! .. Rollback.", Color.INDIAN_RED)
await get_tree().create_timer(3).timeout
_cancel_btn.disabled = false
_update_btn.disabled = false
init_progress(5)
hide()
return
await update_progress("Uninstall GdUnit4.")
disable_gdUnit()
if not _debug_mode:
GdUnitFileAccess.delete_directory("res://addons/gdUnit4/")
# give editor time to react on deleted files
await get_tree().create_timer(1).timeout
await update_progress("Install new GdUnit4 version.")
if _debug_mode:
copy_directory(tmp_path, "res://debug")
else:
copy_directory(tmp_path, "res://")
await update_progress("Patch invalid UID's")
await patch_uids()
await rebuild_project()
await update_progress("New GdUnit version successfully installed, Restarting Godot please wait.")
await get_tree().create_timer(3).timeout
enable_gdUnit()
hide()
GdUnitFileAccess.delete_directory("res://addons/.gdunit_update")
restart_godot()
func patch_uids(path := "res://addons/gdUnit4/src/") -> void:
var to_reimport: PackedStringArray
for file in DirAccess.get_files_at(path):
var file_path := path.path_join(file)
var ext := file.get_extension()
if ext == "tscn" or ext == "scn" or ext == "tres" or ext == "res":
message_h4("Patch GdUnit4 scene: '%s'" % file, Color.WEB_GREEN)
remove_uids_from_file(file_path)
elif FileAccess.file_exists(file_path + ".import"):
to_reimport.append(file_path)
if not to_reimport.is_empty():
message_h4("Reimport resources '%s'" % ", ".join(to_reimport), Color.WEB_GREEN)
if Engine.is_editor_hint():
EditorInterface.get_resource_filesystem().reimport_files(to_reimport)
for dir in DirAccess.get_directories_at(path):
if not dir.begins_with("."):
patch_uids(path.path_join(dir))
await get_tree().process_frame
func remove_uids_from_file(file_path: String) -> bool:
var file := FileAccess.open(file_path, FileAccess.READ)
if file == null:
print("Failed to open file: ", file_path)
return false
var original_content := file.get_as_text()
file.close()
# Remove UIDs using regex
var regex := RegEx.new()
regex.compile("(\\[ext_resource[^\\]]*?)\\s+uid=\"uid://[^\"]*\"")
var modified_content := regex.sub(original_content, "$1", true)
# Check if any changes were made
if original_content != modified_content:
prints("Patched invalid uid's out in '%s'" % file_path)
# Write the modified content back
file = FileAccess.open(file_path, FileAccess.WRITE)
if file == null:
print("Failed to write to file: ", file_path)
return false
file.store_string(modified_content)
file.close()
return true
return false
func restart_godot() -> void:
prints("Force restart Godot")
EditorInterface.restart_editor(true)
@warning_ignore("return_value_discarded")
func enable_gdUnit() -> void:
var enabled_plugins := PackedStringArray()
if ProjectSettings.has_setting("editor_plugins/enabled"):
enabled_plugins = ProjectSettings.get_setting("editor_plugins/enabled")
if not enabled_plugins.has("res://addons/gdUnit4/plugin.cfg"):
enabled_plugins.append("res://addons/gdUnit4/plugin.cfg")
ProjectSettings.set_setting("editor_plugins/enabled", enabled_plugins)
ProjectSettings.save()
func disable_gdUnit() -> void:
EditorInterface.set_plugin_enabled("gdUnit4", false)
func temp_dir() -> String:
if not DirAccess.dir_exists_absolute(GDUNIT_TEMP):
@warning_ignore("return_value_discarded")
DirAccess.make_dir_recursive_absolute(GDUNIT_TEMP)
return GDUNIT_TEMP
func create_temp_dir(folder_name :String) -> String:
var new_folder := temp_dir() + "/" + folder_name
GdUnitFileAccess.delete_directory(new_folder)
if not DirAccess.dir_exists_absolute(new_folder):
@warning_ignore("return_value_discarded")
DirAccess.make_dir_recursive_absolute(new_folder)
return new_folder
func copy_directory(from_dir: String, to_dir: String) -> bool:
if not DirAccess.dir_exists_absolute(from_dir):
printerr("Source directory not found '%s'" % from_dir)
return false
# check if destination exists
if not DirAccess.dir_exists_absolute(to_dir):
# create it
var err := DirAccess.make_dir_recursive_absolute(to_dir)
if err != OK:
printerr("Can't create directory '%s'. Error: %s" % [to_dir, error_string(err)])
return false
var source_dir := DirAccess.open(from_dir)
var dest_dir := DirAccess.open(to_dir)
if source_dir != null:
@warning_ignore("return_value_discarded")
source_dir.list_dir_begin()
var next := "."
while next != "":
next = source_dir.get_next()
if next == "" or next == "." or next == "..":
continue
var source := source_dir.get_current_dir() + "/" + next
var dest := dest_dir.get_current_dir() + "/" + next
if source_dir.current_is_dir():
@warning_ignore("return_value_discarded")
copy_directory(source + "/", dest)
continue
var err := source_dir.copy(source, dest)
if err != OK:
printerr("Error checked copy file '%s' to '%s'" % [source, dest])
return false
return true
else:
printerr("Directory not found: " + from_dir)
return false
func extract_zip(zip_package: String, dest_path: String) -> Variant:
var zip: ZIPReader = ZIPReader.new()
var err := zip.open(zip_package)
if err != OK:
printerr("Extracting `%s` failed! Please collect the error log and report this. Error Code: %s" % [zip_package, err])
return null
var zip_entries: PackedStringArray = zip.get_files()
# Get base path and step over archive folder
var archive_path := zip_entries[0]
zip_entries.remove_at(0)
for zip_entry in zip_entries:
var new_file_path: String = dest_path + "/" + zip_entry.replace(archive_path, "")
if zip_entry.ends_with("/"):
@warning_ignore("return_value_discarded")
DirAccess.make_dir_recursive_absolute(new_file_path)
continue
var file: FileAccess = FileAccess.open(new_file_path, FileAccess.WRITE)
file.store_buffer(zip.read_file(zip_entry))
@warning_ignore("return_value_discarded")
zip.close()
return dest_path
func download_release() -> void:
var zip_file := GdUnitFileAccess.temp_dir() + "/update.zip"
var response :GdUnitUpdateClient.HttpResponse
if _debug_mode:
response = GdUnitUpdateClient.HttpResponse.new(200, PackedByteArray())
zip_file = "res://update.zip"
return
response = await _update_client.request_zip_package(_download_url, zip_file)
if response.status() != 200:
push_warning("Update information cannot be retrieved from GitHub! \n Error code: %d : %s" % [response.status(), response.response()])
message_h4("Download the update failed! Try it later again.", Color.INDIAN_RED)
await get_tree().create_timer(3).timeout
func rebuild_project() -> void:
# Check if this is a Godot .NET runtime instance
if not ClassDB.class_exists("CSharpScript"):
return
update_progress("Rebuild the project ...")
await get_tree().process_frame
var output := []
var exit_code := OS.execute("dotnet", ["build"], output)
if exit_code == -1:
message_h4("Rebuild the project failed, check your project dependencies.", Color.INDIAN_RED)
await get_tree().create_timer(3).timeout
return
for out: String in output:
print_rich("[color=DEEP_SKY_BLUE] %s" % out.strip_edges())
await get_tree().process_frame
func _on_confirmed() -> void:
await run_update()
func _on_cancel_pressed() -> void:
hide()
func _on_update_pressed() -> void:
await run_update()

View File

@@ -0,0 +1 @@
uid://2cvldn16wv2b

View File

@@ -0,0 +1,100 @@
[gd_scene load_steps=6 format=3 uid="uid://2eahgaw88y6q"]
[ext_resource type="Script" path="res://addons/gdUnit4/src/update/GdUnitUpdate.gd" id="1"]
[sub_resource type="Gradient" id="Gradient_wilsr"]
colors = PackedColorArray(0.151276, 0.151276, 0.151276, 1, 1, 1, 1, 1)
[sub_resource type="GradientTexture2D" id="GradientTexture2D_45cww"]
gradient = SubResource("Gradient_wilsr")
fill_to = Vector2(0.75641, 0)
[sub_resource type="Gradient" id="Gradient_i0qp8"]
colors = PackedColorArray(1, 1, 1, 1, 0.20871, 0.20871, 0.20871, 1)
[sub_resource type="GradientTexture2D" id="GradientTexture2D_wilsr"]
gradient = SubResource("Gradient_i0qp8")
fill_from = Vector2(0.794872, 0)
fill_to = Vector2(0, 0)
[node name="GdUnitUpdate" type="MarginContainer"]
clip_contents = true
custom_minimum_size = Vector2(0, 80)
anchors_preset = 10
anchor_right = 1.0
offset_bottom = 80.0
grow_horizontal = 2
size_flags_horizontal = 3
theme_override_constants/margin_left = 10
theme_override_constants/margin_right = 10
script = ExtResource("1")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 2
[node name="Panel" type="Panel" parent="VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
[node name="message" type="RichTextLabel" parent="VBoxContainer/Panel"]
unique_name_in_owner = true
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
bbcode_enabled = true
text = "aaaaa"
fit_content = true
scroll_active = false
shortcut_keys_enabled = false
[node name="Panel2" type="Panel" parent="VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
[node name="progress" type="TextureProgressBar" parent="VBoxContainer/Panel2"]
unique_name_in_owner = true
auto_translate_mode = 2
clip_contents = true
custom_minimum_size = Vector2(0, 20)
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
localize_numeral_system = false
min_value = 1.0
max_value = 5.0
value = 1.0
rounded = true
allow_greater = true
nine_patch_stretch = true
texture_under = SubResource("GradientTexture2D_45cww")
texture_progress = SubResource("GradientTexture2D_wilsr")
tint_under = Color(0.0235294, 0.145098, 0.168627, 1)
tint_progress = Color(0.288912, 0.233442, 0.533772, 1)
[node name="PanelContainer" type="MarginContainer" parent="VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/PanelContainer"]
layout_mode = 2
theme_override_constants/separation = 10
alignment = 2
[node name="update" type="Button" parent="VBoxContainer/PanelContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Update"
[node name="cancel" type="Button" parent="VBoxContainer/PanelContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Cancel"
[connection signal="pressed" from="VBoxContainer/PanelContainer/HBoxContainer/update" to="." method="_on_update_pressed"]
[connection signal="pressed" from="VBoxContainer/PanelContainer/HBoxContainer/cancel" to="." method="_on_cancel_pressed"]

View File

@@ -0,0 +1,98 @@
@tool
extends Node
signal request_completed(response: HttpResponse)
class HttpResponse:
var _http_status: int
var _body: PackedByteArray
func _init(http_status: int, body: PackedByteArray) -> void:
_http_status = http_status
_body = body
func status() -> int:
return _http_status
func response() -> Variant:
if _http_status != 200:
return _body.get_string_from_utf8()
var test_json_conv := JSON.new()
@warning_ignore("return_value_discarded")
var error := test_json_conv.parse(_body.get_string_from_utf8())
if error != OK:
return "HttpResponse: %s Error: %s" % [error_string(error), _body.get_string_from_utf8()]
return test_json_conv.get_data()
func get_body() -> PackedByteArray:
return _body
var _http_request := HTTPRequest.new()
func _ready() -> void:
add_child(_http_request)
@warning_ignore("return_value_discarded")
_http_request.request_completed.connect(_on_request_completed)
func _notification(what: int) -> void:
if what == NOTIFICATION_PREDELETE:
if is_instance_valid(_http_request):
_http_request.queue_free()
#func list_tags() -> void:
# _http_request.connect("request_completed",Callable(self,"_response_request_tags"))
# var error = _http_request.request("https://api.github.com/repos/MikeSchulze/gdUnit4/tags")
# if error != OK:
# push_error("An error occurred in the HTTP request.")
func request_latest_version() -> HttpResponse:
var error := _http_request.request("https://api.github.com/repos/MikeSchulze/gdUnit4/tags")
if error != OK:
var message := "Request latest version failed, %s" % error_string(error)
return HttpResponse.new(error, message.to_utf8_buffer())
return await self.request_completed
func request_releases() -> HttpResponse:
var error := _http_request.request("https://api.github.com/repos/MikeSchulze/gdUnit4/releases")
if error != OK:
var message := "request_releases failed: %d" % error
return HttpResponse.new(error, message.to_utf8_buffer())
return await self.request_completed
func request_image(url: String) -> HttpResponse:
var error := _http_request.request(url)
if error != OK:
var message := "request_image failed: %d" % error
return HttpResponse.new(error, message.to_utf8_buffer())
return await self.request_completed
func request_zip_package(url: String, file: String) -> HttpResponse:
_http_request.set_download_file(file)
var error := _http_request.request(url)
if error != OK:
var message := "request_zip_package failed: %d" % error
return HttpResponse.new(error, message.to_utf8_buffer())
return await self.request_completed
func extract_latest_version(response: HttpResponse) -> GdUnit4Version:
var body: Array = response.response()
return GdUnit4Version.parse(str(body[0]["name"]))
func _on_request_completed(_result: int, response_http_status: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
if _http_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
_http_request.set_download_file("")
request_completed.emit(HttpResponse.new(response_http_status, body))

View File

@@ -0,0 +1 @@
uid://cybga3asuikuv

View File

@@ -0,0 +1,206 @@
@tool
extends MarginContainer
#signal request_completed(response)
const GdMarkDownReader = preload("res://addons/gdUnit4/src/update/GdMarkDownReader.gd")
const GdUnitUpdateClient = preload("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd")
const GdUnitUpdateProgress = preload("res://addons/gdUnit4/src/update/GdUnitUpdate.gd")
@onready var _md_reader: GdMarkDownReader = GdMarkDownReader.new()
@onready var _update_client: GdUnitUpdateClient = $GdUnitUpdateClient
@onready var _header: Label = $Panel/GridContainer/PanelContainer/header
@onready var _update_button: Button = $Panel/GridContainer/Panel/HBoxContainer/update
@onready var _content: RichTextLabel = $Panel/GridContainer/PanelContainer2/ScrollContainer/MarginContainer/content
@onready var _update_progress :GdUnitUpdateProgress = %update_banner
var _debug_mode := false
var _patcher := GdUnitPatcher.new()
var _current_version := GdUnit4Version.current()
func _ready() -> void:
_update_button.set_disabled(false)
_md_reader.set_http_client(_update_client)
@warning_ignore("return_value_discarded")
#GdUnitFonts.init_fonts(_content)
_update_progress.set_visible(false)
_update_progress.hidden.connect(func() -> void:
_update_button.set_disabled(false)
)
func request_releases() -> bool:
if _debug_mode:
_update_progress._debug_mode = _debug_mode
_header.text = "A new version 'v4.4.4' is available"
_update_button.set_disabled(false)
return true
var response :GdUnitUpdateClient.HttpResponse = await _update_client.request_latest_version()
if response.status() != 200:
_header.text = "Update information cannot be retrieved from GitHub!"
message_h4("\n\nError: %s" % response.response(), Color.INDIAN_RED)
return false
var latest_version := _update_client.extract_latest_version(response)
# if same version exit here no update need
if latest_version.is_greater(_current_version):
_patcher.scan(_current_version)
_header.text = "A new version '%s' is available" % latest_version
var download_zip_url := extract_zip_url(response)
_update_progress.setup(_update_client, download_zip_url)
_update_button.set_disabled(false)
return true
else:
_header.text = "No update is available."
_update_button.set_disabled(true)
return false
func _colored(message_: String, color: Color) -> String:
return "[color=#%s]%s[/color]" % [color.to_html(), message_]
func message_h4(message_: String, color: Color, clear := true) -> void:
if clear:
_content.clear()
_content.append_text("[font_size=16]%s[/font_size]" % _colored(message_, color))
func message(message_: String, color: Color) -> void:
_content.clear()
_content.append_text(_colored(message_, color))
func _process(_delta: float) -> void:
if _content != null and _content.is_visible_in_tree():
_content.queue_redraw()
func show_update() -> void:
if not GdUnitSettings.is_update_notification_enabled():
_header.text = "No update is available."
message_h4("The search for updates is deactivated.", Color.CORNFLOWER_BLUE)
_update_button.set_disabled(true)
return
if not await request_releases():
return
_update_button.set_disabled(true)
prints("Scan for GdUnit4 Update ...")
message_h4("\n\n\nRequest release infos ... ", Color.SNOW)
_content.add_image(GdUnitUiTools.get_spinner(), 32, 32)
var content: String
if _debug_mode:
await get_tree().create_timer(.2).timeout
var template := FileAccess.open("res://addons/gdUnit4/test/update/resources/http_response_releases.txt", FileAccess.READ).get_as_text()
content = await _md_reader.to_bbcode(template)
else:
var response :GdUnitUpdateClient.HttpResponse = await _update_client.request_releases()
if response.status() == 200:
content = await extract_releases(response, _current_version)
else:
message_h4("\n\n\nError checked request available releases!", Color.INDIAN_RED)
return
# finally force rescan to import images as textures
if Engine.is_editor_hint():
await rescan()
message(content, Color.CADET_BLUE)
_update_button.set_disabled(false)
func extract_zip_url(response: GdUnitUpdateClient.HttpResponse) -> String:
var body :Array = response.response()
return body[0]["zipball_url"]
func extract_releases(response: GdUnitUpdateClient.HttpResponse, current_version: GdUnit4Version) -> String:
await get_tree().process_frame
var result := ""
for release :Dictionary in response.response():
var release_version := str(release["tag_name"])
if GdUnit4Version.parse(release_version).equals(current_version):
break
var release_description := _colored("<h1>GdUnit Release %s</h1>" % release_version, Color.CORNFLOWER_BLUE)
release_description += "\n"
release_description += release["body"]
release_description += "\n\n"
result += await _md_reader.to_bbcode(release_description)
return result
func rescan() -> void:
if Engine.is_editor_hint():
if OS.is_stdout_verbose():
prints(".. reimport release resources")
var fs := EditorInterface.get_resource_filesystem()
fs.scan()
while fs.is_scanning():
if OS.is_stdout_verbose():
progressBar(fs.get_scanning_progress() * 100 as int)
await get_tree().process_frame
await get_tree().process_frame
await get_tree().create_timer(1).timeout
func progressBar(p_progress: int) -> void:
if p_progress < 0:
p_progress = 0
if p_progress > 100:
p_progress = 100
printraw("scan [%-50s] %-3d%%\r" % ["".lpad(int(p_progress/2.0), "#").rpad(50, "-"), p_progress])
@warning_ignore("return_value_discarded")
func _on_update_pressed() -> void:
_update_button.set_disabled(true)
# close all opend scripts before start the update
if not _debug_mode:
ScriptEditorControls.close_open_editor_scripts()
# copy update source to a temp because the update is deleting the whole gdUnit folder
DirAccess.make_dir_absolute("res://addons/.gdunit_update")
DirAccess.copy_absolute("res://addons/gdUnit4/src/update/GdUnitUpdate.tscn", "res://addons/.gdunit_update/GdUnitUpdate.tscn")
DirAccess.copy_absolute("res://addons/gdUnit4/src/update/GdUnitUpdate.gd", "res://addons/.gdunit_update/GdUnitUpdate.gd")
var source := FileAccess.open("res://addons/gdUnit4/src/update/GdUnitUpdate.tscn", FileAccess.READ)
var content := source.get_as_text().replace("res://addons/gdUnit4/src/update/GdUnitUpdate.gd", "res://addons/.gdunit_update/GdUnitUpdate.gd")
var dest := FileAccess.open("res://addons/.gdunit_update/GdUnitUpdate.tscn", FileAccess.WRITE)
dest.store_string(content)
_update_progress.set_visible(true)
func _on_show_next_toggled(enabled: bool) -> void:
GdUnitSettings.set_update_notification(enabled)
func _on_cancel_pressed() -> void:
hide()
func _on_content_meta_clicked(meta: String) -> void:
var properties: Dictionary = str_to_var(meta)
if properties.has("url"):
@warning_ignore("return_value_discarded")
OS.shell_open(str(properties.get("url")))
func _on_content_meta_hover_started(meta: String) -> void:
var properties: Dictionary = str_to_var(meta)
if properties.has("tool_tip"):
_content.set_tooltip_text(str(properties.get("tool_tip")))
@warning_ignore("unused_parameter")
func _on_content_meta_hover_ended(meta: String) -> void:
_content.set_tooltip_text("")
func _on_visibility_changed() -> void:
if not is_visible_in_tree():
return
if _update_progress != null:
_update_progress.set_visible(false)
await show_update()

View File

@@ -0,0 +1 @@
uid://crycbwg5hjrkl

View File

@@ -0,0 +1,97 @@
[gd_scene load_steps=4 format=3 uid="uid://0xyeci1tqebj"]
[ext_resource type="Script" path="res://addons/gdUnit4/src/update/GdUnitUpdateNotify.gd" id="1_112wo"]
[ext_resource type="Script" path="res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd" id="2_18asx"]
[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/update/GdUnitUpdate.tscn" id="3_x87h6"]
[node name="Control" type="MarginContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
script = ExtResource("1_112wo")
[node name="GdUnitUpdateClient" type="Node" parent="."]
script = ExtResource("2_18asx")
[node name="Panel" type="Panel" parent="."]
layout_mode = 2
[node name="GridContainer" type="VBoxContainer" parent="Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
alignment = 1
[node name="PanelContainer" type="MarginContainer" parent="Panel/GridContainer"]
layout_mode = 2
theme_override_constants/margin_left = 4
theme_override_constants/margin_top = 4
theme_override_constants/margin_right = 4
theme_override_constants/margin_bottom = 4
[node name="header" type="Label" parent="Panel/GridContainer/PanelContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 9
[node name="PanelContainer2" type="PanelContainer" parent="Panel/GridContainer"]
layout_mode = 2
size_flags_vertical = 3
[node name="ScrollContainer" type="ScrollContainer" parent="Panel/GridContainer/PanelContainer2"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="MarginContainer" type="MarginContainer" parent="Panel/GridContainer/PanelContainer2/ScrollContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="content" type="RichTextLabel" parent="Panel/GridContainer/PanelContainer2/ScrollContainer/MarginContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
bbcode_enabled = true
[node name="update_banner" parent="Panel/GridContainer" instance=ExtResource("3_x87h6")]
unique_name_in_owner = true
visible = false
layout_mode = 2
size_flags_horizontal = 1
size_flags_vertical = 8
[node name="Panel" type="MarginContainer" parent="Panel/GridContainer"]
layout_mode = 2
size_flags_vertical = 8
theme_override_constants/margin_left = 4
theme_override_constants/margin_top = 4
theme_override_constants/margin_right = 4
theme_override_constants/margin_bottom = 4
[node name="HBoxContainer" type="HBoxContainer" parent="Panel/GridContainer/Panel"]
use_parent_material = true
layout_mode = 2
theme_override_constants/separation = 4
[node name="update" type="Button" parent="Panel/GridContainer/Panel/HBoxContainer"]
custom_minimum_size = Vector2(100, 40)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 4
text = "Update"
[connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"]
[connection signal="meta_clicked" from="Panel/GridContainer/PanelContainer2/ScrollContainer/MarginContainer/content" to="." method="_on_content_meta_clicked"]
[connection signal="meta_hover_ended" from="Panel/GridContainer/PanelContainer2/ScrollContainer/MarginContainer/content" to="." method="_on_content_meta_hover_ended"]
[connection signal="meta_hover_started" from="Panel/GridContainer/PanelContainer2/ScrollContainer/MarginContainer/content" to="." method="_on_content_meta_hover_started"]
[connection signal="pressed" from="Panel/GridContainer/Panel/HBoxContainer/update" to="." method="_on_update_pressed"]

Binary file not shown.

View File

@@ -2,12 +2,16 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://cb4qnsd0po83o"
valid=false
uid="uid://cse3iwnghcmkn"
path="res://.godot/imported/border_bottom.png-30d66a4c67e3a03ad191e37cdf16549d.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/gdUnit4/src/update/assets/border_bottom.png"
dest_files=["res://.godot/imported/border_bottom.png-30d66a4c67e3a03ad191e37cdf16549d.ctex"]
[params]

Binary file not shown.

View File

@@ -2,12 +2,16 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://p47gfqh53x1q"
valid=false
uid="uid://peoblq5q6qhm"
path="res://.godot/imported/border_top.png-c47cbebdb755144731c6ae309e18bbaa.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/gdUnit4/src/update/assets/border_top.png"
dest_files=["res://.godot/imported/border_top.png-c47cbebdb755144731c6ae309e18bbaa.ctex"]
[params]

BIN
addons/gdUnit4/src/update/assets/dot1.png (Stored with Git LFS)

Binary file not shown.

View File

@@ -2,12 +2,16 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://cdoj1yp8q7x5p"
valid=false
uid="uid://bhfi0qh1a2hwx"
path="res://.godot/imported/dot1.png-380baf1b5247addda93bce3c799aa4e7.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/gdUnit4/src/update/assets/dot1.png"
dest_files=["res://.godot/imported/dot1.png-380baf1b5247addda93bce3c799aa4e7.ctex"]
[params]

BIN
addons/gdUnit4/src/update/assets/dot2.png (Stored with Git LFS)

Binary file not shown.

View File

@@ -2,12 +2,16 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://bbpyk3jcipja7"
valid=false
uid="uid://dg86wkmvp1qyn"
path="res://.godot/imported/dot2.png-86a9db80ef4413e353c4339ad8f68a5f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/gdUnit4/src/update/assets/dot2.png"
dest_files=["res://.godot/imported/dot2.png-86a9db80ef4413e353c4339ad8f68a5f.ctex"]
[params]

Binary file not shown.

View File

@@ -2,12 +2,16 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://u74w6eoyl2mx"
valid=false
uid="uid://d160etflupwba"
path="res://.godot/imported/embedded.png-29390948772209a603567d24f8766495.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/gdUnit4/src/update/assets/embedded.png"
dest_files=["res://.godot/imported/embedded.png-29390948772209a603567d24f8766495.ctex"]
[params]

Binary file not shown.

View File

@@ -2,12 +2,16 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://b2p4jgab5ehs3"
valid=false
uid="uid://bloqm443lywyi"
path="res://.godot/imported/horizontal-line2.png-92618e6ee5cc9002847547a8c9deadbc.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/gdUnit4/src/update/assets/horizontal-line2.png"
dest_files=["res://.godot/imported/horizontal-line2.png-92618e6ee5cc9002847547a8c9deadbc.ctex"]
[params]