└── addons └── hathora ├── plugin ├── dotenv.gd.uid ├── enums.gd.uid ├── client.gd.uid ├── apis │ ├── apps_v2.gd.uid │ ├── builds_v3.gd.uid │ ├── endpoint.gd.uid │ ├── room_v2.gd.uid │ ├── deployments_v3.gd.uid │ ├── poly_result.gd.uid │ ├── deployments_v3.gd │ ├── apps_v2.gd │ ├── builds_v3.gd │ ├── endpoint.gd │ ├── room_v2.gd │ └── poly_result.gd ├── hathora_plugin.gd.uid ├── auth0 │ ├── auth0Client.gd.uid │ └── auth0Client.gd ├── editor │ ├── control_ui.gd.uid │ ├── settings_panel.gd.uid │ ├── header_buttons.gd.uid │ ├── modules │ │ ├── tar_maker.gd.uid │ │ ├── build_deployer.gd.uid │ │ ├── project_exporter.gd.uid │ │ ├── dockerfile_maker.gd.uid │ │ ├── tar_maker.gd │ │ ├── project_exporter.gd │ │ ├── dockerfile_maker.gd │ │ └── build_deployer.gd │ ├── section_toggle.gd.uid │ ├── ascii_art_text_label.gd.uid │ ├── latest_deployment_info.gd.uid │ ├── latest_deployment_getter.gd.uid │ ├── settings_panels │ │ ├── developer_settings.gd.uid │ │ ├── room_settings.gd.uid │ │ ├── deployment_settings.gd.uid │ │ ├── server_build_settings.gd.uid │ │ ├── room_settings.gd │ │ ├── developer_settings.gd │ │ ├── deployment_settings.gd │ │ └── server_build_settings.gd │ ├── latest_deployment_info.gd │ ├── latest_deployment_getter.gd │ ├── section_toggle.gd │ ├── control_ui.gd │ ├── header_buttons.gd │ ├── settings_panel.gd │ └── control_ui.tscn ├── rest-client │ ├── client.gd.uid │ ├── client_result.gd.uid │ ├── client_promise.gd.uid │ ├── client_request.gd.uid │ ├── client_async_result.gd.uid │ ├── client_blocking_result.gd.uid │ ├── client_async_result.gd │ ├── client_request.gd │ ├── client_result.gd │ ├── client_blocking_result.gd │ ├── client_promise.gd │ └── client.gd ├── hathora_project_settings.gd.uid ├── assets │ ├── HathoraConfigBanner.png │ └── HathoraConfigBanner.png.import ├── plugin.cfg ├── hathora_plugin.gd ├── dockerfile_template.txt ├── dotenv.gd ├── client.gd ├── enums.gd └── hathora_project_settings.gd └── sdk ├── client.gd.uid ├── dotenv.gd.uid ├── enums.gd.uid ├── apis ├── auth_v1.gd.uid ├── endpoint.gd.uid ├── lobby_v3.gd.uid ├── room_v2.gd.uid ├── discovery_v2.gd.uid ├── poly_result.gd.uid ├── processes_v3.gd.uid ├── discovery_v2.gd ├── auth_v1.gd ├── processes_v3.gd ├── lobby_v3.gd ├── endpoint.gd ├── room_v2.gd └── poly_result.gd ├── hathora_sdk.gd.uid ├── rest-client ├── client.gd.uid ├── client_promise.gd.uid ├── client_request.gd.uid ├── client_result.gd.uid ├── client_async_result.gd.uid ├── client_blocking_result.gd.uid ├── client_async_result.gd ├── client_request.gd ├── client_result.gd ├── client_blocking_result.gd ├── client_promise.gd └── client.gd ├── hathora_project_settings.gd.uid ├── plugin.cfg ├── hathora_sdk.gd ├── hathora_project_settings.gd ├── enums.gd ├── dotenv.gd └── client.gd /addons/hathora/plugin/dotenv.gd.uid: -------------------------------------------------------------------------------- 1 | uid://2qweqqggndcv 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/enums.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dthnpwvjudykj 2 | -------------------------------------------------------------------------------- /addons/hathora/sdk/client.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c168rhm74f2t8 2 | -------------------------------------------------------------------------------- /addons/hathora/sdk/dotenv.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cbti6t7ktehxb 2 | -------------------------------------------------------------------------------- /addons/hathora/sdk/enums.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b2rn2ox5khcxd 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/client.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c1escxxw1u4eo 2 | -------------------------------------------------------------------------------- /addons/hathora/sdk/apis/auth_v1.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cj1ab746yl3k4 2 | -------------------------------------------------------------------------------- /addons/hathora/sdk/apis/endpoint.gd.uid: -------------------------------------------------------------------------------- 1 | uid://exfkp0lx5msj 2 | -------------------------------------------------------------------------------- /addons/hathora/sdk/apis/lobby_v3.gd.uid: -------------------------------------------------------------------------------- 1 | uid://8ep8cck0kaxk 2 | -------------------------------------------------------------------------------- /addons/hathora/sdk/apis/room_v2.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ck4nlip65qbxd 2 | -------------------------------------------------------------------------------- /addons/hathora/sdk/hathora_sdk.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dis7b5nb2077q 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/apis/apps_v2.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c1doeqnr42ucj 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/apis/builds_v3.gd.uid: -------------------------------------------------------------------------------- 1 | uid://chi3in6ny0r7t 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/apis/endpoint.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bbvovh86uu8yv 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/apis/room_v2.gd.uid: -------------------------------------------------------------------------------- 1 | uid://4kgcfa32lp8g 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/hathora_plugin.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bx2e6cp034k1n 2 | -------------------------------------------------------------------------------- /addons/hathora/sdk/apis/discovery_v2.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bndj0rqdckig0 2 | -------------------------------------------------------------------------------- /addons/hathora/sdk/apis/poly_result.gd.uid: -------------------------------------------------------------------------------- 1 | uid://db0r0lw7wg7er 2 | -------------------------------------------------------------------------------- /addons/hathora/sdk/apis/processes_v3.gd.uid: -------------------------------------------------------------------------------- 1 | uid://de5wm6mvxwih3 2 | -------------------------------------------------------------------------------- /addons/hathora/sdk/rest-client/client.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b3px4lhe8wmkx 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/apis/deployments_v3.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dur5aclv8t238 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/apis/poly_result.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b1o6dp7fe5q7a 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/auth0/auth0Client.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dtnlrqvhw5mda 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/control_ui.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dahgogdw40lhg 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/settings_panel.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ii4vt7rghfkg 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/rest-client/client.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dw0f7pqasgpu3 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/header_buttons.gd.uid: -------------------------------------------------------------------------------- 1 | uid://d1j6n0jj1oyq0 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/modules/tar_maker.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ccm335gfkw6ga 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/section_toggle.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dxaqativol007 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/hathora_project_settings.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cp3fg1hu2rug6 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/rest-client/client_result.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cppnb5d2epc4n 2 | -------------------------------------------------------------------------------- /addons/hathora/sdk/hathora_project_settings.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cxhxjh2vqpwij 2 | -------------------------------------------------------------------------------- /addons/hathora/sdk/rest-client/client_promise.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cchk7a3uqaxdf 2 | -------------------------------------------------------------------------------- /addons/hathora/sdk/rest-client/client_request.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dcqk5nfkrk0qc 2 | -------------------------------------------------------------------------------- /addons/hathora/sdk/rest-client/client_result.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cwiwdjvtrlccw 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/ascii_art_text_label.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b4imq1krirsew 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/latest_deployment_info.gd.uid: -------------------------------------------------------------------------------- 1 | uid://d0jmta2xl6b85 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/modules/build_deployer.gd.uid: -------------------------------------------------------------------------------- 1 | uid://de3q6rccv35ay 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/modules/project_exporter.gd.uid: -------------------------------------------------------------------------------- 1 | uid://4wqnwlv88dnt 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/rest-client/client_promise.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c62cai7llsyp8 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/rest-client/client_request.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dbcvrp3oswiol 2 | -------------------------------------------------------------------------------- /addons/hathora/sdk/rest-client/client_async_result.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c5qeg12unuywv 2 | -------------------------------------------------------------------------------- /addons/hathora/sdk/rest-client/client_blocking_result.gd.uid: -------------------------------------------------------------------------------- 1 | uid://81cw2n1k4dy0 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/latest_deployment_getter.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c6e8ndjbdqpxh 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/modules/dockerfile_maker.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c6qx6k45yim37 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/rest-client/client_async_result.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cvrxqmweba4sg 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/rest-client/client_blocking_result.gd.uid: -------------------------------------------------------------------------------- 1 | uid://3djbnq6etumr 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/settings_panels/developer_settings.gd.uid: -------------------------------------------------------------------------------- 1 | uid://mojbsj0f1j76 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/settings_panels/room_settings.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dlg8ao5fnfnej 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/settings_panels/deployment_settings.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b1mjktdfflucc 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/settings_panels/server_build_settings.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c1xj40x8pl6y6 2 | -------------------------------------------------------------------------------- /addons/hathora/plugin/assets/HathoraConfigBanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hathora/hathora-godot-plugin/HEAD/addons/hathora/plugin/assets/HathoraConfigBanner.png -------------------------------------------------------------------------------- /addons/hathora/sdk/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Hathora SDK" 4 | description="Access certain endpoints of the Hathora API" 5 | author="Hathora" 6 | version="0.0.6" 7 | script="hathora_sdk.gd" 8 | -------------------------------------------------------------------------------- /addons/hathora/plugin/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Hathora Plugin" 4 | description="Create and deploy server builds to Hathora" 5 | author="Hathora" 6 | version="0.0.6" 7 | script="hathora_plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/hathora/sdk/hathora_sdk.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | func _enter_tree(): 5 | add_autoload_singleton("HathoraSDK", "client.gd") 6 | 7 | func _exit_tree(): 8 | remove_autoload_singleton("HathoraSDK") 9 | -------------------------------------------------------------------------------- /addons/hathora/plugin/hathora_plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | const DotEnv = preload("../plugin/dotenv.gd") 5 | const BASE = "hathora" 6 | const HathoraProjectSettings = preload("hathora_project_settings.gd") 7 | # MARK: Plugin 8 | var control_ui: Control 9 | 10 | func _enter_tree(): 11 | HathoraProjectSettings.add_project_settings() 12 | control_ui = preload("editor/control_ui.tscn").instantiate() 13 | add_control_to_dock(DOCK_SLOT_LEFT_BR, control_ui) 14 | 15 | 16 | func _exit_tree(): 17 | # Remove control ui 18 | remove_control_from_docks(control_ui) 19 | control_ui.free() 20 | # Remove project settings 21 | HathoraProjectSettings.erase_project_settings() 22 | -------------------------------------------------------------------------------- /addons/hathora/plugin/apis/deployments_v3.gd: -------------------------------------------------------------------------------- 1 | extends "endpoint.gd" 2 | 3 | func get_deployments(app_id: String): 4 | return GET(app_id + "/deployments").then(func (result): 5 | if result.is_error(): 6 | return result 7 | return result 8 | ) 9 | 10 | func create(app_id: String, deployment_config: Dictionary): 11 | return POST(app_id + "/deployments", deployment_config).then(func (result): 12 | if result.is_error(): 13 | return result 14 | return result 15 | ) 16 | 17 | func get_latest(app_id: String): 18 | return GET(app_id + "/deployments/latest").then(func (result): 19 | if result.is_error(): 20 | return result 21 | return result 22 | ) 23 | 24 | func get_deployment(app_id: String, deployment_id: String): 25 | return GET(app_id + "/deployments/" + deployment_id).then(func (result): 26 | if result.is_error(): 27 | return result 28 | return result 29 | ) 30 | -------------------------------------------------------------------------------- /addons/hathora/plugin/assets/HathoraConfigBanner.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://pjo82re3x7fd" 6 | path="res://.godot/imported/HathoraConfigBanner.png-f6b49a61ecae3a784fb4f01f2c1e3690.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/hathora/plugin/assets/HathoraConfigBanner.png" 14 | dest_files=["res://.godot/imported/HathoraConfigBanner.png-f6b49a61ecae3a784fb4f01f2c1e3690.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/modules/tar_maker.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Node 3 | 4 | ## Create a tarball by specifying its relative output path, 5 | ## path of the folder containing the files to tar, 6 | ## and filenames of the files to tar 7 | 8 | func tar_files(output_path: String, tar_file_name: String, file_names: Array) -> bool: 9 | var absolute_output_path = ProjectSettings.globalize_path(output_path) 10 | var output := [] 11 | var arguments = ["-czvf", absolute_output_path.path_join(tar_file_name), "-C", absolute_output_path] 12 | arguments.append_array(file_names) 13 | 14 | var result = OS.execute("tar", arguments, output, true) 15 | 16 | if result != OK: 17 | print(output) 18 | push_error("[HATHORA] Error occurred while creating the tar file: " + str(result)) 19 | return false 20 | 21 | print_rich("[color=%s][HATHORA] Created tar file at [url=%s]%s[/url][/color]" % [owner.get_theme_color("success_color", "Editor").to_html(), absolute_output_path, absolute_output_path.path_join(tar_file_name)]) 22 | return true 23 | -------------------------------------------------------------------------------- /addons/hathora/plugin/dockerfile_template.txt: -------------------------------------------------------------------------------- 1 | # This template is used by the Hathora Plugin to generate a Dockerfile when making a server build 2 | 3 | FROM centos:centos8 4 | 5 | RUN cd /etc/yum.repos.d/ 6 | RUN sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-* 7 | RUN sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-* 8 | 9 | RUN yum install -y wget unzip libXcursor openssl openssl-libs libXinerama libXrandr-devel libXi alsa-lib pulseaudio-libs mesa-libGL 10 | 11 | ENV GODOT_RELEASE_URL "{godot_release_url}" 12 | ENV GODOT_RELEASE_FILENAME "{godot_release_filename}" 13 | 14 | # Install Godot Server 15 | RUN wget -q ${GODOT_RELEASE_URL} \ 16 | && unzip ${GODOT_RELEASE_FILENAME}.zip \ 17 | && mv ${GODOT_RELEASE_FILENAME} /usr/local/bin/godot \ 18 | && chmod +x /usr/local/bin/godot 19 | 20 | # Create Runtime User 21 | # RUN useradd -d . ruser 22 | 23 | COPY {build_file} . 24 | # Copy hathora_config only if it exists 25 | COPY hathora_config* /usr/local/bin/ 26 | CMD /usr/local/bin/godot --headless --main-pack ./{build_file} 27 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/latest_deployment_info.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends TextEdit 3 | 4 | var ee_counter = 0 5 | 6 | func _ready() -> void: 7 | %LatestDeploymentGetter.updated_deployment.connect(_on_updated_deployment) 8 | 9 | func _on_updated_deployment(data) -> void: 10 | if not "buildId" in data: 11 | text = "Latest deployment not found. It's probably the first time you are deploying this application." 12 | return 13 | text = "appId: " + data.appId + "\n" 14 | text += "createdAt: " + data.createdAt + "\n" 15 | text += "deploymentId: " + str(data.deploymentId) + "\n" 16 | text += "requestedCPU: " + str(data.requestedCPU) + "\n" 17 | text += "requestedMemoryMB: " + str(data.requestedMemoryMB) + "\n" 18 | text += "roomsPerProcess: " + str(data.roomsPerProcess) + "\n" 19 | text += "transport: " + str(data.defaultContainerPort.transportType) + "\n" 20 | text += "port: " + str(data.defaultContainerPort.port) + "\n" 21 | 22 | 23 | func _on_gui_input(event: InputEvent) -> void: 24 | if event.is_action_pressed("ui_accept"): 25 | ee_counter += 1 26 | print(ee_counter) 27 | if ee_counter >= 10: 28 | %ASCIIArt.show_art() 29 | ee_counter = 0 30 | -------------------------------------------------------------------------------- /addons/hathora/sdk/apis/discovery_v2.gd: -------------------------------------------------------------------------------- 1 | ## Service that allows clients to directly ping all Hathora regions to get latency information 2 | extends "endpoint.gd" 3 | 4 | ## Returns an array of all regions with a host and port that a client can directly ping. 5 | ## Open a websocket connection to [code]wss://:/ws[/code] and send a packet. To calculate ping, measure the time it takes to get an echo packet back. 6 | ## [br][br][br][b]If successful, calls to this endpoint return:[/b] 7 | ## [br][br] [Array] of dictionaries. Each element contains: 8 | ## [br]-- [code]port[/code]: [float] 9 | ## [br]-- [code]host[/code]: [String] 10 | ## [br]-- [code]name[/code]: [String] 11 | func get_ping_service_endpoints(): 12 | return GET("ping").then(func (result): 13 | if result.is_error(): 14 | return result 15 | # New regions may be added to the API, which are unknown to the Godot SDK 16 | # They get parsed as null, and we filter them out in this endpoint 17 | var data = result.get_data() 18 | var http_result = result.get_http_result() 19 | var data_unknown_regions_removed = data.filter(func(entry):return entry.region != null) 20 | return PolyResult.new(data_unknown_regions_removed, http_result) 21 | ) 22 | -------------------------------------------------------------------------------- /addons/hathora/plugin/apis/apps_v2.gd: -------------------------------------------------------------------------------- 1 | extends "endpoint.gd" 2 | 3 | 4 | func get_apps(org_id: String = ""): 5 | return GET("apps", empty_string_stripped({"orgId": org_id})).then(func (result): 6 | if result.is_error(): 7 | return result 8 | return result 9 | ) 10 | 11 | func create(auth_config: String, app_name: String, org_id: String = ""): 12 | return POST("apps", empty_string_stripped({ 13 | "authConfiguration": auth_config, 14 | "appName": app_name, 15 | "orgId": org_id})).then(func (result): 16 | if result.is_error(): 17 | return result 18 | return result 19 | ) 20 | 21 | func update(app_id: String, auth_config: String, app_name: String): 22 | return POST("apps/" + app_id, { 23 | "authConfiguration": auth_config, 24 | "appName": app_name}).then(func (result): 25 | if result.is_error(): 26 | return result 27 | return result 28 | ) 29 | 30 | func get_app(app_id: String): 31 | return GET("apps/" + app_id).then(func (result): 32 | if result.is_error(): 33 | return result 34 | return result 35 | ) 36 | 37 | func delete(app_id: String): 38 | return DELETE("apps/" + app_id).then(func (result): 39 | if result.is_error(): 40 | return result 41 | return result 42 | ) 43 | -------------------------------------------------------------------------------- /addons/hathora/sdk/apis/auth_v1.gd: -------------------------------------------------------------------------------- 1 | ## Operations that allow you to generate a Hathora-signed JSON web token (JWT) for player authentication. 2 | extends "endpoint.gd" 3 | ## Returns a unique player token for an anonymous user. 4 | ##[br][br]Example usage: 5 | ##[codeblock] 6 | ## var token := "" 7 | ## 8 | ## func player_login() -> void: 9 | ## var res = await HathoraSDK.auth_v1.login_anonymous().async() 10 | ## if res.is_error(): 11 | ## print("Login error: ", res.as_error().message) 12 | ## return 13 | ## token = res.get_data().token 14 | ##[/codeblock] 15 | func login_anonymous(): 16 | return POST("login/anonymous").then(func (result): 17 | if result.is_error(): 18 | return result 19 | return result 20 | ) 21 | 22 | ## Returns a unique player token with a specified nickname for a user. 23 | func login_nickname(nickname: String): 24 | return POST("login/nickname", { 25 | "nickname": nickname 26 | }).then(func (result): 27 | if result.is_error(): 28 | return result 29 | return result 30 | ) 31 | 32 | ## Returns a unique player token using a Google-signed OIDC idToken. 33 | func login_google(id_token: String): 34 | return POST("login/google", { 35 | "idToken": id_token 36 | }).then(func (result): 37 | if result.is_error(): 38 | return result 39 | return result 40 | ) 41 | -------------------------------------------------------------------------------- /addons/hathora/plugin/dotenv.gd: -------------------------------------------------------------------------------- 1 | extends Object 2 | 3 | const EXPECTED_KEYS = [ 4 | "HATHORA_DEVELOPER_TOKEN", 5 | ] 6 | const BASE_SECTION = "base" 7 | const CONFIG_PATH = "res://.hathora/config" 8 | const BUILDS_PATH = "res://.hathora/builds" 9 | const HathoraProjectSettings = preload("hathora_project_settings.gd") 10 | 11 | static func config(): 12 | HathoraProjectSettings.add_project_settings() 13 | var config = ConfigFile.new() 14 | var err = config.load(CONFIG_PATH) 15 | 16 | if err: 17 | print("[HATHORA] Hathora config not found at " + CONFIG_PATH+". Creating a new one.") 18 | 19 | var dir_err = DirAccess.make_dir_recursive_absolute(BUILDS_PATH) 20 | if dir_err: 21 | print("[HATHORA] Error creating " + BUILDS_PATH + " directory. Could not create Hathora config file.") 22 | return 23 | config.save(CONFIG_PATH) 24 | return 25 | 26 | static func add(key, value): 27 | var config = ConfigFile.new() 28 | var err = config.load(CONFIG_PATH) 29 | 30 | if err != OK: 31 | print("[HATHORA] Error loading the config file") 32 | 33 | config.set_value(BASE_SECTION, key, value) 34 | err = config.save(CONFIG_PATH) 35 | 36 | if err != OK: 37 | print("[HATHORA] Error saving the config file") 38 | 39 | config() 40 | 41 | static func get_k(key) -> String: 42 | var config = ConfigFile.new() 43 | var err = config.load(CONFIG_PATH) 44 | var v = config.get_value(BASE_SECTION, key, "") 45 | return v 46 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/latest_deployment_getter.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Node 3 | 4 | signal updated_deployment(data) 5 | 6 | const _Builds = preload("res://addons/hathora/plugin/apis/builds_v3.gd") 7 | const _Deployments = preload("res://addons/hathora/plugin/apis/deployments_v3.gd") 8 | const _Client = preload("res://addons/hathora/plugin/rest-client/client.gd") 9 | const HathoraProjectSettings = preload("res://addons/hathora/plugin/hathora_project_settings.gd") 10 | const DotEnv = preload("res://addons/hathora/plugin/dotenv.gd") 11 | 12 | @onready var sdk = %SDK 13 | var last_created_build_id: String 14 | 15 | func get_latest_deployment() -> Dictionary: 16 | sdk.set_dev_token(DotEnv.get_k("HATHORA_DEVELOPER_TOKEN")) 17 | 18 | 19 | if HathoraProjectSettings.get_s("application_id").is_empty(): 20 | return {} 21 | %LatestDeploymentTextEdit.text = "Getting latest deployment information..." 22 | var res = await sdk.deployments_v3.get_deployments(HathoraProjectSettings.get_s("application_id")).async() 23 | 24 | if res.is_error(): 25 | print("[HATHORA] Error getting the latest deployment") 26 | print(res.as_error()) 27 | if res.as_error().error == 401: 28 | owner.reset_token() 29 | return {} 30 | res = res.get_data() 31 | if len(res.deployments) == 0: 32 | %LatestDeploymentTextEdit.text = "No deployments found" 33 | return {} 34 | updated_deployment.emit(res.deployments[0]) 35 | return res.deployments[0] 36 | -------------------------------------------------------------------------------- /addons/hathora/sdk/hathora_project_settings.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends RefCounted 3 | const BASE = "hathora" 4 | 5 | static func _add_project_setting(name: String, type: int, default, hint = null, hint_string = null) -> void: 6 | if ProjectSettings.get_setting(name, "").is_empty(): 7 | ProjectSettings.set_setting(name, default) 8 | 9 | ProjectSettings.set_initial_value(name, default) 10 | ProjectSettings.set_as_basic(name, true) 11 | 12 | var info := { 13 | name = name, 14 | type = type, 15 | } 16 | if hint != null: 17 | info['hint'] = hint 18 | if hint_string != null: 19 | info['hint_string'] = hint_string 20 | 21 | ProjectSettings.add_property_info(info) 22 | 23 | static func _erase_project_setting(name: String, type: int, default, hint = null, hint_string = null) -> void: 24 | if ProjectSettings.has_setting(name): 25 | ProjectSettings.set_setting(name, default) 26 | 27 | static func add_project_settings() -> void: 28 | _add_project_setting('hathora/application_id', TYPE_STRING, "") 29 | 30 | static func erase_project_settings() -> void: 31 | _erase_project_setting('hathora/application_id', TYPE_STRING, "") 32 | 33 | static func get_s(key, def=""): 34 | var fk = "%s/%s" % [BASE, key] 35 | return ProjectSettings.get_setting(fk) if ProjectSettings.has_setting(fk) else def 36 | 37 | static func set_s(key, value, save=true): 38 | var fk = "%s/%s" % [BASE, key] 39 | ProjectSettings.set_setting(fk, value) 40 | if save: 41 | ProjectSettings.save() 42 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/section_toggle.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Button 3 | 4 | @export var node_to_show: Control 5 | 6 | func _ready() -> void: 7 | toggle_mode = true 8 | icon = get_theme_icon("CodeFoldedRightArrow", "EditorIcons") 9 | toggled.connect(_on_button_toggled) 10 | node_to_show.visible = button_pressed 11 | add_theme_stylebox_override("normal", get_theme_stylebox("normal", "InspectorActionButton")) 12 | add_theme_stylebox_override("hover", get_theme_stylebox("hover", "InspectorActionButton")) 13 | add_theme_stylebox_override("pressed", get_theme_stylebox("hover", "InspectorActionButton")) 14 | add_theme_stylebox_override("disabled", get_theme_stylebox("disabled", "InspectorActionButton")) 15 | add_theme_font_override("font", get_theme_font("bold", "EditorFonts")) 16 | add_theme_color_override("font_color", get_theme_color("font_color", "Editor")) 17 | add_theme_color_override("font_focus_color", get_theme_color("font_color", "Editor")) 18 | add_theme_color_override("font_pressed_color", get_theme_color("font_color", "Editor")) 19 | add_theme_color_override("font_hover_pressed_color", get_theme_color("font_color", "Editor")) 20 | add_theme_color_override("icon_pressed_color", get_theme_color("font_color", "Editor")) 21 | 22 | func _on_button_toggled(toggled_on: bool) -> void: 23 | node_to_show.visible = toggled_on 24 | if toggled_on: 25 | icon = get_theme_icon("CodeFoldDownArrow", "EditorIcons") 26 | else: 27 | icon = get_theme_icon("CodeFoldedRightArrow", "EditorIcons") 28 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/control_ui.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Node 3 | 4 | const DotEnv = preload("../dotenv.gd") 5 | const HathoraProjectSettings = preload("../hathora_project_settings.gd") 6 | const Auth0Client = preload("../auth0/auth0Client.gd") 7 | 8 | var auth0Client: Auth0Client 9 | 10 | func _ready(): 11 | auth0Client = Auth0Client.new() 12 | add_child(auth0Client) 13 | %LoginContent.visible = should_show_login_content() 14 | %MainContentPanel.visible = !should_show_login_content() 15 | %MainContentPanel.add_theme_stylebox_override("panel", %MainContentPanel.get_theme_stylebox("panel", "EditorValidationPanel")) 16 | 17 | func should_show_login_content() -> bool: 18 | return DotEnv.get_k("HATHORA_DEVELOPER_TOKEN").is_empty() 19 | 20 | 21 | func _on_login_button_pressed(): 22 | auth0Client.get_token_async(_login_complete_callback) 23 | 24 | 25 | func _login_complete_callback(success: bool): 26 | %LoginContent.visible = should_show_login_content() 27 | %MainContentPanel.visible = !should_show_login_content() 28 | %DeveloperSettings.dev_token = DotEnv.get_k("HATHORA_DEVELOPER_TOKEN") 29 | %DeveloperSettings.refresh_applications() 30 | 31 | # We call this function whenver the token becomes invalid 32 | func reset_token() -> void: 33 | %DeveloperSettings.dev_token = "" 34 | print("[HATHORA] Invalid developer token, please press the login button or paste a valid token in the Developer Settings") 35 | %DeveloperSectionToggle.button_pressed = true 36 | %DeveloperSettings.dev_token_n.grab_focus() 37 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/modules/project_exporter.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Node 3 | 4 | ## Exports the project using a given filename, output path, and preset 5 | 6 | const EXPORT_PRESETS_PATH = 'res://export_presets.cfg' 7 | 8 | @export var log_text: RichTextLabel 9 | 10 | func export(p_build_name: String, p_output_path: String, p_export_preset: String) -> bool: 11 | print("[HATHORA] Exporting...this can take a few minutes") 12 | 13 | await get_tree().process_frame 14 | 15 | var absolute_export_path = ProjectSettings.globalize_path(p_output_path.path_join(p_build_name)) 16 | 17 | var args = [ 18 | '--headless', 19 | '--export-pack', 20 | p_export_preset, 21 | absolute_export_path, 22 | ] 23 | 24 | var output = [] 25 | var exit_code = OS.execute(OS.get_executable_path(), args, output, true) 26 | 27 | if exit_code != 0 or _is_dir_empty(p_output_path): 28 | push_error("[HATHORA] Error exporting project:" + "\n".join(output)) 29 | return false 30 | 31 | print_rich("[color=%s][HATHORA] Exported the project to [url=%s]%s[/url][/color]" % [owner.get_theme_color("success_color", "Editor").to_html(), absolute_export_path.get_base_dir(), absolute_export_path]) 32 | return true 33 | 34 | 35 | static func _is_dir_empty(p_path: String) -> bool: 36 | var count := 0 37 | 38 | var dir := DirAccess.open(p_path) 39 | if not dir: 40 | return true 41 | 42 | dir.list_dir_begin() 43 | var fn = dir.get_next() 44 | while fn != '': 45 | if fn != '.' and fn != '..': 46 | count += 1 47 | fn = dir.get_next() 48 | dir.list_dir_end() 49 | 50 | return count == 0 51 | -------------------------------------------------------------------------------- /addons/hathora/plugin/apis/builds_v3.gd: -------------------------------------------------------------------------------- 1 | extends "endpoint.gd" 2 | 3 | 4 | # Fetch all builds with optional orgId 5 | func get_builds(org_id: String = ""): 6 | return GET("builds", empty_string_stripped({"orgId": org_id})).then(func (result): 7 | if result.is_error(): 8 | return result 9 | return result 10 | ) 11 | 12 | ## Creates a new build with optional multipartUploadUrls that can be used to upload larger builds in parts before calling runBuild. 13 | ## Responds with a buildId that you must pass to RunBuild() to build the game server artifact. 14 | ## You can optionally pass in a buildTag to associate an external version with a build. 15 | func create(build_size_in_bytes: int, build_id: String = "", build_tag: String = "", org_id: String = ""): 16 | return POST("builds", empty_string_stripped({ 17 | "orgId": org_id, 18 | "buildId": build_id, 19 | "buildTag": build_tag, 20 | "buildSizeInBytes": build_size_in_bytes})).then(func (result): 21 | if result.is_error(): 22 | return result 23 | return result 24 | ) 25 | 26 | # Fetch a specific build by buildId with optional orgId 27 | func get_build(build_id: String, org_id: String = ""): 28 | return GET("builds/" + build_id, empty_string_stripped({"orgId": org_id})).then(func (result): 29 | if result.is_error(): 30 | return result 31 | return result 32 | ) 33 | 34 | # Delete a build by buildId with optional orgId 35 | func delete(build_id: String, org_id: String = ""): 36 | return DELETE("builds/" + build_id, empty_string_stripped({"orgId": org_id})).then(func (result): 37 | if result.is_error(): 38 | return result 39 | return result 40 | ) 41 | 42 | # Run a specific build by buildId with optional orgId 43 | func run_build(build_id: String, org_id: String = ""): 44 | return POST("builds/" + build_id + "/run", empty_string_stripped({ 45 | "orgId": org_id}), {}, {}, true).then(func (result): 46 | if result.is_error(): 47 | return result 48 | return result 49 | ) 50 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/header_buttons.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends HBoxContainer 3 | 4 | func _ready() -> void: 5 | $Console.icon = get_theme_icon("Tools", "EditorIcons") 6 | $Docs.icon = get_theme_icon("Help", "EditorIcons") 7 | $Discord.icon = get_theme_icon("ExternalLink", "EditorIcons") 8 | $Docs.get_popup().set_item_icon(0, get_theme_icon("ExternalLink", "EditorIcons")) 9 | $Docs.get_popup().set_item_icon(1, get_theme_icon("Help", "EditorIcons")) 10 | $Docs.get_popup().set_item_disabled(1, 11 | not EditorInterface.get_script_editor().has_method("goto_help") 12 | or not FileAccess.file_exists("res://addons/hathora/sdk/client.gd")) 13 | if not EditorInterface.get_script_editor().has_method("goto_help"): 14 | $Docs.get_popup().set_item_tooltip(1, "Only available in Godot 4.3+") 15 | $Docs.get_popup().index_pressed.connect(_on_docs_popup_index_pressed) 16 | 17 | func _on_console_pressed() -> void: 18 | OS.shell_open("https://console.hathora.dev/") 19 | 20 | func _on_tutorial_pressed() -> void: 21 | OS.shell_open("https://hathora.dev/docs/engines/godot") 22 | 23 | func _on_docs_popup_index_pressed(index: int) -> void: 24 | match index: 25 | 0: 26 | OS.shell_open("https://github.com/hathora/hathora-godot-plugin") 27 | 1: 28 | # See https://github.com/godotengine/godot/issues/72406 for why we are doing this 29 | var script_paths = [ 30 | "res://addons/hathora/sdk/client.gd", 31 | "res://addons/hathora/sdk/apis/auth_v1.gd", 32 | "res://addons/hathora/sdk/apis/discovery_v2.gd", 33 | "res://addons/hathora/sdk/apis/lobby_v3.gd", 34 | "res://addons/hathora/sdk/apis/processes_v3.gd", 35 | "res://addons/hathora/sdk/apis/room_v2.gd"] 36 | for path in script_paths: 37 | var script = load(path) 38 | 39 | # Make a small change and save it to refresh the documentation 40 | if script: 41 | ResourceSaver.save(script, path) 42 | 43 | EditorInterface.get_script_editor().goto_help("class_name:HathoraSDK") 44 | 45 | 46 | func _on_discord_pressed() -> void: 47 | OS.shell_open("https://discord.com/invite/hathora") 48 | -------------------------------------------------------------------------------- /addons/hathora/plugin/client.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Node 3 | 4 | const _Builds = preload("res://addons/hathora/plugin/apis/builds_v3.gd") 5 | const _Deployments = preload("res://addons/hathora/plugin/apis/deployments_v3.gd") 6 | const _Apps = preload("res://addons/hathora/plugin/apis/apps_v2.gd") 7 | const _Room = preload("res://addons/hathora/plugin/apis/room_v2.gd") 8 | const _Client = preload("res://addons/hathora/plugin/rest-client/client.gd") 9 | const _HathoraProjectSettings = preload("res://addons/hathora/plugin/hathora_project_settings.gd") 10 | const _DotEnv = preload("res://addons/hathora/plugin/dotenv.gd") 11 | 12 | 13 | var builds_v3: _Builds 14 | var room_v2: _Room 15 | var deployments_v3: _Deployments 16 | var apps_v2: _Apps 17 | var client : _Client 18 | 19 | var _dev_client : _Client 20 | 21 | func _init(): 22 | process_mode = Node.PROCESS_MODE_ALWAYS 23 | _DotEnv.config() 24 | var node = self 25 | var url = "https://api.hathora.dev" 26 | var app_id = _HathoraProjectSettings.get_s("application_id") 27 | var tls_options = null 28 | 29 | # Dev endpoints 30 | _dev_client = _Client.new(node, url, {}, tls_options) 31 | deployments_v3 = _Deployments.new(_dev_client, "/deployments/v3/apps") 32 | builds_v3 = _Builds.new(_dev_client, "/builds/v3/") 33 | apps_v2 = _Apps.new(_dev_client, "/apps/v2/") 34 | room_v2 = _Room.new(_dev_client, "/rooms/v2/".path_join(app_id)) 35 | # Setting the dev token if found in DotEnv 36 | if not _DotEnv.get_k("HATHORA_DEVELOPER_TOKEN").is_empty(): 37 | set_dev_token(_DotEnv.get_k("HATHORA_DEVELOPER_TOKEN")) 38 | # Player endpoints 39 | 40 | ## Set a [param dev_token]. Not recommended, specify the devToken at [code]res://.hathora/config[/code] or at [code]res://hathora_config[/code] instead. 41 | ## [br][br][b]Warning:[/b] the devToken gives privileged access to your Hathora account. Never include the devToken in client builds or in your versioning system. 42 | func set_dev_token(dev_token: String) -> void: 43 | _dev_client.set_header("Authorization", "Bearer " + dev_token) 44 | 45 | func set_app_id(app_id: String) -> void: 46 | room_v2 = _Room.new(_dev_client, "/rooms/v2/".path_join(app_id)) 47 | -------------------------------------------------------------------------------- /addons/hathora/plugin/enums.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | 3 | enum Region { 4 | SEATTLE, 5 | WASHINGTON_DC, 6 | CHICAGO, 7 | LONDON, 8 | FRANKFURT, 9 | MUMBAI, 10 | SINGAPORE, 11 | SYDNEY, 12 | TOKYO, 13 | SAO_PAULO, 14 | LOS_ANGELES, 15 | DALLAS, ##Howdy, Godot developer 16 | } 17 | 18 | enum TransportType { ## Transport type specifies the underlying communication protocol to the exposed port. 19 | UDP, 20 | TCP, 21 | TLS } 22 | 23 | enum Visibility { ## Types of lobbies a player can create. 24 | PRIVATE, ## The player who created the room must share the roomId with their friends 25 | PUBLIC, ## Visible in the public lobby list, anyone can join 26 | LOCAL, ## For testing with a server running locally 27 | } 28 | 29 | enum ProcessStatus { STARTING, RUNNING, DRAINING, STOPPING, STOPPED, FAILED } 30 | 31 | enum RoomStatus { ## The allocation status of a room. 32 | SCHEDULING, 33 | ACTIVE, 34 | SUSPENDED, 35 | DESTROYED } 36 | 37 | const ROOM_STATUSES = { 38 | RoomStatus.SCHEDULING: "scheduling", 39 | RoomStatus.ACTIVE: "active", 40 | RoomStatus.SUSPENDED: "suspended", 41 | RoomStatus.DESTROYED: "destroyed" 42 | } 43 | 44 | const PROCESS_STATUSES = { 45 | ProcessStatus.STARTING: "starting", 46 | ProcessStatus.RUNNING: "running", 47 | ProcessStatus.DRAINING: "draining", 48 | ProcessStatus.STOPPING: "stopping", 49 | ProcessStatus.STOPPED: "stopped", 50 | ProcessStatus.FAILED: "failed", 51 | } 52 | 53 | const REGION_NAMES = { 54 | Region.SEATTLE: "Seattle", 55 | Region.WASHINGTON_DC: "Washington_DC", 56 | Region.CHICAGO: "Chicago", 57 | Region.LONDON: "London", 58 | Region.FRANKFURT: "Frankfurt", 59 | Region.MUMBAI: "Mumbai", 60 | Region.SINGAPORE: "Singapore", 61 | Region.SYDNEY: "Sydney", 62 | Region.TOKYO: "Tokyo", 63 | Region.SAO_PAULO: "Sao_Paulo", 64 | Region.LOS_ANGELES: "Los_Angeles", 65 | Region.DALLAS: "Dallas" 66 | } 67 | 68 | const TRANSPORT_TYPES = { 69 | TransportType.UDP: "udp", 70 | TransportType.TCP: "tcp", 71 | TransportType.TLS: "tls", 72 | } 73 | 74 | const VISIBILITY = { 75 | Visibility.PRIVATE: "private", 76 | Visibility.PUBLIC: "public", 77 | Visibility.LOCAL: "local", 78 | } 79 | -------------------------------------------------------------------------------- /addons/hathora/sdk/enums.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | 3 | class_name Hathora 4 | 5 | enum Region { 6 | SEATTLE, 7 | WASHINGTON_DC, 8 | CHICAGO, 9 | LONDON, 10 | FRANKFURT, 11 | MUMBAI, 12 | SINGAPORE, 13 | SYDNEY, 14 | TOKYO, 15 | SAO_PAULO, 16 | LOS_ANGELES, 17 | DALLAS, ##Howdy, Godot developer 18 | } 19 | 20 | enum TransportType { ## Transport type specifies the underlying communication protocol to the exposed port. 21 | UDP, 22 | TCP, 23 | TLS } 24 | 25 | enum Visibility { ## Types of lobbies a player can create. 26 | PRIVATE, ## The player who created the room must share the roomId with their friends 27 | PUBLIC, ## Visible in the public lobby list, anyone can join 28 | LOCAL, ## For testing with a server running locally 29 | } 30 | 31 | enum ProcessStatus { STARTING, RUNNING, DRAINING, STOPPING, STOPPED, FAILED } 32 | 33 | enum RoomStatus { ## The allocation status of a room. 34 | SCHEDULING, 35 | ACTIVE, 36 | SUSPENDED, 37 | DESTROYED } 38 | 39 | const ROOM_STATUSES = { 40 | RoomStatus.SCHEDULING: "scheduling", 41 | RoomStatus.ACTIVE: "active", 42 | RoomStatus.SUSPENDED: "suspended", 43 | RoomStatus.DESTROYED: "destroyed" 44 | } 45 | 46 | const PROCESS_STATUSES = { 47 | ProcessStatus.STARTING: "starting", 48 | ProcessStatus.RUNNING: "running", 49 | ProcessStatus.DRAINING: "draining", 50 | ProcessStatus.STOPPING: "stopping", 51 | ProcessStatus.STOPPED: "stopped", 52 | ProcessStatus.FAILED: "failed", 53 | } 54 | 55 | const REGION_NAMES = { 56 | Region.SEATTLE: "Seattle", 57 | Region.WASHINGTON_DC: "Washington_DC", 58 | Region.CHICAGO: "Chicago", 59 | Region.LONDON: "London", 60 | Region.FRANKFURT: "Frankfurt", 61 | Region.MUMBAI: "Mumbai", 62 | Region.SINGAPORE: "Singapore", 63 | Region.SYDNEY: "Sydney", 64 | Region.TOKYO: "Tokyo", 65 | Region.SAO_PAULO: "Sao_Paulo", 66 | Region.LOS_ANGELES: "Los_Angeles", 67 | Region.DALLAS: "Dallas" 68 | } 69 | 70 | const TRANSPORT_TYPES = { 71 | TransportType.UDP: "udp", 72 | TransportType.TCP: "tcp", 73 | TransportType.TLS: "tls", 74 | } 75 | 76 | const VISIBILITY = { 77 | Visibility.PRIVATE: "private", 78 | Visibility.PUBLIC: "public", 79 | Visibility.LOCAL: "local", 80 | } 81 | -------------------------------------------------------------------------------- /addons/hathora/plugin/hathora_project_settings.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends RefCounted 3 | const BASE = "hathora" 4 | 5 | static func _add_project_setting(name: String, type: int, default, hint = null, hint_string = null) -> void: 6 | if ProjectSettings.get_setting(name, "").is_empty(): 7 | ProjectSettings.set_setting(name, default) 8 | 9 | ProjectSettings.set_initial_value(name, default) 10 | ProjectSettings.set_as_basic(name, true) 11 | 12 | var info := { 13 | name = name, 14 | type = type, 15 | } 16 | if hint != null: 17 | info['hint'] = hint 18 | if hint_string != null: 19 | info['hint_string'] = hint_string 20 | 21 | ProjectSettings.add_property_info(info) 22 | 23 | static func _erase_project_setting(name: String, type: int, default, hint = null, hint_string = null) -> void: 24 | if ProjectSettings.has_setting(name): 25 | ProjectSettings.set_setting(name, default) 26 | 27 | static func add_project_settings() -> void: 28 | _add_project_setting('hathora/application_id', TYPE_STRING, "") 29 | _add_project_setting('hathora/path_to_tar_file', TYPE_STRING, "res://.hathora/builds/game_server.tgz", PROPERTY_HINT_FILE, "*.tar.tgz,*.tgz,*.tar.gz") 30 | _add_project_setting('hathora/build_directory_path', TYPE_STRING, "res://.hathora/builds", PROPERTY_HINT_DIR) 31 | _add_project_setting('hathora/build_filename', TYPE_STRING, "game_server.pck") 32 | 33 | static func erase_project_settings() -> void: 34 | _erase_project_setting('hathora/application_id', TYPE_STRING, "") 35 | _erase_project_setting('hathora/path_to_tar_file', TYPE_STRING, "") 36 | _erase_project_setting('hathora/build_directory_path', TYPE_STRING, "") 37 | _erase_project_setting('hathora/build_filename', TYPE_STRING, "") 38 | 39 | static func get_s(key, def=""): 40 | var fk = "%s/%s" % [BASE, key] 41 | return ProjectSettings.get_setting(fk) if ProjectSettings.has_setting(fk) else def 42 | 43 | static func set_s(key, value, save=true): 44 | var fk = "%s/%s" % [BASE, key] 45 | ProjectSettings.set_setting(fk, value) 46 | if save: 47 | ProjectSettings.save() 48 | 49 | static func set_m(key, value): 50 | var es = EditorInterface.get_editor_settings() 51 | es.set_project_metadata("hathora", key, value) 52 | -------------------------------------------------------------------------------- /addons/hathora/sdk/dotenv.gd: -------------------------------------------------------------------------------- 1 | extends Object 2 | 3 | const EXPECTED_KEYS = [ 4 | "HATHORA_DEVELOPER_TOKEN", 5 | ] 6 | const BASE_SECTION = "base" 7 | const CONFIG_PATH_EDITOR = "res://.hathora/config" 8 | 9 | static func get_first_existing_file(paths: Array[String]) -> String: 10 | for p in paths: 11 | if FileAccess.file_exists(p): 12 | return p 13 | return "" 14 | 15 | static func config(): 16 | var config_path_server_build = OS.get_executable_path().get_base_dir().path_join("hathora_config") 17 | var config_path = get_first_existing_file([CONFIG_PATH_EDITOR, config_path_server_build]) 18 | 19 | if config_path.is_empty(): 20 | print("[HATHORA] Hathora config not found " + CONFIG_PATH_EDITOR + " or at " + config_path_server_build + ". You will not have access to API endpoints requiring a devToken.") 21 | if not Engine.is_editor_hint(): 22 | return 23 | print("Creating a new config at " + CONFIG_PATH_EDITOR) 24 | DirAccess.make_dir_absolute("res://.hathora") 25 | var config = ConfigFile.new() 26 | var err = config.save(CONFIG_PATH_EDITOR) 27 | if err: 28 | print("[HATHORA] Error creating new config file at " + CONFIG_PATH_EDITOR) 29 | return 30 | 31 | config_path = get_first_existing_file([CONFIG_PATH_EDITOR, config_path_server_build]) 32 | var config = ConfigFile.new() 33 | var err = config.load(config_path) 34 | 35 | if err: 36 | print("[HATHORA] Error loading config file") 37 | return 38 | 39 | print("[HATHORA] Found Hathora config file at " + config_path) 40 | 41 | static func add(key, value): 42 | var config_path_server_build = OS.get_executable_path().get_base_dir().path_join("hathora_config") 43 | var config_path = get_first_existing_file([CONFIG_PATH_EDITOR, config_path_server_build]) 44 | var config = ConfigFile.new() 45 | var err = config.load(config_path) 46 | config.set_value(BASE_SECTION, key, value) 47 | config.save(config_path) 48 | 49 | static func get_k(key) -> String: 50 | var config_path_server_build = OS.get_executable_path().get_base_dir().path_join("hathora_config") 51 | var config_path = get_first_existing_file([CONFIG_PATH_EDITOR, config_path_server_build]) 52 | var config = ConfigFile.new() 53 | var err = config.load(config_path) 54 | var v = config.get_value(BASE_SECTION, key, "") 55 | return v 56 | -------------------------------------------------------------------------------- /addons/hathora/plugin/rest-client/client_async_result.gd: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-present W4 Games Limited. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | ## The result of an async HTTP request. 14 | extends "client_result.gd" 15 | 16 | ## Emitted when the request is complete. 17 | signal completed 18 | 19 | ## The pending HTTP request. 20 | var pending_request = null 21 | 22 | func _init(request: HTTPRequest): 23 | if request != null: 24 | if not request.is_inside_tree(): 25 | request.queue_free() 26 | _error.call_deferred() 27 | else: 28 | pending_request = request 29 | request.request_completed.connect(self._parse_result, Node.CONNECT_ONE_SHOT) 30 | else: 31 | _error.call_deferred() 32 | 33 | func _error(): 34 | if result_status == ResultStatus.CANCELLED: 35 | return 36 | result_status = ResultStatus.ERROR 37 | _done() 38 | 39 | 40 | func _done(): 41 | pending_request = null 42 | completed.emit() 43 | 44 | 45 | func _parse_result( 46 | p_result: int, p_status: int, p_headers: PackedStringArray, p_body: PackedByteArray 47 | ): 48 | http_request_result = p_result 49 | http_status_code = p_status 50 | headers = p_headers 51 | body = p_body 52 | if http_request_result != HTTPRequest.RESULT_SUCCESS and http_request_result != HTTPRequest.RESULT_REDIRECT_LIMIT_REACHED: 53 | result_status = ResultStatus.ERROR 54 | else: 55 | result_status = ResultStatus.DONE 56 | pending_request.queue_free() 57 | _done() 58 | 59 | 60 | ## Cancels the pending request. 61 | func cancel() -> void: 62 | if not is_pending(): 63 | return 64 | 65 | if pending_request != null: 66 | pending_request.cancel_request() 67 | pending_request.request_completed.disconnect(self._parse_result) 68 | pending_request.queue_free() 69 | pending_request = null 70 | 71 | result_status = ResultStatus.CANCELLED 72 | _done() 73 | -------------------------------------------------------------------------------- /addons/hathora/sdk/rest-client/client_async_result.gd: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-present W4 Games Limited. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | ## The result of an async HTTP request. 14 | extends "client_result.gd" 15 | 16 | ## Emitted when the request is complete. 17 | signal completed 18 | 19 | ## The pending HTTP request. 20 | var pending_request = null 21 | 22 | func _init(request: HTTPRequest): 23 | if request != null: 24 | if not request.is_inside_tree(): 25 | request.queue_free() 26 | _error.call_deferred() 27 | else: 28 | pending_request = request 29 | request.request_completed.connect(self._parse_result, Node.CONNECT_ONE_SHOT) 30 | else: 31 | _error.call_deferred() 32 | 33 | func _error(): 34 | if result_status == ResultStatus.CANCELLED: 35 | return 36 | result_status = ResultStatus.ERROR 37 | _done() 38 | 39 | 40 | func _done(): 41 | pending_request = null 42 | completed.emit() 43 | 44 | 45 | func _parse_result( 46 | p_result: int, p_status: int, p_headers: PackedStringArray, p_body: PackedByteArray 47 | ): 48 | http_request_result = p_result 49 | http_status_code = p_status 50 | headers = p_headers 51 | body = p_body 52 | if http_request_result != HTTPRequest.RESULT_SUCCESS and http_request_result != HTTPRequest.RESULT_REDIRECT_LIMIT_REACHED: 53 | result_status = ResultStatus.ERROR 54 | else: 55 | result_status = ResultStatus.DONE 56 | pending_request.queue_free() 57 | _done() 58 | 59 | 60 | ## Cancels the pending request. 61 | func cancel() -> void: 62 | if not is_pending(): 63 | return 64 | 65 | if pending_request != null: 66 | pending_request.cancel_request() 67 | pending_request.request_completed.disconnect(self._parse_result) 68 | pending_request.queue_free() 69 | pending_request = null 70 | 71 | result_status = ResultStatus.CANCELLED 72 | _done() 73 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/modules/dockerfile_maker.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Node 3 | ## Takes a Dockerfile template txt file, inserts ENV variables and saves to disk 4 | 5 | func write_dockerfile(build_filename: String, path: String, overwrite: bool = false, debug: bool = false): 6 | # The Dockerfile template txt file 7 | var docker_template_file = FileAccess.open("res://addons/hathora/plugin/dockerfile_template.txt", FileAccess.READ) 8 | 9 | if not docker_template_file: 10 | var error = FileAccess.get_open_error() 11 | push_error("[HATHORA] Error opening Dockerfile template: "+error_string(error)) 12 | return false 13 | 14 | var docker_template_content = docker_template_file.get_as_text() 15 | var godot_version = str(Engine.get_version_info().major) + "." + str(Engine.get_version_info().minor) + "." + str(Engine.get_version_info().patch) 16 | 17 | var custom_dockerfile = docker_template_content.format({ 18 | "godot_release_url": get_godot_release_url(), 19 | "godot_release_filename": get_godot_release_filename(), 20 | "build_file": build_filename}) 21 | 22 | if FileAccess.file_exists(path) and not overwrite: 23 | print("[HATHORA] Dockerfile found, will not overwrite") 24 | return true 25 | 26 | var file = FileAccess.open(path, FileAccess.WRITE) 27 | 28 | if file == null: 29 | push_error("[HATHORA] Error creating Dockerfile: " + error_string(FileAccess.get_open_error())) 30 | return false 31 | 32 | file.store_string(custom_dockerfile) 33 | file.close() 34 | var absolute_path = ProjectSettings.globalize_path(path) 35 | print_rich("[color=%s][HATHORA] Dockerfile generated at [url=%s]%s[/url]" % [owner.get_theme_color("success_color", "Editor").to_html(), absolute_path, absolute_path]) 36 | 37 | return true 38 | 39 | # 4.2.1-stable 40 | func get_godot_release() -> String: 41 | var v_info = Engine.get_version_info() 42 | var str: String = "{major}.{minor}{patch}-{status}".format( 43 | { 44 | "major" = v_info.major, 45 | "minor" = v_info.minor, 46 | "patch" = "." + str(v_info.patch) if v_info.patch > 0 else "", 47 | "status" = v_info.status} 48 | ) 49 | return str 50 | 51 | # Godot_v4.2.1-stable_linux.x86_64 52 | func get_godot_release_filename() -> String: 53 | var v_info = Engine.get_version_info() 54 | var str: String = "Godot_v{godot_release}_linux.x86_64".format( 55 | { 56 | "godot_release" = get_godot_release(), 57 | }) 58 | return str 59 | 60 | # https://github.com/godotengine/godot-builds/releases/download/4.3-dev5/Godot_v4.3-dev5_linux.x86_64.zip 61 | func get_godot_release_url() -> String: 62 | var v_info = Engine.get_version_info() 63 | var str = "https://github.com/godotengine/godot-builds/releases/download/{godot_release}/{godot_release_filename}.zip".format( 64 | { 65 | "godot_release" = get_godot_release(), 66 | "godot_release_filename" = get_godot_release_filename(), 67 | }) 68 | return str 69 | -------------------------------------------------------------------------------- /addons/hathora/sdk/rest-client/client_request.gd: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-present W4 Games Limited. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | ## A promise that is resolved by making an HTTP request. 14 | extends "client_promise.gd" 15 | 16 | const Result = preload("client_result.gd") 17 | const AsyncResult = preload("client_async_result.gd") 18 | const BlockingResult = preload("client_blocking_result.gd") 19 | 20 | ## The [TLSOptions] for the HTTP request. 21 | var tls_options : TLSOptions = null 22 | ## The request method. 23 | var request_method : int = HTTPClient.METHOD_GET 24 | ## The request headers. 25 | var request_headers : PackedStringArray 26 | ## The request body. 27 | var request_body : PackedByteArray 28 | ## The request URL. 29 | var request_url : String 30 | ## The request path. 31 | var request_path : String 32 | ## A node in the scene tree that we can add an [HTTPRequest] to. 33 | var node : Node 34 | 35 | 36 | func _init(): 37 | _run_async = request_async 38 | _run_blocking = request_blocking 39 | 40 | ## Callback used to execute a blocking HTTP request. 41 | func request_blocking(poll_delay_usec, fail: Callable): 42 | assert(result == null) 43 | result = BlockingResult.new() 44 | result.poll_delay_usec = poll_delay_usec 45 | var err = result.connect_to_url(request_url, tls_options) 46 | if err != OK: 47 | return result 48 | result.make_request(request_path, request_headers, request_method, request_body) 49 | if result.is_http_error(): 50 | fail.call() 51 | return result 52 | 53 | 54 | ## Callback used to execute an asynchronous HTTP request. 55 | func request_async(fail: Callable): 56 | assert(result == null) 57 | var req = HTTPRequest.new() 58 | req.max_redirects = 0 59 | if tls_options != null: 60 | req.set_tls_options(tls_options) 61 | if OS.get_name() != "Web": 62 | req.use_threads = true 63 | self.node.add_child(req) 64 | var err = req.request_raw(request_url + request_path, request_headers, request_method, request_body) 65 | result = AsyncResult.new(req if err == OK else null) 66 | var id = result.get_instance_id() 67 | # The object meta was used to prevent the Result object from going out-of-scope resulting in 68 | # endless awaits under certain conditions in previous versions of Godot. 69 | # TODO: Evaluate if this is still needed. 70 | var keep = ("_keep_%d" % id).replace("-", "N") 71 | node.set_meta(keep, result) 72 | result.completed.connect(func (): node.remove_meta(keep), CONNECT_DEFERRED) 73 | await result.completed 74 | if result.is_http_error(): 75 | fail.call() 76 | return result 77 | -------------------------------------------------------------------------------- /addons/hathora/plugin/rest-client/client_request.gd: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-present W4 Games Limited. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | ## A promise that is resolved by making an HTTP request. 14 | extends "client_promise.gd" 15 | 16 | const Result = preload("client_result.gd") 17 | const AsyncResult = preload("client_async_result.gd") 18 | const BlockingResult = preload("client_blocking_result.gd") 19 | 20 | ## The [TLSOptions] for the HTTP request. 21 | var tls_options : TLSOptions = null 22 | ## The request method. 23 | var request_method : int = HTTPClient.METHOD_GET 24 | ## The request headers. 25 | var request_headers : PackedStringArray 26 | ## The request body. 27 | var request_body : PackedByteArray 28 | ## The request URL. 29 | var request_url : String 30 | ## The request path. 31 | var request_path : String 32 | ## A node in the scene tree that we can add an [HTTPRequest] to. 33 | var node : Node 34 | 35 | 36 | func _init(): 37 | _run_async = request_async 38 | _run_blocking = request_blocking 39 | 40 | ## Callback used to execute a blocking HTTP request. 41 | func request_blocking(poll_delay_usec, fail: Callable): 42 | assert(result == null) 43 | result = BlockingResult.new() 44 | result.poll_delay_usec = poll_delay_usec 45 | var err = result.connect_to_url(request_url, tls_options) 46 | if err != OK: 47 | return result 48 | result.make_request(request_path, request_headers, request_method, request_body) 49 | if result.is_http_error(): 50 | fail.call() 51 | return result 52 | 53 | 54 | ## Callback used to execute an asynchronous HTTP request. 55 | func request_async(fail: Callable): 56 | assert(result == null) 57 | var req = HTTPRequest.new() 58 | req.max_redirects = 0 59 | if tls_options != null: 60 | req.set_tls_options(tls_options) 61 | if OS.get_name() != "Web": 62 | req.use_threads = true 63 | self.node.add_child(req) 64 | var err = req.request_raw(request_url + request_path, request_headers, request_method, request_body) 65 | result = AsyncResult.new(req if err == OK else null) 66 | var id = result.get_instance_id() 67 | # The object meta was used to prevent the Result object from going out-of-scope resulting in 68 | # endless awaits under certain conditions in previous versions of Godot. 69 | # TODO: Evaluate if this is still needed. 70 | var keep = ("_keep_%d" % id).replace("-", "N") 71 | node.set_meta(keep, result) 72 | result.completed.connect(func (): node.remove_meta(keep), CONNECT_DEFERRED) 73 | await result.completed 74 | if result.is_http_error(): 75 | fail.call() 76 | return result 77 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/settings_panels/room_settings.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "../settings_panel.gd" 3 | 4 | 5 | const HathoraProjectSettings = preload("res://addons/hathora/plugin/hathora_project_settings.gd") 6 | const DotEnv = preload("res://addons/hathora/plugin/dotenv.gd") 7 | const Enums = preload("res://addons/hathora/plugin/enums.gd") 8 | 9 | @onready var sdk = %SDK 10 | 11 | var selected_region:String: 12 | get: return region_n.get_item_text(region_n.selected) 13 | 14 | var region_n: OptionButton 15 | var room_id_n: LineEdit 16 | var host_n: LineEdit 17 | var port_n: LineEdit 18 | 19 | func _make_settings() -> void: 20 | sdk.set_dev_token(DotEnv.get_k("HATHORA_DEVELOPER_TOKEN")) 21 | region_n = add_option_button("Region", REGIONS) 22 | room_id_n = add_line_edit_with_icon("Room ID", "", get_theme_icon("ActionCopy", "EditorIcons"), _on_room_id_copy_button_pressed) 23 | host_n = add_line_edit_with_icon("Host", "", get_theme_icon("ActionCopy", "EditorIcons"), _on_host_copy_button_pressed) 24 | port_n = add_line_edit_with_icon("Port", "", get_theme_icon("ActionCopy", "EditorIcons"), _on_port_copy_button_pressed) 25 | room_id_n.editable = false 26 | host_n.editable = false 27 | port_n.editable = false 28 | add_button("Create Room", get_theme_icon("Add", "EditorIcons"), _on_create_room_button_pressed) 29 | 30 | func _on_create_room_button_pressed(): 31 | if not selected_region: 32 | print("[HATHORA] Selected Region required") 33 | return 34 | if HathoraProjectSettings.get_s("application_id").is_empty(): 35 | print("[HATHORA] No application selected") 36 | return 37 | print("[HATHORA] Create room requested...") 38 | sdk.set_dev_token(DotEnv.get_k("HATHORA_DEVELOPER_TOKEN")) 39 | 40 | if Enums.REGION_NAMES.find_key(selected_region) == null: 41 | print("[HATHORA] Invalid region") 42 | return 43 | var res = await sdk.room_v2.create(Enums.REGION_NAMES.find_key(selected_region)).async() 44 | 45 | if res.is_error(): 46 | print("[HATHORA] Error creating a room") 47 | print(res.as_error()) 48 | if res.as_error().error == 401: 49 | owner.reset_token() 50 | return 51 | 52 | var room_id = res.get_data().roomId 53 | print("[HATHORA] Created a new room with roomId: ", room_id) 54 | room_id_n.text = room_id 55 | sdk.set_dev_token(DotEnv.get_k("HATHORA_DEVELOPER_TOKEN")) 56 | _get_connection_info(room_id) 57 | 58 | func _get_connection_info(room_id: String) -> void: 59 | host_n.text = "Polling..." 60 | port_n.text = "Polling..." 61 | 62 | var res = await sdk.room_v2.get_connection_info(room_id).async() 63 | 64 | if res.is_error(): 65 | print(res.as_error()) 66 | return 67 | 68 | var info = res.get_data() 69 | 70 | if info.status != Enums.RoomStatus.ACTIVE: 71 | _get_connection_info(room_id) 72 | return 73 | 74 | host_n.text = str(res.exposedPort.host) 75 | port_n.text = str(res.exposedPort.port) 76 | 77 | func _on_room_id_copy_button_pressed() -> void: 78 | if not room_id_n.text.is_empty(): 79 | DisplayServer.clipboard_set(room_id_n.text) 80 | 81 | func _on_host_copy_button_pressed() -> void: 82 | if not host_n.text.is_empty(): 83 | DisplayServer.clipboard_set(host_n.text) 84 | 85 | func _on_port_copy_button_pressed() -> void: 86 | if not port_n.text.is_empty(): 87 | DisplayServer.clipboard_set(port_n.text) 88 | -------------------------------------------------------------------------------- /addons/hathora/sdk/rest-client/client_result.gd: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-present W4 Games Limited. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | ## The result of an HTTP request. 14 | extends RefCounted 15 | 16 | ## The result status. 17 | enum ResultStatus { DONE = 0, PENDING = 1, CANCELLED = 2, ERROR = 3 } 18 | 19 | var result_status = ResultStatus.PENDING 20 | var http_request_result := 0 21 | var http_status_code := 0 22 | var headers := PackedStringArray() 23 | var body := PackedByteArray() 24 | var json_parser := JSON.new() 25 | 26 | 27 | ## Returns true if the request is still pending; otherwise, false. 28 | func is_pending() -> bool: 29 | return result_status == ResultStatus.PENDING 30 | 31 | 32 | ## Returns a Dictionary of the response headers. 33 | func dict_headers() -> Dictionary: 34 | var out = {} 35 | for v in self.headers: 36 | var split = v.split(':', true, 1) 37 | if split.size() == 2: 38 | out[split[0].strip_edges().to_lower()] = split[1].strip_edges() 39 | return out 40 | 41 | 42 | ## Parses the response body as JSON and returns it. 43 | func json_result() -> Variant: 44 | var json_string = self.body.get_string_from_utf8() 45 | var err = json_parser.parse(json_string) 46 | if err == OK: 47 | var data_received = json_parser.get_data() 48 | var type = typeof(data_received) 49 | if type == TYPE_DICTIONARY or type == TYPE_ARRAY or type == TYPE_NIL: 50 | return data_received 51 | else: 52 | push_error("Unexpected data type: %d" % typeof(data_received)) 53 | else: 54 | push_error( 55 | "JSON Parse Error: ", 56 | json_parser.get_error_message(), 57 | " in ", 58 | json_string, 59 | " at line ", 60 | json_parser.get_error_line() 61 | ) 62 | return null 63 | 64 | 65 | ## Returns the response body as a UTF-8 string. 66 | func text_result() -> String: 67 | return body.get_string_from_utf8() 68 | 69 | 70 | ## Returns the response body as bytes. 71 | func bytes_result() -> PackedByteArray: 72 | return body 73 | 74 | 75 | ## Returns true if the request resulted in an error; otherwise, false. 76 | func is_error() -> bool: 77 | return result_status == ResultStatus.ERROR 78 | 79 | 80 | ## Returns true if the response is an HTTP error; otherwise, false. 81 | func is_http_error() -> bool: 82 | return http_status_code >= 400 83 | 84 | 85 | ## Returns true if the response is an HTTP success; otherwise, false. 86 | func is_http_success() -> bool: 87 | return http_status_code >= 200 and http_status_code < 300 88 | 89 | 90 | ## Returns true if the response is an HTTP redirect; otherwise, false. 91 | func is_http_redirect() -> bool: 92 | return http_status_code >= 300 and http_status_code < 400 93 | 94 | 95 | ## Returns the HTTP status code of the response. 96 | func get_http_status_code() -> int: 97 | return http_status_code 98 | 99 | 100 | func _to_string() -> String: 101 | return str([http_request_result, http_status_code, headers, body.slice(0, 256), result_status]) 102 | -------------------------------------------------------------------------------- /addons/hathora/plugin/rest-client/client_result.gd: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-present W4 Games Limited. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | ## The result of an HTTP request. 14 | extends RefCounted 15 | 16 | ## The result status. 17 | enum ResultStatus { DONE = 0, PENDING = 1, CANCELLED = 2, ERROR = 3 } 18 | 19 | var result_status = ResultStatus.PENDING 20 | var http_request_result := 0 21 | var http_status_code := 0 22 | var headers := PackedStringArray() 23 | var body := PackedByteArray() 24 | var json_parser := JSON.new() 25 | 26 | 27 | ## Returns true if the request is still pending; otherwise, false. 28 | func is_pending() -> bool: 29 | return result_status == ResultStatus.PENDING 30 | 31 | 32 | ## Returns a Dictionary of the response headers. 33 | func dict_headers() -> Dictionary: 34 | var out = {} 35 | for v in self.headers: 36 | var split = v.split(':', true, 1) 37 | if split.size() == 2: 38 | out[split[0].strip_edges().to_lower()] = split[1].strip_edges() 39 | return out 40 | 41 | 42 | ## Parses the response body as JSON and returns it. 43 | func json_result() -> Variant: 44 | var json_string = self.body.get_string_from_utf8() 45 | var err = json_parser.parse(json_string) 46 | if err == OK: 47 | var data_received = json_parser.get_data() 48 | var type = typeof(data_received) 49 | if type == TYPE_DICTIONARY or type == TYPE_ARRAY or type == TYPE_NIL: 50 | return data_received 51 | else: 52 | push_error("Unexpected data type: %d" % typeof(data_received)) 53 | else: 54 | push_error( 55 | "JSON Parse Error: ", 56 | json_parser.get_error_message(), 57 | " in ", 58 | json_string, 59 | " at line ", 60 | json_parser.get_error_line() 61 | ) 62 | return null 63 | 64 | 65 | ## Returns the response body as a UTF-8 string. 66 | func text_result() -> String: 67 | return body.get_string_from_utf8() 68 | 69 | 70 | ## Returns the response body as bytes. 71 | func bytes_result() -> PackedByteArray: 72 | return body 73 | 74 | 75 | ## Returns true if the request resulted in an error; otherwise, false. 76 | func is_error() -> bool: 77 | return result_status == ResultStatus.ERROR 78 | 79 | 80 | ## Returns true if the response is an HTTP error; otherwise, false. 81 | func is_http_error() -> bool: 82 | return http_status_code >= 400 83 | 84 | 85 | ## Returns true if the response is an HTTP success; otherwise, false. 86 | func is_http_success() -> bool: 87 | return http_status_code >= 200 and http_status_code < 300 88 | 89 | 90 | ## Returns true if the response is an HTTP redirect; otherwise, false. 91 | func is_http_redirect() -> bool: 92 | return http_status_code >= 300 and http_status_code < 400 93 | 94 | 95 | ## Returns the HTTP status code of the response. 96 | func get_http_status_code() -> int: 97 | return http_status_code 98 | 99 | 100 | func _to_string() -> String: 101 | return str([http_request_result, http_status_code, headers, body.slice(0, 256), result_status]) 102 | -------------------------------------------------------------------------------- /addons/hathora/plugin/rest-client/client_blocking_result.gd: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-present W4 Games Limited. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | ## The result of a blocking HTTP request. 14 | extends "client_result.gd" 15 | 16 | var http_client := HTTPClient.new() 17 | var poll_delay_usec := 1000 18 | 19 | func connect_to_url(url: String, tls_options : TLSOptions = null): 20 | if not url.begins_with("http://") and not url.begins_with("https://"): 21 | url = "http://" + url 22 | var tls = url.begins_with("https://") 23 | var port := 443 if tls else 80 24 | var proto_idx := url.find("://") + 3 25 | var path_idx := url.find("/", proto_idx) 26 | if path_idx < 0: 27 | path_idx = url.length() 28 | url += "/" 29 | var host := url.substr(proto_idx, path_idx - proto_idx) 30 | var port_idx = host.rfind(":") 31 | if port_idx > 0: 32 | var port_str = host.substr(port_idx + 1) 33 | if port_str.is_valid_int(): 34 | port = port_str.to_int() 35 | host = host.substr(0, port_idx) 36 | var err = http_client.connect_to_host(host, port, tls_options if tls else null) 37 | if err != OK: 38 | http_request_result = HTTPRequest.RESULT_CANT_CONNECT 39 | return err 40 | 41 | 42 | func wait_connection(): 43 | http_client.poll() 44 | while http_client.get_status() == HTTPClient.STATUS_CONNECTING or http_client.get_status() == HTTPClient.STATUS_RESOLVING: 45 | OS.delay_usec(poll_delay_usec) 46 | http_client.poll() 47 | 48 | 49 | func wait_request(): 50 | http_client.poll() 51 | while http_client.get_status() == HTTPClient.STATUS_REQUESTING: 52 | OS.delay_usec(poll_delay_usec) 53 | http_client.poll() 54 | 55 | 56 | func read_body() -> PackedByteArray: 57 | var rb = PackedByteArray() # Array that will hold the data. 58 | while http_client.get_status() == HTTPClient.STATUS_BODY: 59 | # While there is body left to be read 60 | http_client.poll() 61 | # Get a chunk. 62 | var chunk = http_client.read_response_body_chunk() 63 | if chunk.size() == 0: 64 | OS.delay_usec(poll_delay_usec) 65 | else: 66 | rb = rb + chunk # Append to read buffer. 67 | return rb 68 | 69 | 70 | func make_request(path : String, headers : PackedStringArray, method : int, raw_data := PackedByteArray()) -> void: 71 | result_status = ResultStatus.ERROR 72 | 73 | # Wait until resolved and connected. 74 | wait_connection() 75 | 76 | if http_client.get_status() != HTTPClient.STATUS_CONNECTED: 77 | http_request_result = http_client.get_status() 78 | return 79 | 80 | var err = http_client.request(method, path, headers) 81 | if err != OK: 82 | http_request_result = http_client.get_status() 83 | return 84 | 85 | # Keep polling for as long as the request is being processed. 86 | wait_request() 87 | 88 | if not http_client.has_response(): 89 | http_request_result = http_client.get_status() 90 | return 91 | 92 | result_status = ResultStatus.DONE 93 | http_request_result = HTTPRequest.RESULT_SUCCESS 94 | http_status_code = http_client.get_response_code() 95 | headers = http_client.get_response_headers() 96 | body = read_body() 97 | http_client.close() 98 | -------------------------------------------------------------------------------- /addons/hathora/sdk/rest-client/client_blocking_result.gd: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-present W4 Games Limited. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | ## The result of a blocking HTTP request. 14 | extends "client_result.gd" 15 | 16 | var http_client := HTTPClient.new() 17 | var poll_delay_usec := 1000 18 | 19 | func connect_to_url(url: String, tls_options : TLSOptions = null): 20 | if not url.begins_with("http://") and not url.begins_with("https://"): 21 | url = "http://" + url 22 | var tls = url.begins_with("https://") 23 | var port := 443 if tls else 80 24 | var proto_idx := url.find("://") + 3 25 | var path_idx := url.find("/", proto_idx) 26 | if path_idx < 0: 27 | path_idx = url.length() 28 | url += "/" 29 | var host := url.substr(proto_idx, path_idx - proto_idx) 30 | var port_idx = host.rfind(":") 31 | if port_idx > 0: 32 | var port_str = host.substr(port_idx + 1) 33 | if port_str.is_valid_int(): 34 | port = port_str.to_int() 35 | host = host.substr(0, port_idx) 36 | var err = http_client.connect_to_host(host, port, tls_options if tls else null) 37 | if err != OK: 38 | http_request_result = HTTPRequest.RESULT_CANT_CONNECT 39 | return err 40 | 41 | 42 | func wait_connection(): 43 | http_client.poll() 44 | while http_client.get_status() == HTTPClient.STATUS_CONNECTING or http_client.get_status() == HTTPClient.STATUS_RESOLVING: 45 | OS.delay_usec(poll_delay_usec) 46 | http_client.poll() 47 | 48 | 49 | func wait_request(): 50 | http_client.poll() 51 | while http_client.get_status() == HTTPClient.STATUS_REQUESTING: 52 | OS.delay_usec(poll_delay_usec) 53 | http_client.poll() 54 | 55 | 56 | func read_body() -> PackedByteArray: 57 | var rb = PackedByteArray() # Array that will hold the data. 58 | while http_client.get_status() == HTTPClient.STATUS_BODY: 59 | # While there is body left to be read 60 | http_client.poll() 61 | # Get a chunk. 62 | var chunk = http_client.read_response_body_chunk() 63 | if chunk.size() == 0: 64 | OS.delay_usec(poll_delay_usec) 65 | else: 66 | rb = rb + chunk # Append to read buffer. 67 | return rb 68 | 69 | 70 | func make_request(path : String, headers : PackedStringArray, method : int, raw_data := PackedByteArray()) -> void: 71 | result_status = ResultStatus.ERROR 72 | 73 | # Wait until resolved and connected. 74 | wait_connection() 75 | 76 | if http_client.get_status() != HTTPClient.STATUS_CONNECTED: 77 | http_request_result = http_client.get_status() 78 | return 79 | 80 | var err = http_client.request(method, path, headers) 81 | if err != OK: 82 | http_request_result = http_client.get_status() 83 | return 84 | 85 | # Keep polling for as long as the request is being processed. 86 | wait_request() 87 | 88 | if not http_client.has_response(): 89 | http_request_result = http_client.get_status() 90 | return 91 | 92 | result_status = ResultStatus.DONE 93 | http_request_result = HTTPRequest.RESULT_SUCCESS 94 | http_status_code = http_client.get_response_code() 95 | headers = http_client.get_response_headers() 96 | body = read_body() 97 | http_client.close() 98 | -------------------------------------------------------------------------------- /addons/hathora/sdk/apis/processes_v3.gd: -------------------------------------------------------------------------------- 1 | extends "endpoint.gd" 2 | 3 | ## Operations to get data on active and stopped processes. 4 | 5 | ## Retrieve the 10 most recent processes objects for an application. Filter the array by optionally passing in a status or region. 6 | ## [br][br]Returns an array. See [method get_process] for the data contained in each element of the array. 7 | func get_latest(statuses: Array[Hathora.ProcessStatus] = [], regions: Array[Hathora.Region] = []): 8 | return GET("processes/latest", empty_array_stripped({ 9 | "status": to_array_string(statuses, Hathora.PROCESS_STATUSES), 10 | "region": to_array_string(regions, Hathora.REGION_NAMES)})).then(func(result): 11 | if result.is_error(): 12 | return result 13 | return result 14 | ) 15 | 16 | ## Count the number of processes objects for an application. Filter by optionally passing in a status or region. 17 | ## [br][br]Returns process count: [float] 18 | func get_count(statuses: Array[Hathora.ProcessStatus] = [], regions: Array[Hathora.Region] = []): 19 | return GET("processes/count", empty_array_stripped({ 20 | "status": to_array_string(statuses, Hathora.PROCESS_STATUSES), 21 | "region": to_array_string(regions, Hathora.REGION_NAMES)})).then(func(result): 22 | if result.is_error(): 23 | return result 24 | return result 25 | ) 26 | 27 | ## Creates a process without a room. Use this to pre-allocate processes ahead of time so that subsequent room assignment via CreateRoom() can be instant. 28 | ## [br][br]See [method get_process] for the data contained in the result. 29 | func create(region: Hathora.Region): 30 | return POST("processes/regions/" + Hathora.REGION_NAMES[region]).then(func(result): 31 | if result.is_error(): 32 | return result 33 | return result 34 | ) 35 | 36 | ## Get details for a process 37 | ## [br][br][br][b]If successful, calls to this endpoint return:[/b] 38 | ## [br][br][code]status[/code]: [enum Hathora.ProcessStatus]. Status of the process. 39 | ## [br][br][code]roomsAllocated[/code]: [float]. Tracks the number of rooms that have been allocated to the process. 40 | ## [br][br][code]terminatedAt[/code]: [String] or [code]null[/code]. When the process has been terminated. 41 | ## [br][br][code]stoppingAt[/code]: [String] or [code]null[/code]. When the process is issued to stop. Used to determine when billing should stop. 42 | ## [br][br][code]startedAt[/code]: [String] or [code]null[/code]. When the process bound to the specified port. Used to determine when billing should start. 43 | ## [br][br][code]createdAt[/code]: [String] . When the process started being provisioned. 44 | ## [br][br][code]roomsPerProcess[/code]: [float][ 1 .. 10000 ]. Governs how many rooms can be scheduled in a process. 45 | ## [br][br][code]additionalExposedPorts[/code]: [Array] of dictionaries. Connection details for up to 2 exposed ports. 46 | ## [br][br][code]exposedPort[/code]: [Dictionary] or [code]null[/code]. Connection details for an active process. 47 | ## [br][br][code]region[/code]: [enum Hathora.Region]. 48 | ## [br][br][code]processId[/code]: [String]. System generated unique identifier to a runtime instance of your game server. 49 | ## [br][br][code]deploymentId[/code]: [float]. System generated id for a deployment. Increments by 1. 50 | ## [br][br][code]appId[/code]: [String]. System generated unique identifier for an application. 51 | func get_process(process_id: String): 52 | return GET("processes/" + process_id).then(func(result): 53 | if result.is_error(): 54 | return result 55 | return result 56 | ) 57 | 58 | ## Stops a process immediately 59 | func stop(process_id: String): 60 | return POST("processes/" + process_id + "/stop").then(func(result): 61 | if result.is_error(): 62 | return result 63 | return result 64 | ) 65 | -------------------------------------------------------------------------------- /addons/hathora/sdk/rest-client/client_promise.gd: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-present W4 Games Limited. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | ## A promise which can have a chain of callbacks that can be resolved asynchronously or blocking. 14 | ## 15 | ## Calling [method async] or [method blocking] will start resolving the promise asynchronously or blocking, respectively. 16 | ## 17 | ## All of the callbacks added via [method then] will be called with the result. 18 | extends RefCounted 19 | 20 | ## The promise status. 21 | enum Status {PENDING, COMPLETED, FAILED} 22 | 23 | ## The chain of callbacks. 24 | var chain : Array[Callable] 25 | ## The result of the REST request. 26 | var result 27 | ## The promise status. 28 | var status := Status.PENDING 29 | var _run_async : Callable 30 | var _run_blocking : Callable 31 | 32 | func _init(run_async: Callable, run_blocking:=Callable()): 33 | _run_async = run_async 34 | _run_blocking = run_blocking 35 | 36 | 37 | func _hathora_client_promise(): 38 | pass 39 | 40 | 41 | func _fail(): 42 | status = Status.FAILED 43 | 44 | 45 | ## Starts an asynchronous HTTP request to fulfill the promise. 46 | func async(): 47 | assert(status == Status.PENDING) 48 | if _run_async.is_valid(): 49 | result = await _run_async.call(_fail) 50 | # Hang on to old results so they don't go out of scope, leading to endless await. 51 | # @todo This is most likely a "bug" - hopefully it'll get fixed eventually! 52 | var old_results := [] 53 | for c in chain: 54 | old_results.append(result) 55 | result = await c.call(result) 56 | if result is Object and result.has_method("_hathora_client_promise"): 57 | result = await result.async() 58 | if status != Status.FAILED: 59 | status = Status.COMPLETED 60 | return result 61 | 62 | 63 | ## Starts a blocking HTTP request to fulfill the promise. 64 | func blocking(poll_delay_usec:=1000): 65 | assert(status == Status.PENDING) 66 | if _run_blocking.is_valid(): 67 | result = _run_blocking.call(poll_delay_usec, _fail) 68 | for c in chain: 69 | result = c.call(result) 70 | if result is Object and result.has_method("_hathora_client_promise"): 71 | result = result.blocking(poll_delay_usec) 72 | if status != Status.FAILED: 73 | status = Status.COMPLETED 74 | return result 75 | 76 | 77 | func then(callable: Callable): 78 | chain.push_back(callable) 79 | return self 80 | 81 | 82 | static func _make_promise(callable: Callable): 83 | return new(func (_1): return callable.call(), func(_1, _2): return callable.call()) 84 | 85 | 86 | ## Creates a new promise that will resolve the given array of promises in sequence. 87 | static func sequence(promises: Array): 88 | if promises.any(func(e): return not e is Callable and not e.has_method("_hathora_client_promise")): 89 | push_error("The input must be an array of promises") 90 | return null 91 | var run_async = func (fail): 92 | var results = [] 93 | for p in promises: 94 | if p is Callable: 95 | p = _make_promise(p) 96 | results.append(await p.async()) 97 | if p.status == Status.FAILED: 98 | break 99 | return results 100 | var run_blocking = func (poll_delay_usec, reject): 101 | var results = [] 102 | for p in promises: 103 | if p is Callable: 104 | p = _make_promise(p) 105 | results.append(p.blocking(poll_delay_usec)) 106 | if p.status == Status.FAILED: 107 | break 108 | return results 109 | return new(run_async, run_blocking) 110 | -------------------------------------------------------------------------------- /addons/hathora/plugin/rest-client/client_promise.gd: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-present W4 Games Limited. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | ## A promise which can have a chain of callbacks that can be resolved asynchronously or blocking. 14 | ## 15 | ## Calling [method async] or [method blocking] will start resolving the promise asynchronously or blocking, respectively. 16 | ## 17 | ## All of the callbacks added via [method then] will be called with the result. 18 | extends RefCounted 19 | 20 | ## The promise status. 21 | enum Status {PENDING, COMPLETED, FAILED} 22 | 23 | ## The chain of callbacks. 24 | var chain : Array[Callable] 25 | ## The result of the REST request. 26 | var result 27 | ## The promise status. 28 | var status := Status.PENDING 29 | var _run_async : Callable 30 | var _run_blocking : Callable 31 | 32 | func _init(run_async: Callable, run_blocking:=Callable()): 33 | _run_async = run_async 34 | _run_blocking = run_blocking 35 | 36 | 37 | func _hathora_client_promise(): 38 | pass 39 | 40 | 41 | func _fail(): 42 | status = Status.FAILED 43 | 44 | 45 | ## Starts an asynchronous HTTP request to fulfill the promise. 46 | func async(): 47 | assert(status == Status.PENDING) 48 | if _run_async.is_valid(): 49 | result = await _run_async.call(_fail) 50 | # Hang on to old results so they don't go out of scope, leading to endless await. 51 | # @todo This is most likely a "bug" - hopefully it'll get fixed eventually! 52 | var old_results := [] 53 | for c in chain: 54 | old_results.append(result) 55 | result = await c.call(result) 56 | if result is Object and result.has_method("_hathora_client_promise"): 57 | result = await result.async() 58 | if status != Status.FAILED: 59 | status = Status.COMPLETED 60 | return result 61 | 62 | 63 | ## Starts a blocking HTTP request to fulfill the promise. 64 | func blocking(poll_delay_usec:=1000): 65 | assert(status == Status.PENDING) 66 | if _run_blocking.is_valid(): 67 | result = _run_blocking.call(poll_delay_usec, _fail) 68 | for c in chain: 69 | result = c.call(result) 70 | if result is Object and result.has_method("_hathora_client_promise"): 71 | result = result.blocking(poll_delay_usec) 72 | if status != Status.FAILED: 73 | status = Status.COMPLETED 74 | return result 75 | 76 | 77 | func then(callable: Callable): 78 | chain.push_back(callable) 79 | return self 80 | 81 | 82 | static func _make_promise(callable: Callable): 83 | return new(func (_1): return callable.call(), func(_1, _2): return callable.call()) 84 | 85 | 86 | ## Creates a new promise that will resolve the given array of promises in sequence. 87 | static func sequence(promises: Array): 88 | if promises.any(func(e): return not e is Callable and not e.has_method("_hathora_client_promise")): 89 | push_error("The input must be an array of promises") 90 | return null 91 | var run_async = func (fail): 92 | var results = [] 93 | for p in promises: 94 | if p is Callable: 95 | p = _make_promise(p) 96 | results.append(await p.async()) 97 | if p.status == Status.FAILED: 98 | break 99 | return results 100 | var run_blocking = func (poll_delay_usec, reject): 101 | var results = [] 102 | for p in promises: 103 | if p is Callable: 104 | p = _make_promise(p) 105 | results.append(p.blocking(poll_delay_usec)) 106 | if p.status == Status.FAILED: 107 | break 108 | return results 109 | return new(run_async, run_blocking) 110 | -------------------------------------------------------------------------------- /addons/hathora/sdk/apis/lobby_v3.gd: -------------------------------------------------------------------------------- 1 | extends "endpoint.gd" 2 | 3 | ## Operations to create and manage lobbies using Harhora's Lobby Service. 4 | 5 | ## Create a new lobby for an application. 6 | ## A lobby object is a wrapper around a room object. 7 | ## With a lobby, you get additional functionality like 8 | ## configuring the visibility of the room, 9 | ## managing the state of a match, 10 | ## and retrieving a list of public lobbies to display to players. 11 | ## Takes: 12 | ## [br][br]- [param player_token]: [String] 13 | ## [br][br]- [param visibility]: [enum Hathora.Visibility] 14 | ## [br][br]- [param region]: [enum Hathora.Region] 15 | ## [br][br]- optionally [param room_config]: [String]. Optional configuration parameters for the room. It is accessible from the room via [method HathoraSDK.room_v2.get_info]. 16 | ## [br][br]- optionally [param short_code] for a user-defined identifier for a lobby, for example: [code]"LFG4"[/code] 17 | ## [br][br]- optionally [param room_id]([ 1 .. 100 ] characters ^[a-zA-Z0-9_-]*$) which overrides the system generated one 18 | ## [br][br][b]Note:[/b] error will be returned if roomId is not globally unique. 19 | ## [br][br][br][b]If successful, calls to this endpoint return:[/b] 20 | ## [br][br]- [code]shortCode[/code]: [String] (<= 100 characters). User-defined identifier for a lobby. 21 | ## [br][br]- [code]createdAt[/code]: [String] . When the lobby was created. 22 | ## [br][br]- [code]createdBy[/code]: [String]. UserId or email address for the user that created the lobby. 23 | ## [br][br]- [code]roomConfig[/code]: [String](<= 10000 characters) or [code]null[/code]. Optional configuration parameters for the room. Can be any string including stringified JSON. It is accessible from the room via [method HathoraSDK.room_v2.get_info]. 24 | ## [br][br]- [code]visibility[/code]: [enum Hathora.Visibility] 25 | ## [br][br]- [code]region[/code]: [enum Hathora.Region] 26 | ## [br][br]- [code]roomId[/code]: [String] ([ 1 .. 100 ] characters ^[a-zA-Z0-9_-]*$). 27 | ## [br][br]- [code]appId[/code]: [String] 28 | func create(player_token: String, visibility: Hathora.Visibility, region: Hathora.Region, room_config := "", short_code := "", room_id := ""): 29 | client.set_header("Authorization", "Bearer " + player_token) 30 | return POST("create", empty_string_stripped({ 31 | "visibility": Hathora.VISIBILITY[visibility], 32 | "region": Hathora.REGION_NAMES[region], 33 | "roomConfig": room_config, 34 | }), empty_string_stripped({ 35 | "shortCode": short_code, 36 | "roomId": room_id, 37 | })).then(func (result): 38 | if result.is_error(): 39 | return result 40 | return result 41 | ) 42 | 43 | ## Get an [Array] containing all active lobbies for a given application. 44 | ## Filter the [Array] by optionally passing in a [param region]. 45 | ## Use this endpoint to display all public lobbies that a player can join in the game client. 46 | ## See [method create] for the data contained in each element of the array. 47 | func list_active_public(player_token: String, region: Hathora.Region = -1): 48 | client.set_header("Authorization", "Bearer " + player_token) 49 | return GET("list/public", empty_string_stripped({"region": Hathora.REGION_NAMES.get(region, "")})).then(func (result): 50 | if result.is_error(): 51 | return result 52 | return result 53 | ) 54 | 55 | ## Get details for a lobby. 56 | ## See [method create] for the data contained in the result. 57 | func get_info_by_room_id(player_token: String, room_id: String): 58 | client.set_header("Authorization", "Bearer " + player_token) 59 | return GET("info/roomid/" + room_id).then(func (result): 60 | if result.is_error(): 61 | return result 62 | return result 63 | ) 64 | 65 | ## Get details for a lobby. If 2 or more lobbies have the same [param shortCode], then the most recently created lobby will be returned. 66 | ## See [method create] for the data contained in the result. 67 | func get_info_by_short_code(player_token: String, short_code: String): 68 | client.set_header("Authorization", "Bearer " + player_token) 69 | return GET("info/shortcode/" + short_code).then(func (result): 70 | if result.is_error(): 71 | return result 72 | return result 73 | ) 74 | 75 | 76 | -------------------------------------------------------------------------------- /addons/hathora/plugin/apis/endpoint.gd: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-present W4 Games Limited. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | 14 | extends RefCounted 15 | 16 | const Client = preload("../rest-client/client.gd") 17 | const Request = preload("../rest-client/client_request.gd") 18 | const Parser = preload("poly_result.gd") 19 | const PolyResult = Parser.PolyResult 20 | 21 | ## The REST client. 22 | var client : Client 23 | ## The base path. 24 | var endpoint := "" 25 | 26 | 27 | func _init(p_client: Client, p_endpoint: String): 28 | client = p_client 29 | endpoint = p_endpoint 30 | 31 | 32 | func _null_stripped(from : Dictionary): 33 | var out = {} 34 | for k in from: 35 | if typeof(from[k]) == TYPE_NIL: 36 | continue 37 | out[k] = from[k] 38 | return out 39 | 40 | func empty_string_stripped(from : Dictionary) -> Dictionary: 41 | var out = {} 42 | for k in from: 43 | if from[k] is String and from[k].is_empty(): 44 | continue 45 | out[k] = from[k] 46 | return out 47 | 48 | func empty_array_stripped(from: Dictionary) -> Dictionary: 49 | var out = {} 50 | for k in from: 51 | if from[k] is Array and from[k].is_empty(): 52 | continue 53 | out[k] = from[k] 54 | return out 55 | 56 | func to_array_string(from: Array, mapping: Dictionary) -> Array[String]: 57 | var out: Array[String] = [] 58 | for i in from: 59 | if mapping.has(i): 60 | out.append(mapping[i]) 61 | return out 62 | 63 | ## Parses a result from the REST client into a PolyResult. 64 | static func parse_result(result, binary:=false) -> PolyResult: 65 | return Parser.parse_result(result, binary) 66 | 67 | 68 | ## Parses the result of a HEAD request from the REST client into a PolyResult. 69 | static func _parse_head_result(result) -> PolyResult: 70 | if result.is_http_success(): 71 | return Parser.PolyResult.new(result.dict_headers()) 72 | return Parser.parse_result(result, true) 73 | 74 | 75 | ## Makes a GET request. 76 | func GET(path, query : Dictionary = {}, extra_headers : Dictionary = {}, binary:=false) -> Request: 77 | return client.GET(endpoint.path_join(path), query, extra_headers).then(parse_result.bind(binary)) 78 | 79 | 80 | ## Makes a HEAD request. 81 | func HEAD(path, query : Dictionary = {}, extra_headers : Dictionary = {}) -> Request: 82 | return client.HEAD(endpoint.path_join(path), query, extra_headers).then(_parse_head_result) 83 | 84 | 85 | ## Makes a raw GET request, that returns binary data. 86 | func GET_RAW(path, query : Dictionary = {}, extra_headers : Dictionary = {}) -> Request: 87 | return client.GET(endpoint.path_join(path), query, extra_headers).then(parse_result.bind(true)) 88 | 89 | 90 | ## Makes a POST request. 91 | func POST(path, data = null, query : Dictionary = {}, extra_headers : Dictionary = {}, binary:=false) -> Request: 92 | return client.POST(endpoint.path_join(path), data, query, extra_headers).then(parse_result.bind(binary)) 93 | 94 | 95 | ## Makes a PUT request. 96 | func PUT(path, data = null, query : Dictionary = {}, extra_headers : Dictionary = {}, binary:=false) -> Request: 97 | return client.PUT(endpoint.path_join(path), data, query, extra_headers).then(parse_result.bind(binary)) 98 | 99 | 100 | ## Makes a PATCH request. 101 | func PATCH(path, data = null, query : Dictionary = {}, extra_headers : Dictionary = {}, binary:=false) -> Request: 102 | return client.PATCH(endpoint.path_join(path), data, query, extra_headers).then(parse_result.bind(binary)) 103 | 104 | 105 | ## Makes a DELETE request. 106 | func DELETE(path, data = null, query : Dictionary = {}, extra_headers : Dictionary = {}, binary:=false) -> Request: 107 | return client.DELETE(endpoint.path_join(path), data, query, extra_headers).then(parse_result.bind(binary)) 108 | -------------------------------------------------------------------------------- /addons/hathora/sdk/apis/endpoint.gd: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-present W4 Games Limited. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | 14 | extends RefCounted 15 | 16 | const Client = preload("../rest-client/client.gd") 17 | const Request = preload("../rest-client/client_request.gd") 18 | const Parser = preload("poly_result.gd") 19 | const PolyResult = Parser.PolyResult 20 | 21 | ## The REST client. 22 | var client : Client 23 | ## The base path. 24 | var endpoint := "" 25 | 26 | 27 | func _init(p_client: Client, p_endpoint: String): 28 | client = p_client 29 | endpoint = p_endpoint 30 | 31 | 32 | func _null_stripped(from : Dictionary): 33 | var out = {} 34 | for k in from: 35 | if typeof(from[k]) == TYPE_NIL: 36 | continue 37 | out[k] = from[k] 38 | return out 39 | 40 | func empty_string_stripped(from : Dictionary) -> Dictionary: 41 | var out = {} 42 | for k in from: 43 | if from[k] is String and from[k].is_empty(): 44 | continue 45 | out[k] = from[k] 46 | return out 47 | 48 | func empty_array_stripped(from: Dictionary) -> Dictionary: 49 | var out = {} 50 | for k in from: 51 | if from[k] is Array and from[k].is_empty(): 52 | continue 53 | out[k] = from[k] 54 | return out 55 | 56 | func to_array_string(from: Array, mapping: Dictionary) -> Array[String]: 57 | var out: Array[String] = [] 58 | for i in from: 59 | if mapping.has(i): 60 | out.append(mapping[i]) 61 | return out 62 | 63 | ## Parses a result from the REST client into a PolyResult. 64 | static func parse_result(result, binary:=false) -> PolyResult: 65 | return Parser.parse_result(result, binary) 66 | 67 | 68 | ## Parses the result of a HEAD request from the REST client into a PolyResult. 69 | static func _parse_head_result(result) -> PolyResult: 70 | if result.is_http_success(): 71 | return Parser.PolyResult.new(result.dict_headers()) 72 | return Parser.parse_result(result, true) 73 | 74 | 75 | ## Makes a GET request. 76 | func GET(path, query : Dictionary = {}, extra_headers : Dictionary = {}, binary:=false) -> Request: 77 | return client.GET(endpoint.path_join(path), query, extra_headers).then(parse_result.bind(binary)) 78 | 79 | 80 | ## Makes a HEAD request. 81 | func HEAD(path, query : Dictionary = {}, extra_headers : Dictionary = {}) -> Request: 82 | return client.HEAD(endpoint.path_join(path), query, extra_headers).then(_parse_head_result) 83 | 84 | 85 | ## Makes a raw GET request, that returns binary data. 86 | func GET_RAW(path, query : Dictionary = {}, extra_headers : Dictionary = {}) -> Request: 87 | return client.GET(endpoint.path_join(path), query, extra_headers).then(parse_result.bind(true)) 88 | 89 | 90 | ## Makes a POST request. 91 | func POST(path, data = null, query : Dictionary = {}, extra_headers : Dictionary = {}, binary:=false) -> Request: 92 | return client.POST(endpoint.path_join(path), data, query, extra_headers).then(parse_result.bind(binary)) 93 | 94 | 95 | ## Makes a PUT request. 96 | func PUT(path, data = null, query : Dictionary = {}, extra_headers : Dictionary = {}, binary:=false) -> Request: 97 | return client.PUT(endpoint.path_join(path), data, query, extra_headers).then(parse_result.bind(binary)) 98 | 99 | 100 | ## Makes a PATCH request. 101 | func PATCH(path, data = null, query : Dictionary = {}, extra_headers : Dictionary = {}, binary:=false) -> Request: 102 | return client.PATCH(endpoint.path_join(path), data, query, extra_headers).then(parse_result.bind(binary)) 103 | 104 | 105 | ## Makes a DELETE request. 106 | func DELETE(path, data = null, query : Dictionary = {}, extra_headers : Dictionary = {}, binary:=false) -> Request: 107 | return client.DELETE(endpoint.path_join(path), data, query, extra_headers).then(parse_result.bind(binary)) 108 | -------------------------------------------------------------------------------- /addons/hathora/sdk/client.gd: -------------------------------------------------------------------------------- 1 | ## SDK to interact with the Hathora API. 2 | ## @tutorial: https://hathora.dev/docs/engines/godot 3 | ## @tutorial: https://hathora.dev/api 4 | ## See the properties for the documentation of each endpoint. 5 | ## [br][br]Example usage: 6 | ## [codeblock] 7 | ## var last_error = "" 8 | ## 9 | ## func create_lobby() -> bool: 10 | ## last_error = "" 11 | ## 12 | ## # Create a public lobby using a previously obtained playerAuth token 13 | ## # The function will pause until a result is obtained 14 | ## var res = await HathoraSDK.lobby_v3.create(login_token, Hathora.Visibility.PUBLIC, Hathora.Region.FRANKFURT).async() 15 | ## 16 | ## # Having obtained a result, the function continues 17 | ## # If there was an error, store the error message and return 18 | ## if res.is_error(): 19 | ## last_error = res.as_error().message 20 | ## return false 21 | ## 22 | ## # Store the data contained in the Result 23 | ## lobby_data = res.get_data() 24 | ## print("Created lobby with roomId ", lobby_data.roomId) 25 | ## return true 26 | ## [/codeblock] 27 | 28 | extends Node 29 | 30 | const _Client = preload("../sdk/rest-client/client.gd") 31 | const _Lobby = preload("../sdk/apis/lobby_v3.gd") 32 | const _Room = preload("../sdk/apis/room_v2.gd") 33 | const _Auth = preload("../sdk/apis/auth_v1.gd") 34 | const _Processes = preload("../sdk/apis/processes_v3.gd") 35 | const _Discovery = preload("../sdk/apis/discovery_v2.gd") 36 | const _DotEnv = preload("../sdk/dotenv.gd") 37 | const _HathoraProjectSettings = preload("res://addons/hathora/sdk/hathora_project_settings.gd") 38 | 39 | ## Operations to create and manage lobbies using our Lobby Service. 40 | var lobby_v3 : _Lobby 41 | 42 | ## Operations to create, manage, and connect to rooms. 43 | var room_v2 : _Room 44 | 45 | ## Operations that allow you to generate a Hathora-signed JSON web token (JWT) for player authentication. 46 | var auth_v1 : _Auth 47 | 48 | ## Operations to get data on active and stopped processes. 49 | var processes_v3 : _Processes 50 | 51 | ## Service that allows clients to directly ping all Hathora regions to get latency information 52 | var discovery_v2 : _Discovery 53 | 54 | var _no_auth_client: _Client 55 | var _player_client: _Client 56 | var _dev_client : _Client 57 | 58 | func _init(): 59 | process_mode = Node.PROCESS_MODE_ALWAYS 60 | _DotEnv.config() 61 | var node = self 62 | var url = "https://api.hathora.dev" 63 | var app_id = _HathoraProjectSettings.get_s("application_id") 64 | var tls_options = null 65 | 66 | # Dev endpoints 67 | _dev_client = _Client.new(node, url, {}, tls_options) 68 | room_v2 = _Room.new(_dev_client, "/rooms/v2/".path_join(app_id)) 69 | processes_v3 = _Processes.new(_dev_client, "/processes/v3/apps".path_join(app_id)) 70 | # Setting the dev token if found in DotEnv 71 | if not _DotEnv.get_k("HATHORA_DEVELOPER_TOKEN").is_empty(): 72 | set_dev_token(_DotEnv.get_k("HATHORA_DEVELOPER_TOKEN")) 73 | # Player endpoints 74 | _player_client = _Client.new(node, url, {}, tls_options) 75 | lobby_v3 = _Lobby.new(_player_client, "/lobby/v3/".path_join(app_id)) 76 | discovery_v2 = _Discovery.new(_player_client, "/discovery/v2/") 77 | auth_v1 = _Auth.new(_player_client, "/auth/v1/".path_join(app_id)) 78 | 79 | ## Set a [param dev_token]. Not recommended, specify the devToken at [code]res://.hathora/config[/code] or at [code]res://hathora_config[/code] instead. 80 | ## [br][br][b]Warning:[/b] the devToken gives privileged access to your Hathora account. Never include the devToken in client builds or in your versioning system. 81 | func set_dev_token(dev_token: String) -> void: 82 | _dev_client.set_header("Authorization", "Bearer " + dev_token) 83 | 84 | ## Set an [param app_id]. Not recommended, specify the appId in the Godot ProjectSettings instead. 85 | func set_app_id(app_id: String) -> void: 86 | room_v2 = _Room.new(_dev_client, "/rooms/v2/".path_join(app_id)) 87 | processes_v3 = _Processes.new(_dev_client, "/processes/v3/apps".path_join(app_id)) 88 | lobby_v3 = _Lobby.new(_player_client, "/lobby/v3/".path_join(app_id)) 89 | auth_v1 = _Auth.new(_no_auth_client, "/auth/v1/".path_join(app_id)) 90 | 91 | ## Set [param tls_options] for the SDK client. 92 | func set_tls_options(tls_options: TLSOptions) -> void: 93 | _dev_client.default_tls_options = tls_options 94 | _player_client.default_tls_options = tls_options 95 | _no_auth_client.default_tls_options = tls_options 96 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/settings_panels/developer_settings.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "../settings_panel.gd" 3 | 4 | const DotEnv = preload("res://addons/hathora/plugin/dotenv.gd") 5 | const HathoraProjectSettings = preload("res://addons/hathora/plugin/hathora_project_settings.gd") 6 | 7 | @onready var sdk = %SDK 8 | @onready var room_section_toggle: Button = %RoomSectionToggle 9 | 10 | var dev_token : String : 11 | set(v): 12 | if read_only: return 13 | dev_token = v 14 | dev_token_n.text = v 15 | dev_token_n.text_changed.emit(v) 16 | get: return dev_token_n.text 17 | 18 | var app_dictionary: Dictionary 19 | var app_names: Array[String] 20 | var selected_app_id: String : 21 | get: 22 | if target_app_n.get_selected_metadata(): 23 | return target_app_n.get_selected_metadata() 24 | return "" 25 | set(v): 26 | if read_only: return 27 | selected_app_id = v 28 | for i in range(target_app_n.item_count): 29 | if target_app_n.get_item_metadata(i) == v: 30 | target_app_n.select(i) 31 | break 32 | 33 | var dev_token_n : LineEdit 34 | var target_app_n : OptionButton 35 | var login_button_n: Button 36 | 37 | 38 | func _make_settings() -> void: 39 | dev_token_n = add_line_edit_with_icon("Developer token", DotEnv.get_k("HATHORA_DEVELOPER_TOKEN"), get_theme_icon("GuiVisibilityVisible", "EditorIcons"), _on_dev_token_visibility_button_pressed) 40 | dev_token_n.secret = true 41 | dev_token_n.text_changed.connect(_on_dev_token_text_changed) 42 | target_app_n = add_option_button_with_icon("Target application", [], get_theme_icon("Reload", "EditorIcons"), refresh_applications) 43 | target_app_n.item_selected.connect(_on_app_selected) 44 | login_button_n = add_button("Login with another account", get_theme_icon("CryptoKey", "EditorIcons"), owner._on_login_button_pressed) 45 | 46 | sdk.set_dev_token(DotEnv.get_k("HATHORA_DEVELOPER_TOKEN")) 47 | if DotEnv.get_k("HATHORA_DEVELOPER_TOKEN"): 48 | refresh_applications() 49 | ProjectSettings.settings_changed.connect(_on_project_settings_changed) 50 | 51 | func _on_project_settings_changed() -> void: 52 | selected_app_id = HathoraProjectSettings.get_s("application_id") 53 | 54 | func add_app(p_app_name: String, p_app_id: String) -> void: 55 | if not p_app_name.is_empty() and not p_app_id.is_empty(): 56 | target_app_n.add_item(p_app_name) 57 | var i = target_app_n.item_count - 1 58 | target_app_n.set_item_metadata(i, p_app_id) 59 | 60 | 61 | func clear_apps() -> void: 62 | target_app_n.clear() 63 | target_app_n.disabled = false 64 | target_app_n.tooltip_text = "" 65 | 66 | 67 | func _on_app_selected(index:int) -> void: 68 | HathoraProjectSettings.set_s("application_id", selected_app_id) 69 | %SDK.set_app_id(selected_app_id) 70 | %LatestDeploymentGetter.get_latest_deployment() 71 | 72 | 73 | func refresh_applications() -> void: 74 | room_section_toggle.disabled = false 75 | room_section_toggle.tooltip_text = "" 76 | # Update config, in case user has since logged in 77 | sdk.set_dev_token(DotEnv.get_k("HATHORA_DEVELOPER_TOKEN")) 78 | var res = await sdk.apps_v2.get_apps().async() 79 | if res.is_error(): 80 | clear_apps() 81 | %LatestDeploymentTextEdit.text = "Error getting latest deployment information" 82 | print(res.as_error()) 83 | if res.as_error().error == 401: 84 | owner.reset_token() 85 | return 86 | clear_apps() 87 | var apps = res.get_data().applications 88 | # If the user has no applications 89 | if len(apps) == 0: 90 | print_rich("[HATHORA] No applications found, create a new one at [url=http://console.hathora.dev]console.hathora.dev[/url]") 91 | target_app_n.disabled = true 92 | target_app_n.tooltip_text = "No applications found" 93 | HathoraProjectSettings.set_s("application_id", "") 94 | 95 | room_section_toggle.button_pressed = false 96 | room_section_toggle.disabled = true 97 | room_section_toggle.tooltip_text = "No target application selected" 98 | 99 | %LatestDeploymentTextEdit.text = "No applications found, create a new one at console.hathora.dev" 100 | return 101 | for app in apps: 102 | add_app(app.appName, app.appId) 103 | 104 | target_app_n.selected = target_app_n.get_selectable_item() 105 | 106 | # If we have an appId in our environment, try to select that app 107 | if not HathoraProjectSettings.get_s("application_id").is_empty(): 108 | for i in range(target_app_n.item_count): 109 | if target_app_n.get_item_metadata(i) == HathoraProjectSettings.get_s("application_id"): 110 | target_app_n.select(i) 111 | break 112 | 113 | _on_app_selected(target_app_n.selected) 114 | 115 | 116 | # Toggle dev token secret 117 | func _on_dev_token_visibility_button_pressed() -> void: 118 | dev_token_n.secret = !dev_token_n.secret 119 | 120 | 121 | func _on_dev_token_text_changed(new_text: String) -> void: 122 | DotEnv.add("HATHORA_DEVELOPER_TOKEN", dev_token) 123 | if len(new_text) == 842: 124 | # Automatically refresh the applications when a new token is inserted 125 | refresh_applications() 126 | login_button_n.text = "Login with another account" 127 | else: 128 | login_button_n.text = "Login to Hathora" 129 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/settings_panels/deployment_settings.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "../settings_panel.gd" 3 | 4 | const DotEnv = preload("res://addons/hathora/plugin/dotenv.gd") 5 | const HathoraProjectSettings = preload("res://addons/hathora/plugin/hathora_project_settings.gd") 6 | 7 | var requested_cpu: float: 8 | set(v): 9 | if read_only: return 10 | requested_cpu = v 11 | requested_cpu_n.value = v 12 | get: return requested_cpu_n.value 13 | 14 | var requested_memory: float: 15 | set(v): 16 | if read_only: return 17 | requested_memory = v 18 | requested_memory_n.value = v 19 | get: return requested_memory_n.value 20 | 21 | var path_to_tar: String: 22 | set(v): 23 | if read_only: return 24 | path_to_tar = v 25 | var car = tar_file_n.caret_column 26 | tar_file_n.text = v 27 | tar_file_n.caret_column = car 28 | #tar_file_n.text_changed.emit(v) 29 | get: return tar_file_n.text 30 | 31 | var rooms_per_process: int: 32 | set(v): 33 | if read_only: return 34 | rooms_per_process = v 35 | rooms_per_process_n.value = v 36 | get: return rooms_per_process_n.value 37 | 38 | var transport_type: String: 39 | set(v): 40 | if read_only: return 41 | transport_type = v 42 | for i in range(transport_type_n.item_count): 43 | if transport_type_n.get_item_text(i) == v: 44 | transport_type_n.select(i) 45 | get: return transport_type_n.get_item_text(transport_type_n.selected) 46 | 47 | var container_port: int: 48 | set(v): 49 | if read_only: return 50 | container_port = v 51 | container_port_n.value = v 52 | get: return container_port_n.value 53 | 54 | var tar_file_n: LineEdit 55 | var requested_cpu_n: SpinBox 56 | var requested_memory_n: SpinBox 57 | var rooms_per_process_n: SpinBox 58 | var transport_type_n: OptionButton 59 | var container_port_n: SpinBox 60 | var deploy_n: Button 61 | 62 | func _make_settings() -> void: 63 | tar_file_n = add_line_edit_with_icon("Path to tar file", HathoraProjectSettings.get_s("path_to_tar_file"), get_theme_icon("Folder", "EditorIcons"), _on_file_dialog_button_pressed) 64 | tar_file_n.text_changed.connect(_on_tar_file_text_changed) 65 | 66 | requested_cpu_n = add_spinbox("CPU (cores)", 0.5, 4, 0.1) 67 | requested_cpu_n.value_changed.connect(_on_cpu_value_changed) 68 | 69 | requested_memory_n = add_spinbox("Memory (GB)", 1, 8, 0.2) 70 | requested_memory_n.value_changed.connect(_on_memory_value_changed) 71 | 72 | rooms_per_process_n = add_spinbox("Rooms per process", 1, 10000, 1.0) 73 | 74 | transport_type_n = add_option_button("Transport type", TRANSPORT_TYPES) 75 | 76 | container_port_n = add_spinbox("Container port", 1, 65535, 1.0) 77 | 78 | deploy_n = add_button("Deploy to Hathora", get_theme_icon("Environment", "EditorIcons"), _on_deploy_button_pressed) 79 | 80 | var font_size = max(get_theme_font_size("main_size", "EditorFonts") - 4, 4) 81 | add_rich_text_label("[center][font_size=%d]For more advanced configuration: [url=http://console.hathora.dev]Hathora Console[/url][/font_size][/center]" % font_size, _on_label_meta_clicked) 82 | 83 | %LatestDeploymentGetter.updated_deployment.connect(_on_updated_deployment) 84 | ProjectSettings.settings_changed.connect(_on_project_settings_changed) 85 | 86 | func _on_file_dialog_button_pressed() -> void: 87 | %PathToTarFileDialog.current_dir = path_to_tar.get_base_dir() 88 | %PathToTarFileDialog.show() 89 | 90 | func _on_label_meta_clicked(link: Variant) -> void: 91 | if link is String: 92 | OS.shell_open(link) 93 | 94 | func _on_updated_deployment(data: Variant) -> void: 95 | # If there is no data on the latest deployment, we set some defaults 96 | if not "buildId" in data: 97 | requested_cpu = 0.5 98 | requested_memory = 1 99 | rooms_per_process = 1 100 | transport_type = "udp" 101 | container_port = 7777 102 | return 103 | 104 | requested_cpu = data.requestedCPU 105 | requested_memory = data.requestedMemoryMB / 1024 106 | container_port = data.defaultContainerPort.port 107 | transport_type = data.defaultContainerPort.transportType 108 | rooms_per_process = data.roomsPerProcess 109 | 110 | func _on_tar_file_text_changed(_new_text: String): 111 | HathoraProjectSettings.set_s("path_to_tar_file", path_to_tar) 112 | 113 | 114 | func _on_project_settings_changed(): 115 | path_to_tar = HathoraProjectSettings.get_s("path_to_tar_file") 116 | 117 | 118 | func _on_deploy_button_pressed() -> void: 119 | # Validate params 120 | if DotEnv.get_k("HATHORA_DEVELOPER_TOKEN").is_empty(): 121 | print("[HATHORA] Need valid developer token to deploy") 122 | return 123 | if HathoraProjectSettings.get_s("build_directory_path").is_empty(): 124 | print("[HATHORA] Need valid build directory path") 125 | return 126 | if HathoraProjectSettings.get_s("path_to_tar_file").is_empty(): 127 | print("[HATHORA] Need valid path to tar file") 128 | return 129 | 130 | # Create Build 131 | # The BuildDeployer calls LatestDeploymentGetter to get env and additionalPorts, just before deploying 132 | # This call would automatically override any Deployment Settings set by the user 133 | # To avoid Deployment Settings getting overridden, we set them to read only until the deployment is complete 134 | read_only = true 135 | 136 | await get_tree().process_frame 137 | print("[HATHORA] Create build started") 138 | var err = await %BuildDeployer.do_upload_and_create_build() 139 | 140 | if err: 141 | print_rich("[color=%s][HATHORA] [b]DEPLOYMENT ERROR at %s [/b][/color]" % [get_theme_color("error_color", "Editor").to_html(), Time.get_time_string_from_system()]) 142 | else: 143 | print_rich("[color=%s][HATHORA] [b]DEPLOYMENT SUCCESS at %s [/b][/color]" % [get_theme_color("success_color", "Editor").to_html(), Time.get_time_string_from_system()]) 144 | read_only = false 145 | 146 | func _on_cpu_value_changed(v: float) -> void: 147 | requested_memory_n.set_value_no_signal(v*2) 148 | return 149 | 150 | func _on_memory_value_changed(v: float) -> void: 151 | requested_cpu_n.set_value_no_signal(v/2) 152 | return 153 | -------------------------------------------------------------------------------- /addons/hathora/sdk/apis/room_v2.gd: -------------------------------------------------------------------------------- 1 | ## Operations to create, manage, and connect to rooms. 2 | extends "endpoint.gd" 3 | ## Create a new room for an existing application. 4 | ## Poll the [method get_info] endpoint to get connection details for an active room. 5 | ## Takes: 6 | ## [br][br]- [param room_config]: [String]. Optional configuration parameters for the room. It is accessible from the room via [method get_info]. 7 | ## [br][br]- [param region]: [enum Hathora.Region] 8 | ## [br][br]- optionally [param room_id][ 1 .. 100 ] characters ^[a-zA-Z0-9_-]*$ which overrides the system generated one 9 | ## [br][br][b]Note:[/b] error will be returned if roomId is not globally unique. 10 | ## [br][br][br][b]If successful, calls to this endpoint return:[/b] 11 | ## [br][br]- [code]additionalExposedPorts[/code]: [Array] of dictionaries <= 2 items. Each element contains: 12 | ## [br]-- [code]transportType[/code]: [enum Hathora.TransportType] 13 | ## [br]-- [code]port[/code]: [float] 14 | ## [br]-- [code]host[/code]: [String] 15 | ## [br]-- [code]name[/code]: [String] 16 | ## [br][br] [code]exposedPort[/code]: [Dictionary] containing: 17 | ## [br]-- [code]transportType[/code]: [enum Hathora.TransportType] 18 | ## [br]-- [code]port[/code]: [float] 19 | ## [br]-- [code]host[/code]: [String] 20 | ## [br]-- [code]name[/code]: [String] 21 | ## [br][br] [code]status[/code]: [enum Hathora.RoomStatus] 22 | ## [br][br] [code]roomId[/code]: [String] 23 | ## [br][br] [code]processId[/code]: [String] 24 | func create(region: Hathora.Region, room_config:= "", room_id = ""): 25 | return POST("create", empty_string_stripped({ 26 | "region": Hathora.REGION_NAMES[region], 27 | "roomConfig": room_config, 28 | }), 29 | empty_string_stripped({"roomId": room_id})).then(func (result): 30 | if result.is_error(): 31 | return result 32 | return result 33 | ) 34 | 35 | ## Retrieve current and historical allocation data for a room. 36 | ## [br][br][br][b]If successful, calls to this endpoint return:[/b] 37 | ## [br][br]- [code]currentAllocation[/code]: [Dictionary] or [code]null[/code] containing: 38 | ## [br]-- [code]unscheduledAt[/code]: [String] or [code]null[/code] 39 | ## [br]-- [code]scheduledAt[/code]: [String] 40 | ## [br]-- [code]processId[/code]: [String]. System generated unique identifier to a runtime instance of your game server. 41 | ## [br]-- [code]roomAllocationId[/code]: [String]. System generated unique identifier to an allocated instance of a room. 42 | ## [br][br] [code]status[/code]: [enum Hathora.RoomStatus] 43 | ## [br][br]- [code]allocations[/code]: [Array] of dictionaries. Each element contains: 44 | ## [br]-- [code]unscheduledAt[/code] or [code]null[/code]:[String] 45 | ## [br]-- [code]scheduledAt[/code]: [String] 46 | ## [br]-- [code]processId[/code]: [String]. System generated unique identifier to a runtime instance of your game server. 47 | ## [br]-- [code]roomAllocationId[/code]: [String]. System generated unique identifier to an allocated instance of a room. 48 | ## [br][br] [code]roomConfig[/code]: [String](<= 10000 characters) or [code]null[/code] 49 | ## [br][br] [code]roomId[/code]: [String] 50 | ## [br][br] [code]appId[/code]: [String] 51 | func get_info(room_id: String): 52 | return GET("info/" + room_id).then(func (result): 53 | if result.is_error(): 54 | return result 55 | return result 56 | ) 57 | 58 | ## Get all active rooms for a given [param process_id]. 59 | ## [br][br][br][b]If successful, calls to this endpoint return:[/b] 60 | ## [br][br][Array] of dictionaries. Each element contains: 61 | ## [br]-- [code]appId[/code]: [String] 62 | ## [br]-- [code]roomId[/code]: [String] 63 | ## [br]-- [code]roomConfig[/code]: [String] or [code]null[/code] 64 | ## [br]-- [code]status[/code]: [enum Hathora.RoomStatus] 65 | ## [br]-- [code]currentAllocation[/code]: [Dictionary] or [code]null[/code]. See [method get_info] for the contents. 66 | func get_active(process_id: String): 67 | return GET("list/" + process_id + "/active").then(func (result): 68 | if result.is_error(): 69 | return result 70 | return result 71 | ) 72 | 73 | ## Get all inactive rooms for a given [param process_id]. See [method get_active] for the contents. 74 | func get_inactive(process_id: String): 75 | return GET("list/" + process_id + "/inactive").then(func (result): 76 | if result.is_error(): 77 | return result 78 | return result 79 | ) 80 | 81 | ## Destroy a room by its [param room_id]. All associated metadata is deleted. 82 | func destroy(room_id: String): 83 | return POST("destroy/" + room_id).then(func (result): 84 | if result.is_error(): 85 | return result 86 | return result 87 | ) 88 | 89 | ## [b]Deprecated[/b] 90 | ## [br]Suspend a room. The room is unallocated from the process but can be rescheduled later using the same roomId. 91 | func suspend(room_id: String): 92 | return POST("suspend/" + room_id).then(func (result): 93 | if result.is_error(): 94 | return result 95 | return result 96 | ) 97 | 98 | ## Poll this endpoint to get connection details to a room. Clients can call this endpoint without authentication. 99 | ## [br][br][br][b]If successful, calls to this endpoint return:[/b] 100 | ## [br][br][code]additionalExposedPorts[/code]: [Array] of dictionaries. Connection details for up to 2 exposed ports. See [method create] for the contents. 101 | ## [br][br][code]exposedPort[/code]: [Dictionary] or [code]null[/code]. Connection details for an active process. See [method create] for the contents. 102 | ## [br][br]- [code]status[/code]: [enum Hathora.ProcessStatus]. Status of the process. 103 | ## [br][br]- [code]roomId[/code]: [String] [ 1 .. 100 ] characters ^[a-zA-Z0-9_-]*$ 104 | func get_connection_info(room_id: String): 105 | return GET("connectioninfo/" + room_id).then(func (result): 106 | if result.is_error(): 107 | return result 108 | return result 109 | ) 110 | 111 | 112 | func update_config(room_id: String, room_config: String): 113 | return POST("update/" + room_id, {"roomConfig": room_config}).then(func (result): 114 | if result.is_error(): 115 | return result 116 | return result 117 | ) 118 | 119 | 120 | -------------------------------------------------------------------------------- /addons/hathora/plugin/apis/room_v2.gd: -------------------------------------------------------------------------------- 1 | ## Operations to create, manage, and connect to rooms. 2 | extends "endpoint.gd" 3 | 4 | const Enums = preload("res://addons/hathora/plugin/enums.gd") 5 | 6 | ## Create a new room for an existing application. 7 | ## Poll the [method get_info] endpoint to get connection details for an active room. 8 | ## Takes: 9 | ## [br][br]- [param room_config]: [String]. Optional configuration parameters for the room. It is accessible from the room via [method get_info]. 10 | ## [br][br]- [param region]: [enum Hathora.Region] 11 | ## [br][br]- optionally [param room_id][ 1 .. 100 ] characters ^[a-zA-Z0-9_-]*$ which overrides the system generated one 12 | ## [br][br][b]Note:[/b] error will be returned if roomId is not globally unique. 13 | ## [br][br][br][b]If successful, calls to this endpoint return:[/b] 14 | ## [br][br]- [code]additionalExposedPorts[/code]: [Array] of dictionaries <= 2 items. Each element contains: 15 | ## [br]-- [code]transportType[/code]: [enum Hathora.TransportType] 16 | ## [br]-- [code]port[/code]: [float] 17 | ## [br]-- [code]host[/code]: [String] 18 | ## [br]-- [code]name[/code]: [String] 19 | ## [br][br] [code]exposedPort[/code]: [Dictionary] containing: 20 | ## [br]-- [code]transportType[/code]: [enum Hathora.TransportType] 21 | ## [br]-- [code]port[/code]: [float] 22 | ## [br]-- [code]host[/code]: [String] 23 | ## [br]-- [code]name[/code]: [String] 24 | ## [br][br] [code]status[/code]: [enum Hathora.RoomStatus] 25 | ## [br][br] [code]roomId[/code]: [String] 26 | ## [br][br] [code]processId[/code]: [String] 27 | func create(region: Enums.Region, room_config:= "", room_id = ""): 28 | return POST("create", empty_string_stripped({ 29 | "region": Enums.REGION_NAMES[region], 30 | "roomConfig": room_config, 31 | }), 32 | empty_string_stripped({"roomId": room_id})).then(func (result): 33 | if result.is_error(): 34 | return result 35 | return result 36 | ) 37 | 38 | ## Retrieve current and historical allocation data for a room. 39 | ## [br][br][br][b]If successful, calls to this endpoint return:[/b] 40 | ## [br][br]- [code]currentAllocation[/code]: [Dictionary] or [code]null[/code] containing: 41 | ## [br]-- [code]unscheduledAt[/code]: [String] or [code]null[/code] 42 | ## [br]-- [code]scheduledAt[/code]: [String] 43 | ## [br]-- [code]processId[/code]: [String]. System generated unique identifier to a runtime instance of your game server. 44 | ## [br]-- [code]roomAllocationId[/code]: [String]. System generated unique identifier to an allocated instance of a room. 45 | ## [br][br] [code]status[/code]: [enum Hathora.RoomStatus] 46 | ## [br][br]- [code]allocations[/code]: [Array] of dictionaries. Each element contains: 47 | ## [br]-- [code]unscheduledAt[/code] or [code]null[/code]:[String] 48 | ## [br]-- [code]scheduledAt[/code]: [String] 49 | ## [br]-- [code]processId[/code]: [String]. System generated unique identifier to a runtime instance of your game server. 50 | ## [br]-- [code]roomAllocationId[/code]: [String]. System generated unique identifier to an allocated instance of a room. 51 | ## [br][br] [code]roomConfig[/code]: [String](<= 10000 characters) or [code]null[/code] 52 | ## [br][br] [code]roomId[/code]: [String] 53 | ## [br][br] [code]appId[/code]: [String] 54 | func get_info(room_id: String): 55 | return GET("info/" + room_id).then(func (result): 56 | if result.is_error(): 57 | return result 58 | return result 59 | ) 60 | 61 | ## Get all active rooms for a given [param process_id]. 62 | ## [br][br][br][b]If successful, calls to this endpoint return:[/b] 63 | ## [br][br][Array] of dictionaries. Each element contains: 64 | ## [br]-- [code]appId[/code]: [String] 65 | ## [br]-- [code]roomId[/code]: [String] 66 | ## [br]-- [code]roomConfig[/code]: [String] or [code]null[/code] 67 | ## [br]-- [code]status[/code]: [enum Hathora.RoomStatus] 68 | ## [br]-- [code]currentAllocation[/code]: [Dictionary] or [code]null[/code]. See [method get_info] for the contents. 69 | func get_active(process_id: String): 70 | return GET("list/" + process_id + "/active").then(func (result): 71 | if result.is_error(): 72 | return result 73 | return result 74 | ) 75 | 76 | ## Get all inactive rooms for a given [param process_id]. See [method get_active] for the contents. 77 | func get_inactive(process_id: String): 78 | return GET("list/" + process_id + "/inactive").then(func (result): 79 | if result.is_error(): 80 | return result 81 | return result 82 | ) 83 | 84 | ## Destroy a room by its [param room_id]. All associated metadata is deleted. 85 | func destroy(room_id: String): 86 | return POST("destroy/" + room_id).then(func (result): 87 | if result.is_error(): 88 | return result 89 | return result 90 | ) 91 | 92 | ## [b]Deprecated[/b] 93 | ## [br]Suspend a room. The room is unallocated from the process but can be rescheduled later using the same roomId. 94 | func suspend(room_id: String): 95 | return POST("suspend/" + room_id).then(func (result): 96 | if result.is_error(): 97 | return result 98 | return result 99 | ) 100 | 101 | ## Poll this endpoint to get connection details to a room. Clients can call this endpoint without authentication. 102 | ## [br][br][br][b]If successful, calls to this endpoint return:[/b] 103 | ## [br][br][code]additionalExposedPorts[/code]: [Array] of dictionaries. Connection details for up to 2 exposed ports. See [method create] for the contents. 104 | ## [br][br][code]exposedPort[/code]: [Dictionary] or [code]null[/code]. Connection details for an active process. See [method create] for the contents. 105 | ## [br][br]- [code]status[/code]: [enum Hathora.ProcessStatus]. Status of the process. 106 | ## [br][br]- [code]roomId[/code]: [String] [ 1 .. 100 ] characters ^[a-zA-Z0-9_-]*$ 107 | func get_connection_info(room_id: String): 108 | return GET("connectioninfo/" + room_id).then(func (result): 109 | if result.is_error(): 110 | return result 111 | return result 112 | ) 113 | 114 | 115 | func update_config(room_id: String, room_config: String): 116 | return POST("update/" + room_id, {"roomConfig": room_config}).then(func (result): 117 | if result.is_error(): 118 | return result 119 | return result 120 | ) 121 | 122 | 123 | -------------------------------------------------------------------------------- /addons/hathora/sdk/rest-client/client.gd: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-present W4 Games Limited. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | ## A generic REST client. 14 | extends RefCounted 15 | 16 | const Request = preload("client_request.gd") 17 | const Result = preload("client_result.gd") 18 | 19 | ## A node in the scene tree that we can add [HTTPRequest]s to. 20 | var node : Node 21 | ## The base URL. 22 | var base_url : String 23 | ## The default [TLSOptions]. 24 | var default_tls_options : TLSOptions = null 25 | ## The default request headers. 26 | var default_headers : PackedStringArray 27 | ## A callback for formatting request data. 28 | var data_formatter := _default_data_formatter 29 | 30 | 31 | ## Creates a REST client. 32 | ## 33 | ## The [param node] parameter is a [Node] in the scene tree that we can add [HTTPRequest]s to. 34 | ## The [param url] parameter is the base URL of the service we'll be making requests to. 35 | ## The [param headers] parameter is default HTTP headers to pass on each request. 36 | func _init(node : Node, url : String, headers : Dictionary = {}, tls_options : TLSOptions = null): 37 | self.node = node 38 | self.base_url = url 39 | self.default_tls_options = tls_options 40 | default_headers = headers_from_dict(headers) 41 | 42 | 43 | func _default_data_formatter(data, headers : PackedStringArray) -> PackedByteArray: 44 | var type = typeof(data) 45 | if type == TYPE_NIL: 46 | return PackedByteArray() 47 | elif type == TYPE_DICTIONARY or type == TYPE_ARRAY: 48 | var json = JSON.stringify(data) 49 | headers.append("content-type:application/json") 50 | return json.to_utf8_buffer() 51 | elif type == TYPE_STRING: 52 | return data.to_utf8_buffer() 53 | elif type == TYPE_PACKED_BYTE_ARRAY: 54 | return data 55 | push_error("Error, failed to encode: ", data, " unsupported type") 56 | return PackedByteArray() 57 | 58 | 59 | ## Makes a [PackedStringArray] combining the default HTTP headers with the given headers. 60 | func headers_from_dict(headers : Dictionary): 61 | var out = PackedStringArray(default_headers) 62 | for k in headers: 63 | out.append(k + ":" + headers[k]) 64 | return out 65 | 66 | 67 | ## Sets a default HTTP header. 68 | func set_header(header : String, value=null): 69 | var found = -1 70 | var h = header.to_lower() 71 | for i in range(0, default_headers.size()): 72 | if not default_headers[i].to_lower().begins_with(h + ":"): 73 | continue 74 | found = i 75 | break 76 | if found >= 0: 77 | if value == null: 78 | default_headers.remove_at(found) 79 | else: 80 | default_headers[found] = "%s:%s" % [header, value] 81 | elif value != null: 82 | default_headers.append("%s:%s" % [header, value]) 83 | 84 | 85 | ## Returns an HTTP query string from the given Dictionary. 86 | func query_from_dict(query: Dictionary) -> String: 87 | if query.is_empty(): 88 | return "" 89 | var q := "" 90 | for k in query: 91 | if query[k] is Array: # Check if the value is an array 92 | for item in query[k]: 93 | q += str(k).uri_encode() + "=" + str(item).uri_encode() + "&" 94 | else: 95 | q += str(k).uri_encode() + "=" + str(query[k]).uri_encode() + "&" 96 | if q.ends_with("&"): 97 | return q.substr(0, q.length() - 1) 98 | return q 99 | 100 | 101 | ## Parses an HTTP query string into a Dictionary. 102 | func dict_from_query(query: String) -> Dictionary: 103 | var query_array = query.split("&") 104 | var query_dictionary := {} 105 | for value in query_array: 106 | var key_value = value.split("=") 107 | query_dictionary[key_value[0].uri_decode()] = key_value[1].uri_decode() 108 | return query_dictionary 109 | 110 | 111 | func _make_request(endpoint : String, query : Dictionary, custom_headers : Dictionary, method : int, data = null) -> Request: 112 | var req := Request.new() 113 | var headers = headers_from_dict(custom_headers) 114 | var body : PackedByteArray = data_formatter.call(data, headers) # May add content type to headers. 115 | var query_str := query_from_dict(query) 116 | req.node = node 117 | req.tls_options = default_tls_options 118 | req.request_method = method 119 | req.request_headers = headers 120 | req.request_body = body 121 | req.request_url = base_url 122 | req.request_path = endpoint 123 | if not query_str.is_empty(): 124 | req.request_path += "?" + query_str 125 | return req 126 | 127 | 128 | ## Makes a GET request. 129 | func GET(path, query : Dictionary = {}, extra_headers : Dictionary = {}) -> Request: 130 | return _make_request(path, query, extra_headers, HTTPClient.METHOD_GET) 131 | 132 | 133 | ## Makes a HEAD request. 134 | func HEAD(path, query : Dictionary = {}, extra_headers : Dictionary = {}) -> Request: 135 | return _make_request(path, query, extra_headers, HTTPClient.METHOD_HEAD) 136 | 137 | 138 | ## Makes a POST request. 139 | func POST(path, data = null, query : Dictionary = {}, extra_headers : Dictionary = {}) -> Request: 140 | return _make_request(path, query, extra_headers, HTTPClient.METHOD_POST, data) 141 | 142 | 143 | ## Makes a PUT request. 144 | func PUT(path, data = null, query : Dictionary = {}, extra_headers : Dictionary = {}) -> Request: 145 | return _make_request(path, query, extra_headers, HTTPClient.METHOD_PUT, data) 146 | 147 | 148 | ## Makes a PATCH request. 149 | func PATCH(path, data = null, query : Dictionary = {}, extra_headers : Dictionary = {}) -> Request: 150 | return _make_request(path, query, extra_headers, HTTPClient.METHOD_PATCH, data) 151 | 152 | 153 | ## Makes a DELETE request. 154 | func DELETE(path, data = null, query : Dictionary = {}, extra_headers : Dictionary = {}) -> Request: 155 | return _make_request(path, query, extra_headers, HTTPClient.METHOD_DELETE, data) 156 | -------------------------------------------------------------------------------- /addons/hathora/plugin/rest-client/client.gd: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-present W4 Games Limited. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | ## A generic REST client. 14 | extends RefCounted 15 | 16 | const Request = preload("client_request.gd") 17 | const Result = preload("client_result.gd") 18 | 19 | ## A node in the scene tree that we can add [HTTPRequest]s to. 20 | var node : Node 21 | ## The base URL. 22 | var base_url : String 23 | ## The default [TLSOptions]. 24 | var default_tls_options : TLSOptions = null 25 | ## The default request headers. 26 | var default_headers : PackedStringArray 27 | ## A callback for formatting request data. 28 | var data_formatter := _default_data_formatter 29 | 30 | 31 | ## Creates a REST client. 32 | ## 33 | ## The [param node] parameter is a [Node] in the scene tree that we can add [HTTPRequest]s to. 34 | ## The [param url] parameter is the base URL of the service we'll be making requests to. 35 | ## The [param headers] parameter is default HTTP headers to pass on each request. 36 | func _init(node : Node, url : String, headers : Dictionary = {}, tls_options : TLSOptions = null): 37 | self.node = node 38 | self.base_url = url 39 | self.default_tls_options = tls_options 40 | default_headers = headers_from_dict(headers) 41 | 42 | 43 | func _default_data_formatter(data, headers : PackedStringArray) -> PackedByteArray: 44 | var type = typeof(data) 45 | if type == TYPE_NIL: 46 | return PackedByteArray() 47 | elif type == TYPE_DICTIONARY or type == TYPE_ARRAY: 48 | var json = JSON.stringify(data) 49 | headers.append("content-type:application/json") 50 | return json.to_utf8_buffer() 51 | elif type == TYPE_STRING: 52 | return data.to_utf8_buffer() 53 | elif type == TYPE_PACKED_BYTE_ARRAY: 54 | return data 55 | push_error("Error, failed to encode: ", data, " unsupported type") 56 | return PackedByteArray() 57 | 58 | 59 | ## Makes a [PackedStringArray] combining the default HTTP headers with the given headers. 60 | func headers_from_dict(headers : Dictionary): 61 | var out = PackedStringArray(default_headers) 62 | for k in headers: 63 | out.append(k + ":" + headers[k]) 64 | return out 65 | 66 | 67 | ## Sets a default HTTP header. 68 | func set_header(header : String, value=null): 69 | var found = -1 70 | var h = header.to_lower() 71 | for i in range(0, default_headers.size()): 72 | if not default_headers[i].to_lower().begins_with(h + ":"): 73 | continue 74 | found = i 75 | break 76 | if found >= 0: 77 | if value == null: 78 | default_headers.remove_at(found) 79 | else: 80 | default_headers[found] = "%s:%s" % [header, value] 81 | elif value != null: 82 | default_headers.append("%s:%s" % [header, value]) 83 | 84 | 85 | ## Returns an HTTP query string from the given Dictionary. 86 | func query_from_dict(query: Dictionary) -> String: 87 | if query.is_empty(): 88 | return "" 89 | var q := "" 90 | for k in query: 91 | if query[k] is Array: # Check if the value is an array 92 | for item in query[k]: 93 | q += str(k).uri_encode() + "=" + str(item).uri_encode() + "&" 94 | else: 95 | q += str(k).uri_encode() + "=" + str(query[k]).uri_encode() + "&" 96 | if q.ends_with("&"): 97 | return q.substr(0, q.length() - 1) 98 | return q 99 | 100 | 101 | ## Parses an HTTP query string into a Dictionary. 102 | func dict_from_query(query: String) -> Dictionary: 103 | var query_array = query.split("&") 104 | var query_dictionary := {} 105 | for value in query_array: 106 | var key_value = value.split("=") 107 | query_dictionary[key_value[0].uri_decode()] = key_value[1].uri_decode() 108 | return query_dictionary 109 | 110 | 111 | func _make_request(endpoint : String, query : Dictionary, custom_headers : Dictionary, method : int, data = null) -> Request: 112 | var req := Request.new() 113 | var headers = headers_from_dict(custom_headers) 114 | var body : PackedByteArray = data_formatter.call(data, headers) # May add content type to headers. 115 | var query_str := query_from_dict(query) 116 | req.node = node 117 | req.tls_options = default_tls_options 118 | req.request_method = method 119 | req.request_headers = headers 120 | req.request_body = body 121 | req.request_url = base_url 122 | req.request_path = endpoint 123 | if not query_str.is_empty(): 124 | req.request_path += "?" + query_str 125 | return req 126 | 127 | 128 | ## Makes a GET request. 129 | func GET(path, query : Dictionary = {}, extra_headers : Dictionary = {}) -> Request: 130 | return _make_request(path, query, extra_headers, HTTPClient.METHOD_GET) 131 | 132 | 133 | ## Makes a HEAD request. 134 | func HEAD(path, query : Dictionary = {}, extra_headers : Dictionary = {}) -> Request: 135 | return _make_request(path, query, extra_headers, HTTPClient.METHOD_HEAD) 136 | 137 | 138 | ## Makes a POST request. 139 | func POST(path, data = null, query : Dictionary = {}, extra_headers : Dictionary = {}) -> Request: 140 | return _make_request(path, query, extra_headers, HTTPClient.METHOD_POST, data) 141 | 142 | 143 | ## Makes a PUT request. 144 | func PUT(path, data = null, query : Dictionary = {}, extra_headers : Dictionary = {}) -> Request: 145 | return _make_request(path, query, extra_headers, HTTPClient.METHOD_PUT, data) 146 | 147 | 148 | ## Makes a PATCH request. 149 | func PATCH(path, data = null, query : Dictionary = {}, extra_headers : Dictionary = {}) -> Request: 150 | return _make_request(path, query, extra_headers, HTTPClient.METHOD_PATCH, data) 151 | 152 | 153 | ## Makes a DELETE request. 154 | func DELETE(path, data = null, query : Dictionary = {}, extra_headers : Dictionary = {}) -> Request: 155 | return _make_request(path, query, extra_headers, HTTPClient.METHOD_DELETE, data) 156 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/settings_panel.gd: -------------------------------------------------------------------------------- 1 | extends VBoxContainer 2 | ## Add UI elements programatically 3 | ## 4 | ## Similar to a Tree, but intended to replicate the look and feel of the EditorInspector 5 | ## See also https://github.com/godotengine/godot-proposals/issues/123 6 | 7 | const PLANS: Array[String] = ["tiny","small","medium","large"] 8 | const TRANSPORT_TYPES: Array[String] = ["udp", "tcp", "tls"] 9 | const REGIONS: Array[String] = [ 10 | "Seattle", 11 | "Washington_DC", 12 | "Chicago", 13 | "London", 14 | "Frankfurt", 15 | "Mumbai", 16 | "Singapore", 17 | "Tokyo", 18 | "Sydney", 19 | "Sao_Paulo", 20 | ] 21 | 22 | var editable_nodes: Array[Node] 23 | 24 | # Disable all UI elements 25 | var read_only: bool = false: 26 | set(v): 27 | read_only = v 28 | for node in editable_nodes: 29 | if "editable" in node: 30 | node.editable = !v 31 | if "disabled" in node: 32 | node.disabled = v 33 | 34 | func _ready() -> void: 35 | _make_settings() 36 | 37 | # Virtual function to make settings 38 | func _make_settings() -> void: 39 | pass 40 | 41 | # Add a new row with a label 42 | func _add_row(p_label: String) -> HBoxContainer: 43 | var container = HBoxContainer.new() 44 | var label = Label.new() 45 | label.text = p_label 46 | label.size_flags_horizontal = Control.SIZE_EXPAND_FILL 47 | label.clip_text = true 48 | label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS 49 | label.add_theme_color_override("font_color", get_theme_color("font_color", "EditorInspectorSection")) 50 | container.add_child(label) 51 | add_child(container) 52 | return container 53 | 54 | # Add a LineEdit 55 | func add_line_edit(p_label: String, p_text: String = "") -> LineEdit: 56 | var container = _add_row(p_label) 57 | var line_edit = LineEdit.new() 58 | line_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL 59 | line_edit.text = p_text 60 | container.add_child(line_edit) 61 | line_edit.add_theme_stylebox_override("normal", get_theme_stylebox("child_bg", "EditorProperty")) 62 | editable_nodes.append(line_edit) 63 | return line_edit 64 | 65 | # Add a LineEdit with a button next to it 66 | func add_line_edit_with_icon(p_label: String, p_text: String, p_icon: Texture2D, on_icon_pressed: Callable) -> LineEdit: 67 | var line_edit = add_line_edit(p_label, p_text) 68 | var button = Button.new() 69 | button.icon = p_icon 70 | button.flat = true 71 | button.focus_mode = Control.FOCUS_NONE 72 | line_edit.add_sibling(button) 73 | var h_container = HBoxContainer.new() 74 | h_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL 75 | line_edit.add_sibling(h_container) 76 | line_edit.reparent(h_container) 77 | button.reparent(h_container) 78 | button.pressed.connect(on_icon_pressed) 79 | editable_nodes.append(button) 80 | return line_edit 81 | 82 | # Add an OptionButton 83 | func add_option_button(p_label: String, p_choices: Array[String]) -> OptionButton: 84 | var container = _add_row(p_label) 85 | var option_button = OptionButton.new() 86 | option_button.clip_text = true 87 | option_button.flat = true 88 | option_button.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS 89 | option_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL 90 | for c in p_choices: 91 | option_button.add_item(c) 92 | container.add_child(option_button) 93 | editable_nodes.append(option_button) 94 | return option_button 95 | 96 | # Add a Checkbox 97 | func add_checkbox(p_label: String, p_checkbox_label: String, p_checked: bool = false) -> CheckBox: 98 | var container = _add_row(p_label) 99 | var checkbox = CheckBox.new() 100 | checkbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL 101 | checkbox.text = p_checkbox_label 102 | checkbox.button_pressed = p_checked 103 | container.add_child(checkbox) 104 | checkbox.add_theme_stylebox_override("normal", get_theme_stylebox("child_bg", "EditorProperty")) 105 | checkbox.add_theme_stylebox_override("pressed", get_theme_stylebox("child_bg", "EditorProperty")) 106 | editable_nodes.append(checkbox) 107 | return checkbox 108 | 109 | # Add an OptionButton with a button next to it 110 | func add_option_button_with_icon(p_label: String, p_choices: Array[String], icon: Texture2D, on_icon_pressed: Callable) -> OptionButton: 111 | var option_button = add_option_button(p_label, p_choices) 112 | var button = Button.new() 113 | button.icon = icon 114 | button.flat = true 115 | option_button.flat = true 116 | option_button.add_sibling(button) 117 | var h_container = HBoxContainer.new() 118 | h_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL 119 | option_button.add_sibling(h_container) 120 | option_button.reparent(h_container) 121 | button.reparent(h_container) 122 | button.pressed.connect(on_icon_pressed) 123 | button.focus_mode = Control.FOCUS_NONE 124 | editable_nodes.append(option_button) 125 | editable_nodes.append(button) 126 | return option_button 127 | 128 | # Add a SpinBox 129 | func add_spinbox(p_label: String, p_min: float, p_max: float, p_step: float) -> SpinBox: 130 | var container = _add_row(p_label) 131 | var spinbox = SpinBox.new() 132 | spinbox.step = p_step 133 | spinbox.min_value = p_min 134 | spinbox.max_value = p_max 135 | spinbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL 136 | var spinbox_line_edit = spinbox.get_line_edit() 137 | spinbox_line_edit.add_theme_stylebox_override("normal", get_theme_stylebox("child_bg", "EditorProperty")) 138 | container.add_child(spinbox) 139 | editable_nodes.append(spinbox) 140 | return spinbox 141 | 142 | # Add a button with an icon 143 | func add_button(p_text: String, p_icon: Texture2D, on_pressed: Callable) -> Button: 144 | var button = Button.new() 145 | button.text = p_text 146 | button.icon = p_icon 147 | button.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS 148 | button.add_theme_stylebox_override("normal", get_theme_stylebox("normal", "InspectorActionButton")) 149 | button.add_theme_stylebox_override("pressed", get_theme_stylebox("pressed", "InspectorActionButton")) 150 | button.add_theme_stylebox_override("hover", get_theme_stylebox("hover", "InspectorActionButton")) 151 | button.add_theme_stylebox_override("disabled", get_theme_stylebox("disabled", "InspectorActionButton")) 152 | add_child(button) 153 | button.pressed.connect(on_pressed) 154 | editable_nodes.append(button) 155 | return button 156 | 157 | # Add a RichTextLabel 158 | func add_rich_text_label(p_text: String, on_meta_clicked:Callable = func(_link): pass) -> RichTextLabel: 159 | var label = RichTextLabel.new() 160 | label.text = p_text 161 | label.fit_content = true 162 | label.scroll_active = false 163 | label.bbcode_enabled = true 164 | label.meta_clicked.connect(on_meta_clicked) 165 | label.add_theme_color_override("font_color", get_theme_color("font_color", "EditorInspectorSection")) 166 | add_child(label) 167 | return label 168 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/modules/build_deployer.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Node 3 | 4 | @export var log_text: RichTextLabel 5 | @onready var sdk = %SDK 6 | 7 | const HathoraProjectSettings = preload("res://addons/hathora/plugin/hathora_project_settings.gd") 8 | const DotEnv = preload("res://addons/hathora/plugin/dotenv.gd") 9 | 10 | var _profile: Dictionary 11 | var last_created_build_id: String 12 | var http_request: HTTPRequest 13 | 14 | func _ready(): 15 | http_request = HTTPRequest.new() 16 | add_child(http_request) 17 | 18 | func do_upload_and_create_build() -> bool: 19 | # Get the file size in bytes 20 | var file = FileAccess.open(HathoraProjectSettings.get_s("path_to_tar_file"), FileAccess.READ) 21 | if not file: 22 | print("[HATHORA] Tar file not found") 23 | return true 24 | var file_size = file.get_length() 25 | 26 | sdk.set_dev_token(DotEnv.get_k("HATHORA_DEVELOPER_TOKEN")) 27 | 28 | 29 | var res = await sdk.builds_v3.create(file_size).async() 30 | if res.is_error(): 31 | print("[HATHORA] Failed to create build: "+ res.as_error().message) 32 | if res.as_error().error == 401: 33 | owner.reset_token() 34 | return true 35 | 36 | res = res.get_data() 37 | var file_content = file.get_buffer(file_size) 38 | 39 | var path_absolute = file.get_path_absolute() 40 | var mb_size = str(get_size_in_mb(file_content)) 41 | print_rich("[HATHORA] Uploading [url=%s]%s[/url] (%sMB)" % [path_absolute.get_base_dir(), path_absolute, mb_size]) 42 | 43 | 44 | #Upload the build 45 | var err = await upload_to_multipart_url(res.uploadParts, res.maxChunkSize, res.completeUploadPostRequestUrl, file_content) 46 | 47 | if err: 48 | print("[HATHORA] Error uploading the build to multipart URL") 49 | return true 50 | 51 | print("[HATHORA] Upload complete, running build in Hathora, this may take several minutes..") 52 | 53 | last_created_build_id = res.buildId 54 | 55 | res = await sdk.builds_v3.run_build(last_created_build_id).async() 56 | 57 | if res.is_error(): 58 | print("[HATHORA] Failed to run build: "+ res.as_error().message) 59 | return true 60 | 61 | print("[HATHORA] Created new build with buildId: %s" % [str(last_created_build_id)]) 62 | 63 | var deployment_config := { 64 | "requestedCPU" = %DeploymentSettings.requested_cpu, 65 | "requestedMemoryMB" = %DeploymentSettings.requested_memory * 1024, 66 | "roomsPerProcess" = %DeploymentSettings.rooms_per_process, 67 | "transportType" = %DeploymentSettings.transport_type, 68 | "containerPort" = %DeploymentSettings.container_port, 69 | "env" = [], 70 | "buildId" = last_created_build_id, 71 | "idleTimeoutEnabled" = false 72 | } 73 | 74 | 75 | if HathoraProjectSettings.get_s("application_id").is_empty(): 76 | print("[HATHORA] Empty appId, cannot create a new deployment") 77 | return true 78 | 79 | # This would automatically update the Deployment Settings, but we do not care because they are remporarily set to read only 80 | var data : Dictionary = await %LatestDeploymentGetter.get_latest_deployment() 81 | 82 | # The API returns resource arrays, which we need to transform into an array of dictionaries 83 | 84 | if "env" in data: 85 | deployment_config.env = data.env 86 | if "additionalContainerPorts" in data: 87 | deployment_config.additionalContainerPorts = data.additionalContainerPorts 88 | if "idleTimeoutEnabled" in data: 89 | deployment_config.idleTimeoutEnabled = data.idleTimeoutEnabled 90 | 91 | res = await sdk.deployments_v3.create(HathoraProjectSettings.get_s("application_id"), deployment_config).async() 92 | 93 | if res.is_error(): 94 | print("[HATHORA] Failed to create deployment: "+ res.as_error().message) 95 | return true 96 | 97 | print("[HATHORA] Created new deployment with deploymentId: %s" % [str(res.deploymentId)]) 98 | # Wait for 2 seconds otherwise the API does not find the latest deployment 99 | await get_tree().create_timer(2.0) 100 | %LatestDeploymentGetter.get_latest_deployment() 101 | return false 102 | 103 | # Function to upload parts 104 | # Helper function to upload file in parts 105 | func upload_to_multipart_url( 106 | multipart_upload_parts: Array, 107 | max_chunk_size: int, 108 | complete_upload_post_request_url: String, 109 | file: PackedByteArray 110 | ) -> bool: 111 | 112 | http_request.cancel_request() 113 | http_request.timeout = 900 114 | if OS.get_name() != "Web": 115 | http_request.use_threads = true 116 | 117 | var upload_promises = [] 118 | 119 | for part in multipart_upload_parts: 120 | var part_number : int = int(part["partNumber"]) 121 | var put_request_url : String = part["putRequestUrl"] 122 | var start_byte_for_part : int = (part_number - 1) * max_chunk_size 123 | var end_byte_for_part : int = min(part_number * max_chunk_size, file.size()) 124 | var file_chunk : PackedByteArray = file.slice(start_byte_for_part, end_byte_for_part) 125 | 126 | # Upload each chunk using an HTTPRequest node 127 | var mb_size = str(get_size_in_mb(file_chunk)) 128 | print("[HATHORA] Uploading part {part_number} ({mb_size}MB) of {total_parts}...".format({"part_number":part_number, "mb_size": mb_size, "total_parts":len(multipart_upload_parts)})) 129 | var err = http_request.request_raw(put_request_url, PackedStringArray(["Content-Type : application/octet-stream", "Content-Length : "+ str(len(file_chunk))]), HTTPClient.METHOD_PUT, file_chunk) 130 | if err != OK: 131 | print("[HATHORA] Build upload HTTP request fail") 132 | return true 133 | 134 | var res = await http_request.request_completed 135 | if res[1] != 200: 136 | print("[HATHORA] HTTP error " + str(res[1])) 137 | var xml : PackedByteArray = res[3] 138 | print("[HATHORA] HTTP response body: " + xml.get_string_from_utf8()) 139 | return true 140 | var headers = res[2] 141 | var etag := "" 142 | for header in headers: 143 | if header.begins_with("ETag:"): 144 | etag = header.split(": ")[1] 145 | 146 | if etag.is_empty(): 147 | print("[HATHORA] ETag not found in response headers for part " + str(part_number)) 148 | return true 149 | 150 | upload_promises.append({ "ETag": etag, "PartNumber": part_number }) 151 | print("[HATHORA] Uploaded part {part_number} of {total_parts}".format({"part_number":part_number, "total_parts":len(multipart_upload_parts)})) 152 | 153 | 154 | # Now, finalize the upload with a POST request containing the parts' ETags 155 | var xml_parts = "" 156 | for part in upload_promises: 157 | xml_parts += """ 158 | 159 | %s 160 | %s 161 | 162 | """ % [str(part["PartNumber"]), str(part["ETag"])] 163 | 164 | var xml_body = "%s" % xml_parts 165 | var err = http_request.request(complete_upload_post_request_url, PackedStringArray(["Content-Type : application/xml"]), HTTPClient.METHOD_POST, xml_body) 166 | if err != OK: 167 | print("[HATHORA] Build upload HTTP request fail") 168 | return true 169 | 170 | var res = await http_request.request_completed 171 | 172 | if res[1] != 200: 173 | print("[HATHORA] Build upload HTTP request fail") 174 | return true 175 | 176 | return false 177 | 178 | func get_size_in_mb(chunk: PackedByteArray) -> float: 179 | var mb_size = float(len(chunk))/1024/1024 180 | mb_size = snapped(mb_size, 0.01) 181 | return mb_size 182 | 183 | # Otherwise Godot hangs when trying to close while an HTTP request is active 184 | func _notification(what): 185 | if what == NOTIFICATION_WM_CLOSE_REQUEST: 186 | http_request.cancel_request() 187 | get_tree().quit() 188 | -------------------------------------------------------------------------------- /addons/hathora/plugin/auth0/auth0Client.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Node 3 | 4 | const DotEnv = preload("../dotenv.gd") 5 | 6 | const CLIENT_ID = "tWjDhuzPmuIWrI8R9s3yV3BQVw2tW0yq"; 7 | const AUTH0_DOMAIN = "https://auth.hathora.com"; 8 | const AUDIENCE_URI = "https://cloud.hathora.com"; 9 | const HATHORA_API_BASE_URL = "https://api.hathora.dev"; 10 | 11 | var isReady = false 12 | var device_code: String 13 | var interval_timer: Timer 14 | var temp: String 15 | 16 | 17 | func _ready(): 18 | if not isReady: 19 | start() 20 | 21 | 22 | func start(): 23 | isReady = true 24 | 25 | func get_token_async(login_complete_cb: Callable) -> void: 26 | # Start the device authorization flow 27 | request_device_authorization(login_complete_cb) 28 | 29 | func request_device_authorization(login_complete_cb: Callable) -> void: 30 | var url = AUTH0_DOMAIN + "/oauth/device/code" 31 | var body = { 32 | "client_id": CLIENT_ID, 33 | "scope": "openid profile email offline_access", 34 | "audience": AUDIENCE_URI 35 | } 36 | 37 | var http_request = HTTPRequest.new() 38 | add_child(http_request) 39 | var err = http_request.request(url, ["Content-Type: application/json"], HTTPClient.METHOD_POST, JSON.stringify(body)) 40 | # print(err) 41 | http_request.request_completed.connect(self._on_request_completed.bind(login_complete_cb)) 42 | 43 | func _on_request_completed(result, response_code, headers, body, login_complete_cb: Callable) -> void: 44 | if response_code == 200: 45 | var j = JSON.new() 46 | var response = j.parse(body.get_string_from_utf8()) 47 | device_code = j.data["device_code"] 48 | var user_code = j.data["user_code"] 49 | var verification_uri = j.data["verification_uri"] 50 | var verification_uri_complete = j.data["verification_uri_complete"] 51 | 52 | # Display the user code and verification URL to the user 53 | print("[HATHORA] Please visit the following URL on your computer or mobile device: %s" % verification_uri_complete) 54 | print("[HATHORA] Confirm this same code is displayed: %s" % user_code) 55 | 56 | print("[HATHORA] Opening browser window..") 57 | OS.shell_open(verification_uri_complete) 58 | 59 | # Start polling for token 60 | interval_timer = Timer.new() 61 | interval_timer.set_wait_time(j.data["interval"]) 62 | interval_timer.connect("timeout", poll_for_token.bind(login_complete_cb)) 63 | add_child(interval_timer) 64 | interval_timer.start() 65 | else: 66 | print("[HATHORA] Device authorization request failed with response code:", response_code) 67 | 68 | func poll_for_token(login_complete_cb: Callable) -> void: 69 | var url = AUTH0_DOMAIN + "/oauth/token" 70 | var body = { 71 | "grant_type": "urn:ietf:params:oauth:grant-type:device_code", 72 | "client_id": CLIENT_ID, 73 | "device_code": device_code 74 | } 75 | 76 | var http_request = HTTPRequest.new() 77 | add_child(http_request) 78 | http_request.request(url, ["Content-Type: application/json"], HTTPClient.METHOD_POST, JSON.stringify(body)) 79 | http_request.request_completed.connect(self._on_token_received.bind(login_complete_cb)) 80 | 81 | func _on_token_received(result, response_code, headers, body, login_complete_cb: Callable) -> void: 82 | if response_code == 200: 83 | var j = JSON.new() 84 | var parse_result = j.parse(body.get_string_from_utf8()) 85 | if parse_result != OK: 86 | print("[HATHORA] Failed to parse token response JSON") 87 | _cleanup_and_fail(login_complete_cb) 88 | return 89 | 90 | if not j.data.has("access_token") or not j.data.has("id_token"): 91 | print("[HATHORA] Expected token fields missing from response") 92 | _cleanup_and_fail(login_complete_cb) 93 | return 94 | 95 | var access_token = j.data["access_token"] 96 | var id_token = j.data["id_token"] 97 | 98 | print("[HATHORA] Authentication complete, beginning process to create Hathora API token") 99 | 100 | # Stop polling for token 101 | _cleanup_timer() 102 | 103 | get_orgs.bind(access_token, login_complete_cb).call() 104 | 105 | elif (response_code == 400 || response_code == 403): 106 | # Token not yet available, continue polling 107 | pass 108 | else: 109 | print("[HATHORA] Token request failed with response code:", response_code) 110 | _cleanup_and_fail(login_complete_cb) 111 | 112 | func _cleanup_timer() -> void: 113 | if interval_timer: 114 | interval_timer.stop() 115 | interval_timer.queue_free() 116 | interval_timer = null 117 | 118 | func _cleanup_and_fail(login_complete_cb: Callable) -> void: 119 | _cleanup_timer() 120 | if login_complete_cb: 121 | login_complete_cb.call(false) 122 | 123 | func get_orgs(access_token: String, login_complete_cb: Callable) -> void: 124 | var url = HATHORA_API_BASE_URL + "/orgs/v1" 125 | 126 | var http_request = HTTPRequest.new() 127 | add_child(http_request) 128 | var auth_header = "Authorization: Bearer " + access_token 129 | var err = http_request.request(url, ["Content-Type: application/json", auth_header], HTTPClient.METHOD_GET) 130 | if err != OK: 131 | print("[HATHORA] Error making request to get orgs:", err) 132 | _cleanup_and_fail(login_complete_cb) 133 | http_request.queue_free() 134 | return 135 | 136 | http_request.request_completed.connect( 137 | func(result, response_code, headers, body): 138 | create_org_token(result, response_code, headers, body, access_token, login_complete_cb) 139 | http_request.queue_free() # Clean up the request node 140 | ) 141 | 142 | func create_org_token(result, response_code, headers, body, access_token: String, login_complete_cb: Callable) -> void: 143 | if response_code != 200: 144 | print("[HATHORA] Get orgs request failed with response code:", response_code) 145 | _cleanup_and_fail(login_complete_cb) 146 | return 147 | 148 | var j = JSON.new() 149 | var parse_result = j.parse(body.get_string_from_utf8()) 150 | if parse_result != OK: 151 | print("[HATHORA] Failed to parse orgs response JSON") 152 | _cleanup_and_fail(login_complete_cb) 153 | return 154 | 155 | if not j.data.has("orgs") or j.data["orgs"].size() == 0: 156 | print("[HATHORA] No organizations found in account") 157 | _cleanup_and_fail(login_complete_cb) 158 | return 159 | 160 | var org_id = j.data["orgs"][0]["orgId"] 161 | var org_scopes = j.data["orgs"][0]["scopes"] 162 | 163 | var url = HATHORA_API_BASE_URL + "/tokens/v1/orgs/" + org_id + "/create" 164 | var reqBody = { 165 | "scopes": org_scopes, 166 | "name": "godot-plugin-token_" + get_formatted_datetime() 167 | } 168 | 169 | var http_request = HTTPRequest.new() 170 | add_child(http_request) 171 | var auth_header = "Authorization: Bearer " + access_token 172 | var err = http_request.request(url, ["Content-Type: application/json", auth_header], HTTPClient.METHOD_POST, JSON.stringify(reqBody)) 173 | if err != OK: 174 | print("[HATHORA] Error making request to create token:", err) 175 | _cleanup_and_fail(login_complete_cb) 176 | http_request.queue_free() 177 | return 178 | 179 | http_request.request_completed.connect( 180 | func(result, response_code, headers, body): 181 | _create_org_token_completed(result, response_code, headers, body, login_complete_cb) 182 | http_request.queue_free() # Clean up the request node 183 | ) 184 | 185 | func _create_org_token_completed(result, response_code, headers, body, login_complete_cb: Callable) -> void: 186 | if response_code >= 200 && response_code < 300: 187 | var j = JSON.new() 188 | var parse_result = j.parse(body.get_string_from_utf8()) 189 | if parse_result != OK: 190 | print("[HATHORA] Failed to parse token creation response JSON") 191 | _cleanup_and_fail(login_complete_cb) 192 | return 193 | 194 | if not j.data.has("plainTextToken"): 195 | print("[HATHORA] Token field missing from response") 196 | _cleanup_and_fail(login_complete_cb) 197 | return 198 | 199 | var org_token = j.data["plainTextToken"] 200 | print("[HATHORA] Successfully created and stored Hathora API token") 201 | DotEnv.add("HATHORA_DEVELOPER_TOKEN", org_token) 202 | 203 | # Login completed, trigger callback 204 | login_complete_cb.call(true) 205 | else: 206 | print("[HATHORA] Create Hathora API token request failed with response code:", response_code) 207 | _cleanup_and_fail(login_complete_cb) 208 | 209 | func get_formatted_datetime() -> String: 210 | var dt = Time.get_datetime_dict_from_system() 211 | return "%02d-%02d-%04d_%02d:%02d:%02d" % [dt.month, dt.day, dt.year, dt.hour, dt.minute, dt.second] 212 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/settings_panels/server_build_settings.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "../settings_panel.gd" 3 | 4 | const DotEnv = preload("res://addons/hathora/plugin/dotenv.gd") 5 | const EXPORT_PRESETS_PATH = 'res://export_presets.cfg' 6 | const CONFIG_PATH = "res://.hathora/config" 7 | const HathoraProjectSettings = preload("res://addons/hathora/plugin/hathora_project_settings.gd") 8 | 9 | var build_dir_path : String : 10 | set(v): 11 | build_dir_path = v 12 | var car = build_dir_n.caret_column 13 | build_dir_n.text = v 14 | build_dir_n.caret_column = car 15 | get: return build_dir_n.text 16 | 17 | var build_filename : String : 18 | set(v): 19 | build_filename = v 20 | var car = build_filename_n.caret_column 21 | build_filename_n.text = v 22 | build_filename_n.caret_column = car 23 | get: return build_filename_n.text 24 | 25 | var selected_preset: String: 26 | get: 27 | if export_preset_n.selected == -1: 28 | return "" 29 | return export_preset_n.get_item_text(export_preset_n.selected) 30 | 31 | var generate_tar_file: bool: 32 | get: return generate_tar_n.button_pressed 33 | 34 | var overwrite_dockerfile: bool: 35 | get: return overwrite_df_n.button_pressed 36 | 37 | var include_config: bool: 38 | get: return include_config_n.button_pressed 39 | 40 | var build_dir_n: LineEdit 41 | var build_filename_n: LineEdit 42 | var export_preset_n: OptionButton 43 | var overwrite_df_n: CheckBox 44 | var include_config_n: CheckBox 45 | var generate_tar_n: CheckBox 46 | 47 | func _make_settings() -> void: 48 | build_dir_n = add_line_edit_with_icon("Build directory", HathoraProjectSettings.get_s("build_directory_path"), get_theme_icon("Folder", "EditorIcons"), _on_folder_button_pressed) 49 | build_dir_n.text_changed.connect(_on_build_dir_text_changed) 50 | build_filename_n = add_line_edit("Build filename", HathoraProjectSettings.get_s("build_filename")) 51 | build_filename_n.text_changed.connect(_on_build_filename_text_changed) 52 | export_preset_n = add_option_button_with_icon("Export preset", [], get_theme_icon("Reload", "EditorIcons"), update_export_presets) 53 | update_export_presets() 54 | overwrite_df_n = add_checkbox("Overwrite Dockerfile", "On") 55 | include_config_n = add_checkbox("Include Hathora config", "On") 56 | generate_tar_n = add_checkbox("Generate tar file", "On", true) 57 | add_button("Generate Server Build", get_theme_icon("Save", "EditorIcons"), _on_generate_server_build_button_pressed) 58 | #We add a spacer under the generate button, then move the build logs under it 59 | add_spacer(false) 60 | %BuildDirFileDialog.dir_selected.connect(_on_dir_selected) 61 | var i = get_child_count() 62 | add_spacer(false) 63 | ProjectSettings.settings_changed.connect(_on_project_settings_changed) 64 | 65 | 66 | func _on_build_dir_text_changed(_new_text: String) -> void: 67 | HathoraProjectSettings.set_s("build_directory_path", build_dir_path) 68 | 69 | func _on_build_filename_text_changed(_new_text: String) -> void: 70 | HathoraProjectSettings.set_s("build_filename", build_filename) 71 | 72 | func _on_project_settings_changed() -> void: 73 | build_dir_path = HathoraProjectSettings.get_s("build_directory_path") 74 | build_filename = HathoraProjectSettings.get_s("build_filename") 75 | 76 | 77 | func update_export_presets() -> void: 78 | clear_presets() 79 | 80 | var file := ConfigFile.new() 81 | if file.load(EXPORT_PRESETS_PATH) != OK: 82 | return 83 | 84 | var presets := [] 85 | for section in file.get_sections(): 86 | if not file.has_section_key(section, 'name'): 87 | continue 88 | var platform = file.get_value(section, 'platform') 89 | # Only add Linux presets 90 | if platform != 'Linux' and platform != 'Linux/X11': 91 | continue 92 | var architecture = file.get_value(section + ".options", 'binary_format/architecture') 93 | presets.push_back({ 94 | name = file.get_value(section, 'name'), 95 | dedicated_server = file.get_value(section, 'dedicated_server', false), 96 | architecture = architecture, 97 | # Only x86_64 and x86_32 supported 98 | architecture_supported = architecture == 'x86_64' or architecture == 'x86_32' 99 | }) 100 | 101 | presets.sort_custom(func (a, b): 102 | if a['dedicated_server'] == b['dedicated_server']: 103 | return a['name'].nocasecmp_to(b['name']) <= 0 104 | return a['dedicated_server'] 105 | ) 106 | 107 | for preset in presets: 108 | export_preset_n.add_item(preset['name']) 109 | # Set disabled if the architecture is unsupported 110 | export_preset_n.set_item_disabled(export_preset_n.item_count - 1, not preset['architecture_supported']) 111 | 112 | if not preset['architecture_supported']: 113 | export_preset_n.set_item_text(export_preset_n.item_count - 1, preset['name'] + " (" + preset['architecture'] + " not supported)") 114 | 115 | var i = export_preset_n.get_selectable_item() 116 | if i == -1: 117 | export_preset_n.disabled = true 118 | export_preset_n.tooltip_text = "No Linux presets found" 119 | return 120 | export_preset_n.selected = i 121 | 122 | func clear_presets() -> void: 123 | export_preset_n.clear() 124 | export_preset_n.tooltip_text = "" 125 | export_preset_n.disabled = false 126 | 127 | func _on_generate_server_build_button_pressed(): 128 | if ! await _generate_server_build(): 129 | _print_fail() 130 | else: 131 | _print_success() 132 | 133 | 134 | func _generate_server_build() -> bool: 135 | if HathoraProjectSettings.get_s("build_directory_path").is_empty(): 136 | print("[HATHORA] Build directory path required") 137 | return false 138 | 139 | if not DirAccess.dir_exists_absolute(HathoraProjectSettings.get_s("build_directory_path")): 140 | print("[HATHORA] Build directory path does not exist") 141 | return false 142 | 143 | if HathoraProjectSettings.get_s("build_filename").is_empty(): 144 | print("[HATHORA] Build filename required") 145 | return false 146 | 147 | if HathoraProjectSettings.get_s("build_filename").get_extension() != "pck": 148 | print("[HATHORA] Build filename must end in .pck") 149 | return false 150 | 151 | if %ServerBuildSettings.selected_preset.is_empty(): 152 | print("[HATHORA] Must select an existing Linux export preset") 153 | return false 154 | 155 | var build_name : String = HathoraProjectSettings.get_s("build_filename") 156 | var build_dir_path : String = HathoraProjectSettings.get_s("build_directory_path") 157 | var build_name_no_ext : String = HathoraProjectSettings.get_s("build_filename").get_slice(".", 0) 158 | var export_preset = %ServerBuildSettings.selected_preset 159 | 160 | if ! await %DockerfileMaker.write_dockerfile( 161 | build_name, 162 | build_dir_path + "/Dockerfile", 163 | %ServerBuildSettings.overwrite_dockerfile): 164 | return false 165 | 166 | await get_tree().process_frame 167 | 168 | if ! await %ProjectExporter.export(build_name, build_dir_path, export_preset): 169 | return false 170 | 171 | 172 | await get_tree().process_frame 173 | 174 | # Put a copy of the config file (.hathora/config) in the root of the build directory 175 | if %ServerBuildSettings.include_config: 176 | if ! await copy_config_file(CONFIG_PATH, build_dir_path.path_join("hathora_config")): 177 | return false 178 | 179 | 180 | if %ServerBuildSettings.generate_tar_file: 181 | # Setting output tar path to HATHORA_BUILD_DIR_PATH+HATHORA_BUILD_FILENAME.tgz 182 | var output_tar_path = HathoraProjectSettings.get_s("build_directory_path").path_join(build_name_no_ext+".tgz") 183 | var file_names := ["Dockerfile", build_name] 184 | 185 | if %ServerBuildSettings.include_config: 186 | file_names.append("hathora_config") 187 | 188 | if ! await %TarMaker.tar_files( 189 | HathoraProjectSettings.get_s("build_directory_path"), 190 | build_name_no_ext+".tgz", 191 | # Adding Dockerfile, and PCK file to the tarball 192 | file_names 193 | ): 194 | return false 195 | 196 | 197 | # Set the path to tar in the plugin UI to our new tarball path 198 | %DeploymentSettings.path_to_tar = output_tar_path 199 | HathoraProjectSettings.set_s("path_to_tar_file", output_tar_path) 200 | 201 | print("[HATHORA] Updated path to tar file in the Deployment Settings") 202 | return true 203 | return true 204 | 205 | 206 | func _on_folder_button_pressed() -> void: 207 | %BuildDirFileDialog.current_dir = build_dir_path 208 | %BuildDirFileDialog.show() 209 | 210 | 211 | func _on_dir_selected(dir: String) -> void: 212 | build_dir_path = dir 213 | HathoraProjectSettings.set_s("build_directory_path", dir) 214 | 215 | 216 | func _print_success(): 217 | print_rich("[color=%s][HATHORA] [b]BUILD SUCCESS at %s [/b][/color]" % [get_theme_color("success_color", "Editor").to_html(), Time.get_time_string_from_system()]) 218 | 219 | 220 | func _print_fail(): 221 | print_rich("[color=%s][HATHORA] [b]BUILD ERROR at %s [/b][/color]" % [get_theme_color("error_color", "Editor").to_html(), Time.get_time_string_from_system()]) 222 | 223 | 224 | func copy_config_file(from_path: String, to_path: String) -> bool: 225 | var config = ConfigFile.new() 226 | var err = config.load(from_path) 227 | 228 | if err: 229 | print("[HATHORA] Could not find Hathora config file") 230 | return false 231 | 232 | err = config.save(to_path) 233 | 234 | if err != OK: 235 | print("[HATHORA] Error saving the config file") 236 | return false 237 | 238 | var absolute_output_path = ProjectSettings.globalize_path(to_path) 239 | print_rich("[color=%s][HATHORA] Saved Hathora config file at [url=%s]%s[/url][/color]" % [get_theme_color("success_color", "Editor").to_html(), absolute_output_path, absolute_output_path]) 240 | return true 241 | -------------------------------------------------------------------------------- /addons/hathora/sdk/apis/poly_result.gd: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-present W4 Games Limited. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | extends RefCounted 14 | 15 | const Result = preload("../rest-client/client_result.gd") 16 | 17 | ## An error result. 18 | class ResultError extends RefCounted: 19 | 20 | ## The error code. 21 | var error = 0 22 | ## The error message. 23 | var message = "" 24 | ## The error data. 25 | var data = null 26 | 27 | func _init(p_error, p_message:="", p_data=null): 28 | error = p_error 29 | message = p_message 30 | data = p_data 31 | 32 | 33 | func _to_string() -> String: 34 | return "Error%s" % { 35 | "error": error, 36 | "message": message, 37 | "data": data if typeof(data) != TYPE_PACKED_BYTE_ARRAY else (str(data.slice(0, 256)) + "..."), 38 | } 39 | 40 | 41 | 42 | ## PolyResult wraps some piece of data, which can be almost any type, including Object, 43 | ## ResultError, Dictionary, Array, PackedByteArray, String, int, float, or bool. 44 | ## 45 | ## Use the [code]is_*()[/code] methods (like [code]is_dict()[/code] to check the type, 46 | ## or the [code]as_*()[/code] methods (like [code]as_dict()[/code] to get the data in 47 | ## the given type (or some "zero" version of that type if it's a different type). 48 | ## 49 | ## You can also refer to sub-properties of the wrapped data, and get them as a new 50 | ## PolyResult object. For example: 51 | ## 52 | ## [codeblock] 53 | ## var dict := { 54 | ## key = "a string" 55 | ## } 56 | ## var result = PolyResult.new(dict) 57 | ## print(result.key.as_string()) 58 | ## [/codeblock] 59 | class PolyResult extends RefCounted: 60 | 61 | var _data = null 62 | var _result : Result = null 63 | 64 | func _init(data=null, result=null): 65 | _data = data 66 | _result = result 67 | 68 | 69 | func _to_string() -> String: 70 | return str(_data) if not is_byte_array() else (str(_data.slice(0, 256)) + "...") 71 | 72 | 73 | func _get(property: StringName): 74 | if property == &"_data": 75 | return _data 76 | elif property == &"_result": 77 | return _result 78 | elif property == &"script": 79 | # Avoid breaking debug. 80 | return get_script() 81 | var err = "Unknown" 82 | if is_dict(): 83 | if property in _data: 84 | return PolyResult.new(_data[property]) 85 | err = "Property %s not found in data." % property 86 | elif is_array() or is_byte_array(): 87 | if str(property).is_valid_int(): 88 | return get_at(str(property).to_int()) 89 | err = "Array access not supported yet. Use get_at(idx) for now" 90 | elif is_error(): 91 | return PolyResult.new(_data.get(property)) 92 | else: 93 | err = "Unsupported data type %d" % typeof(_data) 94 | push_error(err, property, _to_string()) 95 | return PolyResult.new(ResultError.new(-1, err, _data)) 96 | 97 | 98 | ## Returns a new PolyResult for the item at the given index. 99 | ## 100 | ## If the wrapped data isn't an Array or PackedByteArray, or the index is 101 | ## out of bounds, it will return a wrapped ResultError object. 102 | func get_at(idx : int): 103 | var err = "Error: 'get_at'. " 104 | if not is_array() and not is_byte_array(): 105 | err += "Can't use 'get_at(idx)' on non array type (type=%d)" % typeof(_data) 106 | elif idx < 0 or _data.size() <= idx: 107 | err += "Invalid index %s, size: %s" % [idx, _data.size()] 108 | else: 109 | return PolyResult.new(_data[idx]) 110 | push_error(err, _to_string()) 111 | return PolyResult.new(ResultError.new(-1, err, _data)) 112 | 113 | 114 | ## Returns the size or length of the wrapped data. 115 | ## 116 | ## If the wrapped data isn't a Dictionary, Array or String, it will 117 | ## return 0; 118 | func size() -> int: 119 | if is_array() or is_dict(): 120 | return _data.size() 121 | if is_string(): 122 | return _data.length() 123 | return 0 124 | 125 | 126 | ## Returns an Array of the keys from the wrapped data. 127 | ## 128 | ## If the wrapped data isn't a Dictionary, it will return an empty Array. 129 | func keys() -> Array: 130 | if is_dict(): 131 | return _data.keys() 132 | return [] 133 | 134 | 135 | ## Returns an Array of the values from the wrapped data. 136 | ## 137 | ## If the wrapped data isn't an Array or Dictionary, it will return an empty Array. 138 | func values() -> Array: 139 | if is_array(): 140 | return _data 141 | if is_dict(): 142 | return _data.values() 143 | return [] 144 | 145 | 146 | ## Returns the wrapped data. 147 | func get_data(): 148 | return _data 149 | 150 | 151 | ## Returns true if the wrapped data is empty; otherwise, false. 152 | func is_empty() -> bool: 153 | if is_array() or is_dict() or is_string(): 154 | return _data.is_empty() 155 | return is_null() 156 | 157 | 158 | ## Returns true if the wrapped data is [code]null[/code]; otherwise, false. 159 | func is_null() -> bool: 160 | return _data == null 161 | 162 | 163 | ## Returns true if the wrapped data is a string; otherwise, false. 164 | func is_string() -> bool: 165 | return typeof(_data) == TYPE_STRING 166 | 167 | 168 | ## Returns true if the wrapped data is a Dictionary; otherwise, false. 169 | func is_dict() -> bool: 170 | return typeof(_data) == TYPE_DICTIONARY 171 | 172 | 173 | ## Returns true if the wrapped data is an Array; otherwise, false. 174 | func is_array() -> bool: 175 | return typeof(_data) == TYPE_ARRAY 176 | 177 | 178 | ## Returns true if the wrapped data is a PackedByteArray; otherwise, false. 179 | func is_byte_array() -> bool: 180 | return typeof(_data) == TYPE_PACKED_BYTE_ARRAY 181 | 182 | 183 | ## Returns true if the wrapped data is a ResultError; otherwise, false. 184 | func is_error() -> bool: 185 | return typeof(_data) == TYPE_OBJECT and _data is ResultError 186 | 187 | 188 | ## Returns true if the wrapped data is an int; otherwise, false. 189 | func is_int() -> bool: 190 | return typeof(_data) == TYPE_INT 191 | 192 | 193 | ## Returns true if the wrapped data is a float; otherwise, false. 194 | func is_float() -> bool: 195 | return typeof(_data) == TYPE_FLOAT 196 | 197 | 198 | ## Returns true if the wrapped data is a bool; otherwise, false. 199 | func is_bool() -> bool: 200 | return typeof(_data) == TYPE_BOOL 201 | 202 | 203 | ## Returns the wrapped data as a ResultError. 204 | ## 205 | ## If the wrapped data isn't a ResultError, it will return [code]null[/code]. 206 | func as_error() -> ResultError: 207 | return _data if is_error() else null 208 | 209 | 210 | ## Returns the wrapped data as a Dictionary. 211 | ## 212 | ## If the wrapped data isn't a Dictionary, it will return an empty Dictionary. 213 | func as_dict() -> Dictionary: 214 | return _data if is_dict() else {} 215 | 216 | 217 | ## Returns the wrapped data as an Array. 218 | ## 219 | ## If the wrapped data isn't an Array, it will return an empty Array. 220 | func as_array() -> Array: 221 | return _data if is_array() else [] 222 | 223 | 224 | ## Returns the wrapped data as a PackedByteArray. 225 | ## 226 | ## If the wrapped data isn't a PackedByteArray, it will return an empty PackedByteArray. 227 | func as_bytes() -> PackedByteArray: 228 | return _data if is_byte_array() else PackedByteArray() 229 | 230 | 231 | ## Returns the wrapped data as a String. 232 | ## 233 | ## If the wrapped data is a bool, int or float, it will be converted to a String. 234 | ## 235 | ## If the wrapped data isn't a String or any of those, it will return an empty String. 236 | func as_string() -> String: 237 | return str(_data) if is_string() or is_bool() or is_int() or is_float() else "" 238 | 239 | 240 | ## Returns the wrapped data as an int. 241 | ## 242 | ## If the wrapped data is a bool or float, it will be converted to an int. 243 | ## 244 | ## If the wrapped data isn't an int or any of those, it will return [code]0[/code]. 245 | func as_int() -> int: 246 | return int(_data) if is_bool() or is_int() or is_float() else 0 247 | 248 | 249 | ## Returns the wrapped data as a float. 250 | ## 251 | ## If the wrapped data is a bool or int, it will be converted to a float. 252 | ## 253 | ## If the wrapped data isn't a float or any of those, it will return [code]0.0[/code]. 254 | func as_float() -> float: 255 | return float(_data) if is_bool() or is_int() or is_float() else 0.0 256 | 257 | 258 | ## Returns the wrapped data as a bool. 259 | ## 260 | ## If the wrapped data is a int or float, it will be converted to a bool. 261 | ## 262 | ## If the wrapped data isn't a bool or any of those, it will return [code]false[/code]. 263 | func as_bool() -> bool: 264 | return bool(_data) if is_bool() or is_int() or is_float() else false 265 | 266 | 267 | ## Returns the headers from the HTTP result associated with this PolyResult. 268 | ## 269 | ## If there is no associated HTTP result, it will return an empty Dictionary. 270 | func get_headers() -> Dictionary: 271 | return _result.dict_headers() if _result != null else {} 272 | 273 | 274 | ## Returns the HTTP result associated with this PolyResult. 275 | ## 276 | ## If there is no associated HTTP result, it will return [code]null[/code]. 277 | func get_http_result(): 278 | return _result 279 | 280 | 281 | ## Parses an HTTP result into a PolyResult. 282 | ## 283 | ## If [param p_raw] is [code]false[/code] (the default) then the response body will be parsed as JSON, 284 | ## and wrapped in the PolyResult. Otherwise, the body will be wrapped in the PolyResult as a PackedByteArray. 285 | static func parse_result(result : Result, p_raw:=false) -> PolyResult: 286 | if result.is_error(): 287 | return PolyResult.new(ResultError.new(result.http_request_result, "Network Error", result), result) 288 | elif result.is_http_success(): 289 | if p_raw: 290 | return PolyResult.new(result.bytes_result(), result) 291 | if result.body.size(): 292 | return PolyResult.new(parse_enums(result.json_result()), result) 293 | return PolyResult.new(null, result) 294 | elif result.is_http_redirect(): 295 | return PolyResult.new(null, result) 296 | elif p_raw: 297 | return PolyResult.new(ResultError.new(result.get_http_status_code(), "HTTP Error", result.bytes_result()), result) 298 | else: 299 | var json = result.json_result() 300 | var error = result.get_http_status_code() 301 | var msg = "Unknown error" 302 | if typeof(json) == TYPE_DICTIONARY: 303 | if "reason" in json: 304 | msg = json["reason"] 305 | if "error" in json: 306 | error = json["error"] 307 | if "error_description" in json: 308 | msg = json["error_description"] 309 | if "msg" in json: 310 | msg = json["msg"] 311 | if "message" in json: 312 | msg = json["message"] 313 | else: 314 | # So that we have _something_ from the remote server that might give a 315 | # clue as to the error, use the text content of the result. 316 | json = result.text_result() 317 | return PolyResult.new(ResultError.new(error, msg, json), result) 318 | 319 | static func parse_enums(result: Variant) -> Variant: 320 | match typeof(result): 321 | TYPE_ARRAY: 322 | for i in result: 323 | i = parse_entry(i) 324 | return result 325 | TYPE_DICTIONARY: 326 | return parse_entry(result) 327 | _: 328 | return result 329 | 330 | static func parse_entry(result: Dictionary) -> Dictionary: 331 | if "region" in result and result.region: 332 | result.region = Hathora.REGION_NAMES.find_key(result.region) 333 | if "status" in result and result.status and "processId" in result: 334 | result.status = Hathora.PROCESS_STATUSES.find_key(result.status) 335 | if "status" in result and result.status and "roomId" in result: 336 | result.status = Hathora.ROOM_STATUSES.find_key(result.status) 337 | if "exposedPort" in result and result.exposedPort and "transportType" in result.exposedPort: 338 | result.exposedPort.transportType = Hathora.TRANSPORT_TYPES.find_key(result.exposedPort.transportType) 339 | if "additionalExposedPorts" in result and result.additionalExposedPorts: 340 | for port in result.additionalExposedPorts: 341 | port.transportType = Hathora.TRANSPORT_TYPES.find_key(port.transportType) 342 | return result 343 | -------------------------------------------------------------------------------- /addons/hathora/plugin/apis/poly_result.gd: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-present W4 Games Limited. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | extends RefCounted 14 | 15 | const Result = preload("../rest-client/client_result.gd") 16 | const Enums = preload("res://addons/hathora/plugin/enums.gd") 17 | 18 | ## An error result. 19 | class ResultError extends RefCounted: 20 | 21 | ## The error code. 22 | var error = 0 23 | ## The error message. 24 | var message = "" 25 | ## The error data. 26 | var data = null 27 | 28 | func _init(p_error, p_message:="", p_data=null): 29 | error = p_error 30 | message = p_message 31 | data = p_data 32 | 33 | 34 | func _to_string() -> String: 35 | return "Error%s" % { 36 | "error": error, 37 | "message": message, 38 | "data": data if typeof(data) != TYPE_PACKED_BYTE_ARRAY else (str(data.slice(0, 256)) + "..."), 39 | } 40 | 41 | 42 | 43 | ## PolyResult wraps some piece of data, which can be almost any type, including Object, 44 | ## ResultError, Dictionary, Array, PackedByteArray, String, int, float, or bool. 45 | ## 46 | ## Use the [code]is_*()[/code] methods (like [code]is_dict()[/code] to check the type, 47 | ## or the [code]as_*()[/code] methods (like [code]as_dict()[/code] to get the data in 48 | ## the given type (or some "zero" version of that type if it's a different type). 49 | ## 50 | ## You can also refer to sub-properties of the wrapped data, and get them as a new 51 | ## PolyResult object. For example: 52 | ## 53 | ## [codeblock] 54 | ## var dict := { 55 | ## key = "a string" 56 | ## } 57 | ## var result = PolyResult.new(dict) 58 | ## print(result.key.as_string()) 59 | ## [/codeblock] 60 | class PolyResult extends RefCounted: 61 | 62 | var _data = null 63 | var _result : Result = null 64 | 65 | func _init(data=null, result=null): 66 | _data = data 67 | _result = result 68 | 69 | 70 | func _to_string() -> String: 71 | return str(_data) if not is_byte_array() else (str(_data.slice(0, 256)) + "...") 72 | 73 | 74 | func _get(property: StringName): 75 | if property == &"_data": 76 | return _data 77 | elif property == &"_result": 78 | return _result 79 | elif property == &"script": 80 | # Avoid breaking debug. 81 | return get_script() 82 | var err = "Unknown" 83 | if is_dict(): 84 | if property in _data: 85 | return PolyResult.new(_data[property]) 86 | err = "Property %s not found in data." % property 87 | elif is_array() or is_byte_array(): 88 | if str(property).is_valid_int(): 89 | return get_at(str(property).to_int()) 90 | err = "Array access not supported yet. Use get_at(idx) for now" 91 | elif is_error(): 92 | return PolyResult.new(_data.get(property)) 93 | else: 94 | err = "Unsupported data type %d" % typeof(_data) 95 | push_error(err, property, _to_string()) 96 | return PolyResult.new(ResultError.new(-1, err, _data)) 97 | 98 | 99 | ## Returns a new PolyResult for the item at the given index. 100 | ## 101 | ## If the wrapped data isn't an Array or PackedByteArray, or the index is 102 | ## out of bounds, it will return a wrapped ResultError object. 103 | func get_at(idx : int): 104 | var err = "Error: 'get_at'. " 105 | if not is_array() and not is_byte_array(): 106 | err += "Can't use 'get_at(idx)' on non array type (type=%d)" % typeof(_data) 107 | elif idx < 0 or _data.size() <= idx: 108 | err += "Invalid index %s, size: %s" % [idx, _data.size()] 109 | else: 110 | return PolyResult.new(_data[idx]) 111 | push_error(err, _to_string()) 112 | return PolyResult.new(ResultError.new(-1, err, _data)) 113 | 114 | 115 | ## Returns the size or length of the wrapped data. 116 | ## 117 | ## If the wrapped data isn't a Dictionary, Array or String, it will 118 | ## return 0; 119 | func size() -> int: 120 | if is_array() or is_dict(): 121 | return _data.size() 122 | if is_string(): 123 | return _data.length() 124 | return 0 125 | 126 | 127 | ## Returns an Array of the keys from the wrapped data. 128 | ## 129 | ## If the wrapped data isn't a Dictionary, it will return an empty Array. 130 | func keys() -> Array: 131 | if is_dict(): 132 | return _data.keys() 133 | return [] 134 | 135 | 136 | ## Returns an Array of the values from the wrapped data. 137 | ## 138 | ## If the wrapped data isn't an Array or Dictionary, it will return an empty Array. 139 | func values() -> Array: 140 | if is_array(): 141 | return _data 142 | if is_dict(): 143 | return _data.values() 144 | return [] 145 | 146 | 147 | ## Returns the wrapped data. 148 | func get_data(): 149 | return _data 150 | 151 | 152 | ## Returns true if the wrapped data is empty; otherwise, false. 153 | func is_empty() -> bool: 154 | if is_array() or is_dict() or is_string(): 155 | return _data.is_empty() 156 | return is_null() 157 | 158 | 159 | ## Returns true if the wrapped data is [code]null[/code]; otherwise, false. 160 | func is_null() -> bool: 161 | return _data == null 162 | 163 | 164 | ## Returns true if the wrapped data is a string; otherwise, false. 165 | func is_string() -> bool: 166 | return typeof(_data) == TYPE_STRING 167 | 168 | 169 | ## Returns true if the wrapped data is a Dictionary; otherwise, false. 170 | func is_dict() -> bool: 171 | return typeof(_data) == TYPE_DICTIONARY 172 | 173 | 174 | ## Returns true if the wrapped data is an Array; otherwise, false. 175 | func is_array() -> bool: 176 | return typeof(_data) == TYPE_ARRAY 177 | 178 | 179 | ## Returns true if the wrapped data is a PackedByteArray; otherwise, false. 180 | func is_byte_array() -> bool: 181 | return typeof(_data) == TYPE_PACKED_BYTE_ARRAY 182 | 183 | 184 | ## Returns true if the wrapped data is a ResultError; otherwise, false. 185 | func is_error() -> bool: 186 | return typeof(_data) == TYPE_OBJECT and _data is ResultError 187 | 188 | 189 | ## Returns true if the wrapped data is an int; otherwise, false. 190 | func is_int() -> bool: 191 | return typeof(_data) == TYPE_INT 192 | 193 | 194 | ## Returns true if the wrapped data is a float; otherwise, false. 195 | func is_float() -> bool: 196 | return typeof(_data) == TYPE_FLOAT 197 | 198 | 199 | ## Returns true if the wrapped data is a bool; otherwise, false. 200 | func is_bool() -> bool: 201 | return typeof(_data) == TYPE_BOOL 202 | 203 | 204 | ## Returns the wrapped data as a ResultError. 205 | ## 206 | ## If the wrapped data isn't a ResultError, it will return [code]null[/code]. 207 | func as_error() -> ResultError: 208 | return _data if is_error() else null 209 | 210 | 211 | ## Returns the wrapped data as a Dictionary. 212 | ## 213 | ## If the wrapped data isn't a Dictionary, it will return an empty Dictionary. 214 | func as_dict() -> Dictionary: 215 | return _data if is_dict() else {} 216 | 217 | 218 | ## Returns the wrapped data as an Array. 219 | ## 220 | ## If the wrapped data isn't an Array, it will return an empty Array. 221 | func as_array() -> Array: 222 | return _data if is_array() else [] 223 | 224 | 225 | ## Returns the wrapped data as a PackedByteArray. 226 | ## 227 | ## If the wrapped data isn't a PackedByteArray, it will return an empty PackedByteArray. 228 | func as_bytes() -> PackedByteArray: 229 | return _data if is_byte_array() else PackedByteArray() 230 | 231 | 232 | ## Returns the wrapped data as a String. 233 | ## 234 | ## If the wrapped data is a bool, int or float, it will be converted to a String. 235 | ## 236 | ## If the wrapped data isn't a String or any of those, it will return an empty String. 237 | func as_string() -> String: 238 | return str(_data) if is_string() or is_bool() or is_int() or is_float() else "" 239 | 240 | 241 | ## Returns the wrapped data as an int. 242 | ## 243 | ## If the wrapped data is a bool or float, it will be converted to an int. 244 | ## 245 | ## If the wrapped data isn't an int or any of those, it will return [code]0[/code]. 246 | func as_int() -> int: 247 | return int(_data) if is_bool() or is_int() or is_float() else 0 248 | 249 | 250 | ## Returns the wrapped data as a float. 251 | ## 252 | ## If the wrapped data is a bool or int, it will be converted to a float. 253 | ## 254 | ## If the wrapped data isn't a float or any of those, it will return [code]0.0[/code]. 255 | func as_float() -> float: 256 | return float(_data) if is_bool() or is_int() or is_float() else 0.0 257 | 258 | 259 | ## Returns the wrapped data as a bool. 260 | ## 261 | ## If the wrapped data is a int or float, it will be converted to a bool. 262 | ## 263 | ## If the wrapped data isn't a bool or any of those, it will return [code]false[/code]. 264 | func as_bool() -> bool: 265 | return bool(_data) if is_bool() or is_int() or is_float() else false 266 | 267 | 268 | ## Returns the headers from the HTTP result associated with this PolyResult. 269 | ## 270 | ## If there is no associated HTTP result, it will return an empty Dictionary. 271 | func get_headers() -> Dictionary: 272 | return _result.dict_headers() if _result != null else {} 273 | 274 | 275 | ## Returns the HTTP result associated with this PolyResult. 276 | ## 277 | ## If there is no associated HTTP result, it will return [code]null[/code]. 278 | func get_http_result(): 279 | return _result 280 | 281 | 282 | ## Parses an HTTP result into a PolyResult. 283 | ## 284 | ## If [param p_raw] is [code]false[/code] (the default) then the response body will be parsed as JSON, 285 | ## and wrapped in the PolyResult. Otherwise, the body will be wrapped in the PolyResult as a PackedByteArray. 286 | static func parse_result(result : Result, p_raw:=false) -> PolyResult: 287 | if result.is_error(): 288 | return PolyResult.new(ResultError.new(result.http_request_result, "Network Error", result), result) 289 | elif result.is_http_success(): 290 | if p_raw: 291 | return PolyResult.new(result.bytes_result(), result) 292 | if result.body.size(): 293 | return PolyResult.new(parse_enums(result.json_result()), result) 294 | return PolyResult.new(null, result) 295 | elif result.is_http_redirect(): 296 | return PolyResult.new(null, result) 297 | elif p_raw: 298 | return PolyResult.new(ResultError.new(result.get_http_status_code(), "HTTP Error", result.bytes_result()), result) 299 | else: 300 | var json = result.json_result() 301 | var error = result.get_http_status_code() 302 | var msg = "Unknown error" 303 | if typeof(json) == TYPE_DICTIONARY: 304 | if "reason" in json: 305 | msg = json["reason"] 306 | if "error" in json: 307 | error = json["error"] 308 | if "error_description" in json: 309 | msg = json["error_description"] 310 | if "msg" in json: 311 | msg = json["msg"] 312 | if "message" in json: 313 | msg = json["message"] 314 | else: 315 | # So that we have _something_ from the remote server that might give a 316 | # clue as to the error, use the text content of the result. 317 | json = result.text_result() 318 | return PolyResult.new(ResultError.new(error, msg, json), result) 319 | 320 | static func parse_enums(result: Variant) -> Variant: 321 | match typeof(result): 322 | TYPE_ARRAY: 323 | for i in result: 324 | i = parse_entry(i) 325 | return result 326 | TYPE_DICTIONARY: 327 | return parse_entry(result) 328 | _: 329 | return result 330 | 331 | static func parse_entry(result: Dictionary) -> Dictionary: 332 | if "region" in result and result.region: 333 | result.region = Enums.REGION_NAMES.find_key(result.region) 334 | if "status" in result and result.status and "processId" in result: 335 | result.status = Enums.PROCESS_STATUSES.find_key(result.status) 336 | if "status" in result and result.status and "roomId" in result: 337 | result.status = Enums.ROOM_STATUSES.find_key(result.status) 338 | if "exposedPort" in result and result.exposedPort and "transportType" in result.exposedPort: 339 | result.exposedPort.transportType = Enums.TRANSPORT_TYPES.find_key(result.exposedPort.transportType) 340 | if "additionalExposedPorts" in result and result.additionalExposedPorts: 341 | for port in result.additionalExposedPorts: 342 | port.transportType = Enums.TRANSPORT_TYPES.find_key(port.transportType) 343 | return result 344 | -------------------------------------------------------------------------------- /addons/hathora/plugin/editor/control_ui.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=17 format=3 uid="uid://cme6nmukjil8s"] 2 | 3 | [ext_resource type="Script" path="res://addons/hathora/plugin/editor/control_ui.gd" id="1_1eov4"] 4 | [ext_resource type="Texture2D" uid="uid://pjo82re3x7fd" path="res://addons/hathora/plugin/assets/HathoraConfigBanner.png" id="1_ov2ni"] 5 | [ext_resource type="Script" path="res://addons/hathora/plugin/editor/ascii_art_text_label.gd" id="3_0p138"] 6 | [ext_resource type="Script" path="res://addons/hathora/plugin/editor/modules/project_exporter.gd" id="3_bp7wg"] 7 | [ext_resource type="Script" path="res://addons/hathora/plugin/editor/section_toggle.gd" id="3_mi8w7"] 8 | [ext_resource type="Script" path="res://addons/hathora/plugin/editor/header_buttons.gd" id="3_uvapc"] 9 | [ext_resource type="Script" path="res://addons/hathora/plugin/editor/modules/tar_maker.gd" id="4_fftlc"] 10 | [ext_resource type="Script" path="res://addons/hathora/plugin/editor/settings_panels/developer_settings.gd" id="4_lffpm"] 11 | [ext_resource type="Script" path="res://addons/hathora/plugin/editor/modules/dockerfile_maker.gd" id="5_uqf2c"] 12 | [ext_resource type="Script" path="res://addons/hathora/plugin/editor/modules/build_deployer.gd" id="6_2m224"] 13 | [ext_resource type="Script" path="res://addons/hathora/plugin/editor/settings_panels/server_build_settings.gd" id="6_o00nj"] 14 | [ext_resource type="Script" path="res://addons/hathora/plugin/editor/settings_panels/deployment_settings.gd" id="8_kwcbp"] 15 | [ext_resource type="Script" path="res://addons/hathora/plugin/editor/settings_panels/room_settings.gd" id="9_lsqtf"] 16 | [ext_resource type="Script" path="res://addons/hathora/plugin/editor/latest_deployment_info.gd" id="9_qjw1b"] 17 | [ext_resource type="Script" path="res://addons/hathora/plugin/editor/latest_deployment_getter.gd" id="15_mejlx"] 18 | [ext_resource type="Script" path="res://addons/hathora/plugin/client.gd" id="16_qf06x"] 19 | 20 | [node name="Hathora" type="VBoxContainer"] 21 | anchors_preset = 9 22 | anchor_bottom = 1.0 23 | offset_right = 8.0 24 | grow_vertical = 2 25 | script = ExtResource("1_1eov4") 26 | 27 | [node name="Scroll" type="ScrollContainer" parent="."] 28 | layout_mode = 2 29 | size_flags_vertical = 3 30 | follow_focus = true 31 | horizontal_scroll_mode = 0 32 | vertical_scroll_mode = 2 33 | 34 | [node name="ScrollChild" type="VBoxContainer" parent="Scroll"] 35 | layout_mode = 2 36 | size_flags_horizontal = 3 37 | 38 | [node name="MarginContainer" type="MarginContainer" parent="Scroll/ScrollChild"] 39 | layout_mode = 2 40 | theme_override_constants/margin_top = 16 41 | 42 | [node name="VBoxContainer" type="VBoxContainer" parent="Scroll/ScrollChild/MarginContainer"] 43 | layout_mode = 2 44 | 45 | [node name="HathoraLogo" type="TextureRect" parent="Scroll/ScrollChild/MarginContainer/VBoxContainer"] 46 | custom_minimum_size = Vector2(300, 50) 47 | layout_mode = 2 48 | texture = ExtResource("1_ov2ni") 49 | expand_mode = 1 50 | stretch_mode = 5 51 | 52 | [node name="Label" type="Label" parent="Scroll/ScrollChild/MarginContainer/VBoxContainer"] 53 | layout_mode = 2 54 | text = "Multiplayer Server Hosting" 55 | horizontal_alignment = 1 56 | 57 | [node name="HeaderButtonsContainer" type="HBoxContainer" parent="Scroll/ScrollChild/MarginContainer/VBoxContainer"] 58 | layout_mode = 2 59 | alignment = 1 60 | script = ExtResource("3_uvapc") 61 | 62 | [node name="Console" type="Button" parent="Scroll/ScrollChild/MarginContainer/VBoxContainer/HeaderButtonsContainer"] 63 | layout_mode = 2 64 | focus_mode = 0 65 | text = "Console" 66 | flat = true 67 | 68 | [node name="Tutorial" type="Button" parent="Scroll/ScrollChild/MarginContainer/VBoxContainer/HeaderButtonsContainer"] 69 | visible = false 70 | layout_mode = 2 71 | focus_mode = 0 72 | text = "Tutorial" 73 | flat = true 74 | 75 | [node name="Docs" type="MenuButton" parent="Scroll/ScrollChild/MarginContainer/VBoxContainer/HeaderButtonsContainer"] 76 | layout_mode = 2 77 | text = "Docs" 78 | item_count = 2 79 | popup/item_0/text = "Plugin" 80 | popup/item_0/id = 1 81 | popup/item_1/text = "SDK" 82 | popup/item_1/id = 1 83 | 84 | [node name="Discord" type="Button" parent="Scroll/ScrollChild/MarginContainer/VBoxContainer/HeaderButtonsContainer"] 85 | layout_mode = 2 86 | focus_mode = 0 87 | text = "Discord" 88 | flat = true 89 | 90 | [node name="LoginContent" type="VBoxContainer" parent="Scroll/ScrollChild"] 91 | unique_name_in_owner = true 92 | visible = false 93 | layout_mode = 2 94 | 95 | [node name="Label" type="Label" parent="Scroll/ScrollChild/LoginContent"] 96 | layout_mode = 2 97 | theme_override_colors/font_color = Color(0.760784, 0.760784, 0.760784, 1) 98 | text = "Create an account or log in to Hathora Cloud to get started" 99 | horizontal_alignment = 1 100 | autowrap_mode = 3 101 | 102 | [node name="LoginButton" type="Button" parent="Scroll/ScrollChild/LoginContent"] 103 | layout_mode = 2 104 | text = "Login to Hathora" 105 | 106 | [node name="MainContentPanel" type="PanelContainer" parent="Scroll/ScrollChild"] 107 | unique_name_in_owner = true 108 | layout_mode = 2 109 | 110 | [node name="MainContent" type="VBoxContainer" parent="Scroll/ScrollChild/MainContentPanel"] 111 | layout_mode = 2 112 | 113 | [node name="ASCIIArt" type="RichTextLabel" parent="Scroll/ScrollChild/MainContentPanel/MainContent"] 114 | unique_name_in_owner = true 115 | visible = false 116 | custom_minimum_size = Vector2(2.08165e-12, 150) 117 | layout_mode = 2 118 | focus_mode = 1 119 | theme_override_colors/default_color = Color(0, 0, 0, 1) 120 | scroll_active = false 121 | autowrap_mode = 0 122 | script = ExtResource("3_0p138") 123 | 124 | [node name="DeveloperSectionToggle" type="Button" parent="Scroll/ScrollChild/MainContentPanel/MainContent" node_paths=PackedStringArray("node_to_show")] 125 | unique_name_in_owner = true 126 | layout_mode = 2 127 | toggle_mode = true 128 | button_pressed = true 129 | text = "Developer Settings" 130 | alignment = 0 131 | script = ExtResource("3_mi8w7") 132 | node_to_show = NodePath("../DeveloperSectionContainer") 133 | 134 | [node name="DeveloperSectionContainer" type="MarginContainer" parent="Scroll/ScrollChild/MainContentPanel/MainContent"] 135 | layout_mode = 2 136 | theme_override_constants/margin_left = 40 137 | theme_override_constants/margin_bottom = 10 138 | 139 | [node name="DeveloperSettings" type="VBoxContainer" parent="Scroll/ScrollChild/MainContentPanel/MainContent/DeveloperSectionContainer"] 140 | unique_name_in_owner = true 141 | layout_mode = 2 142 | script = ExtResource("4_lffpm") 143 | 144 | [node name="BuildSectionToggle" type="Button" parent="Scroll/ScrollChild/MainContentPanel/MainContent" node_paths=PackedStringArray("node_to_show")] 145 | layout_mode = 2 146 | theme_override_colors/font_hover_pressed_color = Color(1, 1, 1, 1) 147 | theme_override_colors/font_pressed_color = Color(1, 1, 1, 1) 148 | theme_override_colors/font_focus_color = Color(0.95, 0.95, 0.95, 1) 149 | theme_override_colors/font_color = Color(0.875, 0.875, 0.875, 1) 150 | theme_override_colors/icon_pressed_color = Color(1, 1, 1, 1) 151 | toggle_mode = true 152 | text = "Server Build Settings" 153 | alignment = 0 154 | script = ExtResource("3_mi8w7") 155 | node_to_show = NodePath("../BuildSectionContainer") 156 | 157 | [node name="BuildSectionContainer" type="MarginContainer" parent="Scroll/ScrollChild/MainContentPanel/MainContent"] 158 | visible = false 159 | layout_mode = 2 160 | theme_override_constants/margin_left = 40 161 | theme_override_constants/margin_bottom = 10 162 | 163 | [node name="ServerBuildSettings" type="VBoxContainer" parent="Scroll/ScrollChild/MainContentPanel/MainContent/BuildSectionContainer"] 164 | unique_name_in_owner = true 165 | layout_mode = 2 166 | script = ExtResource("6_o00nj") 167 | 168 | [node name="DeploySectionToggle" type="Button" parent="Scroll/ScrollChild/MainContentPanel/MainContent" node_paths=PackedStringArray("node_to_show")] 169 | layout_mode = 2 170 | theme_override_colors/font_hover_pressed_color = Color(1, 1, 1, 1) 171 | theme_override_colors/font_pressed_color = Color(1, 1, 1, 1) 172 | theme_override_colors/font_focus_color = Color(0.95, 0.95, 0.95, 1) 173 | theme_override_colors/font_color = Color(0.875, 0.875, 0.875, 1) 174 | theme_override_colors/icon_pressed_color = Color(1, 1, 1, 1) 175 | toggle_mode = true 176 | text = "Deployment Settings" 177 | alignment = 0 178 | script = ExtResource("3_mi8w7") 179 | node_to_show = NodePath("../DeploySectionContainer") 180 | 181 | [node name="DeploySectionContainer" type="MarginContainer" parent="Scroll/ScrollChild/MainContentPanel/MainContent"] 182 | visible = false 183 | layout_mode = 2 184 | theme_override_constants/margin_left = 40 185 | theme_override_constants/margin_bottom = 10 186 | 187 | [node name="DeploymentSettings" type="VBoxContainer" parent="Scroll/ScrollChild/MainContentPanel/MainContent/DeploySectionContainer"] 188 | unique_name_in_owner = true 189 | layout_mode = 2 190 | script = ExtResource("8_kwcbp") 191 | 192 | [node name="RoomSectionToggle" type="Button" parent="Scroll/ScrollChild/MainContentPanel/MainContent" node_paths=PackedStringArray("node_to_show")] 193 | unique_name_in_owner = true 194 | layout_mode = 2 195 | theme_override_colors/font_hover_pressed_color = Color(1, 1, 1, 1) 196 | theme_override_colors/font_pressed_color = Color(1, 1, 1, 1) 197 | theme_override_colors/font_focus_color = Color(0.95, 0.95, 0.95, 1) 198 | theme_override_colors/font_color = Color(0.875, 0.875, 0.875, 1) 199 | theme_override_colors/icon_pressed_color = Color(1, 1, 1, 1) 200 | toggle_mode = true 201 | text = "Create Room" 202 | alignment = 0 203 | script = ExtResource("3_mi8w7") 204 | node_to_show = NodePath("../RoomSectionContainer") 205 | 206 | [node name="RoomSectionContainer" type="MarginContainer" parent="Scroll/ScrollChild/MainContentPanel/MainContent"] 207 | visible = false 208 | layout_mode = 2 209 | theme_override_constants/margin_left = 40 210 | theme_override_constants/margin_bottom = 10 211 | 212 | [node name="RoomSettings" type="VBoxContainer" parent="Scroll/ScrollChild/MainContentPanel/MainContent/RoomSectionContainer"] 213 | layout_mode = 2 214 | script = ExtResource("9_lsqtf") 215 | 216 | [node name="LatestDeployentInfo" type="VBoxContainer" parent="Scroll/ScrollChild/MainContentPanel/MainContent"] 217 | layout_mode = 2 218 | 219 | [node name="HSeparator" type="HSeparator" parent="Scroll/ScrollChild/MainContentPanel/MainContent/LatestDeployentInfo"] 220 | layout_mode = 2 221 | 222 | [node name="LatestDeploymentLabel" type="Label" parent="Scroll/ScrollChild/MainContentPanel/MainContent/LatestDeployentInfo"] 223 | layout_mode = 2 224 | text = "Latest Deployment" 225 | 226 | [node name="LatestDeploymentTextEdit" type="TextEdit" parent="Scroll/ScrollChild/MainContentPanel/MainContent/LatestDeployentInfo"] 227 | unique_name_in_owner = true 228 | custom_minimum_size = Vector2(2.08165e-12, 200) 229 | layout_mode = 2 230 | 231 | editable = false 232 | wrap_mode = 1 233 | autowrap_mode = 1 234 | scroll_smooth = true 235 | scroll_fit_content_height = true 236 | script = ExtResource("9_qjw1b") 237 | 238 | [node name="MarginContainer" type="MarginContainer" parent="Scroll/ScrollChild/MainContentPanel/MainContent/LatestDeployentInfo"] 239 | layout_mode = 2 240 | theme_override_constants/margin_bottom = 50 241 | 242 | [node name="ProjectExporter" type="Node" parent="."] 243 | unique_name_in_owner = true 244 | script = ExtResource("3_bp7wg") 245 | 246 | [node name="TarMaker" type="Node" parent="."] 247 | unique_name_in_owner = true 248 | script = ExtResource("4_fftlc") 249 | 250 | [node name="DockerfileMaker" type="Node" parent="."] 251 | unique_name_in_owner = true 252 | script = ExtResource("5_uqf2c") 253 | 254 | [node name="BuildDeployer" type="Node" parent="."] 255 | unique_name_in_owner = true 256 | script = ExtResource("6_2m224") 257 | 258 | [node name="LatestDeploymentGetter" type="Node" parent="."] 259 | unique_name_in_owner = true 260 | script = ExtResource("15_mejlx") 261 | 262 | [node name="SDK" type="Node" parent="."] 263 | unique_name_in_owner = true 264 | process_mode = 3 265 | script = ExtResource("16_qf06x") 266 | 267 | [node name="BuildDirFileDialog" type="FileDialog" parent="."] 268 | unique_name_in_owner = true 269 | title = "Select Hathora Build Directory" 270 | initial_position = 1 271 | size = Vector2i(800, 700) 272 | min_size = Vector2i(400, 700) 273 | ok_button_text = "Select Current Folder" 274 | mode_overrides_title = false 275 | file_mode = 2 276 | show_hidden_files = true 277 | 278 | [node name="PathToTarFileDialog" type="FileDialog" parent="."] 279 | unique_name_in_owner = true 280 | title = "Select a Hathora Tar File" 281 | initial_position = 1 282 | size = Vector2i(800, 700) 283 | ok_button_text = "Open" 284 | mode_overrides_title = false 285 | file_mode = 0 286 | access = 2 287 | filters = PackedStringArray("*.tar.tgz", "*.tgz", "*.tar.gz") 288 | show_hidden_files = true 289 | 290 | [connection signal="pressed" from="Scroll/ScrollChild/MarginContainer/VBoxContainer/HeaderButtonsContainer/Console" to="Scroll/ScrollChild/MarginContainer/VBoxContainer/HeaderButtonsContainer" method="_on_console_pressed"] 291 | [connection signal="pressed" from="Scroll/ScrollChild/MarginContainer/VBoxContainer/HeaderButtonsContainer/Tutorial" to="Scroll/ScrollChild/MarginContainer/VBoxContainer/HeaderButtonsContainer" method="_on_tutorial_pressed"] 292 | [connection signal="pressed" from="Scroll/ScrollChild/MarginContainer/VBoxContainer/HeaderButtonsContainer/Discord" to="Scroll/ScrollChild/MarginContainer/VBoxContainer/HeaderButtonsContainer" method="_on_discord_pressed"] 293 | [connection signal="pressed" from="Scroll/ScrollChild/LoginContent/LoginButton" to="." method="_on_login_button_pressed"] 294 | [connection signal="gui_input" from="Scroll/ScrollChild/MainContentPanel/MainContent/LatestDeployentInfo/LatestDeploymentTextEdit" to="Scroll/ScrollChild/MainContentPanel/MainContent/LatestDeployentInfo/LatestDeploymentTextEdit" method="_on_gui_input"] 295 | --------------------------------------------------------------------------------