diff options
| author | radiohotline <radoihotline@disroot.rg> | 2026-08-25 16:33:56 +0300 |
|---|---|---|
| committer | radiohotline <radoihotline@disroot.rg> | 2026-08-25 16:33:56 +0300 |
| commit | a89e744ed0e8c759caf041a580112365bbfe4303 (patch) | |
| tree | dd61e320904176995727e86a857d00c90d4933ba /addons/gdUnit4/src/update | |
| parent | 6d53d8cac78250dc888aa49d5f9d4175c6652feb (diff) | |
Diffstat (limited to 'addons/gdUnit4/src/update')
26 files changed, 1552 insertions, 0 deletions
diff --git a/addons/gdUnit4/src/update/GdMarkDownReader.gd b/addons/gdUnit4/src/update/GdMarkDownReader.gd new file mode 100644 index 0000000..dab19e4 --- /dev/null +++ b/addons/gdUnit4/src/update/GdMarkDownReader.gd @@ -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 diff --git a/addons/gdUnit4/src/update/GdMarkDownReader.gd.uid b/addons/gdUnit4/src/update/GdMarkDownReader.gd.uid new file mode 100644 index 0000000..6817573 --- /dev/null +++ b/addons/gdUnit4/src/update/GdMarkDownReader.gd.uid @@ -0,0 +1 @@ +uid://d1dpjlnngiokr diff --git a/addons/gdUnit4/src/update/GdUnitPatch.gd b/addons/gdUnit4/src/update/GdUnitPatch.gd new file mode 100644 index 0000000..daa20f7 --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitPatch.gd @@ -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 diff --git a/addons/gdUnit4/src/update/GdUnitPatch.gd.uid b/addons/gdUnit4/src/update/GdUnitPatch.gd.uid new file mode 100644 index 0000000..ca688a3 --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitPatch.gd.uid @@ -0,0 +1 @@ +uid://b0il3gifqhk77 diff --git a/addons/gdUnit4/src/update/GdUnitPatcher.gd b/addons/gdUnit4/src/update/GdUnitPatcher.gd new file mode 100644 index 0000000..73d25c9 --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitPatcher.gd @@ -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) diff --git a/addons/gdUnit4/src/update/GdUnitPatcher.gd.uid b/addons/gdUnit4/src/update/GdUnitPatcher.gd.uid new file mode 100644 index 0000000..241850e --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitPatcher.gd.uid @@ -0,0 +1 @@ +uid://b81y3i61xqoiw diff --git a/addons/gdUnit4/src/update/GdUnitUpdate.gd b/addons/gdUnit4/src/update/GdUnitUpdate.gd new file mode 100644 index 0000000..97b7fdd --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitUpdate.gd @@ -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() diff --git a/addons/gdUnit4/src/update/GdUnitUpdate.gd.uid b/addons/gdUnit4/src/update/GdUnitUpdate.gd.uid new file mode 100644 index 0000000..a72d4a5 --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitUpdate.gd.uid @@ -0,0 +1 @@ +uid://dyr407n7f3td6 diff --git a/addons/gdUnit4/src/update/GdUnitUpdate.tscn b/addons/gdUnit4/src/update/GdUnitUpdate.tscn new file mode 100644 index 0000000..dc0511a --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitUpdate.tscn @@ -0,0 +1,100 @@ +[gd_scene 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"] diff --git a/addons/gdUnit4/src/update/GdUnitUpdateClient.gd b/addons/gdUnit4/src/update/GdUnitUpdateClient.gd new file mode 100644 index 0000000..b4f54b7 --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitUpdateClient.gd @@ -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)) diff --git a/addons/gdUnit4/src/update/GdUnitUpdateClient.gd.uid b/addons/gdUnit4/src/update/GdUnitUpdateClient.gd.uid new file mode 100644 index 0000000..5b6e2d4 --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitUpdateClient.gd.uid @@ -0,0 +1 @@ +uid://c0aqh8gtyaxls diff --git a/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd b/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd new file mode 100644 index 0000000..f4ae9ac --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd @@ -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() diff --git a/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd.uid b/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd.uid new file mode 100644 index 0000000..fa36885 --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd.uid @@ -0,0 +1 @@ +uid://ciyg27plnsvyr diff --git a/addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn b/addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn new file mode 100644 index 0000000..7305f61 --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn @@ -0,0 +1,97 @@ +[gd_scene 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"] diff --git a/addons/gdUnit4/src/update/assets/border_bottom.png b/addons/gdUnit4/src/update/assets/border_bottom.png Binary files differnew file mode 100644 index 0000000..aa16bb7 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/border_bottom.png diff --git a/addons/gdUnit4/src/update/assets/border_bottom.png.import b/addons/gdUnit4/src/update/assets/border_bottom.png.import new file mode 100644 index 0000000..752bf40 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/border_bottom.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dns6mtm6xif3" +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] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/src/update/assets/border_top.png b/addons/gdUnit4/src/update/assets/border_top.png Binary files differnew file mode 100644 index 0000000..b1b1039 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/border_top.png diff --git a/addons/gdUnit4/src/update/assets/border_top.png.import b/addons/gdUnit4/src/update/assets/border_top.png.import new file mode 100644 index 0000000..dae66df --- /dev/null +++ b/addons/gdUnit4/src/update/assets/border_top.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cvg6r1fdk1p01" +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] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/src/update/assets/dot1.png b/addons/gdUnit4/src/update/assets/dot1.png Binary files differnew file mode 100644 index 0000000..d5ea77b --- /dev/null +++ b/addons/gdUnit4/src/update/assets/dot1.png diff --git a/addons/gdUnit4/src/update/assets/dot1.png.import b/addons/gdUnit4/src/update/assets/dot1.png.import new file mode 100644 index 0000000..a7f74d7 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/dot1.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://t4fmqmj3ffcd" +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] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/src/update/assets/dot2.png b/addons/gdUnit4/src/update/assets/dot2.png Binary files differnew file mode 100644 index 0000000..4a74498 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/dot2.png diff --git a/addons/gdUnit4/src/update/assets/dot2.png.import b/addons/gdUnit4/src/update/assets/dot2.png.import new file mode 100644 index 0000000..8b5b001 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/dot2.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c2usgitbic18b" +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] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/src/update/assets/embedded.png b/addons/gdUnit4/src/update/assets/embedded.png Binary files differnew file mode 100644 index 0000000..15cb9ab --- /dev/null +++ b/addons/gdUnit4/src/update/assets/embedded.png diff --git a/addons/gdUnit4/src/update/assets/embedded.png.import b/addons/gdUnit4/src/update/assets/embedded.png.import new file mode 100644 index 0000000..a02de65 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/embedded.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bm8tdlw36fu7q" +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] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/src/update/assets/horizontal-line2.png b/addons/gdUnit4/src/update/assets/horizontal-line2.png Binary files differnew file mode 100644 index 0000000..66aa098 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/horizontal-line2.png diff --git a/addons/gdUnit4/src/update/assets/horizontal-line2.png.import b/addons/gdUnit4/src/update/assets/horizontal-line2.png.import new file mode 100644 index 0000000..3148524 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/horizontal-line2.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://w1d7ughjf1b3" +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] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 |
