Skip to content

Instantly share code, notes, and snippets.

@BOSS-DALE
Forked from renggli/portal_tools_dock.gd
Last active March 18, 2026 18:28
Show Gist options
  • Select an option

  • Save BOSS-DALE/2f75a7b72bfe34106dc482cf0aa1366c to your computer and use it in GitHub Desktop.

Select an option

Save BOSS-DALE/2f75a7b72bfe34106dc482cf0aa1366c to your computer and use it in GitHub Desktop.
Patched version of portal_tools_dock.gd for MacOS
@tool
extends Control
var portal_tools_plugin # cannot type hint because cyclic
var _output_dir: String = ""
var _current_export_level_path: String = ""
var _config: Dictionary = {}
var _thread = Thread.new()
var _setup_dialog: AcceptDialog
var _levels: Array[String] = []
@onready var setup: Button = %Setup_Button
@onready var export_level: Button = %ExportLevel_Button
@onready var open_exports: Button = %OpenExports_Button
@onready var download_models: Button = %DownloadModels_Button
@onready var export_level_label: Label = %ExportLevel_Label
func _ready() -> void:
_config = PortalPlugin.read_config()
if _config.size() == 0:
return
_output_dir = _config["export"]
if not _config["setupEnabled"]:
setup.disabled = true
setup.tooltip_text = "Setup has been disabled. Check the config for any misconfiguration"
else:
setup.disabled = false
setup.tooltip_text = "Setup python and virtual environment"
_levels = _get_all_levels(_config)
if not setup.pressed.is_connected(_setup):
setup.pressed.connect(_setup)
if not export_level.pressed.is_connected(_export_levels):
export_level.pressed.connect(_export_levels)
if not open_exports.pressed.is_connected(_on_open_exports):
open_exports.pressed.connect(_on_open_exports)
if not download_models.pressed.is_connected(_on_download_models):
download_models.pressed.connect(_on_download_models)
func _process(_delta: float) -> void:
if not _thread.is_alive() and _thread.is_started():
_thread.wait_to_finish()
func is_scene_a_level(scene: Node) -> bool:
if scene is not Node3D:
return false
return scene.name in _levels
func change_scene(scene: Node) -> void:
if not is_scene_a_level(scene):
return
var path = scene.scene_file_path
_current_export_level_path = path
var level_name = path.get_file().rstrip(".tscn")
export_level_label.text = level_name
export_level.disabled = false
func _setup() -> void:
if not _config["setupEnabled"]:
return
portal_tools_plugin.show_log_panel()
# forced to be on main thread
print("Generating object library")
var library_path = GenerateLibraryScript.generate_library()
var scene_library: SceneLibrary = portal_tools_plugin.get_scene_library_instance()
if scene_library != null:
scene_library.load_library(library_path)
var platform = OS.get_name()
# ==> if platform == "Windows":
if platform == "macOS":
_thread.start(_setup_work_windows)
_setup_dialog = AcceptDialog.new()
_setup_dialog.title = "Setup"
_setup_dialog.dialog_text = "Please wait while setup finishes..."
_setup_dialog.get_ok_button().visible = false
_setup_dialog.dialog_close_on_escape = false
EditorInterface.popup_dialog_centered(_setup_dialog)
else:
var dialog = AcceptDialog.new() # not member variable because simply one-off
var msg = "Setup has not been implemented for your platform: %s" % platform
dialog.dialog_text = msg
EditorInterface.popup_dialog_centered(dialog)
printerr(msg)
func _setup_work_windows() -> void:
var output = []
var exit_code = 0
var venv_path: String = _config["venv"]
if DirAccess.dir_exists_absolute(venv_path):
print("Cleaning previous virtual environment")
# ==> OS.execute("powershell.exe", ["-Command", "Remove-Item -Recurse -Force %s -ErrorAction SilentlyContinue" % venv_path], output, true)
OS.execute("bash", ["-c", "rm -rf %s" % venv_path], output, true)
if DirAccess.dir_exists_absolute(venv_path):
printerr(output)
printerr("Failed to cleanup previous setup")
call_deferred("_setup_error", "Failed to cleanup previous setup")
return
output.pop_back()
print("Creating virtual environment...")
# ==> var python = "%s/python.exe" % _config["python"]
var python = "python3"
exit_code = OS.execute(python, ["-m", "venv", venv_path], output, true)
if exit_code != 0:
printerr(output)
printerr("Failed to create virtual environment")
call_deferred("_setup_error", "Failed to create virtual environment")
return
output.pop_back()
print("Installing packages to virtual environment...")
# ==> var python_venv = "%s/Scripts/python.exe" % venv_path
var python_venv = "%s/bin/python3" % venv_path
exit_code = OS.execute(python_venv, ["-m", "pip", "install", "--upgrade", "pip"], output, true)
if exit_code != 0:
printerr(output)
printerr("Upgrading pip failed")
call_deferred("_setup_error", "Upgrading pip failed")
return
output.pop_back()
# requirements.txt uses relative paths and cannot change cwd with OS.execute yet so need pwsh here
var venv_path_split = venv_path.rsplit("venv", true, 1)
var base_path = venv_path_split[0] if venv_path_split.size() > 1 else "."
# ==> var args = "cd %s ; venv/Scripts/python.exe -m pip install -r ./requirements.txt" % base_path
var args = "cd %s ; venv/bin/python3 -m pip install -r ./requirements.txt" % base_path
# ==> exit_code = OS.execute("powershell.exe", ["-Command", args], output, true)
exit_code = OS.execute("bash", ["-c", args], output, true)
if exit_code != 0:
print(output)
printerr("Installing requirements failed")
call_deferred("_setup_error", "Installing requirements failed")
return
print("Completed setup")
call_deferred("_setup_success", "Completed setup")
func _setup_error(msg: String = "") -> void:
_setup_dialog.dialog_text = "An error occurred when setting up:%s\nSee Output window for more details" % ("\n%s\n" % msg if msg else "")
_setup_dialog.get_ok_button().visible = true
func _setup_success(msg: String = "") -> void:
_setup_dialog.dialog_text = msg
_setup_dialog.get_ok_button().visible = true
func _export_levels() -> void:
# ==> if OS.get_name() != "Windows":
if OS.get_name() != "macOS":
return
var dialog = AcceptDialog.new()
var output = []
# ==> var python_venv = "%s/Scripts/python.exe" % _config["venv"]
var python_venv = "%s/bin/python3" % _config["venv"]
if not FileAccess.file_exists(python_venv):
portal_tools_plugin.show_log_panel()
var msg = "Cannot export level when python is not in a virtual environment. Has setup been ran yet?"
printerr(msg)
dialog.dialog_text = msg
EditorInterface.popup_dialog_centered(dialog)
return
var export_tscn = "%s/src/gdconverter/export_tscn.py" % _config["gdconverter"]
var scene_path = ProjectSettings.globalize_path(_current_export_level_path)
var level_name = scene_path.get_file().get_basename()
EditorInterface.save_scene()
var fb_export_dir = _config["fbExportData"]
var dialog_text = ""
var exit_code = OS.execute(python_venv, [export_tscn, scene_path, fb_export_dir, _output_dir], output, true)
if exit_code != 0:
dialog.title = "Error"
dialog_text = "Failed to export %s\n" % level_name
var err: String = (output.pop_back() as String).replace("\r\n", "\n").strip_edges()
if err:
var err_lines = err.split("\n", true)
var line_count_limit = 15
if err_lines.size() > line_count_limit:
var err_truncated = err_lines.slice(0, line_count_limit)
dialog_text += "\n".join(err_truncated)
dialog_text += "\n...\n(see Output window for more details)"
else:
dialog_text += err
portal_tools_plugin.show_log_panel()
printerr(err)
else:
dialog.title = "Success"
dialog_text = "Successfully exported %s" % level_name
dialog.dialog_text = dialog_text
EditorInterface.popup_dialog_centered(dialog)
func _on_open_exports() -> void:
if not DirAccess.dir_exists_absolute(_output_dir):
DirAccess.make_dir_recursive_absolute(_output_dir)
if _current_export_level_path:
var file = _current_export_level_path.get_file()
var json_file = file.replace(".tscn", ".json")
var supposed_path = _output_dir + "/" + json_file
if FileAccess.file_exists(supposed_path):
OS.shell_show_in_file_manager(supposed_path)
return
OS.shell_show_in_file_manager(_output_dir)
func _on_download_models() -> void:
if not "modelsUrl" in _config:
return
var models_url: String = _config["modelsUrl"]
if not "fbExportData" in _config:
return
var output_path: String = "%s/%s" % [_config["fbExportData"], models_url.get_file()]
var download_success = func download_success() -> void:
var success_msg = "Download complete! %s" % output_path
EditorInterface.get_editor_toaster().push_toast(success_msg)
print(success_msg)
var download_file = func download_file() -> void:
var request = HTTPRequest.new()
add_child(request)
request.request_completed.connect(_on_request_completed.bind(request, Callable(), download_success))
request.download_chunk_size = 2 ** 24 # max val
request.download_file = output_path
EditorInterface.get_editor_toaster().push_toast("Downloading %s to %s" % [models_url.get_file(), output_path])
var error = request.request(models_url, [], HTTPClient.METHOD_GET)
if error != OK:
printerr("Error %s GET failed for url %s" % [error, models_url])
var check_header = func check_header(headers: PackedStringArray) -> bool:
var is_zip = "Content-Type: application/zip" in headers
if not is_zip:
EditorInterface.get_editor_toaster().push_toast("Failed to download. Could not verify zip file exists", EditorToaster.SEVERITY_ERROR)
return is_zip
# need to check before downloading, otherwise godot inadvertently creates a file regardless of request failure
var check_req = HTTPRequest.new()
add_child(check_req)
check_req.request_completed.connect(_on_request_completed.bind(check_req, check_header, download_file))
var error = check_req.request(models_url, [], HTTPClient.METHOD_HEAD)
if error != OK:
printerr("Error %s HEAD failed for url %s" % [error, models_url])
func _on_request_completed(
result: int,
response_code: int,
headers: PackedStringArray,
body: PackedByteArray,
req: HTTPRequest,
check_headers: Callable = Callable(),
on_success: Callable = Callable()) -> void:
req.queue_free()
if result != HTTPRequest.RESULT_SUCCESS:
printerr("HTTPRequest Error %d" % result)
return
if response_code != 200:
printerr("Response Code %d" % response_code)
return
if check_headers.is_valid():
if check_headers.call(headers):
if on_success.is_valid():
on_success.call()
else:
if on_success.is_valid():
on_success.call()
func _get_all_levels(config: Dictionary) -> Array[String]:
if not "fbExportData" in config:
return []
var fb_data = config["fbExportData"]
var level_info_path = fb_data + "/level_info.json"
var file = FileAccess.open(level_info_path, FileAccess.READ)
if file == null:
printerr("Unable to read path: %s" % level_info_path)
return []
var contents = file.get_as_text()
var level_info: Dictionary = JSON.parse_string(contents)
var levels: Array[String] = []
for level_name in level_info:
levels.append(level_name)
return levels
@BOSS-DALE
Copy link
Author

BOSS-DALE commented Mar 18, 2026

Forked this gist and enhanced it for my use case, focusing on performance and scalability.

Updates made:

  • Refined logic flow to reduce unnecessary processing
  • Cleaned up structure for better readability and reuse
  • Adjusted values/handling to support larger-scale scenarios
  • Minor optimizations for more consistent behavior under load

This is part of my ongoing B.O.S.S (Battlefield Operation Support System) work.

Huge credit to the original author—this was a great foundation to build on 🙌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment