From a52ef9e039e2722b874990fa6db6f80192731018 Mon Sep 17 00:00:00 2001 From: Bryan Date: Tue, 29 Oct 2024 11:14:59 +0100 Subject: [PATCH] added strong typing --- api/JamCallbackApi.gd | 9 +- api/JamClientApi.gd | 90 ++++++----- api/JamDataApi.gd | 6 +- api/JamDevModeApi.gd | 2 +- api/JamHttpBase.gd | 81 +++++----- api/JamHttpRequestPool.gd | 32 ++-- api/JamJwt.gd | 77 ++++----- api/JamLoginApi.gd | 8 +- api/JamProjectApi.gd | 30 +++- client/ClientKeys.gd | 22 +-- client/JamWebRTCHelper.gd | 75 +++++---- client/JamWebRTCSignalling.gd | 103 +++++++----- core/JamClient.gd | 86 +++++----- core/JamConnect.gd | 93 ++++++----- core/JamReplicator.gd | 209 ++++++++++++------------ core/JamRoot.gd | 9 +- core/JamServer.gd | 130 +++++++-------- core/JamSync.gd | 46 +++--- core/JamThreadHelper.gd | 78 ++++----- editor_plugin/ChannelSummary.gd | 25 ++- editor_plugin/Dashboard.gd | 68 ++++---- editor_plugin/JamAuthProxy.gd | 227 ++++++++++++++------------- editor_plugin/JamEditorPluginPage.gd | 13 +- editor_plugin/LoadLocker.gd | 12 +- editor_plugin/Login.gd | 80 +++++----- editor_plugin/NewProject.gd | 29 ++-- editor_plugin/Project.gd | 219 +++++++++++++------------- editor_plugin/ProjectPacker.gd | 37 ++--- editor_plugin/ProjectSelect.gd | 31 ++-- editor_plugin/ReleaseSummary.gd | 93 +++++------ editor_plugin/Sessions.gd | 113 ++++++------- editor_plugin/plugin.gd | 27 ++-- export/AutoExport.gd | 225 +++++++++++++------------- server/JamDB.gd | 81 ++++++---- server/JamDBDynamo.gd | 28 ++-- server/JamFiles.gd | 59 ++++--- server/JamFilesS3.gd | 22 ++- ui/BusyCircle.gd | 14 +- ui/ChatConsole.gd | 22 ++- ui/MessagePanel.gd | 28 ++-- ui/client/DeviceAuthUI.gd | 74 ++++----- ui/client/ExampleClientUI.gd | 159 +++++++++++-------- ui/client/JamClientUI.gd | 12 +- ui/pages/PageStack.gd | 6 +- util/JamConfig.gd | 83 +++++----- util/JamError.gd | 3 +- util/JamResult.gd | 5 +- util/KeyValCache.gd | 79 +++++----- util/ScopeLocker.gd | 36 +++-- util/StreamingUpload.gd | 133 ++++++++-------- 50 files changed, 1742 insertions(+), 1487 deletions(-) diff --git a/api/JamCallbackApi.gd b/api/JamCallbackApi.gd index a7505f5..9057235 100644 --- a/api/JamCallbackApi.gd +++ b/api/JamCallbackApi.gd @@ -1,14 +1,15 @@ @tool -extends JamHttpBase class_name JamCallbackApi +extends JamHttpBase -var session_id := OS.get_environment("SESSION_ID") +var session_id: String = OS.get_environment("SESSION_ID") -func _ready(): +func _ready() -> void: super() api_url = OS.get_environment("CALLBACK_URL") jwt.set_token(OS.get_environment("CALLBACK_KEY")) + func send_ready() -> Result: print("Sending ready to %s" % api_url) return await _json_http( @@ -20,6 +21,7 @@ func send_ready() -> Result: } ) + func check_token(username: String, token: String) -> Result: return await _json_http( "", @@ -33,6 +35,7 @@ func check_token(username: String, token: String) -> Result: } ) + func get_vars(var_names: Array[String]) -> Result: return await _json_http( "", diff --git a/api/JamClientApi.gd b/api/JamClientApi.gd index dab4e62..0597cff 100644 --- a/api/JamClientApi.gd +++ b/api/JamClientApi.gd @@ -8,17 +8,59 @@ enum SessionStatus { INIT, UNKNOWN, PROVISIONING, PENDING, ACTIVATING, RUNNING, READY, DEACTIVATING, STOPPING, DEPROVISIONING, STOPPED } +func create_game_session(region: String="us-east-2") -> Result: + return await _json_http( + "/sessions/%s" % game_id, + HTTPClient.METHOD_POST, + { + "region": region + } + ) + + +func join_game_session(join_id: String) -> Result: + return await _json_http( + "/sessions/%s/join/%s" % [game_id, join_id], + HTTPClient.METHOD_POST, + {} + ) + + +func get_game_session(session_id: String) -> GameSessionResult: + return GameSessionResult.from_result( + await _json_http("/sessions/%s/check/%s" % [game_id, session_id]) + ) + + +func leave_game_session(session_id: String) -> Result: + return await _json_http( + "/sessions/%s/leave/%s" % [game_id, session_id], + HTTPClient.METHOD_POST, + {} + ) + + +func get_guest_jwt(game_id_url_parameter: String) -> Result: + return await _json_http("/guest-auth/%s" % [game_id_url_parameter]) + + +func check_guests_allowed(game_id_url_parameter: String) -> Result: + return await _json_http("/guest-auth/%s/allowed" % [game_id_url_parameter]) + + class GameSessionAddress: extends RefCounted var ip: String var port: int var domain: String + class PlayerInfo: extends RefCounted var user_id: String var token: String + class GameSessionResult: extends JamHttpBase.Result var session_id: String = "" @@ -29,10 +71,8 @@ class GameSessionResult: var join_id: String = "" static func from_result(res: Result) -> GameSessionResult: - #print(res.data) - - var gres = GameSessionResult.new() + var gres: GameSessionResult = GameSessionResult.new() gres.data = res.data gres.errored = res.errored gres.error_msg = res.error_msg @@ -43,8 +83,8 @@ class GameSessionResult: gres.status = SessionStatus.get(res.data["state"], SessionStatus.UNKNOWN) gres.session_id = res.data["id"] gres.region = res.data["region"] - for p in res.data["players"]: - var pinfo = PlayerInfo.new() + for p: Variant in res.data["players"]: + var pinfo: PlayerInfo = PlayerInfo.new() pinfo.user_id = p["username"] gres.players.push_back(pinfo) if "joinToken" in p: @@ -57,7 +97,8 @@ class GameSessionResult: gres.join_id = res.data["joinCode"] return gres - + + func has_unusable_status() -> bool: return errored or [ SessionStatus.UNKNOWN, @@ -66,7 +107,8 @@ class GameSessionResult: SessionStatus.DEPROVISIONING, SessionStatus.STOPPED ].has(status) - + + func busy_progress() -> float: if status == SessionStatus.INIT: return 1.0 / 6.0 @@ -82,37 +124,3 @@ class GameSessionResult: return 1.0 else: return 0.0 - -func create_game_session(region: String="us-east-2") -> Result: - return await _json_http( - "/sessions/%s" % game_id, - HTTPClient.METHOD_POST, - { - "region": region - } - ) - -func join_game_session(join_id: String) -> Result: - return await _json_http( - "/sessions/%s/join/%s" % [game_id, join_id], - HTTPClient.METHOD_POST, - {} - ) - -func get_game_session(session_id: String) -> GameSessionResult: - return GameSessionResult.from_result( - await _json_http("/sessions/%s/check/%s" % [game_id, session_id]) - ) - -func leave_game_session(session_id: String) -> Result: - return await _json_http( - "/sessions/%s/leave/%s" % [game_id, session_id], - HTTPClient.METHOD_POST, - {} - ) - -func get_guest_jwt(game_id: String) -> Result: - return await _json_http("/guest-auth/%s" % [game_id]) - -func check_guests_allowed(game_id: String) -> Result: - return await _json_http("/guest-auth/%s/allowed" % [game_id]) diff --git a/api/JamDataApi.gd b/api/JamDataApi.gd index cd7cea5..f8fafcb 100644 --- a/api/JamDataApi.gd +++ b/api/JamDataApi.gd @@ -4,11 +4,12 @@ extends JamHttpBase var project_id: String = "" -func _ready(): +func _ready() -> void: super() api_url = OS.get_environment("DATA_URL") jwt.set_token(OS.get_environment("DATA_KEY")) + func put_object(key: String, object: Dictionary) -> Result: var path := "/proj/%s/data/%s" % [project_id, key] return await _json_http( @@ -16,7 +17,8 @@ func put_object(key: String, object: Dictionary) -> Result: HTTPClient.METHOD_POST, object ) - + + func get_object(key: String) -> Result: return await _json_http( "/proj/%s/data/%s" % [project_id, key], diff --git a/api/JamDevModeApi.gd b/api/JamDevModeApi.gd index 2d4fd43..97acffe 100644 --- a/api/JamDevModeApi.gd +++ b/api/JamDevModeApi.gd @@ -1,5 +1,5 @@ -extends JamHttpBase class_name JamDevModeApi +extends JamHttpBase func get_test_key(project_id: String, release: String, test_num: int) -> Result: return await _json_http( diff --git a/api/JamHttpBase.gd b/api/JamHttpBase.gd index 32c3e4d..0fd1d69 100644 --- a/api/JamHttpBase.gd +++ b/api/JamHttpBase.gd @@ -1,35 +1,18 @@ @tool -extends Node class_name JamHttpBase +extends Node var addon_version: String = "unknown" var api_url: String var jwt: JamJwt = JamJwt.new() - var pool: JamHttpRequestPool -class Result: - var data: Dictionary = {} - var errored: bool = false - var error_msg: String = "" - - static func err(msg: String): - var r = Result.new() - r.errored = true - r.error_msg = msg - return r - - static func ok(d: Dictionary): - var r = Result.new() - r.data = d - return r - -func _ready(): - var dir := (self.get_script() as Script).get_path().get_base_dir() +func _ready() -> void: + var dir: String = (self.get_script() as Script).get_path().get_base_dir() - var settings = ConfigFile.new() - var err = settings.load(dir + "/../settings.cfg") - if err != OK: + var settings: ConfigFile = ConfigFile.new() + var err: Error = settings.load(dir + "/../settings.cfg") + if not err == OK: printerr("Failed to load api settings") return api_url = "https://%s" % settings.get_value("api", "domain") @@ -39,36 +22,40 @@ func _ready(): pool = JamHttpRequestPool.new() add_child(pool) + func _auth_headers() -> Array: if not jwt.has_token(): return [] else: return ["Authorization: Bearer %s" % jwt.get_token()] + func _json_auth_headers() -> Array: return _auth_headers() + ["Content-type: application/json"] + func get_string_data(url: String) -> JamResult: - var h = pool.get_client() - var err = h.http.request(url) - if err != OK: + var h: JamHttpRequestPool.ScopedClient = pool.get_client() + var err: Error = h.http.request(url) + if not err == OK: return JamResult.err("HTTP request error") - var resp = await h.http.request_completed + var resp: Variant = await h.http.request_completed if resp[1] > 299: return JamResult.err("HTTP error code %d" % [resp[1]]) - var s = resp[3].get_string_from_utf8() + var s: String = resp[3].get_string_from_utf8() if len(s) < 1: return JamResult.err("Empty of invalid HTTP response") - + return JamResult.ok(s) + func _json_http(route: String, method: HTTPClient.Method=HTTPClient.METHOD_GET, body: Variant=null) -> Result: - var result = Result.new() - var err - var h = pool.get_client() - if body != null: + var result: Result = Result.new() + var err: Error + var h: JamHttpRequestPool.ScopedClient = pool.get_client() + if not body == null: err = h.http.request( api_url + route, _json_auth_headers(), @@ -81,16 +68,16 @@ func _json_http(route: String, method: HTTPClient.Method=HTTPClient.METHOD_GET, _json_auth_headers(), method ) - if err != OK: + if not err == OK: result.errored = true result.error_msg = "HTTP request error" return result - var resp = await h.http.request_completed - var response_code = resp[1] + var resp: Variant = await h.http.request_completed + var response_code: Variant = resp[1] var response_body: String = resp[3].get_string_from_utf8() - var data = {} - if len(response_body) > 0: + var data: Dictionary = {} + if not response_body.is_empty(): data = JSON.parse_string(response_body) if response_code > 299: result.errored = true @@ -106,3 +93,21 @@ func _json_http(route: String, method: HTTPClient.Method=HTTPClient.METHOD_GET, result.data = data return result + + +class Result: + var data: Dictionary = {} + var errored: bool = false + var error_msg: String = "" + + static func err(msg: String) -> Result: + var r: Result = Result.new() + r.errored = true + r.error_msg = msg + return r + + + static func ok(d: Dictionary) -> Result: + var r: Result = Result.new() + r.data = d + return r \ No newline at end of file diff --git a/api/JamHttpRequestPool.gd b/api/JamHttpRequestPool.gd index 30f0891..4c60b46 100644 --- a/api/JamHttpRequestPool.gd +++ b/api/JamHttpRequestPool.gd @@ -1,31 +1,35 @@ @tool -extends Node class_name JamHttpRequestPool +extends Node + +func get_client() -> ScopedClient: + for c in get_children(): + if not c.in_use: + return ScopedClient.new(c as HttpHolder) + + var hh := HttpHolder.new() + add_child(hh) + return ScopedClient.new(hh) + class HttpHolder: extends HTTPRequest - var in_use: bool =false + var in_use: bool = false + class ScopedClient: extends RefCounted var http: HttpHolder - - func _init(holder: HttpHolder): + + func _init(holder: HttpHolder) -> void: http = holder http.in_use = true - - func _notification(what): + + func _notification(what: int) -> void: if what == NOTIFICATION_PREDELETE: http.in_use = false -func get_client() -> ScopedClient: - for c in get_children(): - if not c.in_use: - return ScopedClient.new(c as HttpHolder) - - var hh := HttpHolder.new() - add_child(hh) - return ScopedClient.new(hh) + diff --git a/api/JamJwt.gd b/api/JamJwt.gd index 9174788..3d06ada 100644 --- a/api/JamJwt.gd +++ b/api/JamJwt.gd @@ -1,19 +1,7 @@ -extends RefCounted class_name JamJwt +extends RefCounted -signal token_changed(String) - -class TokenData: - extends RefCounted - var header: Dictionary - var claims: Dictionary - var signature: String - -class TokenParseResult: - extends RefCounted - var error: String = "" - var errored: bool = false - var data: TokenData +signal token_changed(jwt_token: String) var username: String: get: @@ -23,7 +11,7 @@ var jwt_token: String = "" var claims: Dictionary = {} func set_token(jwt: String) -> TokenParseResult: - var result = JamJwt.parse_token(jwt) + var result: TokenParseResult = JamJwt.parse_token(jwt) if result.errored: return result jwt_token = jwt @@ -31,62 +19,79 @@ func set_token(jwt: String) -> TokenParseResult: token_changed.emit(jwt_token) return result + func get_token() -> String: return jwt_token -func clear(): + +func clear() -> void: jwt_token = "" token_changed.emit("") + func has_token() -> bool: return len(jwt_token) > 0 + static func b64url_to_b64(b64_url: String) -> String: - var b64 = b64_url + var b64: String = b64_url b64 = b64.replace("-", "+") b64 = b64.replace("_", "/") - var overhang = len(b64) % 4 - if overhang != 0: - for i in range(4 - overhang): + var overhang: int = len(b64) % 4 + if not overhang == 0: + for i: int in range(4 - overhang): b64 += "=" return b64 + static func parse_token(token: String) -> TokenParseResult: - var result = TokenParseResult.new() - var parts := token.split(".") - if len(parts) != 3: + var result: TokenParseResult = TokenParseResult.new() + var parts: PackedStringArray = token.split(".") + if not len(parts) == 3: result.errored = true result.error = "Invalid JWT token format" return result - var tkn = TokenData.new() - - var header_b64 := b64url_to_b64(parts[0]) - var header_json := Marshalls.base64_to_utf8(header_b64) - var header = JSON.parse_string(header_json) + var tkn: TokenData = TokenData.new() + var header_b64: String = b64url_to_b64(parts[0]) + var header_json: String = Marshalls.base64_to_utf8(header_b64) + var header: Dictionary = JSON.parse_string(header_json) if header == null: result.errored = true result.error = "Failed to parse JWT header" return result + tkn.header = header - - var claims_b64 := b64url_to_b64(parts[1]) - var claims_json := Marshalls.base64_to_utf8(claims_b64) - var jwt_claims = JSON.parse_string(claims_json) + var claims_b64: String = b64url_to_b64(parts[1]) + var claims_json: String = Marshalls.base64_to_utf8(claims_b64) + var jwt_claims: Variant = JSON.parse_string(claims_json) if jwt_claims == null: result.errored = true result.error = "Failed to parse JWT claims" return result + tkn.claims = jwt_claims - - var sig_b64 := b64url_to_b64(parts[2]) - var raw_sig := Marshalls.base64_to_raw(sig_b64) + var sig_b64: String = b64url_to_b64(parts[2]) + var raw_sig: PackedByteArray = Marshalls.base64_to_raw(sig_b64) if not raw_sig or raw_sig.is_empty(): result.errored = true result.error = "Failed to parse JWT signature" return result tkn.signature = parts[2] - result.data = tkn return result + + +class TokenData: + extends RefCounted + var header: Dictionary + var claims: Dictionary + var signature: String + + +class TokenParseResult: + extends RefCounted + var error: String = "" + var errored: bool = false + var data: TokenData diff --git a/api/JamLoginApi.gd b/api/JamLoginApi.gd index a4b9177..fb9d62b 100644 --- a/api/JamLoginApi.gd +++ b/api/JamLoginApi.gd @@ -1,11 +1,11 @@ @tool -extends JamHttpBase class_name JamLoginApi +extends JamHttpBase const DEV_SCOPE: String = "developer" const USER_SCOPE: String = "user" -func _ready(): +func _ready() -> void: super() func request_developer_auth() -> Result: @@ -17,7 +17,8 @@ func request_developer_auth() -> Result: "scope": "developer" } ) - + + func request_user_auth(gameId: String) -> Result: return await _json_http( "/device-auth/request", @@ -29,5 +30,6 @@ func request_user_auth(gameId: String) -> Result: } ) + func check_auth(user_code: String, device_code: String) -> Result: return await _json_http("/device-auth/request/%s/%s" % [user_code, device_code]) diff --git a/api/JamProjectApi.gd b/api/JamProjectApi.gd index 75a0bcd..f2fe008 100644 --- a/api/JamProjectApi.gd +++ b/api/JamProjectApi.gd @@ -1,11 +1,11 @@ @tool -extends JamHttpBase class_name JamProjectApi +extends JamHttpBase - -func _ready(): +func _ready() -> void: super() + func create_project(project_name: String) -> Result: return await _json_http( "/projects", @@ -13,22 +13,26 @@ func create_project(project_name: String) -> Result: {"project_name": project_name} ) + func list_projects() -> Result: return await _json_http("/projects") + func get_project(project_id: String) -> Result: return await _json_http("/projects/%s" % project_id) + func delete_project(project_id: String) -> Result: return await _json_http( "/projects/%s" % project_id, HTTPClient.METHOD_DELETE ) + func prepare_release(project_id: String, config: Dictionary) -> Result: - var req = { + var req: Dictionary = { "network_mode": config["network_mode"], - "builds": config["builds"].map(func(b): return { + "builds": config["builds"].map(func(b: Variant) -> Dictionary: return { "name": b["name"], "export_name": b["export_name"], "is_server": b.get("is_server", false), @@ -42,6 +46,7 @@ func prepare_release(project_id: String, config: Dictionary) -> Result: req ) + func update_release(project_id: String, release_id: String, props: Dictionary) -> Result: return await _json_http( "/projects/%s/releases/%s" % [project_id, release_id], @@ -49,6 +54,7 @@ func update_release(project_id: String, release_id: String, props: Dictionary) - props ) + func post_config(project_id: String, cfg: Dictionary) -> Result: return await _json_http( "/projects/%s/config" % project_id, @@ -56,6 +62,7 @@ func post_config(project_id: String, cfg: Dictionary) -> Result: cfg ) + func get_build_logs(project_id: String, release_id: String, log_id: String) -> Result: return await _json_http( "/projects/%s/releases/%s/buildlogs/%s" % [ @@ -64,8 +71,9 @@ func get_build_logs(project_id: String, release_id: String, log_id: String) -> R log_id, ]) + func get_sessions(project_id: String, active: bool) -> Result: - var state = "up" + var state: String = "up" if not active: state = "down" return await _json_http( @@ -74,6 +82,7 @@ func get_sessions(project_id: String, active: bool) -> Result: state, ]) + func get_session(project_id: String, release_id: String, session_id: String) -> Result: return await _json_http( "/projects/%s/releases/%s/sessions/%s" % [ @@ -82,6 +91,7 @@ func get_session(project_id: String, release_id: String, session_id: String) -> session_id, ]) + func get_session_logs(project_id: String, release_id: String, session_id: String) -> Result: return await _json_http( "/projects/%s/releases/%s/sessions/%s/logs" % [ @@ -90,6 +100,7 @@ func get_session_logs(project_id: String, release_id: String, session_id: String session_id, ]) + func terminate_session(project_id: String, release_id: String, session_id: String) -> Result: return await _json_http( "/projects/%s/releases/%s/sessions/%s/terminate" % [ @@ -101,6 +112,7 @@ func terminate_session(project_id: String, release_id: String, session_id: Strin {} ) + func get_test_key(project_id: String, release: String, test_num: int) -> Result: return await _json_http( "/projects/%s/testkey" % [project_id], @@ -111,6 +123,7 @@ func get_test_key(project_id: String, release: String, test_num: int) -> Result: } ) + func get_local_server_keys(project_id: String, release: String) -> Result: return await _json_http( "/projects/%s/localserverkeys" % [project_id], @@ -121,7 +134,6 @@ func get_local_server_keys(project_id: String, release: String) -> Result: ) - func create_channel(project_id: String, channel: String) -> Result: return await _json_http( "/projects/%s/channels" % [project_id], @@ -131,6 +143,7 @@ func create_channel(project_id: String, channel: String) -> Result: } ) + func update_channel(project_id: String, channel: String, props: Dictionary) -> Result: return await _json_http( "/projects/%s/channels/%s" % [project_id, channel], @@ -138,7 +151,8 @@ func update_channel(project_id: String, channel: String, props: Dictionary) -> R props ) -func get_channels(project_id: String, release: String) -> Result: + +func get_channels(project_id: String, _release: String) -> Result: return await _json_http( "/projects/%s/channels" % [project_id], HTTPClient.METHOD_GET diff --git a/client/ClientKeys.gd b/client/ClientKeys.gd index c785dad..056a95f 100644 --- a/client/ClientKeys.gd +++ b/client/ClientKeys.gd @@ -1,26 +1,27 @@ -extends Node class_name ClientKeys +extends Node var dev_mode_api: JamDevModeApi - var dev_jwt: JamJwt -func _init(): +func _init() -> void: if OS.is_debug_build(): dev_mode_api = JamDevModeApi.new() add_child(dev_mode_api) + func _get_web_gjwt() -> Variant: - var js_return = JavaScriptBridge.eval("window.getJamLaunchGJWT?.() ?? null;") - if !js_return: + var js_return: Variant = JavaScriptBridge.eval("window.getJamLaunchGJWT?.() ?? null;") + if not js_return: printerr("failed to retrieve GJWT in browser context") return js_return + func get_included_gjwt(game_id: String) -> Variant: if OS.get_name() == "Web": return _get_web_gjwt() - var gjwt_path := OS.get_executable_path().get_base_dir() + var gjwt_path: String = OS.get_executable_path().get_base_dir() if OS.get_name() == "macOS": gjwt_path += "/../Resources" gjwt_path += "/gjwt" @@ -35,12 +36,13 @@ func get_included_gjwt(game_id: String) -> Variant: return null + func get_test_gjwt(gameId: String) -> Variant: if not OS.is_debug_build(): push_error("can't get test gjwt if not in dev mode") return null - var peer = StreamPeerTCP.new() + var peer: StreamPeerTCP = StreamPeerTCP.new() peer.connect_to_host("127.0.0.1", 17343) while true: await get_tree().create_timer(0.1).timeout @@ -51,9 +53,8 @@ func get_test_gjwt(gameId: String) -> Variant: if peer.get_status() == StreamPeerTCP.STATUS_CONNECTED: break - var parts = gameId.split("-") + var parts: PackedStringArray = gameId.split("-") peer.put_string("key/%s/%s" % [parts[0], parts[1]]) - while true: await get_tree().create_timer(0.1).timeout var err := peer.poll() @@ -63,8 +64,7 @@ func get_test_gjwt(gameId: String) -> Variant: if peer.get_available_bytes() > 0: break - var jwt_response = peer.get_string() - + var jwt_response: String = peer.get_string() if jwt_response.begins_with("Error:"): push_error("failed to get test creds - %s" % jwt_response) return null diff --git a/client/JamWebRTCHelper.gd b/client/JamWebRTCHelper.gd index a3896ac..57ac3cd 100644 --- a/client/JamWebRTCHelper.gd +++ b/client/JamWebRTCHelper.gd @@ -1,23 +1,20 @@ class_name JamWebRTCHelper extends Node -var signalling: JamWebRTCSignalling -var rtc_mp: WebRTCMultiplayerPeer = WebRTCMultiplayerPeer.new() -var sealed := false - -var peers: Dictionary = {} -var local_pid: int = -1 - signal errored(msg: String) signal multiplayer_initialized(pid: int, pinfo: Dictionary) signal multiplayer_terminating() - signal peer_added(pid: int) signal peer_removed(pid: int) - signal session_sealed() -func _init(): +var signalling: JamWebRTCSignalling +var rtc_mp: WebRTCMultiplayerPeer = WebRTCMultiplayerPeer.new() +var sealed: bool = false +var peers: Dictionary = {} +var local_pid: int = -1 + +func _init() -> void: signalling = JamWebRTCSignalling.new() add_child(signalling) @@ -32,12 +29,14 @@ func _init(): signalling.peer_connected.connect(self._peer_connected) signalling.peer_disconnected.connect(self._peer_disconnected) + func start(url: String) -> JamError: await stop() sealed = false return await signalling.connect_to_url(url) -func stop(): + +func stop() -> void: if multiplayer.has_multiplayer_peer(): multiplayer_terminating.emit() multiplayer.multiplayer_peer = null @@ -46,30 +45,33 @@ func stop(): rtc_mp.close() await signalling.close() + func _add_webrtc_peer(id: int) -> Variant: var peer: WebRTCPeerConnection = WebRTCPeerConnection.new() - var err = peer.initialize({ + var err: Error = peer.initialize({ "iceServers": [ {"urls": [ "stun:stun1.jamlaunch.com:13478", "stun:stun.l.google.com:19302" ] } ] }) - if err != OK: + if not err == OK: _err("Failed to initialize peer %d (possibly due to missing WebRTC GDExtension)" % [id], err) return null peer.session_description_created.connect(self._offer_created.bind(id)) peer.ice_candidate_created.connect(self._new_ice_candidate.bind(id)) err = rtc_mp.add_peer(peer, id) - if err != OK: + if not err == OK: _err("Failed to add peer %d (possibly due to missing WebRTC GDExtension)" % [id], err) return null return peer -func _new_ice_candidate(mid_name: String, index_name: int, sdp_name: String, id: int): + +func _new_ice_candidate(mid_name: String, index_name: int, sdp_name: String, id: int) -> void: #print("new ice candidate: %d %s %d %s" % [id, mid_name, index_name, sdp_name]) signalling.send_candidate(id, mid_name, index_name, sdp_name) -func _offer_created(type: String, data: String, id: int): + +func _offer_created(type: String, data: String, id: int) -> void: if not rtc_mp.has_peer(id): printerr("offer created by unknown peer %d" % id) printerr(rtc_mp.get_peers()) @@ -80,17 +82,18 @@ func _offer_created(type: String, data: String, id: int): else: signalling.send_answer(id, data) -func _connected(pid: int, pinfo: Dictionary): + +func _connected(pid: int, pinfo: Dictionary) -> void: if pid == 1: - var err = rtc_mp.create_server() - if err != OK: + var err: Error = rtc_mp.create_server() + if not err == OK: _err("Failed to create WebRTC server peer", err) await signalling.close() return peers[pid] = pinfo else: - var err = rtc_mp.create_client(pid) - if err != OK: + var err: Error = rtc_mp.create_client(pid) + if not err == OK: _err("Failed to create WebRTC client peer", err) await signalling.close() return @@ -99,11 +102,13 @@ func _connected(pid: int, pinfo: Dictionary): local_pid = pid multiplayer_initialized.emit(pid, pinfo) -func _disconnected(): + +func _disconnected() -> void: if not sealed: await stop() -func _peer_connected(pid: int, pinfo: Dictionary): + +func _peer_connected(pid: int, pinfo: Dictionary) -> void: #print("Peer connected %d (in %d)" % [pid, multiplayer.get_unique_id()]) #print(pinfo) if multiplayer.is_server(): @@ -111,15 +116,16 @@ func _peer_connected(pid: int, pinfo: Dictionary): peers[pid] = pinfo peer_added.emit(pid) elif pid == 1: - var p = _add_webrtc_peer(pid) - if p != null: + var p: Variant = _add_webrtc_peer(pid) + if not p == null: peers[pid] = pinfo peer_added.emit(pid) p.create_offer() else: printerr("Unexpected client-to-client peer connection message %d - %d" % [pid, multiplayer.get_unique_id()]) -func _peer_disconnected(id: int): + +func _peer_disconnected(id: int) -> void: if rtc_mp.has_peer(id): rtc_mp.remove_peer(id) peer_removed.emit(id) @@ -128,18 +134,21 @@ func _peer_disconnected(id: int): printerr(rtc_mp.get_peers()) peers.erase(id) -func _session_sealed(): + +func _session_sealed() -> void: sealed = true session_sealed.emit() -func _offer_received(id: int, offer: String): + +func _offer_received(id: int, offer: String) -> void: if rtc_mp.has_peer(id): rtc_mp.get_peer(id).connection.set_remote_description("offer", offer) else: printerr("offer received from unknown peer %d" % id) printerr(rtc_mp.get_peers()) -func _answer_received(id: int, answer: String): + +func _answer_received(id: int, answer: String) -> void: #print("Got answer: %d" % id) if rtc_mp.has_peer(id): rtc_mp.get_peer(id).connection.set_remote_description("answer", answer) @@ -147,7 +156,8 @@ func _answer_received(id: int, answer: String): printerr("answer received from unknown peer %d" % id) printerr(rtc_mp.get_peers()) -func _candidate_received(id: int, mid: String, index: int, sdp: String): + +func _candidate_received(id: int, mid: String, index: int, sdp: String) -> void: if rtc_mp.has_peer(id): #push_warning("%s -- %d -- %s" % [mid, index, sdp]) # TODO: figure out why this can produce spurrious error messages @@ -156,8 +166,9 @@ func _candidate_received(id: int, mid: String, index: int, sdp: String): printerr("candidate received from unknown peer %d" % id) printerr(rtc_mp.get_peers()) -func _err(msg: String, code: Error = OK): - if code != OK: + +func _err(msg: String, code: Error = OK) -> void: + if not code == OK: msg += " - error code: %d" % code printerr(msg) errored.emit(msg) diff --git a/client/JamWebRTCSignalling.gd b/client/JamWebRTCSignalling.gd index c4d210c..9bd6f19 100644 --- a/client/JamWebRTCSignalling.gd +++ b/client/JamWebRTCSignalling.gd @@ -1,13 +1,6 @@ class_name JamWebRTCSignalling extends Node -enum Message {JOIN, ID, PEER_CONNECT, PEER_DISCONNECT, OFFER, ANSWER, CANDIDATE, SEAL, PING} - -var ws: WebSocketPeer = WebSocketPeer.new() -var code = 1000 -var reason = "Unknown" -var state := WebSocketPeer.STATE_CLOSED - signal session_joined() signal connected(id: int, pinfo: Dictionary) signal disconnected() @@ -17,17 +10,41 @@ signal offer_received(id: int, offer: String) signal answer_received(id: int, answer: String) signal candidate_received(id: int, mid: String, index: int, sdp: String) signal session_sealed() - signal ws_connecting() signal ws_opened() signal ws_closing() signal ws_closed() signal ws_opened_or_closed(state: WebSocketPeer.State) +enum Message { + JOIN, + ID, + PEER_CONNECT, + PEER_DISCONNECT, + OFFER, + ANSWER, + CANDIDATE, + SEAL, + PING +} + +var ws: WebSocketPeer = WebSocketPeer.new() +var code: int = 1000 +var reason: String = "Unknown" +var state: int = WebSocketPeer.STATE_CLOSED + +func _process(_delta: float) -> void: + _update_ws_state() + while state == WebSocketPeer.STATE_OPEN and ws.get_available_packet_count(): + var err: JamError = _parse_msg() + if err.errored: + print("Error parsing message from server: %s" % err.error_msg) + + func connect_to_url(url: String) -> JamError: await close() - var err = ws.connect_to_url(url) - if err != OK: + var err: Error = ws.connect_to_url(url) + if not err == OK: return JamError.err("Failed to connect WebSocket to provided URL. Error code: %d" % err) await ws_opened_or_closed @@ -36,24 +53,19 @@ func connect_to_url(url: String) -> JamError: else: return JamError.err("WebSocket not in opened state at end of connection request (%d)" % state) -func close(): + +func close() -> void: if state < WebSocketPeer.STATE_CLOSING: ws.close() code = 1000 reason = "Unknown" - if state != WebSocketPeer.STATE_CLOSED: + if not state == WebSocketPeer.STATE_CLOSED: await ws_closed -func _process(_delta): - _update_ws_state() - while state == WebSocketPeer.STATE_OPEN and ws.get_available_packet_count(): - var err := _parse_msg() - if err.errored: - print("Error parsing message from server: %s" % err.error_msg) -func _update_ws_state(): +func _update_ws_state() -> void: ws.poll() - var new_state := ws.get_ready_state() + var new_state: int = ws.get_ready_state() if new_state == state: return @@ -72,13 +84,15 @@ func _update_ws_state(): break x = (x + 1) % 4 -func _on_ws_opened(): + +func _on_ws_opened() -> void: print("joining session via websocket...") join_session() ws_opened.emit() ws_opened_or_closed.emit(WebSocketPeer.STATE_OPEN) -func _on_ws_closed(): + +func _on_ws_closed() -> void: code = ws.get_close_code() reason = ws.get_close_reason() print("websocket closed... %d - %s" % [code, reason]) @@ -86,34 +100,35 @@ func _on_ws_closed(): ws_closed.emit() ws_opened_or_closed.emit(WebSocketPeer.STATE_CLOSED) + func _parse_msg() -> JamError: - var parsed = JSON.parse_string(ws.get_packet().get_string_from_utf8()) - if typeof(parsed) != TYPE_DICTIONARY \ + var parsed: Variant = JSON.parse_string(ws.get_packet().get_string_from_utf8()) + if not typeof(parsed) == TYPE_DICTIONARY \ or not parsed.has("type") \ or not parsed.has("id") \ - or typeof(parsed.get("data")) != TYPE_STRING: - return JamError.err("Message format incorrect: %s" % parsed) + or not typeof(parsed.get("data")) == TYPE_STRING: + return JamError.err("Message format incorrect: %s" % parsed as String) - var msg := parsed as Dictionary + var msg: Dictionary = parsed as Dictionary if not str(msg.type).is_valid_int() or not str(msg.id).is_valid_int(): return JamError.err("Message type and id are not both integers: %s" % msg) - var type := str(msg.type).to_int() - var src_id := str(msg.id).to_int() + var type: int = str(msg.type).to_int() + var src_id: int = str(msg.id).to_int() if type == Message.ID: - var pinfo = JSON.parse_string(msg.data) - if typeof(pinfo) != TYPE_DICTIONARY: - return JamError.err("ID message data is not a parseable JSON dictionary: %s" % msg.data) + var pinfo: Dictionary = JSON.parse_string(msg.data as String) + if not typeof(pinfo) == TYPE_DICTIONARY: + return JamError.err("ID message data is not a parseable JSON dictionary: %s" % msg.data as String) connected.emit(src_id, pinfo) elif type == Message.JOIN: session_joined.emit() elif type == Message.SEAL: session_sealed.emit() elif type == Message.PEER_CONNECT: - var pinfo = JSON.parse_string(msg.data) - if typeof(pinfo) != TYPE_DICTIONARY: - return JamError.err("PEER_CONNECT message data is not a parseable JSON dictionary: %s" % msg.data) + var pinfo: Dictionary = JSON.parse_string(msg.data as String) + if not typeof(pinfo) == TYPE_DICTIONARY: + return JamError.err("PEER_CONNECT message data is not a parseable JSON dictionary: %s" % msg.data as String) peer_connected.emit(src_id, pinfo) elif type == Message.PEER_DISCONNECT: peer_disconnected.emit(src_id) @@ -123,10 +138,10 @@ func _parse_msg() -> JamError: answer_received.emit(src_id, msg.data) elif type == Message.CANDIDATE: var candidate: PackedStringArray = msg.data.split("\n", false) - if candidate.size() != 3: - return JamError.err("CANDIDATE data is not exactly 3 lines: %s" % msg.data) + if not candidate.size() == 3: + return JamError.err("CANDIDATE data is not exactly 3 lines: %s" % msg.data as String) if not candidate[1].is_valid_int(): - return JamError.err("CANDIDATE index is not a valid integer: %s" % msg.data) + return JamError.err("CANDIDATE index is not a valid integer: %s" % msg.data as String) candidate_received.emit(src_id, candidate[0], candidate[1].to_int(), candidate[2]) elif type == Message.PING: print("got ping from signalling system") @@ -135,22 +150,28 @@ func _parse_msg() -> JamError: return JamError.ok() -func join_session(): + +func join_session() -> int: return _send_msg(Message.JOIN, 0, "") -func seal_lobby(): + +func seal_lobby() -> int: return _send_msg(Message.SEAL, 0) + func send_candidate(id: int, mid: String, index: int, sdp: String) -> int: return _send_msg(Message.CANDIDATE, id, "\n%s\n%d\n%s" % [mid, index, sdp]) + func send_offer(id: int, offer: String) -> int: return _send_msg(Message.OFFER, id, offer) + func send_answer(id: int, answer: String) -> int: return _send_msg(Message.ANSWER, id, answer) -func _send_msg(type: int, id: int, data:="") -> int: + +func _send_msg(type: int, id: int, data: Variant="") -> int: return ws.send_text(JSON.stringify({ "type": type, "id": id, diff --git a/core/JamClient.gd b/core/JamClient.gd index a20de08..bb59622 100644 --- a/core/JamClient.gd +++ b/core/JamClient.gd @@ -3,6 +3,8 @@ extends CanvasLayer ## A [CanvasLayer] that provides client-specific multiplayer capabilities as the ## child of a [JamConnect] Node +signal fetching_test_gjwt(busy: bool) + ## The client UI used to configure and initiate sessions var client_ui: JamClientUI @@ -11,8 +13,7 @@ var client_ui: JamClientUI var current_client_token: String ## A dictionary mapping peer id [code]int[/code]s to username [code]String[/code]s -var peer_usernames := {} - +var peer_usernames: Dictionary = {} var test_client_number: int = 1: set(val): if val != test_client_number and OS.is_debug_build(): @@ -20,8 +21,6 @@ var test_client_number: int = 1: await _setup_test_gjwt() var session_id: String = "" - -signal fetching_test_gjwt(busy: bool) var gjwt_fetch_busy: bool = false: set(val): gjwt_fetch_busy = val @@ -33,17 +32,19 @@ var jwt: JamJwt = JamJwt.new() var api: JamClientApi ## Helper object for acquiring a Game JWT for authentication var keys: ClientKeys - var _jc: JamConnect: get: return get_parent() -func _init(): - layer = 512 +const CANVAS_LAYER = 512 + +func _init() -> void: + layer = CANVAS_LAYER keys = ClientKeys.new() add_child(keys) -func _ready(): + +func _ready() -> void: api = JamClientApi.new() api.game_id = _jc.game_id api.jwt = jwt @@ -53,44 +54,48 @@ func _ready(): _jc.player_joined.connect(_on_player_joined) _jc.player_left.connect(_on_player_left) - var gjwt = keys.get_included_gjwt(_jc.get_game_id()) + var gjwt: Variant = keys.get_included_gjwt(_jc.get_game_id()) if gjwt == null: - if OS.is_debug_build() and OS.get_name() != "Android": + if OS.is_debug_build() and not OS.get_name() == "Android": _setup_test_gjwt() else: push_error("Failed to load GJWT") else: set_gjwt(gjwt as String) -func _setup_test_gjwt(): + +func _setup_test_gjwt() -> void: gjwt_fetch_busy = true - var gjwt = await keys.get_test_gjwt(_jc.game_id) + var gjwt: Variant = await keys.get_test_gjwt(_jc.game_id) gjwt_fetch_busy = false - if gjwt != null: + if not gjwt == null: set_gjwt(gjwt as String) else: push_error("Failed to load GJWT") -func set_gjwt(gjwt: String): - var gjwtRes = jwt.set_token(gjwt) + +func set_gjwt(gjwt: String) -> void: + var gjwtRes: JamJwt.TokenParseResult = jwt.set_token(gjwt) if gjwtRes.errored: push_error(gjwtRes.error) else: _jc.gjwt_acquired.emit() + ## Persists the GJWT so that it can be retrieved in the next run func persist_gjwt() -> bool: if not jwt.has_token(): return false - var gjwt_file = FileAccess.open("user://gjwt-%s" % _jc.get_game_id(), FileAccess.WRITE) + var gjwt_file: FileAccess = FileAccess.open("user://gjwt-%s" % _jc.get_game_id(), FileAccess.WRITE) if gjwt_file == null: return false gjwt_file.store_string(jwt.get_token()) gjwt_file.close() return true + ## Configures and starts client functionality -func client_start(): +func client_start() -> void: print("Starting as client...") _jc.m.connected_to_server.connect(_on_client_connect) @@ -102,20 +107,21 @@ func client_start(): client_ui.client_ui_initialization(_jc) add_child(client_ui) + ## Initiates a connection to a provisioned server -func client_session_request(host: String, port: int, token: String): +func client_session_request(host: String, port: int, token: String) -> void: current_client_token = token client_ui.visible = false _jc.log_event.emit("Attempting to connect to %s:%d..." % [host, port]) print("Attempting to connect to %s:%d..." % [host, port]) - var peer - var err + var peer: MultiplayerPeer + var err: Error if _jc.network_mode == "websocket": var chain: X509Certificate = null if host == "localhost" and OS.is_debug_build(): - var localchain = await _jc.fetch_dev_localhost_cert() + var localchain: Variant = await _jc.fetch_dev_localhost_cert() if localchain != null: chain = X509Certificate.new() chain.load_from_string(localchain as String) @@ -136,25 +142,29 @@ func client_session_request(host: String, port: int, token: String): _jc.m.auth_callback = _on_auth + ## Elegantly leaves the game by disconnecting from the server and notifying the ## Jam Launch API -func leave_game(): +func leave_game() -> void: client_ui.visible = true await client_ui.leave_game_session() _jc.m.multiplayer_peer.close() -func _on_client_connect_fail(): + +func _on_client_connect_fail() -> void: _jc.log_event.emit("Connection to server failed") _jc.local_player_left.emit() client_ui.visible = true -func _on_client_authentication_failed(): + +func _on_client_authentication_failed() -> void: _jc.log_event.emit("Authentication failed") _on_client_connect_fail() - -func _on_client_authenticating(peer_id: int): + + +func _on_client_authenticating(peer_id: int) -> void: _jc.log_event.emit("Authenticating...") - var auth_data = JSON.stringify({ + var auth_data: String = JSON.stringify({ "username": jwt.claims.get("username", "dev-%d" % _jc.m.multiplayer_peer.get_unique_id()), "token": current_client_token }) @@ -163,27 +173,33 @@ func _on_client_authenticating(peer_id: int): # accept the server peer without additional validation _on_auth(1, PackedByteArray()) -func _on_auth(peer_id: int, _data: PackedByteArray): + +func _on_auth(peer_id: int, _data: PackedByteArray) -> void: # For now, clients do not validate their peers. # Websocket servers with certs provide good server peer validation. - var err := _jc.m.complete_auth(peer_id) - if err != OK: + var err: Error = _jc.m.complete_auth(peer_id) + if not err == OK: _jc.log_event.emit("Unexpected failure to complete auth for %d..." % peer_id) -func _on_client_connect(): + +func _on_client_connect() -> void: _jc.log_event.emit("Connected to server") #_jc.local_player_joined.emit() -func _on_server_disconnect(): + +func _on_server_disconnect() -> void: _jc.log_event.emit("Server disconnected") _jc.local_player_left.emit() client_ui.visible = true -func _on_game_init_finalized(): + +func _on_game_init_finalized() -> void: client_ui.visible = false -func _on_player_joined(peer_id: int, username: String): + +func _on_player_joined(peer_id: int, username: String) -> void: peer_usernames[peer_id] = username -func _on_player_left(peer_id: int, _username: String): + +func _on_player_left(peer_id: int, _username: String) -> void: peer_usernames.erase(peer_id) diff --git a/core/JamConnect.gd b/core/JamConnect.gd index aae8056..e8ff658 100644 --- a/core/JamConnect.gd +++ b/core/JamConnect.gd @@ -119,16 +119,16 @@ var m: SceneMultiplayer # ----- Core Methods ----- # -func _init(): +func _init() -> void: print("Creating game node...") thread_helper = JamThreadHelper.new() add_child(thread_helper) - var dir := (self.get_script() as Script).get_path().get_base_dir() - var deployment_info = ConfigFile.new() - var err = deployment_info.load(dir + "/../deployment.cfg") - if err != OK: + var dir: String = (self.get_script() as Script).get_path().get_base_dir() + var deployment_info: ConfigFile = ConfigFile.new() + var err: Error = deployment_info.load(dir + "/../deployment.cfg") + if not err == OK: print("Game deployment settings could not be located - only the local hosting features will be available...") game_id = "init-undeployed" else: @@ -141,13 +141,15 @@ func _init(): network_mode = deployment_info.get_value("game", "network_mode", "enet") has_deployment = true -func _notification(what): + +func _notification(what: int) -> void: if what == NOTIFICATION_WM_CLOSE_REQUEST: # force quit after a timeout in case graceful shutdown blocks up await get_tree().create_timer(4.0).timeout get_tree().quit(1) -func _ready(): + +func _ready() -> void: print("JamConnect node ready, deferring auto start-up...") if not client_ui_scene: client_ui_scene = preload("../ui/client/ExampleClientUI.tscn") @@ -155,17 +157,18 @@ func _ready(): m = multiplayer start_up.call_deferred() + ## Start the JamConnect functionality including client/server determination and ## multiplayer peer creation and configuration. -func start_up(): +func start_up() -> void: print("Running JamConnect start-up...") get_tree().set_auto_accept_quit(false) JamRoot.get_jam_root(get_tree()).jam_connect = self - var args := {} - for a in OS.get_cmdline_args(): + var args: Dictionary = {} + for a: String in OS.get_cmdline_args(): if a.find("=") > -1: - var key_value = a.split("=") + var key_value: PackedStringArray = a.split("=") args[key_value[0].lstrip("--")] = key_value[1] elif a.begins_with("--"): args[a.lstrip("--")] = true @@ -180,10 +183,11 @@ func start_up(): add_child(client) client.client_start() + ## Converts this JamConnect node from being configured as a client to being ## being configured as a server in "dev" mode. Used for simplified local hosting ## in debug instances launched from the Godot editor. -func start_as_dev_server(): +func start_as_dev_server() -> void: client.queue_free() client = null @@ -191,15 +195,18 @@ func start_as_dev_server(): add_child(server) await server.server_start({"dev": true}) + ## Gets the project ID (the game ID without the release string) func get_project_id() -> String: return game_id.split("-")[0] + ## Gets the game ID (a.k.a. release ID - the project ID concatenated with the ## release string func get_game_id() -> String: return game_id + ## Gets the session ID func get_session_id() -> String: if server: @@ -209,104 +216,110 @@ func get_session_id() -> String: else: return "" + func is_webrtc_mode() -> bool: return network_mode == "webrtc" -func is_websocket_mode(): + +func is_websocket_mode() -> bool: return network_mode == "websocket" - + + func is_dedicated_server() -> bool: return multiplayer.is_server() and not is_webrtc_mode() + func is_player_server() -> bool: return multiplayer.is_server() and is_webrtc_mode() + func is_player() -> bool: return not multiplayer.is_server() or is_player_server() -# -# ----- Server methods ----- -# + +#region Server methods + @rpc("reliable") -func _send_player_joined(pid: int, username: String): +func _send_player_joined(pid: int, username: String) -> void: player_joined.emit(pid, username) + @rpc("reliable") -func _send_player_left(pid: int, username: String): +func _send_player_left(pid: int, username: String) -> void: player_left.emit(pid, username) + @rpc("reliable", "call_local") -func _send_game_init_finalized(): +func _send_game_init_finalized() -> void: game_init_finalized.emit() + ## A server-callable RPC method for broadcasting informational server messages ## to clients @rpc("reliable") -func notify_players(msg: String): +func notify_players(msg: String) -> void: log_event.emit(msg) +#endregion func fetch_dev_localhost_key() -> Variant: - var peer = StreamPeerTCP.new() + var peer: StreamPeerTCP = StreamPeerTCP.new() peer.connect_to_host("127.0.0.1", 17343) while true: await get_tree().create_timer(0.1).timeout - var err := peer.poll() - if err != OK: + var err: Error = peer.poll() + if not err == OK: push_error("failed to connect to local auth proxy for localhost cert key info - this might result in TLS handshake errors") peer.disconnect_from_host() return null if peer.get_status() == StreamPeerTCP.STATUS_CONNECTED: break - + peer.put_string("localhostkey") - while true: await get_tree().create_timer(0.1).timeout - var err := peer.poll() - if err != OK: + var err: Error = peer.poll() + if not err == OK: push_error("failed to get response from local auth proxy for localhost cert key") peer.disconnect_from_host() return null if peer.get_available_bytes() > 0: break - - var response := peer.get_string() - + + var response: String = peer.get_string() if response.begins_with("Error:"): push_error("failed to get localhost cert key - %s" % response) return null return response + func fetch_dev_localhost_cert() -> Variant: - var peer = StreamPeerTCP.new() + var peer: StreamPeerTCP = StreamPeerTCP.new() peer.connect_to_host("127.0.0.1", 17343) while true: await get_tree().create_timer(0.1).timeout - var err := peer.poll() - if err != OK: + var err: Error = peer.poll() + if not err == OK: push_error("failed to connect to local auth proxy for localhost cert - this might result in TLS handshake errors") peer.disconnect_from_host() return null if peer.get_status() == StreamPeerTCP.STATUS_CONNECTED: break - + peer.put_string("localhostcert") - while true: await get_tree().create_timer(0.1).timeout - var err := peer.poll() - if err != OK: + var err: Error = peer.poll() + if not err == OK: push_error("failed to get response from local auth proxy for localhost cert") peer.disconnect_from_host() return null if peer.get_available_bytes() > 0: break - + var response := peer.get_string() - if response.begins_with("Error:"): push_error("failed to get localhost cert - %s" % response) return null diff --git a/core/JamReplicator.gd b/core/JamReplicator.gd index df1646e..4e44544 100644 --- a/core/JamReplicator.gd +++ b/core/JamReplicator.gd @@ -1,16 +1,15 @@ class_name JamReplicator extends Node -const sync_interval = 1.0 / 30.0 -var sync_seq = 0 +const SYNC_INTERVAL: float = 1.0 / 30.0 +var sync_seq: int = 0 var sync_clock: float = 0.0 var sync_stable: bool = false - var state_buffer: Array[StateFrame] = [] var state_interp: float = 0.0 -var got_initial_state = false -var target_state_buffer_len = 4 -var target_state_buffer_len_min = 4 +var got_initial_state: bool = false +var target_state_buffer_len: int = 4 +var target_state_buffer_len_min: int = 4 # for dynamic client state buffer limiting var segment_time: float = 0.0 @@ -20,100 +19,40 @@ var drops_threshold: int = 3 var dropless_segments: int = 0 var dropless_threshold: int = 5 var buffer_increases: int = 0 - -var server_state = {} - -var sync_refs = {} - -class StateFrame: - extends RefCounted - var seq: int - var data: Dictionary - - static func from(s: int, d: Dictionary) -> StateFrame: - var f = StateFrame.new() - f.seq = s - f.data = d - return f - -class StateInterp: - extends RefCounted - var start_state: Variant - var end_state: Variant - var progress: float - var valid: bool - - static func invalid(): - var s = StateInterp.new() - s.valid = false - return s - - static func create(sbuf: Array[StateFrame], sync_id: int, interp: float): - if len(sbuf) < 1: - return StateInterp.invalid() - - var s = StateInterp.new() - s.valid = true - if sync_id not in sbuf[0].data: - return StateInterp.invalid() - s.start_state = sbuf[0].data[sync_id] - - if len(sbuf) == 1: - s.end_state = s.start_state - s.progress = 0.0 - else: - if sync_id not in sbuf[1].data: - return StateInterp.invalid() - s.end_state = sbuf[1].data[sync_id] - s.progress = interp - - return s +var server_state: Dictionary = {} +var sync_refs: Dictionary = {} +var spawn_scene_cache: Dictionary = {} static func get_replicator(tree: SceneTree) -> JamReplicator: return JamRoot.get_jam_root(tree).jam_replicator -func _ready(): + +func _ready() -> void: if get_parent().jam_connect: _jam_connect_init(get_parent().jam_connect as JamConnect) get_parent().has_jam_connect.connect(_jam_connect_init) -func _jam_connect_init(jc: JamConnect): - if not jc.m.is_server(): - return - jc.m.peer_connected.connect(_on_peer_connected) - jc.m.peer_disconnected.connect(_on_peer_disconnected) - -func _on_peer_connected(pid: int): - if not multiplayer.is_server(): - return - for sync_id in sync_refs: - scene_spawn(sync_refs[sync_id] as JamSync, pid) - -func _on_peer_disconnected(_pid: int): - if not multiplayer.is_server(): - return -func _process(delta): +func _process(delta: float) -> void: if not multiplayer.has_multiplayer_peer(): return - + if multiplayer.is_server(): sync_clock += delta - if sync_clock > sync_interval: + if sync_clock > SYNC_INTERVAL: sync_seq += 1 - sync_clock -= sync_interval - if sync_clock > sync_interval: + sync_clock -= SYNC_INTERVAL + if sync_clock > SYNC_INTERVAL: if sync_stable: push_warning("sync interval lagging due to large _process delta - resetting sync clock") sync_clock = 0.0 else: sync_stable = true - + sync_state.rpc(sync_seq, server_state) server_state = {} else: state_interp += delta - # adjust state buffer max based on quality of network segment_time += delta if segment_time >= segment_length: @@ -133,13 +72,13 @@ func _process(delta): if buffer_increases > 3: buffer_increases = 0 target_state_buffer_len_min += 1 - + drops_in_segment = 0 segment_time = 0.0 - + # remove expired states - while state_interp > sync_interval: - state_interp -= sync_interval + while state_interp > SYNC_INTERVAL: + state_interp -= SYNC_INTERVAL if len(state_buffer) > 1: state_buffer.pop_front() else: @@ -147,22 +86,43 @@ func _process(delta): push_warning("state buffer depleted") state_interp = 0.0 return StateInterp.invalid() - + # remove over-buffered states var overbuffered: int = len(state_buffer) - target_state_buffer_len if overbuffered > 0: push_warning("dropping %d over-buffered states" % overbuffered) drops_in_segment += 1 - state_interp = sync_interval + state_interp = SYNC_INTERVAL state_buffer = state_buffer.slice(overbuffered) elif len(state_buffer) == 1: state_interp = 0.0 -func amend_server_state(sync_id: int, value: Variant): + +func _jam_connect_init(jc: JamConnect) -> void: + if not jc.m.is_server(): + return + jc.m.peer_connected.connect(_on_peer_connected) + jc.m.peer_disconnected.connect(_on_peer_disconnected) + + +func _on_peer_connected(pid: int) -> void: + if not multiplayer.is_server(): + return + for sync_id: Variant in sync_refs: + scene_spawn(sync_refs[sync_id] as JamSync, pid) + + +func _on_peer_disconnected(_pid: int) -> void: + if not multiplayer.is_server(): + return + + +func amend_server_state(sync_id: int, value: Variant) -> void: server_state[sync_id] = value + @rpc("authority", "call_remote", "unreliable") -func sync_state(seq: int, data: Dictionary): +func sync_state(seq: int, data: Dictionary) -> void: if len(state_buffer) < 1: got_initial_state = true state_interp = 0.0 @@ -175,7 +135,7 @@ func sync_state(seq: int, data: Dictionary): elif seq < state_buffer[0].seq: push_warning("state drop %d" % seq) else: - var idx = 1 + var idx: int = 1 while idx < len(state_buffer): if seq == state_buffer[idx].seq: push_warning("state dupe %d" % seq) @@ -185,22 +145,23 @@ func sync_state(seq: int, data: Dictionary): break idx += 1 + func get_state(sync_id: int) -> StateInterp: - return StateInterp.create(state_buffer, sync_id, state_interp / sync_interval) + return StateInterp.create(state_buffer, sync_id, state_interp / SYNC_INTERVAL) -var spawn_scene_cache = {} func _instantiate_spawn_scene(scene_path: String) -> Node: if scene_path not in spawn_scene_cache: - var scene = load(scene_path) + var scene: Resource = load(scene_path) spawn_scene_cache[scene_path] = scene return spawn_scene_cache[scene_path].instantiate() -func scene_spawn(sync_node: JamSync, peer_id: int=-1): - var target = sync_node.get_parent() - var target_node_path = "/" + target.get_path().get_concatenated_names() + +func scene_spawn(sync_node: JamSync, peer_id: int=-1) -> void: + var target: Node = sync_node.get_parent() + var target_node_path: String = "/" + target.get_path().get_concatenated_names() - var sprops = {} + var sprops: Dictionary = {} for p in sync_node.spawn_properties: sprops[p] = target.get(p) @@ -209,21 +170,23 @@ func scene_spawn(sync_node: JamSync, peer_id: int=-1): else: _scene_spawn.rpc_id(peer_id, target_node_path, target.scene_file_path, sprops, sync_node.sync_id) -func scene_despawn(sync_node: JamSync): + +func scene_despawn(sync_node: JamSync) -> void: _scene_despawn.rpc(sync_node.sync_id) + @rpc("authority", "call_remote", "reliable") -func _scene_spawn(node_path: String, scene_path: String, spawn_properties: Dictionary, sync_id: int): +func _scene_spawn(node_path: String, scene_path: String, spawn_properties: Dictionary, sync_id: int) -> void: if sync_id in sync_refs: push_warning("sync id already in refs, no need to spawn: %d - %s" % [sync_id, node_path]) return var parent_path := node_path.rsplit("/", true, 1)[0] - var parent_node = get_node_or_null(parent_path) + var parent_node: Variant = get_node_or_null(parent_path) if parent_node == null: push_warning("received scene spawn sync for '%s' on missing parent node '%s'" % [scene_path, parent_path]) return var spawned_node := _instantiate_spawn_scene(scene_path) - for k in spawn_properties: + for k: Variant in spawn_properties: spawned_node.set(k as String, spawn_properties[k]) spawned_node.name = node_path.rsplit("/", true, 1)[1] for child in spawned_node.get_children(): @@ -232,11 +195,59 @@ func _scene_spawn(node_path: String, scene_path: String, spawn_properties: Dicti break parent_node.add_child(spawned_node) + @rpc("authority", "call_local", "reliable") -func _scene_despawn(sync_id: int): +func _scene_despawn(sync_id: int) -> void: if sync_id in sync_refs: sync_refs[sync_id].get_parent().queue_free() clear_sync_ref(sync_id) -func clear_sync_ref(sync_id: int): + +func clear_sync_ref(sync_id: int) -> void: sync_refs.erase(sync_id) + + +class StateFrame: + extends RefCounted + var seq: int + var data: Dictionary + + static func from(s: int, d: Dictionary) -> StateFrame: + var f: StateFrame = StateFrame.new() + f.seq = s + f.data = d + return f + + +class StateInterp: + extends RefCounted + var start_state: Variant + var end_state: Variant + var progress: float + var valid: bool + + static func invalid() -> StateInterp: + var s: StateInterp = StateInterp.new() + s.valid = false + return s + + + static func create(sbuf: Array[StateFrame], sync_id: int, interp: float) -> StateInterp: + if len(sbuf) < 1: + return StateInterp.invalid() + + var s: StateInterp = StateInterp.new() + s.valid = true + if sync_id not in sbuf[0].data: + return StateInterp.invalid() + s.start_state = sbuf[0].data[sync_id] + if len(sbuf) == 1: + s.end_state = s.start_state + s.progress = 0.0 + else: + if sync_id not in sbuf[1].data: + return StateInterp.invalid() + s.end_state = sbuf[1].data[sync_id] + s.progress = interp + + return s diff --git a/core/JamRoot.gd b/core/JamRoot.gd index 06f4460..3ec0a7b 100644 --- a/core/JamRoot.gd +++ b/core/JamRoot.gd @@ -1,16 +1,17 @@ class_name JamRoot extends Node -const JAM_ROOT_NAME = "JamRoot" - signal has_jam_connect(jam_connect: JamConnect) +const JAM_ROOT_NAME: String = "JamRoot" + +var jam_replicator: JamReplicator + var jam_connect: JamConnect: set(val): jam_connect = val has_jam_connect.emit(val) - -var jam_replicator: JamReplicator + static func get_jam_root(tree: SceneTree) -> JamRoot: var r: JamRoot = tree.root.get_node_or_null(JAM_ROOT_NAME) diff --git a/core/JamServer.gd b/core/JamServer.gd index 035bb44..2f027c2 100644 --- a/core/JamServer.gd +++ b/core/JamServer.gd @@ -3,8 +3,8 @@ extends Node ## A [Node] that provides server-specific multiplayer capabilities as the child ## of a [JamConnect] Node -const MAX_CLIENTS = 100 -const DEFAULT_PORT = 7437 +const MAX_CLIENTS: int = 100 +const DEFAULT_PORT: int = 7437 ## A dictionary mapping peer id [code]int[/code]s to username [code]String[/code]s var peer_usernames := {} @@ -12,18 +12,17 @@ var peer_usernames := {} ## True if the server is in developer mode. In developer mode, the clients are ## not verified and dummy API implementations are used instead of the AWS-driven ## implementations. -var dev_mode := false +var dev_mode: bool = false ## True if the local dev mode server has keys for accessing server APIs like ## Data and Callback -var has_dev_keys := false +var has_dev_keys: bool = false ## A callback API client for communicating back to Jam Launch var callback_api: JamCallbackApi ## A data API client for persisting project information var data_api: JamDataApi - var session_id: String = "unset": set(v): session_id = v @@ -34,7 +33,7 @@ var _jc: JamConnect: get: return get_parent() -func _ready(): +func _ready() -> void: session_id = OS.get_environment("SESSION_ID") callback_api = JamCallbackApi.new() add_child(callback_api) @@ -42,13 +41,15 @@ func _ready(): data_api = JamDataApi.new() add_child(data_api) -func _notification(what): + +func _notification(what: int) -> void: if what == NOTIFICATION_WM_CLOSE_REQUEST: printerr("server got quit notification") shut_down() + ## Configures and starts server functionality -func server_start(args: Dictionary): +func server_start(args: Dictionary) -> void: print("Starting as server...") data_api.project_id = _jc.get_project_id() @@ -61,35 +62,35 @@ func server_start(args: Dictionary): dev_mode = "dev" in args and args["dev"] - var peer - var err + var peer: MultiplayerPeer + var err: Error if _jc.network_mode == "websocket": peer = WebSocketMultiplayerPeer.new() var key: CryptoKey var cert: X509Certificate if OS.is_debug_build() or dev_mode: - var crypto = Crypto.new() - var localkey = await _jc.fetch_dev_localhost_key() - if localkey != null: + var crypto: Crypto = Crypto.new() + var localkey: Variant = await _jc.fetch_dev_localhost_key() + if not localkey == null: key = CryptoKey.new() key.load_from_string(localkey as String) else: key = crypto.generate_rsa(2048) - - var localcert = await _jc.fetch_dev_localhost_cert() - if localcert != null: + + var localcert: Variant = await _jc.fetch_dev_localhost_cert() + if not localcert == null: cert = X509Certificate.new() cert.load_from_string(localcert as String) else: cert = crypto.generate_self_signed_certificate(key, "CN=localhost") else: print("Setting up certificates for secure websockets...") - var extra_downloads := OS.get_environment("EXTRA_DOWNLOAD_URLS") + var extra_downloads: String = OS.get_environment("EXTRA_DOWNLOAD_URLS") if len(extra_downloads) < 0: push_error("FATAL: Missing EXTRA_DOWNLOAD_URLS environment variable for cert and key downloads") get_tree().quit(1) return - var extras = JSON.parse_string(extra_downloads) + var extras: Variant = JSON.parse_string(extra_downloads) if extras == null: push_error("FATAL: EXTRA_DOWNLOAD_URLS environment variable failed to parse as valid JSON") get_tree().quit(1) @@ -98,25 +99,25 @@ func server_start(args: Dictionary): push_error("FATAL: Missing cert and key downloads") get_tree().quit(1) return - var key_res := await callback_api.get_string_data(extras["server.key"] as String) + var key_res: JamResult = await callback_api.get_string_data(extras["server.key"] as String) if key_res.errored: push_error("FATAL: server key download failed - %s" % key_res.error_msg) get_tree().quit(1) return - var cert_res := await callback_api.get_string_data(extras["server.crt"] as String) + var cert_res: JamResult = await callback_api.get_string_data(extras["server.crt"] as String) if cert_res.errored: push_error("FATAL: server cert download failed - %s" % cert_res.error_msg) get_tree().quit(1) return key = CryptoKey.new() err = key.load_from_string(key_res.value as String) - if err != OK: + if not err == OK: push_error("FATAL: Failed to load server key %s" % key_res.value) get_tree().quit(1) return cert = X509Certificate.new() err = cert.load_from_string(cert_res.value as String) - if err != OK: + if not err == OK: push_error("FATAL: Failed to load server cert %s" % cert_res.value) get_tree().quit(1) return @@ -124,7 +125,7 @@ func server_start(args: Dictionary): else: peer = ENetMultiplayerPeer.new() err = peer.create_server(listen_port, MAX_CLIENTS) - if err != OK: + if not err == OK: push_error("FATAL: Failed to start server on port %d - err code %d" % [listen_port, err]) get_tree().quit(1) return @@ -142,44 +143,46 @@ func server_start(args: Dictionary): else: # TODO: new file/DB system _jc.server_pre_ready.emit() - var res = await callback_api.send_ready() + var res: JamHttpBase.Result = await callback_api.send_ready() if res.errored: printerr("FATAL: Failed to set READY status in database - %s - aborting..." % res.error_msg) get_tree().quit() _jc.server_post_ready.emit() + ## Authenticates a player by verifying the provided [code]username[/code] and ## [code]token[/code] with the Jam Launch callback. In developer mode, a token ## with the value [code]"localdev"[/code] will always verify successfully. -func _auth_callback(peer_id: int, data: PackedByteArray): - var data_json := data.get_string_from_utf8() +func _auth_callback(peer_id: int, data: PackedByteArray) -> void: + var data_json: String = data.get_string_from_utf8() if data_json == "": printerr("Invalid UTF-8 auth data provided by peer %d" % peer_id) - var data_obj = JSON.parse_string(data_json) + var data_obj: Variant = JSON.parse_string(data_json) if data_obj == null: printerr("Invalid JSON auth data provided by peer %d" % peer_id) print("peer ", peer_id, ", data: ", data_obj) - - var username := data_obj["username"] as String + + var username: String = data_obj["username"] as String if peer_id in peer_usernames: printerr("Unexpected duplicate auth call for peer %d : %s : %s" % [peer_id, peer_usernames[peer_id], username]) - - var res := await _check_auth(username, data_obj["token"] as String) + + var res: JamError = await _check_auth(username, data_obj["token"] as String) if res.errored: push_error("Auth failure - peer id %d - %s" % [peer_id, res.error_msg]) _jc.m.disconnect_peer(peer_id) return - + print("Correlating peer %d with username %s" % [peer_id, username]) peer_usernames[peer_id] = username - var err := _jc.m.complete_auth(peer_id) - if err != OK: + var err: Error = _jc.m.complete_auth(peer_id) + if not err == OK: printerr("Unexpected error when completing %d auth - code %d" % [peer_id, err]) peer_usernames.erase(peer_id) return - + print("Authenticated peer %d as %s!" % [peer_id, username]) + func _check_auth(username: String, join_token: String) -> JamError: if dev_mode: if join_token == "localdev": @@ -187,29 +190,31 @@ func _check_auth(username: String, join_token: String) -> JamError: else: return JamError.err("Failed local dev auth for %s - token %s" % [username, join_token]) - var res := await callback_api.check_token(username, join_token) + var res: JamHttpBase.Result = await callback_api.check_token(username, join_token) if res.errored: return JamError.err("Failed verification of join token for %s (%s) - %s" % [username, join_token, res.error_msg]) else: return JamError.ok() -func _on_peer_connect(peer_id: int): + +func _on_peer_connect(peer_id: int) -> void: if peer_id not in peer_usernames: printerr("Unexpected connect without username record - peer_id %d" % peer_id) return - var username = peer_usernames[peer_id] - for other in peer_usernames.keys(): - if peer_usernames[other] != username: + var username: String = peer_usernames[peer_id] + for other: Variant in peer_usernames.keys(): + if not peer_usernames[other] == username: _jc._send_player_joined.rpc_id(peer_id, other, peer_usernames[other]) _jc.notify_players.rpc_id(peer_id, "'%s' is here" % peer_usernames[other]) _jc.notify_players.rpc("'%s' has connected" % username) _jc.player_connected.emit(peer_id, username) _jc._send_player_joined.rpc(peer_id, username) -func _on_peer_disconnect(pid: int): + +func _on_peer_disconnect(pid: int) -> void: if pid in peer_usernames: - var username = peer_usernames[pid] + var username: String = peer_usernames[pid] peer_usernames.erase(pid) _jc.notify_players.rpc("'%s' has disconnected" % username) _jc.player_disconnected.emit(pid, username) @@ -219,24 +224,26 @@ func _on_peer_disconnect(pid: int): print("All peers disconnected - shutting down...") shut_down(false) + ## Shuts down the server elegantly -func shut_down(do_disconnect: bool = true): +func shut_down(do_disconnect: bool = true) -> void: if do_disconnect: - for pid in _jc.m.get_authenticating_peers(): + for pid: int in _jc.m.get_authenticating_peers(): multiplayer.multiplayer_peer.disconnect_peer(pid, true) - for pid in peer_usernames.keys(): + for pid: int in peer_usernames.keys(): multiplayer.multiplayer_peer.disconnect_peer(pid as int, true) _jc.server_shutting_down.emit() get_tree().quit() -func _setup_local_dev_keys(): - var local_keys = await _fetch_local_dev_keys() + +func _setup_local_dev_keys() -> void: + var local_keys: Variant = await _fetch_local_dev_keys() if local_keys == null: return - + session_id = local_keys["session_id"] callback_api.api_url = local_keys["callback_url"] as String - var res := callback_api.jwt.set_token(local_keys["callback_key"] as String) + var res: JamJwt.TokenParseResult = callback_api.jwt.set_token(local_keys["callback_key"] as String) if res.errored: printerr("failed to set dev key for callback API - %s" % [res.error]) return @@ -245,12 +252,13 @@ func _setup_local_dev_keys(): if res.errored: printerr("failed to set dev key for data API - %s" % [res.error]) return - + print("Setup local server dev keys for data and callback access - session %s" % [session_id]) has_dev_keys = true + func _fetch_local_dev_keys() -> Variant: - var peer = StreamPeerTCP.new() + var peer: StreamPeerTCP = StreamPeerTCP.new() peer.connect_to_host("127.0.0.1", 17343) while true: await get_tree().create_timer(0.1).timeout @@ -260,29 +268,27 @@ func _fetch_local_dev_keys() -> Variant: return null if peer.get_status() == StreamPeerTCP.STATUS_CONNECTED: break - - var parts = _jc.get_game_id().split("-") + + var parts: PackedStringArray = _jc.get_game_id().split("-") peer.put_string("serverkeys/%s/%s" % [parts[0], parts[1] + "dev"]) - while true: await get_tree().create_timer(0.1).timeout - var err := peer.poll() - if err != OK: + var err: Error = peer.poll() + if not err == OK: push_error("failed to get response from local auth proxy for server creds") return null if peer.get_available_bytes() > 0: break - - var json_response := peer.get_string() - + + var json_response: String = peer.get_string() if json_response.begins_with("Error:"): push_error("failed to get server creds - %s" % json_response) return null - - var result = JSON.parse_string(json_response) + + var result: Variant = JSON.parse_string(json_response) if result == null: push_error("failed to parse server creds result - %s" % json_response) return null - + peer.disconnect_from_host() return result diff --git a/core/JamSync.gd b/core/JamSync.gd index 2792611..cab72db 100644 --- a/core/JamSync.gd +++ b/core/JamSync.gd @@ -1,23 +1,34 @@ class_name JamSync extends Node +@export var spawn_properties: Array[String] = [] +@export var sync_properties: Array[String] = [] + var jam_root: JamRoot var replicator: JamReplicator var sync_id: int -@export var spawn_properties: Array[String] = [] -@export var sync_properties: Array[String] = [] +const LERP_TYPES = [ + TYPE_INT, + TYPE_FLOAT, + TYPE_VECTOR2, + TYPE_VECTOR3, + TYPE_VECTOR4, + TYPE_COLOR, + TYPE_QUATERNION, + TYPE_BASIS +] -func _ready(): +func _ready() -> void: jam_root = JamRoot.get_jam_root(get_tree()) replicator = JamReplicator.get_replicator(get_tree()) - get_parent().ready.connect(_target_ready) -func _target_ready(): + +func _target_ready() -> void: if not multiplayer.has_multiplayer_peer() or not jam_root.jam_connect: return - + if multiplayer.is_server(): sync_id = get_instance_id() replicator.sync_refs[sync_id] = self @@ -25,35 +36,32 @@ func _target_ready(): else: replicator.sync_refs[sync_id] = self -func _exit_tree(): + +func _exit_tree() -> void: if multiplayer.is_server(): replicator.scene_despawn(self) else: replicator.clear_sync_ref(sync_id) -func _process(_delta): - pass - -const LERP_TYPES = [TYPE_INT, TYPE_FLOAT, TYPE_VECTOR2, TYPE_VECTOR3, TYPE_VECTOR4, TYPE_COLOR, TYPE_QUATERNION, TYPE_BASIS] -func _physics_process(_delta): +func _physics_process(_delta: float) -> void: if not multiplayer.has_multiplayer_peer(): return - + if not jam_root.jam_connect: return - - var target = get_parent() + + var target: Node = get_parent() if multiplayer.is_server(): - var server_state = {} - for p in sync_properties: + var server_state: Dictionary = {} + for p: String in sync_properties: server_state[p] = target.get(p) replicator.amend_server_state(sync_id, server_state) else: - var s = replicator.get_state(sync_id) + var s: JamReplicator.StateInterp = replicator.get_state(sync_id) if not s.valid: return - for p in s.start_state: + for p: Variant in s.start_state: var val: Variant = s.start_state[p] if typeof(val) in LERP_TYPES: val = lerp(val, s.end_state[p], s.progress) diff --git a/core/JamThreadHelper.gd b/core/JamThreadHelper.gd index ca96537..5b39ea7 100644 --- a/core/JamThreadHelper.gd +++ b/core/JamThreadHelper.gd @@ -3,9 +3,8 @@ class_name JamThreadHelper extends Node ## A [Node] that provides utilities for simplifying common [Thread] operations -var _product_mtx := Mutex.new() +var _product_mtx: Mutex = Mutex.new() var _product_map: Dictionary = {} - var _lazy_timer: Timer var _thread_wait_timer: Timer: get: @@ -15,23 +14,6 @@ var _thread_wait_timer: Timer: _lazy_timer.start(.25) return _lazy_timer -class ThreadProduct: - extends RefCounted - var value: Variant = null - var errored: bool = false - var error_msg: String = "" - - static func make(val: Variant) -> ThreadProduct: - var p = ThreadProduct.new() - p.value = val - return p - - static func err(msg: String) -> ThreadProduct: - var p = ThreadProduct.new() - p.errored = true - p.error_msg = msg - return p - func take_product(id: int) -> ThreadProduct: var product: Variant = _product_map.get(id, null) if product == null: @@ -41,54 +23,78 @@ func take_product(id: int) -> ThreadProduct: _product_mtx.unlock() return product + func put_product(id: int, product: ThreadProduct) -> void: _product_mtx.lock() _product_map[id] = product _product_mtx.unlock() -func producer_wrapper(id: int, producer: Callable): - var r = producer.call() + +func producer_wrapper(id: int, producer: Callable) -> void: + var r: Variant = producer.call() put_product(id, ThreadProduct.make(r)) -class ProducerHandle: - extends RefCounted - var product_id: int - var task_id: int - - func _init(prod_id: int, tsk_id: int): - self.product_id = prod_id - self.task_id = tsk_id func _add_a_producer(producer: Callable) -> ProducerHandle: - var product_id := randi() + var product_id: int = randi() while product_id in _product_map: product_id = randi() - var task_id := WorkerThreadPool.add_task(producer_wrapper.bind(product_id, producer)) + var task_id: int = WorkerThreadPool.add_task(producer_wrapper.bind(product_id, producer)) return ProducerHandle.new(product_id, task_id) + ## An awaitable function that runs a thread-safe function on a separate thread ## and retrieves the return value. Useful for async-ifying functions and ## retrieving their result. func run_threaded_producer(producer: Callable) -> ThreadProduct: - var handle := _add_a_producer(producer) + var handle: ProducerHandle = _add_a_producer(producer) while true: await _thread_wait_timer.timeout if WorkerThreadPool.is_task_completed(handle.task_id): return take_product(handle.product_id) - + return ThreadProduct.err("unexpected failure while waiting for threaded task completion") + func run_multiple_producers(producers: Array[Callable]) -> Array[ThreadProduct]: var handles: Array[ProducerHandle] = [] - for producer in producers: + for producer: Callable in producers: handles.append(_add_a_producer(producer)) var products: Array[ThreadProduct] = [] - while len(handles) > 0: + while not handles.is_empty(): await _thread_wait_timer.timeout if WorkerThreadPool.is_task_completed(handles.front().task_id as int): print("task done") - var handle := handles.pop_front() as ProducerHandle + var handle: ProducerHandle = handles.pop_front() as ProducerHandle products.append(take_product(handle.product_id)) return products + + +class ProducerHandle: + extends RefCounted + var product_id: int + var task_id: int + + func _init(prod_id: int, tsk_id: int) -> void: + self.product_id = prod_id + self.task_id = tsk_id + + +class ThreadProduct: + extends RefCounted + var value: Variant = null + var errored: bool = false + var error_msg: String = "" + + static func make(val: Variant) -> ThreadProduct: + var p: ThreadProduct = ThreadProduct.new() + p.value = val + return p + + static func err(msg: String) -> ThreadProduct: + var p: ThreadProduct = ThreadProduct.new() + p.errored = true + p.error_msg = msg + return p diff --git a/editor_plugin/ChannelSummary.gd b/editor_plugin/ChannelSummary.gd index 1d07dd6..64329b9 100644 --- a/editor_plugin/ChannelSummary.gd +++ b/editor_plugin/ChannelSummary.gd @@ -1,41 +1,39 @@ @tool extends MarginContainer -@onready var title: RichTextLabel = $M/VB/Title +signal update_channel(channel: String, data: Dictionary) +@onready var title: RichTextLabel = $M/VB/Title @onready var link_nav: Button = $M/VB/Config/MC/VB/HB/PageLink @onready var link_copy: Button = $M/VB/Config/MC/VB/HB/Copy - @onready var access_icon: TextureRect = $M/VB/Config/MC/VB/Access/AccessIcon @onready var check_public: CheckButton = $M/VB/Config/MC/VB/Access/CheckPublic @onready var check_guests: CheckButton = $M/VB/Config/MC/VB/CheckGuests @onready var check_default: CheckButton = $M/VB/Config/MC/VB/CheckDefault -signal update_channel(channel: String, data: Dictionary) - var c: Dictionary = {} - -func _load_lock_changed(locked: bool): +func _load_lock_changed(locked: bool) -> void: check_public.disabled = locked check_guests.disabled = locked check_default.disabled = locked -func set_channel(channel_data: Dictionary, release_name: String = ""): + +func set_channel(channel_data: Dictionary, release_name: String = "") -> void: c = channel_data - + if len(release_name) < 1: release_name = "No Release" title.clear() title.push_font_size(18) title.push_bold() - title.add_text(channel_data.get("name")) + title.add_text(channel_data.get("name") as String) title.pop_all() title.push_context() title.push_color(Color(1, 1, 1, 0.4)) title.add_text("\n%s" % [release_name]) title.pop_context() - + if channel_data.get("public_release", false): check_public.text = "Public" access_icon.texture = preload("res://addons/jam_launch/assets/icons/public.svg") @@ -44,15 +42,16 @@ func set_channel(channel_data: Dictionary, release_name: String = ""): check_public.text = "Private" access_icon.texture = preload("res://addons/jam_launch/assets/icons/lock.svg") access_icon.modulate = Color("white") - + check_default.set_pressed_no_signal(channel_data.get("default_release", false)) check_public.set_pressed_no_signal(channel_data.get("public_release", false)) check_guests.set_pressed_no_signal(channel_data.get("allow_guests", false)) -func _on_config_change(_toggled_on: bool): + +func _on_config_change(_toggled_on: bool) -> void: if not c.get("name"): return - + update_channel.emit(c.get("name"), { "default_release": check_default.button_pressed, "public_release": check_public.button_pressed, diff --git a/editor_plugin/Dashboard.gd b/editor_plugin/Dashboard.gd index 002c46e..40009c8 100644 --- a/editor_plugin/Dashboard.gd +++ b/editor_plugin/Dashboard.gd @@ -2,92 +2,95 @@ class_name JamEditorPluginDashboard extends MarginContainer -var msg_scn = preload("res://addons/jam_launch/ui/MessagePanel.tscn") -var plugin: EditorPlugin @onready var project_api: JamProjectApi = $ProjectApi - @onready var load_locker: ScopeLocker = $LoadLocker - @onready var pages: JamPageStack = $VB/PageStack @onready var login_page: JamEditorPluginPage = $VB/PageStack/Login @onready var project_select_page: JamEditorPluginPage = $VB/PageStack/ProjectSelect @onready var project_page: JamEditorPluginPage = $VB/PageStack/Project @onready var new_project_page: JamEditorPluginPage = $VB/PageStack/NewProject @onready var sessions_page: JamEditorPluginPage = $VB/PageStack/Sessions - @onready var errors: VBoxContainer = $Errors @onready var toolbar: HBoxContainer = $VB/ToolBar @onready var toolbar_refresh: Button = $VB/ToolBar/Refresh @onready var toolbar_back: Button = $VB/ToolBar/Back @onready var toolbar_title: Label = $VB/ToolBar/Title - @onready var auth_proxy: JamAuthProxy = $JamAuthProxy +var msg_scn: Resource = preload("res://addons/jam_launch/ui/MessagePanel.tscn") +var plugin: EditorPlugin + func _ready() -> void: if not plugin: return - + if not Engine.is_editor_hint(): return - + toolbar_refresh.icon = editor_icon("Reload") toolbar_back.icon = editor_icon("Back") - toolbar_back.visible = false - pages.go_back_enabled.connect(func(enabled: bool): toolbar_back.visible = enabled) - + pages.go_back_enabled.connect(func(enabled: bool) -> void: toolbar_back.visible = enabled) for page in pages.get_children(): if is_instance_of(page, JamEditorPluginPage): page.page_init() auth_proxy.api = project_api auth_proxy.start() - login_page.jwt.token_changed.connect(_on_jwt_changed) project_api.jwt = login_page.jwt - _on_jwt_changed(login_page.jwt.get_token()) - + _on_jwt_changed(login_page.jwt.get_token() as String) _on_page_stack_tab_changed(pages.current_tab) -func editor_icon(name: StringName) -> Texture2D: - return plugin.get_editor_interface().get_base_control().get_theme_icon(name, "EditorIcons") -func _on_page_stack_tab_changed(_tab): - var active_page = pages.get_current_tab_control() +func editor_icon(iconName: StringName) -> Texture2D: + return EditorInterface.get_base_control().get_theme_icon(iconName, "EditorIcons") + + +func _on_page_stack_tab_changed(_tab: Variant) -> void: + var active_page: Control = pages.get_current_tab_control() toolbar_refresh.visible = active_page.has_method("refresh_page") toolbar.visible = true toolbar_title.text = "" active_page.show_init() -func show_page(page: JamEditorPluginPage, push_to_stack: bool = true): + +func show_page(page: JamEditorPluginPage, push_to_stack: bool = true) -> void: pages.show_page_node(page, push_to_stack) -func _on_jwt_changed(token): - if len(token) > 0: + +func _on_jwt_changed(token: String) -> void: + if not token.is_empty(): show_page(project_select_page, false) else: show_page(login_page, false) + func _on_project_select_open_project(project_id: String, project_name: String) -> void: show_page(project_page) project_page.show_project(project_id, project_name) + func _on_project_select_new_project() -> void: show_page(new_project_page) + func _on_new_project_cancel() -> void: pages.go_back() + func _on_new_project_create_done(project_id: String, project_name: String) -> void: pages.go_back() show_page(project_page) project_page.show_project.call_deferred(project_id, project_name) -func _on_project_session_page_selected(project_id: String, project_name: String): + +func _on_project_session_page_selected(project_id: String, project_name: String) -> void: show_page(sessions_page) sessions_page.show_game(project_id, project_name) -func show_error(msg: String, auto_dismiss_delay: float = 0.0): + +func show_error(msg: String, auto_dismiss_delay: float = 0.0) -> void: printerr(msg) var msg_panel: MessagePanel = msg_scn.instantiate() errors.add_child(msg_panel) @@ -96,25 +99,30 @@ func show_error(msg: String, auto_dismiss_delay: float = 0.0): if auto_dismiss_delay > 0.0: msg_panel.set_auto_dismiss(auto_dismiss_delay) -func show_message(msg: String, auto_dismiss: float = 0.0): + +func show_message(msg: String, auto_dismiss: float = 0.0) -> void: print(msg) - var msg_box := msg_scn.instantiate() + var msg_box: MessagePanel = msg_scn.instantiate() errors.add_child(msg_box) errors.move_child(msg_box, 0) msg_box.message = msg if auto_dismiss > 0.0: msg_box.set_auto_dismiss(auto_dismiss) -func _on_log_out_pressed(): + +func _on_log_out_pressed() -> void: login_page.jwt.clear() -func _on_refresh_pressed(): - var active_page = pages.get_current_tab_control() + +func _on_refresh_pressed() -> void: + var active_page: Control = pages.get_current_tab_control() if active_page.has_method("refresh_page"): active_page.call("refresh_page") -func _on_back_pressed(): + +func _on_back_pressed() -> void: pages.go_back() -func _on_load_locker_lock_changed(locked): + +func _on_load_locker_lock_changed(locked: bool) -> void: toolbar_refresh.disabled = locked diff --git a/editor_plugin/JamAuthProxy.gd b/editor_plugin/JamAuthProxy.gd index 9befc67..86bf245 100644 --- a/editor_plugin/JamAuthProxy.gd +++ b/editor_plugin/JamAuthProxy.gd @@ -1,18 +1,104 @@ @tool -extends Node class_name JamAuthProxy +extends Node -var server := TCPServer.new() +var server: TCPServer = TCPServer.new() var connections: Array[StreamPeerTCP] -var req_counter := 0 +var req_counter: int = 0 var api: JamProjectApi - var localhostKey: String var localhostCert: String +const PORT: int = 17343 +const RSA_SIZE: int = 2048 +const LOCALHOST: String = "127.0.0.1" + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + pass + + +func start() -> void: + if server.is_listening(): + return + + print_debug("starting Jam Launch auth proxy server...") + var crypto: Crypto = Crypto.new() + var key: CryptoKey = crypto.generate_rsa(RSA_SIZE) + localhostKey = key.save_to_string() + localhostCert = crypto.generate_self_signed_certificate(key, "CN=localhost").save_to_string() + var err: Error = server.listen(PORT, LOCALHOST) + if not err == OK: + printerr("failed to start auth proxy server - code %d" % err) + + +func _exit_tree() -> void: + if server.is_listening(): + print_debug("stopping Jam Launch auth proxy server...") + server.stop() + + +func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE: + if server.is_listening(): + print_debug("stopping Jam Launch auth proxy server...") + server.stop() + + +func get_port() -> int: + return server.get_local_port() + + +# Called every frame. 'delta' is the elapsed time since the previous frame. +func _process(_delta: float) -> void: + if not server.is_listening(): + return + + var to_remove: Array[StreamPeerTCP] = [] + for c: StreamPeerTCP in connections: + var err: Error = c.poll() + if not err == OK: + to_remove.push_back(c) + continue + + var status: int = c.get_status() + if status == StreamPeerTCP.STATUS_ERROR: + to_remove.push_back(c) + + if status != StreamPeerTCP.STATUS_CONNECTED: + continue + + if c.get_available_bytes() < 1: + continue + + var req_string: String = c.get_string() + var req: RequestHandler = RequestHandler.new() + req.localhostKey = localhostKey + req.localhostCert = localhostCert + req.request = req_string + req.request_num = req_counter + req.api = api + req.peer = c + add_child(req) + req_counter += 1 + + for c: StreamPeerTCP in to_remove: + connections.erase(c) + + for r: Node in get_children(): + if r.is_done: + r.queue_free() + + while true: + if not server.is_connection_available(): + return + + connections.append(server.take_connection()) + + class RequestHandler: extends Node - + var peer: StreamPeerTCP var request: String var request_num: int @@ -21,12 +107,12 @@ class RequestHandler: var is_started: bool = false var localhostKey: String = "" var localhostCert: String = "" - - func _process(_delta): + + func _process(_delta: float) -> void: if is_started: return + is_started = true - var req_parts := request.split("/") if req_parts[0] == "key": _get_testclient_key(req_parts) @@ -39,131 +125,58 @@ class RequestHandler: else: await _err("Unexpected auth proxy request: %s" % req_parts[0]) return - - func _err(msg: String): + + func _err(msg: String) -> void: printerr(msg) peer.put_string("Error: %s" % msg) await get_tree().create_timer(1.0).timeout is_done = true - - func _get_testclient_key(req_parts: PackedStringArray): - if len(req_parts) != 3: + + + func _get_testclient_key(req_parts: PackedStringArray) -> void: + if not len(req_parts) == 3: await _err("Expected 3 parts in key request, got %d" % len(req_parts)) return - - var test_num = (request_num % 9) + 1 - var res := await api.get_test_key(req_parts[1], req_parts[2], test_num) + + var test_num: int = (request_num % 9) + 1 + var res: JamHttpBase.Result = await api.get_test_key(req_parts[1], req_parts[2], test_num) if res.errored: await _err(res.error_msg) return - peer.put_string(res.data["test_jwt"]) + + peer.put_string(res.data["test_jwt"] as String) await get_tree().create_timer(1.0).timeout is_done = true - - func _get_server_keys(req_parts: PackedStringArray): - if len(req_parts) != 3: + + + func _get_server_keys(req_parts: PackedStringArray) -> void: + if not len(req_parts) == 3: await _err("Expected 3 parts in server keys request, got %d" % len(req_parts)) return - - var res := await api.get_local_server_keys(req_parts[1], req_parts[2]) + + var res: JamHttpBase.Result = await api.get_local_server_keys(req_parts[1], req_parts[2]) if res.errored: await _err(res.error_msg) return + peer.put_string(JSON.stringify(res.data)) await get_tree().create_timer(1.0).timeout is_done = true - - func _get_localhost_key(req_parts: PackedStringArray): - if len(req_parts) != 1: + + + func _get_localhost_key(req_parts: PackedStringArray) -> void: + if not len(req_parts) == 1: await _err("Expected 1 part in localhost key request, got %d" % len(req_parts)) return peer.put_string(localhostKey) await get_tree().create_timer(1.0).timeout is_done = true - - func _get_localhost_cert(req_parts: PackedStringArray): - if len(req_parts) != 1: + + + func _get_localhost_cert(req_parts: PackedStringArray) -> void: + if not len(req_parts) == 1: await _err("Expected 1 parts in localhost cert request, got %d" % len(req_parts)) return peer.put_string(localhostCert) await get_tree().create_timer(1.0).timeout is_done = true - - -# Called when the node enters the scene tree for the first time. -func _ready(): - pass - - -func start(): - if server.is_listening(): - return - print_debug("starting Jam Launch auth proxy server...") - - var crypto := Crypto.new() - var key := crypto.generate_rsa(2048) - localhostKey = key.save_to_string() - localhostCert = crypto.generate_self_signed_certificate(key, "CN=localhost").save_to_string() - - var port := 17343 - var err := server.listen(port, "127.0.0.1") - if err != OK: - printerr("failed to start auth proxy server - code %d" % err) - -func _exit_tree(): - if server.is_listening(): - print_debug("stopping Jam Launch auth proxy server...") - server.stop() - -func _notification(what): - if what == NOTIFICATION_PREDELETE: - if server.is_listening(): - print_debug("stopping Jam Launch auth proxy server...") - server.stop() - -func get_port(): - return server.get_local_port() - -# Called every frame. 'delta' is the elapsed time since the previous frame. -func _process(delta): - if not server.is_listening(): - return - - var to_remove = [] - for c in connections: - var err = c.poll() - if err != OK: - to_remove.push_back(c) - continue - var status = c.get_status() - if status == StreamPeerTCP.STATUS_ERROR: - to_remove.push_back(c) - if status != StreamPeerTCP.STATUS_CONNECTED: - continue - - if c.get_available_bytes() < 1: - continue - - var req_string = c.get_string() - var req = RequestHandler.new() - req.localhostKey = localhostKey - req.localhostCert = localhostCert - req.request = req_string - req.request_num = req_counter - req.api = api - req.peer = c - add_child(req) - - req_counter += 1 - - for c in to_remove: - connections.erase(c) - - for r in get_children(): - if r.is_done: - r.queue_free() - - while true: - if not server.is_connection_available(): - return - connections.append(server.take_connection()) diff --git a/editor_plugin/JamEditorPluginPage.gd b/editor_plugin/JamEditorPluginPage.gd index fc1dc1c..27f9968 100644 --- a/editor_plugin/JamEditorPluginPage.gd +++ b/editor_plugin/JamEditorPluginPage.gd @@ -6,8 +6,8 @@ var dashboard: JamEditorPluginDashboard var project_api: JamProjectApi var plugin: EditorPlugin -func page_init(): - var d = get_parent() +func page_init() -> void: + var d: Node = get_parent() while not d.is_in_group("jam_launch_dashboard"): d = d.get_parent() if not d: @@ -18,11 +18,14 @@ func page_init(): plugin = dashboard.plugin _page_init() -func _page_init(): + +func _page_init() -> void: pass -func show_init(): + +func show_init() -> void: pass -func _ready(): + +func _ready() -> void: add_to_group("jam_editor_plugin_page") diff --git a/editor_plugin/LoadLocker.gd b/editor_plugin/LoadLocker.gd index 8fc125c..45aac7b 100644 --- a/editor_plugin/LoadLocker.gd +++ b/editor_plugin/LoadLocker.gd @@ -1,11 +1 @@ -extends "res://addons/jam_launch/util/ScopeLocker.gd" - - -# Called when the node enters the scene tree for the first time. -func _ready(): - pass # Replace with function body. - - -# Called every frame. 'delta' is the elapsed time since the previous frame. -func _process(delta): - pass +extends ScopeLocker diff --git a/editor_plugin/Login.gd b/editor_plugin/Login.gd index 194134f..5e72b2f 100644 --- a/editor_plugin/Login.gd +++ b/editor_plugin/Login.gd @@ -4,101 +4,107 @@ extends JamEditorPluginPage @onready var login_api: JamLoginApi = $JamLoginApi @onready var notes: RichTextLabel = $Waiting/Notes @onready var user_code_label: Label = $Waiting/PC/M/UserCode - @onready var busy_scope: ScopeLocker = $BusyScope @onready var waiting_scope: ScopeLocker = $WaitingScope var jwt: JamJwt = JamJwt.new() var cache: KeyValCache = KeyValCache.new() -const jwt_cache_idx = "addon_jwt_dev_key" - var cancel_auth: bool = false var device_auth_url: String var client_id: String -func _ready(): +const JWT_CACHE_IDX = "addon_jwt_dev_key" + +func _ready() -> void: $Base.visible = true $Busy.visible = false $Waiting.visible = false jwt.token_changed.connect(_on_token_changed) - - var dir := (self.get_script() as Script).get_path().get_base_dir() - var settings = ConfigFile.new() - var err = settings.load(dir + "/../settings.cfg") - if err != OK: + var dir: String = (self.get_script() as Script).get_path().get_base_dir() + var settings: ConfigFile = ConfigFile.new() + var err: Error = settings.load(dir + "/../settings.cfg") + if not err == OK: printerr("Failed to load auth settings") return device_auth_url = settings.get_value("auth", "url") client_id = settings.get_value("auth", "client_id") -func _page_init(): - var key = cache.get_val(jwt_cache_idx) + +func _page_init() -> void: + var key: Variant = cache.get_val(JWT_CACHE_IDX) if key == null or key == "": return - var res = jwt.set_token(key) + var res: JamJwt.TokenParseResult = jwt.set_token(key as String) if res.errored: _err("Error with cached key: %s" % res.error) -func show_init(): + +func show_init() -> void: dashboard.toolbar.visible = false -func _on_token_changed(tkn: String): - cache.store(jwt_cache_idx, tkn) -func _err(msg: String): +func _on_token_changed(tkn: String) -> void: + cache.store(JWT_CACHE_IDX, tkn) + + +func _err(msg: String) -> void: dashboard.show_error(msg) -func _on_notes_meta_hover_started(meta): + +func _on_notes_meta_hover_started(_meta: String) -> void: mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND -func _on_notes_meta_hover_ended(meta): + +func _on_notes_meta_hover_ended(_meta: String) -> void: mouse_default_cursor_shape = Control.CURSOR_ARROW -func _on_notes_meta_clicked(meta): + +func _on_notes_meta_clicked(meta: String) -> void: OS.shell_open(meta) -func _on_login_button_pressed(): - - var lock = busy_scope.get_lock() - var res = await login_api.request_developer_auth() +func _on_login_button_pressed() -> void: + var _lock: ScopeLocker.ScopeLock = busy_scope.get_lock() + var res: JamHttpBase.Result = await login_api.request_developer_auth() if res.errored: _err(res.error_msg) return - - var userCode = res.data["userCode"] - var deviceCode = res.data["deviceCode"] - var authUrl = "%s?user_code=%s" % [device_auth_url, userCode] - + + var userCode: String = res.data["userCode"] + var deviceCode: String = res.data["deviceCode"] + var authUrl: String = "%s?user_code=%s" % [device_auth_url, userCode] + OS.shell_open(authUrl) user_code_label.text = userCode - notes.parse_bbcode("[center][color=#eeeeee][bgcolor=#00000000]Confirm the following code at -[url]%s[/url][/bgcolor][/color][/center]" % device_auth_url) - - lock = waiting_scope.get_lock() + notes.parse_bbcode("[center][color=#eeeeee][bgcolor=#00000000]Confirm the following code at [url]%s[/url][/bgcolor][/color][/center]" % device_auth_url) + + _lock = waiting_scope.get_lock() while true: if cancel_auth: cancel_auth = false return await get_tree().create_timer(1).timeout - var authRes = await login_api.check_auth(userCode, deviceCode) + var authRes: JamHttpBase.Result = await login_api.check_auth(userCode, deviceCode) if authRes.errored: _err(authRes.error_msg) elif authRes.data["state"] == "pending": continue elif authRes.data["state"] == "allowed": - jwt.set_token(authRes.data["accessKey"]) + jwt.set_token(authRes.data["accessKey"] as String) else: _err("Access was not granted") break -func _on_busy_scope_lock_changed(locked): + +func _on_busy_scope_lock_changed(locked: bool) -> void: $Busy.visible = locked $Base.visible = not (locked || waiting_scope.is_locked()) -func _on_waiting_scope_lock_changed(locked): + +func _on_waiting_scope_lock_changed(locked: bool) -> void: $Waiting.visible = locked $Base.visible = not (locked || busy_scope.is_locked()) -func _on_cancel_auth_pressed(): + +func _on_cancel_auth_pressed() -> void: cancel_auth = true diff --git a/editor_plugin/NewProject.gd b/editor_plugin/NewProject.gd index 5c9d853..f44c799 100644 --- a/editor_plugin/NewProject.gd +++ b/editor_plugin/NewProject.gd @@ -4,35 +4,39 @@ extends JamEditorPluginPage signal cancel() signal create_done(project_id: String, project_name: String) -@onready var name_edit = $VB/ProjectName +@onready var name_edit: Node = $VB/ProjectName -func _page_init(): - $VB/Options/Create.icon = dashboard.editor_icon("Add") - -func show_init(): - dashboard.toolbar_title.text = "Create a new Project" - -var waiting = false: +var waiting: bool = false: set(v): waiting = v $VB/Options/Create.disabled = v name_edit.editable = not v -func initialize(): +func _page_init() -> void: + $VB/Options/Create.icon = dashboard.editor_icon("Add") + + +func show_init() -> void: + dashboard.toolbar_title.text = "Create a new Project" + + +func initialize() -> void: project_api = get_parent().project_api + func _on_cancel_pressed() -> void: name_edit.clear() cancel.emit() + func _on_create_pressed() -> void: if name_edit.text == "": dashboard.show_error("project name must be non-empty", 5.0) return - var pending_name = name_edit.text + var pending_name: String = name_edit.text waiting = true - var res = await project_api.create_project(pending_name) + var res: JamHttpBase.Result = await project_api.create_project(pending_name) waiting = false if res.errored: dashboard.show_error(res.error_msg) @@ -40,5 +44,6 @@ func _on_create_pressed() -> void: name_edit.clear() create_done.emit(res.data["id"], pending_name) -func _on_project_name_text_submitted(_new_text): + +func _on_project_name_text_submitted(_new_text: String) -> void: _on_create_pressed() diff --git a/editor_plugin/Project.gd b/editor_plugin/Project.gd index 0e2c2d7..07d6027 100644 --- a/editor_plugin/Project.gd +++ b/editor_plugin/Project.gd @@ -1,89 +1,78 @@ @tool extends JamEditorPluginPage -@onready var net_mode_box: OptionButton = $HB/Config/NetworkMode +signal request_projects_update() +signal go_back() +signal session_page_selected(project_id: String, project_name: String) +@onready var net_mode_box: OptionButton = $HB/Config/NetworkMode @onready var log_popup: Popup = $LogPopup @onready var log_display: TextEdit = $LogPopup/Logs - @onready var btn_deploy: Button = $HB/Config/BtnDeploy @onready var btn_delete: Button = $HB/Config/BtnDelete @onready var btn_sessions: Button = $HB/Config/BtnSessions - @onready var deploy_busy: Control = $HB/Releases/VB/PreparingBusy - @onready var platform_options: MenuButton = $HB/Config/PlatformOptions - @onready var no_deployments: Control = $HB/Releases/VB/NoDeployments - @onready var channels_root: VBoxContainer = $HB/Channels/VB/VB @onready var releases_root: VBoxContainer = $HB/Releases/VB/VB - @onready var export_busy: ScopeLocker = $ExportBusy @onready var export_prep_busy: ScopeLocker = $ExportPrepBusy @onready var export_timeout: SpinBox = $HB/Config/Timeout/Minutes @onready var export_parallel: CheckBox = $HB/Config/Parallel - @onready var log_request: HTTPRequest = $LogRequest -signal request_projects_update() -signal go_back() -signal session_page_selected(project_id: String, project_name: String) - -var project_data = [] - -var refresh_retries = 0 - -var active_project -var active_id = "" - +var project_data: Array = [] +var refresh_retries: int = 0 +var active_project: Dictionary +var active_id: String = "" var waiting_for_export: bool = false var auto_export: JamAutoExport -func _ready(): +func _ready() -> void: auto_export = JamAutoExport.new() add_child(auto_export) -func _page_init(): + +func _page_init() -> void: log_popup.visible = false btn_deploy.icon = dashboard.editor_icon("ArrowUp") btn_sessions.icon = dashboard.editor_icon("GuiVisibilityVisible") - dashboard.load_locker.lock_changed.connect(_load_lock_changed) - platform_options.get_popup().id_pressed.connect(_on_platform_option_selected) - -func show_init(): + +func show_init() -> void: if active_project: dashboard.toolbar_title.text = active_project["project_name"] -func _load_lock_changed(locked: bool): + +func _load_lock_changed(locked: bool) -> void: btn_deploy.disabled = locked net_mode_box.disabled = locked btn_delete.disabled = locked -func refresh_page(): + +func refresh_page() -> void: refresh_project() -func show_project(project_id: String, project_name: String = "..."): + +func show_project(project_id: String, project_name: String = "...") -> void: active_id = project_id dashboard.toolbar_title.text = project_name releases_root.visible = false if not await refresh_project(): $AutoRefreshTimer.start(1.0) + func refresh_project(repeat: float = 0.0) -> bool: no_deployments.visible = false - if len(active_id) < 1: dashboard.show_error("invalid project ID") return false - var lock = dashboard.load_locker.get_lock() - - var res = await project_api.get_project(active_id) - + var _lock: ScopeLocker.ScopeLock = dashboard.load_locker.get_lock() + var res: JamHttpBase.Result = await project_api.get_project(active_id) if res.errored: dashboard.show_error(res.error_msg) return false @@ -93,43 +82,43 @@ func refresh_project(repeat: float = 0.0) -> bool: $AutoRefreshTimer.start(repeat) return true -func setup_project_data(p): - + +func setup_project_data(p: Dictionary) -> void: active_project = p dashboard.toolbar_title.text = p["project_name"] - - var plat_menu = platform_options.get_popup() - - var available_channels = [] + var plat_menu: PopupMenu = platform_options.get_popup() + var available_channels: Array = [] while channels_root.get_child_count() > 0: - var c = channels_root.get_child(0) + var c: Node = channels_root.get_child(0) channels_root.remove_child(c) c.queue_free() - var sorted_channels = active_project.get("channels", []) - sorted_channels.sort_custom(func (a, _b): return a.get("default_release", false)) - for channel in sorted_channels: + + var sorted_channels: Variant = active_project.get("channels", []) + sorted_channels.sort_custom(func(a: Variant, _b: Variant)->Variant: return a.get("default_release", false)) + for channel: Variant in sorted_channels: available_channels.append(channel.get("name")) - var channel_release = "No Release" - for rel in active_project.get("releases", []): + var channel_release: String = "No Release" + for rel: Variant in active_project.get("releases", []): if rel.get("channel") == channel.get("name"): channel_release = "Release: %s" % [rel.get("id", "")] - var summary = preload("res://addons/jam_launch/editor_plugin/ChannelSummary.tscn").instantiate() + var summary: Node = preload("res://addons/jam_launch/editor_plugin/ChannelSummary.tscn").instantiate() channels_root.add_child(summary) summary.set_channel(channel, channel_release) summary.update_channel.connect(_update_channel) - + while releases_root.get_child_count() > 0: - var rel = releases_root.get_child(0) + var rel: Node = releases_root.get_child(0) releases_root.remove_child(rel) rel.queue_free() - var sorted_releases = active_project.get("releases", []) + + var sorted_releases: Variant = active_project.get("releases", []) sorted_releases.reverse() - for r in sorted_releases: - var rel_summary = preload("res://addons/jam_launch/editor_plugin/ReleaseSummary.tscn").instantiate() + for r: Variant in sorted_releases: + var rel_summary: Node = preload("res://addons/jam_launch/editor_plugin/ReleaseSummary.tscn").instantiate() releases_root.add_child(rel_summary) rel_summary.dashboard = dashboard rel_summary.build_busy.connect(_on_build_busy) - export_busy.lock_changed.connect(rel_summary.on_export_active_changed) + export_busy.lock_changed.connect(rel_summary.on_export_active_changed as Callable) rel_summary.set_channels(available_channels) rel_summary.set_release(active_id, r) @@ -138,8 +127,8 @@ func setup_project_data(p): if len(sorted_releases) > 0: releases_root.visible = true - var r = sorted_releases[0] - var net_mode = r["network_mode"] + var r: Dictionary = sorted_releases[0] + var net_mode: String = r["network_mode"] net_mode_box.disabled = false if net_mode == "enet": net_mode_box.select(0) @@ -154,7 +143,7 @@ func setup_project_data(p): for idx in range(plat_menu.item_count): plat_menu.set_item_checked(idx, false) - for b in r["builds"]: + for b: Dictionary in r["builds"]: var bname: String = b["name"] if "Linux" == bname: plat_menu.set_item_checked(0, true) @@ -167,14 +156,14 @@ func setup_project_data(p): elif "Android" == bname: plat_menu.set_item_checked(4, true) - if r["id"] != null: - var dir = self.get_script().get_path().get_base_dir() - var deployment_cfg = ConfigFile.new() + if not r["id"] == null: + var dir: String = self.get_script().get_path().get_base_dir() + var deployment_cfg: ConfigFile = ConfigFile.new() deployment_cfg.set_value("game", "id", "%s-%s" % [active_id, r["id"]]) deployment_cfg.set_value("game", "network_mode", net_mode) deployment_cfg.set_value("game", "allow_guests", r.get("allow_guests", false)) - var err = deployment_cfg.save(dir + "/../deployment.cfg") - if err != OK: + var err: Error = deployment_cfg.save(dir + "/../deployment.cfg") + if not err == OK: dashboard.show_error("Failed to save current deployment configuration") return else: @@ -185,53 +174,55 @@ func setup_project_data(p): plat_menu.set_item_checked(3, false) plat_menu.set_item_checked(4, false) -func _update_release(release_id: String, props: Dictionary): + +func _update_release(release_id: String, props: Dictionary) -> void: if len(active_id) < 1: - return false + return if dashboard.load_locker.is_locked(): dashboard.show_error("cannot update release while handling another request") - return - var lock = dashboard.load_locker.get_lock() - - var res = await project_api.update_release(active_id, release_id, props) + return + var _lock: ScopeLocker.ScopeLock = dashboard.load_locker.get_lock() + var res: JamHttpBase.Result = await project_api.update_release(active_id, release_id, props) if res.errored: dashboard.show_error(res.error_msg) refresh_project.call_deferred() -func _update_channel(channel: String, props: Dictionary): + +func _update_channel(channel: String, props: Dictionary) -> void: if len(active_id) < 1: - return false + return if dashboard.load_locker.is_locked(): dashboard.show_error("cannot update channel while handling another request") return - var lock = dashboard.load_locker.get_lock() - - var res = await project_api.update_channel(active_id, channel, props) - + var _lock: ScopeLocker.ScopeLock = dashboard.load_locker.get_lock() + var res: JamHttpBase.Result = await project_api.update_channel(active_id, channel, props) + if res.errored: dashboard.show_error(res.error_msg) refresh_project.call_deferred() -func _on_build_busy(): + +func _on_build_busy() -> void: pass #print("setting auto-refresh") #$AutoRefreshTimer.start(3.0) + func _show_logs(log_url: String) -> void: log_display.text = "fetching logs..." log_popup.popup_centered() - var err := log_request.request(log_url) - if err != OK: + var err: Error = log_request.request(log_url) + if not err == OK: log_display.text = "failed to fetch logs - error code %d" % err return - var resp = await log_request.request_completed + var resp: Dictionary = await log_request.request_completed var code: int = resp[1] if code > 299: log_display.text = "failed to fetch logs - HTTP status %d" % code @@ -240,6 +231,7 @@ func _show_logs(log_url: String) -> void: var response_body: String = resp[3].get_string_from_utf8() log_display.text = response_body + func _on_btn_deploy_pressed() -> void: if not active_project: dashboard.show_error("Project is not correctly loaded") @@ -248,8 +240,8 @@ func _on_btn_deploy_pressed() -> void: if export_busy.is_locked() or export_prep_busy.is_locked(): dashboard.show_error("Cannot release while release tasks are still active") return - - var net_mode + + var net_mode: String if net_mode_box.get_selected_id() == 0: net_mode = "enet" elif net_mode_box.get_selected_id() == 1: @@ -260,8 +252,8 @@ func _on_btn_deploy_pressed() -> void: dashboard.show_error("Invalid network mode selection") return - var builds = [] - var plat_menu = platform_options.get_popup() + var builds: Array = [] + var plat_menu: PopupMenu = platform_options.get_popup() if plat_menu.is_item_checked(0): builds.append({ "name": "Linux", @@ -303,53 +295,54 @@ func _on_btn_deploy_pressed() -> void: "is_server": true }) - var cfg = { + var cfg: Dictionary = { "network_mode": net_mode, "export_timeout": export_timeout.value * 60, "parallel": export_parallel.button_pressed, "builds": builds } - var prep_res := await _export_prep(cfg) + var prep_res: JamResult = await _export_prep(cfg) if prep_res.errored: dashboard.show_error(prep_res.error_msg) return refresh_project.call_deferred() - var export_res := await _do_export(cfg, prep_res.value) + var export_res: JamError = await _do_export(cfg, prep_res.value as Dictionary) if export_res.errored: dashboard.show_error(export_res.error_msg) refresh_project.call_deferred(3.0) + func _export_prep(cfg: Dictionary) -> JamResult: if dashboard.load_locker.is_locked(): return JamResult.err("cannot deploy while handling another request") - var lock = dashboard.load_locker.get_lock() - - var busy_lock = export_prep_busy.get_lock() - var res = await project_api.prepare_release(active_id, cfg) - if !res: + var _lock: ScopeLocker.ScopeLock = dashboard.load_locker.get_lock() + var _busy_lock: ScopeLocker.ScopeLock = export_prep_busy.get_lock() + var res: JamHttpBase.Result = await project_api.prepare_release(active_id, cfg) + if not res: return JamResult.err("invalid result from local export attempt") if res.errored: return JamResult.err(res.error_msg) return JamResult.ok(res.data) + func _do_export(config: Dictionary, prepare_result: Dictionary) -> JamError: - var busy_lock = export_busy.get_lock() - var export_config = JamAutoExport.ExportConfig.new() + var _busy_lock: ScopeLocker.ScopeLock = export_busy.get_lock() + var export_config: JamAutoExport.ExportConfig = JamAutoExport.ExportConfig.new() export_config.network_mode = config["network_mode"] export_config.export_timeout = config["export_timeout"] export_config.parallel = config["parallel"] export_config.game_id = "%s-%s" % [active_id, prepare_result["id"]] export_config.build_configs = ([] as Array[JamAutoExport.BuildConfig]) - for b in config["builds"]: + for b: Dictionary in config["builds"]: var c := JamAutoExport.BuildConfig.new() c.output_target = b["export_name"] c.template_name = b["template_name"] c.no_zip = b.get("no_zip", false) var mapped := false - for t in prepare_result["builds"]: + for t: Dictionary in prepare_result["builds"]: if t["build_name"] == b["name"]: c.presigned_post = t["upload_target"] c.log_presigned_post = t["log_upload_target"] @@ -362,38 +355,44 @@ func _do_export(config: Dictionary, prepare_result: Dictionary) -> JamError: return await auto_export.auto_export(export_config) -func _on_auto_refresh_timer_timeout(): + +func _on_auto_refresh_timer_timeout() -> void: $AutoRefreshTimer.stop() await refresh_project() -func _on_btn_delete_pressed(): + +func _on_btn_delete_pressed() -> void: $ConfirmDelete.popup() -func _on_confirm_delete_confirmed(): + +func _on_confirm_delete_confirmed() -> void: if dashboard.load_locker.is_locked(): dashboard.show_error("cannot delete while handling another request") return - var lock = dashboard.load_locker.get_lock() - - var res = await project_api.delete_project(active_id) + + var _lock: ScopeLocker.ScopeLock = dashboard.load_locker.get_lock() + var res: JamHttpBase.Result = await project_api.delete_project(active_id) if res.errored: dashboard.show_error(res.error_msg) return + dashboard.pages.go_back() -func _on_btn_sessions_pressed(): + +func _on_btn_sessions_pressed() -> void: session_page_selected.emit(active_id, active_project["project_name"]) -func _on_config_item_selected(_index): + +func _on_config_item_selected(_index: int) -> void: if not active_project: return if dashboard.load_locker.is_locked(): dashboard.show_error("cannot submit config while handling another request") return - var lock = dashboard.load_locker.get_lock() + var _lock: ScopeLocker.ScopeLock = dashboard.load_locker.get_lock() - var cfg = {} + var cfg: Dictionary = {} if net_mode_box.get_selected_id() == 0: cfg["network_mode"] = "enet" elif net_mode_box.get_selected_id() == 1: @@ -404,25 +403,27 @@ func _on_config_item_selected(_index): dashboard.show_error("invalid network mode selected") return - var plat_menu = platform_options.get_popup() + var plat_menu: PopupMenu = platform_options.get_popup() if cfg["network_mode"] == "enet": plat_menu.set_item_disabled(3, true) plat_menu.set_item_checked(3, false) else: plat_menu.set_item_disabled(3, false) -func _on_platform_option_selected(idx: int): - var menu = platform_options.get_popup() + +func _on_platform_option_selected(idx: int) -> void: + var menu: PopupMenu = platform_options.get_popup() if menu.is_item_disabled(idx): return menu.set_item_checked(idx, not menu.is_item_checked(idx)) menu.show.call_deferred() -func _on_export_busy_lock_changed(locked): +func _on_export_busy_lock_changed(locked: bool) -> void: waiting_for_export = locked -func _on_export_prep_busy_lock_changed(locked): + +func _on_export_prep_busy_lock_changed(locked: bool) -> void: deploy_busy.visible = locked releases_root.visible = not locked @@ -433,8 +434,8 @@ func _on_add_channel_pressed() -> void: func _on_create_channel_confirmed() -> void: - var lock = $ChannelUpdateBusy.get_lock() - var res = await project_api.create_channel(active_id, $CreateChannel/VB/NewChannelName.text) + var _lock: ScopeLocker.ScopeLock = $ChannelUpdateBusy.get_lock() + var res: JamHttpBase.Result = await project_api.create_channel(active_id, $CreateChannel/VB/NewChannelName.text as String) if res.errored: dashboard.show_error(res.error_msg) diff --git a/editor_plugin/ProjectPacker.gd b/editor_plugin/ProjectPacker.gd index 6f43108..3437ad9 100644 --- a/editor_plugin/ProjectPacker.gd +++ b/editor_plugin/ProjectPacker.gd @@ -1,9 +1,9 @@ @tool -extends Object class_name ProjectPacker +extends Object -static func pack(project_dir: EditorFileSystemDirectory): - var dir = DirAccess.open("user://") +static func pack(project_dir: EditorFileSystemDirectory) -> Variant: + var dir: DirAccess = DirAccess.open("user://") if dir.file_exists("jamlaunchexport.zip"): dir.remove("jamlaunchexport.zip") dir.remove("project.godot") @@ -11,31 +11,27 @@ static func pack(project_dir: EditorFileSystemDirectory): var err := writer.open("user://jamlaunchexport.zip") if err != OK: return "failed to stage local project archive" - + if dir.file_exists("jamlaunchexportproject.godot"): dir.remove("jamlaunchexportproject.godot") err = ProjectSettings.save_custom("user://jamlaunchexportproject.godot") if err != OK: return "failed to stage project settings" - + writer.start_file("project.godot") writer.write_file(FileAccess.get_file_as_bytes("user://jamlaunchexportproject.godot")) writer.close_file() - _pack_dir(project_dir, writer) - writer.close() - return FileAccess.get_file_as_bytes("user://jamlaunchexport.zip") -static func _pack_full_dir(dir: DirAccess, writer: ZIPPacker): +static func _pack_full_dir(dir: DirAccess, writer: ZIPPacker) -> void: dir.include_hidden = true - dir.list_dir_begin() - var file_name = dir.get_next() - while file_name != "": - var abs_file = dir.get_current_dir() + "/" + file_name + var file_name: String = dir.get_next() + while not file_name.is_empty(): + var abs_file: String = dir.get_current_dir() + "/" + file_name if dir.current_is_dir(): _pack_full_dir(DirAccess.open(abs_file), writer) else: @@ -44,13 +40,12 @@ static func _pack_full_dir(dir: DirAccess, writer: ZIPPacker): writer.close_file() file_name = dir.get_next() -static func _pack_dir(dir: EditorFileSystemDirectory, writer: ZIPPacker): - var base_path = dir.get_path() - - var extension_base = false - + +static func _pack_dir(dir: EditorFileSystemDirectory, writer: ZIPPacker) -> void: + var base_path: String = dir.get_path() + var extension_base: bool = false for idx in range(dir.get_file_count()): - var file_path = base_path + "/" + dir.get_file(idx) + var file_path: String = base_path + "/" + dir.get_file(idx) if file_path.ends_with(".gdextension"): # if there is an extension spec, assume the subdirectories should be fully packed extension_base = true @@ -58,8 +53,8 @@ static func _pack_dir(dir: EditorFileSystemDirectory, writer: ZIPPacker): writer.write_file(FileAccess.get_file_as_bytes(file_path)) writer.close_file() - for idx in range(dir.get_subdir_count()): - var subdir = dir.get_subdir(idx) + for idx: int in range(dir.get_subdir_count()): + var subdir: EditorFileSystemDirectory = dir.get_subdir(idx) if extension_base: _pack_full_dir(DirAccess.open(base_path + "/" + subdir.get_name()), writer) else: diff --git a/editor_plugin/ProjectSelect.gd b/editor_plugin/ProjectSelect.gd index 3de32e2..3c60e01 100644 --- a/editor_plugin/ProjectSelect.gd +++ b/editor_plugin/ProjectSelect.gd @@ -1,13 +1,13 @@ @tool extends JamEditorPluginPage -@onready var projects: ItemList = $VB/Projects -@onready var no_projects: Label = $VB/NoProjects - signal open_project(project_id: String, project_name: String) signal new_project() -var project_map = {} +@onready var projects: ItemList = $VB/Projects +@onready var no_projects: Label = $VB/NoProjects + +var project_map: Dictionary = {} var loading: bool = false: set(v): @@ -15,39 +15,42 @@ var loading: bool = false: $Loading.visible = v $VB.visible = not v -func _page_init(): + +func _page_init() -> void: $VB/TopBar/NewBtn.icon = dashboard.editor_icon("Add") -func show_init(): + +func show_init() -> void: dashboard.toolbar_title.text = "Projects" get_projects() -func refresh_page(): + +func refresh_page() -> void: get_projects() -func get_projects(): + +func get_projects() -> void: if dashboard.load_locker.is_locked(): return - var lock = dashboard.load_locker.get_lock() - + var _lock: ScopeLocker.ScopeLock = dashboard.load_locker.get_lock() projects.clear() loading = true - var res = await project_api.list_projects() + var res: JamHttpBase.Result = await project_api.list_projects() loading = false if res.errored: dashboard.show_error(res.error_msg) return - for p in res.data["projects"]: - projects.add_item(p["project_name"]) + for p: Dictionary in res.data["projects"]: + projects.add_item(p["project_name"] as String) project_map[p["project_name"]] = p projects.visible = projects.item_count > 0 no_projects.visible = not projects.visible func _on_projects_item_activated(index: int) -> void: - var pname = projects.get_item_text(index) + var pname: String = projects.get_item_text(index) open_project.emit(project_map[pname]["id"], pname) func _on_new_btn_pressed() -> void: diff --git a/editor_plugin/ReleaseSummary.gd b/editor_plugin/ReleaseSummary.gd index f3f5345..618dde0 100644 --- a/editor_plugin/ReleaseSummary.gd +++ b/editor_plugin/ReleaseSummary.gd @@ -1,15 +1,17 @@ @tool extends MarginContainer +signal update_release(release_id: String, data: Dictionary) +signal show_logs(log_url: String) +signal build_busy() + @onready var title: RichTextLabel = $M/HB/VB/Title @onready var link_nav: Button = $M/HB/VB/Config/MC/VB/HB/PageLink @onready var link_copy: Button = $M/HB/VB/Config/MC/VB/HB/Copy @onready var check_public: CheckButton = $M/HB/VB/Config/MC/VB/Access/CheckPublic @onready var access_icon: TextureRect = $M/HB/VB/Config/MC/VB/Access/AccessIcon @onready var jobs: GridContainer = $M/HB/M/PC/MC/VB/Jobs - @onready var check_guests: CheckButton = $M/HB/VB/Config/MC/VB/Guests/CheckGuests - @onready var channel_menu: MenuButton = $M/HB/VB/Config/MC/VB/SetChannel @onready var current_channel: Control = $M/HB/VB/Config/MC/VB/CurrentChannel @onready var current_channel_lbl: Label = $M/HB/VB/Config/MC/VB/CurrentChannel/Channel @@ -23,58 +25,50 @@ var dashboard: JamEditorPluginDashboard: var project_id: String = "" var release_id: String = "" -var release_data +var release_data: Dictionary = {} +var dashboard_url: String = "https://app.jamlaunch.com" var local_export_active: bool = false -func _load_lock_changed(locked: bool): - check_public.disabled = locked - check_guests.disabled = locked - channel_menu.disabled = locked - -signal update_release(release_id: String, data: Dictionary) -signal show_logs(log_url: String) -signal build_busy() - -var dashboard_url = "https://app.jamlaunch.com" - -func _ready(): - var dir := (self.get_script() as Script).get_path().get_base_dir() - var settings = ConfigFile.new() - var err = settings.load(dir + "/../settings.cfg") - if err != OK: +func _ready() -> void: + var dir: String = (self.get_script() as Script).get_path().get_base_dir() + var settings: ConfigFile = ConfigFile.new() + var err: Error = settings.load(dir + "/../settings.cfg") + if not err == OK: printerr("Failed to load settings for dashboard address") return dashboard_url = "https://%s" % settings.get_value("api", "dashboard_domain") - channel_menu.get_popup().index_pressed.connect(_on_channel_selected) -func set_channels(channels: Array): - var popup = channel_menu.get_popup() + +func _load_lock_changed(locked: bool) -> void: + check_public.disabled = locked + check_guests.disabled = locked + channel_menu.disabled = locked + + +func set_channels(channels: Array) -> void: + var popup: PopupMenu = channel_menu.get_popup() popup.clear() - for c in channels: + for c: String in channels: popup.add_item(c) -func set_release(proj_id: String, r: Dictionary): - + +func set_release(proj_id: String, r: Dictionary) -> void: project_id = proj_id release_id = r["id"] release_data = r - title.clear() - title.push_color(Color(1, 1, 1, 0.4)) title.add_text("Release ") title.pop_all() - title.push_font_size(18) title.push_bold() - title.add_text(r["id"]) + title.add_text(r["id"] as String) title.pop_all() - title.push_context() title.push_color(Color(1, 1, 1, 0.4)) - var rt = Time.get_datetime_dict_from_datetime_string(r["created_at"], false) + var rt: Dictionary = Time.get_datetime_dict_from_datetime_string(r["created_at"] as String, false) title.add_text("\n%s-%02d-%02d\n%02d:%02d:%02d" % [ rt["year"], int(rt["month"]), @@ -84,7 +78,6 @@ func set_release(proj_id: String, r: Dictionary): int(rt["second"]), ]) title.pop_context() - if r["public"]: check_public.text = "Public" check_public.button_pressed = true @@ -114,8 +107,8 @@ func set_release(proj_id: String, r: Dictionary): child.queue_free() var sorted_job_names: Array = [] - var builds_by_name = {} - for b in r["builds"]: + var builds_by_name: Dictionary = {} + for b: Dictionary in r["builds"]: sorted_job_names.append(b["name"]) builds_by_name[b["name"]] = b sorted_job_names.sort() @@ -123,9 +116,9 @@ func set_release(proj_id: String, r: Dictionary): sorted_job_names.erase("Server") sorted_job_names.push_front("Server") - var is_busy = false - for bname in sorted_job_names: - var b = builds_by_name[bname] + var is_busy: bool = false + for bname: String in sorted_job_names: + var b: Dictionary = builds_by_name[bname] if b["has_build"]: jobs.add_child(preload("res://addons/jam_launch/ui/SuccessBadge.tscn").instantiate()) elif b["has_log"]: @@ -137,45 +130,52 @@ func set_release(proj_id: String, r: Dictionary): else: jobs.add_child(preload("res://addons/jam_launch/ui/ErrorBadge.tscn").instantiate()) - var name_lbl = Label.new() + var name_lbl: Label = Label.new() name_lbl.text = bname jobs.add_child(name_lbl) if b["has_log"]: - var log_btn = Button.new() + var log_btn: Button = Button.new() log_btn.icon = dashboard.editor_icon("Script") log_btn.pressed.connect(_show_logs.bind(b["log_url"])) log_btn.flat = true log_btn.size_flags_horizontal = Control.SIZE_SHRINK_CENTER jobs.add_child(log_btn) else: - var blank = Label.new() + var blank: Label = Label.new() jobs.add_child(blank) if is_busy: build_busy.emit() + func release_page_uri() -> String: return "%s/g/%s-%s" % [dashboard_url, project_id, release_id] -func _on_copy_pressed(): + +func _on_copy_pressed() -> void: DisplayServer.clipboard_set(release_page_uri()) -func _on_check_public_toggled(toggled_on: bool): + +func _on_check_public_toggled(toggled_on: bool) -> void: if check_public.disabled: # ignore check state changes that happen during an update return update_release.emit(release_id, {"public": toggled_on}) -func _show_logs(log_url: String): + +func _show_logs(log_url: String) -> void: show_logs.emit(log_url) -func _on_page_link_pressed(): + +func _on_page_link_pressed() -> void: OS.shell_open(release_page_uri()) -func on_export_active_changed(export_active: bool): + +func on_export_active_changed(export_active: bool) -> void: local_export_active = export_active + func _on_check_guests_toggled(toggled_on: bool) -> void: if check_guests.disabled: # ignore check state changes that happen during an update @@ -188,6 +188,7 @@ func _on_clear_channel_pressed() -> void: return update_release.emit(release_id, {"channel": null}) + func _on_channel_selected(idx: int) -> void: - var channel = channel_menu.get_popup().get_item_text(idx) + var channel: String = channel_menu.get_popup().get_item_text(idx) update_release.emit(release_id, {"channel": channel}) diff --git a/editor_plugin/Sessions.gd b/editor_plugin/Sessions.gd index bcf81aa..7e249d1 100644 --- a/editor_plugin/Sessions.gd +++ b/editor_plugin/Sessions.gd @@ -2,83 +2,78 @@ extends JamEditorPluginPage @onready var load_locker: ScopeLocker = $LoadLocker - @onready var log_popup: Popup = $LogPopup @onready var log_display: TextEdit = $LogPopup/Logs - @onready var no_sessions_lbl: Label = $M/HB/VB/SC/VB/NoSessions @onready var session_list: ItemList = $M/HB/VB/SC/VB/SessionList -@onready var session_details_layout = $M/HB/Details +@onready var session_details_layout: VBoxContainer = $M/HB/Details @onready var session_title: Label = $M/HB/Details/Session @onready var session_data: TextEdit = $M/HB/Details/SessionData - @onready var terminate_btn: Button = $M/HB/Details/HB/BtnDelete -var disable_terminate := false - -var project_data = [] - -var refresh_retries = 0 +var disable_terminate: bool = false +var project_data: Array = [] +var refresh_retries: int = 0 var project_name: String var project_id: String var sessions: Array = [] - var filter_active_sessions := true - var session_details: Dictionary = {}: set(val): session_details = val - var sid = session_details.get("id") + var sid: Variant = session_details.get("id") if sid == null: session_details_layout.visible = false else: session_title.text = "Session " + session_details.get("join_code", sid) session_details_layout.visible = true -func _page_init(): + +func _page_init() -> void: $M/HB/Details/HB/BtnLogs.icon = dashboard.editor_icon("Script") session_details_layout.visible = false log_popup.visible = false -func show_init(): - if len(project_name) > 0: + +func show_init() -> void: + if not project_name.is_empty(): dashboard.toolbar_title.text = "Sessions: %s" % project_name -func refresh_page(): + +func refresh_page() -> void: refresh_sessions() -func show_game(proj_id: String, proj_name: String): + +func show_game(proj_id: String, proj_name: String) -> void: project_id = proj_id project_name = proj_name dashboard.toolbar_title.text = "Sessions: %s" % project_name refresh_sessions() -func refresh_sessions(): - if len(project_id) < 1: + +func refresh_sessions() -> void: + if project_id.is_empty(): return if load_locker.is_locked(): show_error("cannot refresh sessions while loading...", 5.0) return - var _lock = load_locker.get_lock() + var _lock: ScopeLocker.ScopeLock = load_locker.get_lock() session_details = {} session_list.clear() session_list.visible = false no_sessions_lbl.visible = false - - var res = await project_api.get_sessions(project_id, filter_active_sessions) - + var res: JamHttpBase.Result = await project_api.get_sessions(project_id, filter_active_sessions) if res.errored: show_error(res.error_msg) return sessions = res.data["sessions"] - - for s in sessions: - var rt = Time.get_datetime_dict_from_datetime_string(s["created_at"], false) - var now_utc = Time.get_datetime_dict_from_system(true) - var time_text = "%s-%02d-%02d %02d:%02d" % [ + for s: Dictionary in sessions: + var rt: Dictionary = Time.get_datetime_dict_from_datetime_string(s["created_at"] as String, false) + var now_utc: Dictionary = Time.get_datetime_dict_from_system(true) + var time_text: String = "%s-%02d-%02d %02d:%02d" % [ rt["year"], rt["month"], rt["day"], @@ -107,69 +102,75 @@ func refresh_sessions(): ] session_list.add_item(time_text) - if len(sessions) < 1: + if sessions.is_empty(): no_sessions_lbl.visible = true else: session_list.visible = true -func _get_session_details(p, r, s): + +func _get_session_details(p: String, r: String, s: String) -> void: if load_locker.is_locked(): show_error("cannot get session details while loading...", 5.0) return session_data.text = "" - var lock = load_locker.get_lock() - var res = await project_api.get_session(p, r, s) - + var _lock: ScopeLocker.ScopeLock = load_locker.get_lock() + var res: JamHttpBase.Result = await project_api.get_session(p, r, s) + if res.errored: show_error("Failed to get session %s: %s" % [s, res.error_msg]) return - + session_details = res.data session_data.text = JSON.stringify(res.data, " ") - disable_terminate = session_details.get("force_terminated", false) terminate_btn.disabled = disable_terminate -func _show_logs(p, r, s) -> void: + +func _show_logs(p: String, r: String, s: String) -> void: log_popup.popup_centered_ratio(0.8) log_display.text = "loading logs..." - var res = await project_api.get_session_logs(p, r, s) + var res: JamHttpBase.Result = await project_api.get_session_logs(p, r, s) if res.errored: log_display.text = "Error fetching logs: %s" % res.error_msg else: print("got %d log events..." % len(res.data["events"])) - var log_text = "" - for e in res.data["events"]: - log_text += Time.get_datetime_string_from_unix_time(e["t"] / 1000.0) + var log_text: String = "" + for e:Dictionary in res.data["events"]: + log_text += Time.get_datetime_string_from_unix_time((e["t"] / 1000) as int) log_text += " " + e["msg"] + "\n" log_display.text = log_text -func show_error(msg: String, auto_dismiss: float=0.0): + +func show_error(msg: String, auto_dismiss: float = 0.0) -> void: dashboard.show_error(msg, auto_dismiss) -func _on_load_locker_lock_changed(locked: bool): + +func _on_load_locker_lock_changed(locked: bool) -> void: $M/HB/Details/HB/BtnLogs.disabled = locked terminate_btn.disabled = locked or disable_terminate -func _on_btn_logs_pressed(): - var session_id = session_details.get("id") - var release_id = session_details.get("release_id") + +func _on_btn_logs_pressed() -> void: + var session_id: String = session_details.get("id") + var release_id: String = session_details.get("release_id") if session_id == null or release_id == null: show_error("Cannot get logs - session details are not correctly loaded") return _show_logs(project_id, release_id, session_id) -func _on_btn_delete_pressed(): + +func _on_btn_delete_pressed() -> void: $ConfirmDelete.popup() -func _on_confirm_delete_confirmed(): - var session_id = session_details.get("id") - var release_id = session_details.get("release_id") + +func _on_confirm_delete_confirmed() -> void: + var session_id: String = session_details.get("id") + var release_id: String = session_details.get("release_id") if session_id == null or release_id == null: show_error("Cannot delete session - session details are not correctly loaded") return - var res = await project_api.terminate_session(project_id, release_id, session_id) + var res: JamHttpBase.Result = await project_api.terminate_session(project_id, release_id, session_id) if res.errored: show_error(res.error_msg) return @@ -177,14 +178,16 @@ func _on_confirm_delete_confirmed(): session_details = {} _get_session_details(project_id, release_id, session_id) -func _on_session_list_item_selected(index): + +func _on_session_list_item_selected(index: int) -> void: if len(sessions) <= index or index < 0: return - - var s = sessions[index] - _get_session_details(project_id, s["release_id"], s["id"]) -func _on_filter_item_selected(index): + var s:Dictionary = sessions[index] + _get_session_details(project_id, s["release_id"] as String, s["id"] as String) + + +func _on_filter_item_selected(index: int) -> void: if index == 0: filter_active_sessions = true terminate_btn.visible = true diff --git a/editor_plugin/plugin.gd b/editor_plugin/plugin.gd index 11cac42..585d164 100644 --- a/editor_plugin/plugin.gd +++ b/editor_plugin/plugin.gd @@ -2,26 +2,27 @@ extends EditorPlugin # A class member to hold the dock during the plugin life cycle. -var dashboard = null +var dashboard: JamEditorPluginDashboard = null -func _enter_tree(): - var editor_interface = get_editor_interface() +func _enter_tree() -> void: + var editor_interface: EditorInterface = get_editor_interface() if !editor_interface: return - var main_screen = editor_interface.get_editor_main_screen() + var main_screen: VBoxContainer = EditorInterface.get_editor_main_screen() if !main_screen: return - var dashboard_scn = preload("res://addons/jam_launch/editor_plugin/Dashboard.tscn") + var dashboard_scn: Resource = preload("res://addons/jam_launch/editor_plugin/Dashboard.tscn") dashboard = dashboard_scn.instantiate() dashboard.plugin = self main_screen.add_child(dashboard) dashboard.hide() add_custom_type("JamConnect", "Node", preload("../core/JamConnect.gd"), preload("../assets/star-jar-outlined_16x16.png")) - add_custom_type("ScopeLocker", "Node", preload("../util/ScopeLocker.gd"), editor_interface.get_base_control().get_theme_icon("Lock", "EditorIcons")) + add_custom_type("ScopeLocker", "Node", preload("../util/ScopeLocker.gd"), EditorInterface.get_base_control().get_theme_icon("Lock", "EditorIcons")) add_custom_type("JamSync", "Node", preload("../core/JamSync.gd"), preload("../assets/icons/JamSync.png")) -func _exit_tree(): + +func _exit_tree() -> void: remove_custom_type("JamConnect") remove_custom_type("ScopeLocker") remove_custom_type("JamSync") @@ -30,15 +31,19 @@ func _exit_tree(): dashboard.free() dashboard = null -func _has_main_screen(): + +func _has_main_screen() -> bool: return true -func _make_visible(visible): + +func _make_visible(visible: bool) -> void: if dashboard: dashboard.visible = visible -func _get_plugin_name(): + +func _get_plugin_name() -> String: return "Jam Launch" -func _get_plugin_icon(): + +func _get_plugin_icon() -> Texture2D: return load("res://addons/jam_launch/assets/star-jar-outlined_16x16.png") diff --git a/export/AutoExport.gd b/export/AutoExport.gd index e1dd766..c06a625 100644 --- a/export/AutoExport.gd +++ b/export/AutoExport.gd @@ -2,71 +2,56 @@ extends Node class_name JamAutoExport -class BuildConfig: - extends RefCounted - var template_name: String - var output_target: String - var no_zip: bool - var presigned_post: Dictionary - var log_presigned_post: Dictionary - -class ExportConfig: - extends RefCounted - var game_id: String - var network_mode: String - var build_configs: Array[BuildConfig] - var parallel: bool - var export_timeout: int - var thread_helper: JamThreadHelper -func _init(): +func _init() -> void: thread_helper = JamThreadHelper.new() add_child(thread_helper) + func auto_export(export_config: ExportConfig, staging_dir: String = "user://jam-auto-export") -> JamError: # set up staging directory where exports will be placed print(export_config.game_id) if DirAccess.dir_exists_absolute(staging_dir) or FileAccess.file_exists(staging_dir): - var deleteRes := JamAutoExport.recursive_delete(staging_dir) + var deleteRes: JamError = JamAutoExport.recursive_delete(staging_dir) if deleteRes.errored: return JamError.err("Failed to remove old staging directory at %s - %s" % [staging_dir, deleteRes.error_msg]) - var err = DirAccess.make_dir_recursive_absolute(staging_dir) - if err != OK: + var err: Error = DirAccess.make_dir_recursive_absolute(staging_dir) + if not err == OK: return JamError.err("Failed to create staging directory at %s - code %d" % [staging_dir, err]) - + # set the export presets if they are not already there - var res := JamAutoExport.merge_presets("res://addons/jam_launch/export/preset_base.cfg") + var res: JamError = JamAutoExport.merge_presets("res://addons/jam_launch/export/preset_base.cfg") if res.errored: return res - + # prepare the export tasks var tasks: Array[Callable] = [] for build_config in export_config.build_configs: print("adding task for ", build_config.template_name) # template export - var out_base := staging_dir.path_join(build_config.template_name) + var out_base: String = staging_dir.path_join(build_config.template_name) err = DirAccess.make_dir_recursive_absolute(out_base) - if err != OK: + if not err == OK: return JamError.err("Failed to create staging directory at %s - code %d" % [staging_dir, err]) tasks.append(perform_export.bind(out_base, build_config, export_config.export_timeout)) - + # run the export tasks var results: Array[JamThreadHelper.ThreadProduct] = [] if export_config.parallel: results = await thread_helper.run_multiple_producers(tasks) else: - for t in tasks: + for t: Callable in tasks: results.append(await thread_helper.run_threaded_producer(t)) - + # handle the export results var errors: PackedStringArray = [] - for task_result in results: + for task_result: JamThreadHelper.ThreadProduct in results: if task_result.errored: errors.append(task_result.error_msg) continue - - var export_result := task_result.value as JamError + + var export_result: JamError = task_result.value as JamError if export_result.errored: errors.append(export_result.error_msg) @@ -75,22 +60,23 @@ func auto_export(export_config: ExportConfig, staging_dir: String = "user://jam- else: return JamError.ok() + func perform_export(output_base: String, config: BuildConfig, timeout: int) -> JamError: # Prepare the godot export - var output := [] - var err := JamAutoExport.perform_godot_export(output_base, config, timeout, output) + var output: Array = [] + var err: JamError = JamAutoExport.perform_godot_export(output_base, config, timeout, output) if not err.errored: err = JamAutoExport.upload_export(output_base, config) - var reader := StreamingUpload.StringReader.new() - if len(output) > 0: + var reader: StreamingUpload.StringReader = StreamingUpload.StringReader.new() + if not output.is_empty(): reader.data = output[0] reader.data_length = len(output[0]) else: reader.data = "No export output log was available" reader.data_length = len(reader.data) reader.filename = "%s-build.log" % config.template_name - var log_err := StreamingUpload.streaming_upload(config.log_presigned_post["url"] as String, config.log_presigned_post["fields"] as Dictionary, reader) + var log_err: JamError = StreamingUpload.streaming_upload(config.log_presigned_post["url"] as String, config.log_presigned_post["fields"] as Dictionary, reader) if err.errored: if log_err.errored: @@ -103,28 +89,28 @@ func perform_export(output_base: String, config: BuildConfig, timeout: int) -> J return JamError.ok() static func perform_godot_export(output_base: String, config: BuildConfig, timeout: int, output: Array) -> JamError: - var godot := OS.get_executable_path() - var project_path := ProjectSettings.globalize_path("res://") - var output_target := ProjectSettings.globalize_path(output_base.path_join(config.output_target)) - var export_arg := "--export-release" + var godot: String = OS.get_executable_path() + var project_path: String = ProjectSettings.globalize_path("res://") + var output_target: String = ProjectSettings.globalize_path(output_base.path_join(config.output_target)) + var export_arg: String = "--export-release" if config.template_name.to_lower().contains("android"): export_arg = "--export-debug" - var exit_code + var exit_code: int if OS.get_name() == "Windows": - var ps_check_out := [] - var ps_check = OS.execute("powershell.exe", ["Get-ExecutionPolicy"], ps_check_out, true) + var ps_check_out: Array = [] + var ps_check: int = OS.execute("powershell.exe", ["Get-ExecutionPolicy"], ps_check_out, true) if ps_check == 0 and ps_check_out[0].strip_edges() == "Unrestricted": - var timeout_script = ProjectSettings.globalize_path("res://addons/jam_launch/export/run-with-timeout.ps1") + var timeout_script: String = ProjectSettings.globalize_path("res://addons/jam_launch/export/run-with-timeout.ps1") exit_code = OS.execute("powershell.exe", ["-file", timeout_script, timeout, godot, "--headless", export_arg, config.template_name, "--path", project_path, output_target], output, true) else: - if ps_check != 0: + if not ps_check == 0: push_warning("powershell.exe failed to execute - export timeout will be ignored") else: push_warning("cannot execute timeout script due to '%s' powershell script execution policy - set 'Set-ExecutionPolicy -Scope CurrentUser unrestricted' in an admin powershell to enable the timeout functionality" % [ps_check_out[0].strip_edges()]) exit_code = OS.execute(godot, ["--headless", export_arg, config.template_name, "--path", project_path, output_target], output, true) else: - var timeout_check = OS.execute("command", ["-v", "timeout"]) - var gtimeout_check = OS.execute("command", ["-v", "gtimeout"]) + var timeout_check: int = OS.execute("command", ["-v", "timeout"]) + var gtimeout_check: int = OS.execute("command", ["-v", "gtimeout"]) if timeout_check == 0: exit_code = OS.execute("timeout", [timeout * 60, godot, "--headless", export_arg, config.template_name, "--path", project_path, output_target], output, true) elif gtimeout_check == 0: @@ -132,7 +118,7 @@ static func perform_godot_export(output_base: String, config: BuildConfig, timeo else: push_warning("Neither the 'timeout' or 'gtimeout' command could be found on this system - ignoring export timout") exit_code = OS.execute(godot, ["--headless", export_arg, config.template_name, "--path", project_path, output_target], output, true) - if exit_code != 0: + if not exit_code == 0: if exit_code == 124: return JamError.err("Export timed out") else: @@ -145,25 +131,26 @@ static func perform_godot_export(output_base: String, config: BuildConfig, timeo return JamError.err("Export failed to produce desired output target") return JamError.ok() + static func upload_export(output_base: String, config: BuildConfig) -> JamError: - var staging_dir = output_base.path_join("..").simplify_path() + var staging_dir: String = output_base.path_join("..").simplify_path() var artifact_path: String if config.no_zip: artifact_path = ProjectSettings.globalize_path(output_base.path_join(config.output_target)) else: # Archive the export output in a zip file - var zip_name := "%s.zip" % config.template_name + var zip_name: String = "%s.zip" % config.template_name artifact_path = staging_dir.path_join(zip_name) - var zip_err := zip_folder(output_base, artifact_path) + var zip_err: JamError = zip_folder(output_base, artifact_path) if zip_err.errored: return JamError.err("Failed to create zip for %s export - %s" % [config.template_name, zip_err.error_msg]) - + # Perform streaming upload of the zip file - var stream_reader_res := StreamingUpload.FileReader.from_path(artifact_path) + var stream_reader_res: JamResult = StreamingUpload.FileReader.from_path(artifact_path) if stream_reader_res.errored: return JamError.err(stream_reader_res.error_msg) - - var upload_res := StreamingUpload.streaming_upload( + + var upload_res: JamError = StreamingUpload.streaming_upload( config.presigned_post["url"] as String, config.presigned_post["fields"] as Dictionary, stream_reader_res.value as StreamingUpload.FileReader @@ -172,21 +159,22 @@ static func upload_export(output_base: String, config: BuildConfig) -> JamError: return JamError.err("export upload failed: %s" % upload_res.error_msg) return JamError.ok() + static func recursive_delete(directory: String) -> JamError: if FileAccess.file_exists(directory): - var err = DirAccess.remove_absolute(directory) - if err != OK: + var err: Error = DirAccess.remove_absolute(directory) + if not err == OK: return JamError.err("Failed to remove file %s: %d" % [directory, err]) return JamError.ok() - var dir = DirAccess.open(directory) + var dir: DirAccess = DirAccess.open(directory) if dir == null: return JamError.err("Directory open error: %d" % DirAccess.get_open_error()) - for f in dir.get_files(): - var err = dir.remove(f) - if err != OK: + for f: String in dir.get_files(): + var err: Error = dir.remove(f) + if not err == OK: return JamError.err("Failed to remove file %s: %d" % [f, err]) - for d in dir.get_directories(): - var res = recursive_delete(directory.path_join(d)) + for d: String in dir.get_directories(): + var res: JamError = recursive_delete(directory.path_join(d)) if res.errored: return res @@ -194,7 +182,7 @@ static func recursive_delete(directory: String) -> JamError: return JamError.ok() static func zip_folder(source_root: String, zip_path: String) -> JamError: - var output := [] + var output: Array = [] var exit_code: int = 0 if OS.get_name() == "Windows": @@ -204,75 +192,75 @@ static func zip_folder(source_root: String, zip_path: String) -> JamError: else: return JamError.err("Failed zip step - unsupported editor platform '%s'" % OS.get_name()) - if exit_code != 0: + if not exit_code == 0: return JamError.err("Failed zip file command:\n%s" % "\n".join(output)) return JamError.ok() # TODO: maybe this can still be used as a fallback if the required system utilities are not available - #var zip := ZIPPacker.new() - #var err := zip.open(zip_path) - #if err != OK: + #var zip: ZIPPacker = ZIPPacker.new() + #var err: Error = zip.open(zip_path) + #if not err == OK: #return JamError.err("Failed to open zip file '%s' (error code %d)" % [zip_path, err]) - #var out_dir = DirAccess.open(source_root) + #var out_dir: DirAccess = DirAccess.open(source_root) #if out_dir == null: #return JamError.err("Failed to open zip target directory '%s' (error code %d)" % [out_dir, err]) #recursive_zip(out_dir, zip) #err = zip.close() - #if err != OK: + #if not err == OK: #return JamError.err("Failed to close zip file (error code %d)" % err) #return JamError.ok() -static func recursive_zip(dir: DirAccess, writer: ZIPPacker, root_folder: String = ""): + +static func recursive_zip(dir: DirAccess, writer: ZIPPacker, root_folder: String = "") -> void: dir.include_hidden = true - if len(root_folder) == 0: root_folder = dir.get_current_dir() - - var err: int + + var err: Error dir.list_dir_begin() - var file_name := dir.get_next() - while file_name != "": - var abs_file := dir.get_current_dir() + "/" + file_name + var file_name: String = dir.get_next() + while not file_name.is_empty(): + var abs_file: String = dir.get_current_dir() + "/" + file_name if dir.current_is_dir(): recursive_zip(DirAccess.open(abs_file), writer, root_folder) else: err = writer.start_file(abs_file.right(-1 * len(root_folder)).lstrip("/")) - if err != OK: + if not err == OK: printerr("Unexpected error when starting file write: %d" % err) err = writer.write_file(FileAccess.get_file_as_bytes(abs_file)) - if err != OK: + if not err == OK: printerr("Unexpected error when performing file write: %d" % err) err = writer.close_file() - if err != OK: + if not err == OK: printerr("Unexpected error when closing file write: %d" % err) file_name = dir.get_next() + static func merge_presets(additions_path: String, base_path: String = "res://export_presets.cfg") -> JamError: - if not FileAccess.file_exists(base_path): - var err = DirAccess.copy_absolute(additions_path, base_path) - if err != OK: + var err: Error = DirAccess.copy_absolute(additions_path, base_path) + if not err == OK: return JamError.err("Failed to initialize export_presets.cfg from base copy") else: - var export_cfg = ConfigFile.new() - var err = export_cfg.load(base_path) - if err != OK: + var export_cfg: ConfigFile = ConfigFile.new() + var err: Error = export_cfg.load(base_path) + if not err == OK: return JamError.err("Failed to load existing export presets at '%s'" % base_path) - var jam_export_cfg = ConfigFile.new() + var jam_export_cfg: ConfigFile = ConfigFile.new() err = jam_export_cfg.load(additions_path) - if err != OK: + if not err == OK: return JamError.err("Failed to load additional export presets at '%s'" % additions_path) var jam_preset_map: Dictionary = {} var jam_preset_options_map: Dictionary = {} - var preset_regex = RegEx.create_from_string("^preset\\.(\\d+)$") + var preset_regex: RegEx = RegEx.create_from_string("^preset\\.(\\d+)$") for section in jam_export_cfg.get_sections(): if preset_regex.search(section) == null: continue # determine name and get values - var vals = {} - var preset_name = "" + var vals: Dictionary = {} + var preset_name: String = "" for key in jam_export_cfg.get_section_keys(section): vals[key] = jam_export_cfg.get_value(section, key) if key == "name": @@ -280,36 +268,53 @@ static func merge_presets(additions_path: String, base_path: String = "res://exp jam_preset_map[preset_name] = vals # get options section vals = {} - var opt_section := section + ".options" - for key in jam_export_cfg.get_section_keys(opt_section): + var opt_section: String = section + ".options" + for key: String in jam_export_cfg.get_section_keys(opt_section): vals[key] = jam_export_cfg.get_value(opt_section, key) jam_preset_options_map[preset_name] = vals - - var highest_section_num := -1 - for section in export_cfg.get_sections(): - var preset_match = preset_regex.search(section) + + var highest_section_num: int = -1 + for section: String in export_cfg.get_sections(): + var preset_match: RegExMatch = preset_regex.search(section) if preset_match == null: continue highest_section_num = maxi(preset_match.get_string(1).to_int(), highest_section_num) - for key in export_cfg.get_section_keys(section): + for key: String in export_cfg.get_section_keys(section): if key == "name": jam_preset_map.erase(export_cfg.get_value(section, key)) break - - var insert_index = highest_section_num + 1 - for preset_name in jam_preset_map: - var section := "preset.%d" % [insert_index] + + var insert_index: int = highest_section_num + 1 + for preset_name: Variant in jam_preset_map: + var section: String = "preset.%d" % [insert_index] insert_index += 1 - for key in jam_preset_map[preset_name]: + for key: Variant in jam_preset_map[preset_name]: export_cfg.set_value(section, key as String, jam_preset_map[preset_name][key]) - - var opt_section := "%s.options" % [section] - for key in jam_preset_options_map[preset_name]: + + var opt_section: String = "%s.options" % [section] + for key: Variant in jam_preset_options_map[preset_name]: export_cfg.set_value(opt_section, key as String, jam_preset_options_map[preset_name][key]) - - if len(jam_preset_map.keys()) > 0: + + if not jam_preset_map.keys().is_empty(): err = export_cfg.save(base_path) - if err != OK: + if not err == OK: return JamError.err("Failed to save updated export presets at '%s'" % base_path) - + return JamError.ok() + + +class BuildConfig: + extends RefCounted + var template_name: String + var output_target: String + var no_zip: bool + var presigned_post: Dictionary + var log_presigned_post: Dictionary + +class ExportConfig: + extends RefCounted + var game_id: String + var network_mode: String + var build_configs: Array[BuildConfig] + var parallel: bool + var export_timeout: int \ No newline at end of file diff --git a/server/JamDB.gd b/server/JamDB.gd index 59edb27..70c598a 100644 --- a/server/JamDB.gd +++ b/server/JamDB.gd @@ -1,5 +1,5 @@ -extends RefCounted class_name JamDB +extends RefCounted ## An interface for a document database for Jam Launch servers ## ## A database interface that allows the server to persist data rows within and @@ -10,98 +10,111 @@ class_name JamDB var _jc: JamConnect -func _init(jam_connect: JamConnect): +func _init(jam_connect: JamConnect) -> void: _jc = jam_connect -@warning_ignore("unused_parameter") -func get_db_data(key_1: String, key_2: String): + +func get_db_data(_key_1: String, _key_2: String) -> Variant: return null + ## Gets the project-scoped data row for the provided key -func get_game_data(key_2: String): +func get_game_data(key_2: String) -> Variant: return get_db_data(_jc.get_project_id(), key_2) - + + ## Gets the release-scoped data row for the provided key -func get_release_data(key_2: String): +func get_release_data(key_2: String) -> Variant: return get_db_data(_jc.get_game_id(), key_2) + ## Gets the session-scoped data row for the provided key -func get_session_data(key_2: String): +func get_session_data(key_2: String) -> Variant: return get_db_data(_jc.get_session_id(), key_2) -@warning_ignore("unused_parameter") -func put_db_data(key_1: String, key_2: String, data: Dictionary): + +func put_db_data(_key_1: String, _key_2: String, _data: Dictionary) -> Variant: return false + ## Puts a project-scoped data row in the database -func put_game_data(key_2: String, data: Dictionary): +func put_game_data(key_2: String, data: Dictionary) -> Variant: return put_db_data(_jc.get_project_id(), key_2, data) + ## Puts a release-scoped data row in the database -func put_release_data(key_2: String, data: Dictionary): +func put_release_data(key_2: String, data: Dictionary) -> Variant: return put_db_data(_jc.get_game_id(), key_2, data) + ## Puts a session-scoped data row in the database -func put_session_data(key_2: String, data: Dictionary): +func put_session_data(key_2: String, data: Dictionary) -> Variant: return put_db_data(_jc.get_session_id(), key_2, data) + ## Queries for data rows using an AWS DynamoDB filter syntax -@warning_ignore("unused_parameter") func query_db_data( - key_condition_expression: String, - filter_expression: String, - expression_attribute_names: Dictionary, - expression_attribute_values: Dictionary) -> Array: + _key_condition_expression: String, + _filter_expression: String, + _expression_attribute_names: Dictionary, + _expression_attribute_values: Dictionary) -> Array: return [] -@warning_ignore("unused_parameter") -func get_db_data_async(key_1: String, key_2: String): + +func get_db_data_async(_key_1: String, _key_2: String) -> void: _jc.game_db_async_result.emit(null, "No DB available in dev mode") + ## Asynchronously gets the project-scoped data row for the provided key. ## Triggers [signal JamConnect.game_db_async_result] upon completion. -func get_game_data_async(key_2: String): +func get_game_data_async(key_2: String) -> void: return get_db_data_async(_jc.get_project_id(), key_2) + ## Asynchronously gets the release-scoped data row for the provided key. ## Triggers [signal JamConnect.game_db_async_result] upon completion. -func get_release_data_async(key_2: String): +func get_release_data_async(key_2: String) -> void: return get_db_data_async(_jc.get_game_id(), key_2) + ## Asynchronously gets the session-scoped data row for the provided key. ## Triggers [signal JamConnect.game_db_async_result] upon completion. -func get_session_data_async(key_2: String): +func get_session_data_async(key_2: String) -> void: return get_db_data_async(_jc.get_session_id(), key_2) -@warning_ignore("unused_parameter") -func put_db_data_async(key_1: String, key_2: String, data: Dictionary): + +func put_db_data_async(_key_1: String, _key_2: String, _data: Dictionary) -> void: _jc.game_db_async_result.emit(null, "No DB available in dev mode") + ## Asynchronously puts a project-scoped data row in the database. Triggers ## [signal JamConnect.game_db_async_result] upon completion. -func put_game_data_async(key_2: String, data: Dictionary): +func put_game_data_async(key_2: String, data: Dictionary) -> void: return put_db_data_async(_jc.get_project_id(), key_2, data) + ## Asynchronously puts a release-scoped data row in the database. Triggers ## [signal JamConnect.game_db_async_result] upon completion. -func put_release_data_async(key_2: String, data: Dictionary): +func put_release_data_async(key_2: String, data: Dictionary) -> void: return put_db_data_async(_jc.get_game_id(), key_2, data) + ## Asynchronously puts a session-scoped data row in the database. Triggers ## [signal JamConnect.game_db_async_result] upon completion. -func put_session_data_async(key_2: String, data: Dictionary): +func put_session_data_async(key_2: String, data: Dictionary) -> void: return put_db_data_async(_jc.get_session_id(), key_2, data) + ## Asynchronously queries for data rows using an AWS DynamoDB filter syntax. ## Triggers [signal JamConnect.game_db_async_result] upon completion. -@warning_ignore("unused_parameter") func query_db_data_async( - key_condition_expression: String, - filter_expression: String, - expression_attribute_names: Dictionary, - expression_attribute_values: Dictionary): + _key_condition_expression: String, + _filter_expression: String, + _expression_attribute_names: Dictionary, + _expression_attribute_values: Dictionary) -> void: _jc.game_db_async_result.emit(null, "No DB available in dev mode") + ## Gets the last error generated by the DB client -func get_last_error(): +func get_last_error() -> String: return "server is in dev mode - no DB available" diff --git a/server/JamDBDynamo.gd b/server/JamDBDynamo.gd index aa0deb6..2d1524b 100644 --- a/server/JamDBDynamo.gd +++ b/server/JamDBDynamo.gd @@ -1,24 +1,27 @@ -extends JamDB class_name JamDBDynamo +extends JamDB -var ddb +var ddb: Variant -func _init(jam_connect: JamConnect, ddb_client): +func _init(jam_connect: JamConnect, ddb_client: Variant) -> void: super(jam_connect) ddb = ddb_client ddb.async_result.connect(_relay_result) -func _relay_result(result, err): + +func _relay_result(result: Variant, err: Variant) -> void: _jc.game_db_async_result.emit(result, err) -func get_db_data(key_1: String, key_2: String): + +func get_db_data(key_1: String, key_2: String) -> Variant: return ddb.get_item( OS.get_environment("GAME_DATA_TABLE"), key_1, key_2 ) -func put_db_data(key_1: String, key_2: String, data: Dictionary): + +func put_db_data(key_1: String, key_2: String, data: Dictionary) -> Variant: data["session_id"] = key_1 data["record_type"] = key_2 return ddb.put_item( @@ -26,6 +29,7 @@ func put_db_data(key_1: String, key_2: String, data: Dictionary): data ) + func query_db_data( key_condition_expression: String, filter_expression: String, @@ -39,14 +43,16 @@ func query_db_data( expression_attribute_values ) -func get_db_data_async(key_1: String, key_2: String): + +func get_db_data_async(key_1: String, key_2: String) -> void: ddb.get_item_async( OS.get_environment("GAME_DATA_TABLE"), key_1, key_2 ) -func put_db_data_async(key_1: String, key_2: String, data: Dictionary): + +func put_db_data_async(key_1: String, key_2: String, data: Dictionary) -> void: data["session_id"] = key_1 data["record_type"] = key_2 ddb.put_item_async( @@ -54,11 +60,12 @@ func put_db_data_async(key_1: String, key_2: String, data: Dictionary): data ) + func query_db_data_async( key_condition_expression: String, filter_expression: String, expression_attribute_names: Dictionary, - expression_attribute_values: Dictionary): + expression_attribute_values: Dictionary) -> void: ddb.query_async( OS.get_environment("GAME_DATA_TABLE"), key_condition_expression, @@ -67,5 +74,6 @@ func query_db_data_async( expression_attribute_values ) -func get_last_error(): + +func get_last_error() -> String: return ddb.last_error() diff --git a/server/JamFiles.gd b/server/JamFiles.gd index 0076106..22788c1 100644 --- a/server/JamFiles.gd +++ b/server/JamFiles.gd @@ -1,5 +1,5 @@ -extends RefCounted class_name JamFiles +extends RefCounted ## An interface for file storage for Jam Launch servers ## ## A file storage interface that allows the server to persist data within and @@ -11,79 +11,94 @@ class_name JamFiles var _jc: JamConnect -func _init(jam_connect): +func _init(jam_connect: JamConnect) -> void: _jc = jam_connect -@warning_ignore("unused_parameter") -func get_file(key: String, file_name: String) -> bool: + +func get_file(_key: String, _file_name: String) -> bool: return false + ## Gets the project-scoped data file for the provided key -func get_game_file(key: String, file_name: String): +func get_game_file(key: String, file_name: String) -> bool: return get_file(_jc.get_project_id() + "/" + key, file_name) - + + ## Gets the release-scoped data file for the provided key -func get_release_file(key: String, file_name: String): +func get_release_file(key: String, file_name: String) -> bool: return get_file(_jc.get_game_id() + "/" + key, file_name) + ## Gets the session-scoped data file for the provided key -func get_session_file(key: String, file_name: String): +func get_session_file(key: String, file_name: String) -> bool: return get_file(_jc.get_session_id() + "/" + key, file_name) + @warning_ignore("unused_parameter") func put_file(key: String, file_name: String) -> bool: return false - + + ## Persists a project-scoped data file -func put_game_file(key: String, file_name: String): +func put_game_file(key: String, file_name: String) -> bool: return put_file(_jc.get_project_id() + "/" + key, file_name) + ## Persists a release-scoped data file -func put_release_file(key: String, file_name: String): +func put_release_file(key: String, file_name: String) -> bool: return put_file(_jc.get_game_id() + "/" + key, file_name) + ## Persists a session-scoped data file -func put_session_file(key: String, file_name: String): +func put_session_file(key: String, file_name: String) -> bool: return put_file(_jc.get_session_id() + "/" + key, file_name) -@warning_ignore("unused_parameter") -func get_file_async(key: String, file_name: String): + +func get_file_async(_key: String, _file_name: String) -> void: _jc.game_files_async_result.emit(null, "No DB available in dev mode") + ## Asynchronously gets the project-scoped data file for the provided key. ## Triggers [signal JamConnect.game_files_async_result] upon completion. -func get_game_file_async(key: String, file_name: String): +func get_game_file_async(key: String, file_name: String) -> void: get_file_async(_jc.get_project_id() + "/" + key, file_name) + ## Asynchronously gets the release-scoped data file for the provided key. ## Triggers [signal JamConnect.game_files_async_result] upon completion. -func get_release_file_async(key: String, file_name: String): +func get_release_file_async(key: String, file_name: String) -> void: get_file_async(_jc.get_game_id() + "/" + key, file_name) + ## Asynchronously gets the session-scoped data file for the provided key. ## Triggers [signal JamConnect.game_files_async_result] upon completion. -func get_session_file_async(key: String, file_name: String): +func get_session_file_async(key: String, file_name: String) -> void: get_file_async(_jc.get_session_id() + "/" + key, file_name) + @warning_ignore("unused_parameter") -func put_file_async(key: String, file_name: String): +func put_file_async(_key: String, _file_name: String) -> void: _jc.game_files_async_result.emit(null, "No DB available in dev mode") + ## Asynchronously persists a project-scoped data file. ## Triggers [signal JamConnect.game_files_async_result] upon completion. -func put_game_file_async(key: String, file_name: String): +func put_game_file_async(key: String, file_name: String) -> void: put_file_async(_jc.get_project_id() + "/" + key, file_name) + ## Asynchronously persists a release-scoped data file. ## Triggers [signal JamConnect.game_files_async_result] upon completion. -func put_release_file_async(key: String, file_name: String): +func put_release_file_async(key: String, file_name: String) -> void: put_file_async(_jc.get_game_id() + "/" + key, file_name) + ## Asynchronously persists a session-scoped data file. ## Triggers [signal JamConnect.game_files_async_result] upon completion. -func put_session_file_async(key: String, file_name: String): +func put_session_file_async(key: String, file_name: String) -> void: put_file_async(_jc.get_session_id() + "/" + key, file_name) + ## Gets the last error generated by the files client -func get_last_error(): +func get_last_error() -> String: return "server is in dev mode - no DB available" diff --git a/server/JamFilesS3.gd b/server/JamFilesS3.gd index 41f0579..38c435d 100644 --- a/server/JamFilesS3.gd +++ b/server/JamFilesS3.gd @@ -1,16 +1,18 @@ -extends JamFiles class_name JamFilesS3 +extends JamFiles -var s3 +var s3: Variant -func _init(jam_connect: JamConnect, s3_client): +func _init(jam_connect: JamConnect, s3_client: Variant) -> void: super(jam_connect) s3 = s3_client s3.async_result.connect(_relay_result) -func _relay_result(key, err): + +func _relay_result(key: Variant, err: Variant) -> void: _jc.game_files_async_result.emit(key, err) + func get_file(key: String, file_name: String) -> bool: return s3.get_file( OS.get_environment("GAME_DATA_BUCKET"), @@ -18,26 +20,30 @@ func get_file(key: String, file_name: String) -> bool: file_name ) + func put_file(key: String, file_name: String) -> bool: return s3.put_file( OS.get_environment("GAME_DATA_BUCKET"), key, file_name ) - -func get_file_async(key: String, file_name: String): + + +func get_file_async(key: String, file_name: String) -> void: s3.get_file_async( OS.get_environment("GAME_DATA_BUCKET"), key, file_name ) -func put_file_async(key: String, file_name: String): + +func put_file_async(key: String, file_name: String) -> void: s3.put_file_async( OS.get_environment("GAME_DATA_BUCKET"), key, file_name ) -func get_last_error(): + +func get_last_error() -> String: return s3.last_error() diff --git a/ui/BusyCircle.gd b/ui/BusyCircle.gd index c8a09c6..c6dc81c 100644 --- a/ui/BusyCircle.gd +++ b/ui/BusyCircle.gd @@ -1,25 +1,25 @@ @tool extends CenterContainer -@export var speed = 1.0 -@export var padding_ratio = 0.1: +@export var speed: float = 1.0 +@export var padding_ratio: float = 0.1: set(p): padding_ratio = p _on_resized() @onready var circle: Sprite2D = $C/Circle -func _ready(): +func _ready() -> void: _on_resized() -func _process(delta): +func _process(delta: float) -> void: var amount: float = delta * speed * 2.0 * PI circle.rotate(amount) -func _on_resized(): +func _on_resized() -> void: if not circle: return - var bounds = min(size.x, size.y) + var bounds: float = min(size.x, size.y) bounds -= bounds * padding_ratio if bounds > 48: @@ -31,7 +31,7 @@ func _on_resized(): else: circle.texture = preload("res://addons/jam_launch/assets/icons/progress_16x16.svg") - var circle_bounds = circle.texture.get_height() + var circle_bounds: int = circle.texture.get_height() if bounds > circle_bounds: circle.scale = Vector2(1.0, 1.0) else: diff --git a/ui/ChatConsole.gd b/ui/ChatConsole.gd index 0734f44..8650518 100644 --- a/ui/ChatConsole.gd +++ b/ui/ChatConsole.gd @@ -1,28 +1,28 @@ extends MarginContainer class_name ChatConsole -@onready var chat_log = $VB/P/ChatLog -@onready var msg_line = $VB/MsgHB/Msg +@onready var chat_log: RichTextLabel = $VB/P/ChatLog +@onready var msg_line: LineEdit = $VB/MsgHB/Msg var jam_connect: JamConnect: set(val): val.log_event.connect(append_status_message) jam_connect = val -func append_status_message(msg: String): +func append_status_message(msg: String) -> void: print(msg) chat_log.text += "[color=#aaa][i]" + msg + "[/i][/color]\n" -func append_chat_message(sender: String, msg: String): +func append_chat_message(sender: String, msg: String) -> void: chat_log.text += "[color=#bbb][b]%s:[/b][/color] %s\n" % [sender, msg] -func is_chat_focused(): +func is_chat_focused() -> bool: return msg_line.has_focus() -func give_chat_focus(): +func give_chat_focus() -> void: msg_line.grab_focus() -func _on_text_submit(): +func _on_text_submit() -> void: var msg := sanitize(msg_line.text as String) msg_line.clear() msg_line.release_focus() @@ -30,16 +30,15 @@ func _on_text_submit(): _send_chat_msg.rpc(msg) @rpc("any_peer", "call_local") -func _send_chat_msg(msg: String): +func _send_chat_msg(msg: String) -> void: msg = sanitize(msg) - var peer_id := multiplayer.get_remote_sender_id() var username: String = "<>" if jam_connect.server: username = jam_connect.server.peer_usernames.get(peer_id, "<>") elif jam_connect.client: username = jam_connect.client.peer_usernames.get(peer_id, "<>") - + _print_chat_msg(username, msg) func sanitize(msg: String) -> String: @@ -49,6 +48,5 @@ func sanitize(msg: String) -> String: msg = msg.replace("]", ")") return msg -func _print_chat_msg(username: String, msg: String): - +func _print_chat_msg(username: String, msg: String) -> void: append_chat_message(username, msg) diff --git a/ui/MessagePanel.gd b/ui/MessagePanel.gd index 2546580..1c0bc5d 100644 --- a/ui/MessagePanel.gd +++ b/ui/MessagePanel.gd @@ -2,16 +2,16 @@ extends MarginContainer class_name MessagePanel -@onready var dismiss_progress = $MC/HB/VB/ProgressBar -@onready var msg_txt = $MC/HB/Message -@onready var show_all = $MC/HB/ShowAll -@onready var full_message = $Full/M/FullMessage -@onready var full_popup = $Full +@onready var dismiss_progress: ProgressBar = $MC/HB/VB/ProgressBar +@onready var msg_txt: RichTextLabel = $MC/HB/Message +@onready var show_all: Button = $MC/HB/ShowAll +@onready var full_message: RichTextLabel = $Full/M/FullMessage +@onready var full_popup: Popup = $Full var message: String: set(val): full_message.text = val - var preview = val + var preview: String = val if len(val) > 130: preview = val.substr(0, 127) + "..." show_all.visible = true @@ -24,17 +24,17 @@ var _auto_dismissed: bool = false var _auto_dismiss_delay: float = 10.0 var elapsed: float = 0.0 -func _ready(): +func _ready() -> void: $MC/HB/VB/ProgressBar.visible = false -func _process(delta): +func _process(delta: float) -> void: if not _auto_dismissed: return elapsed += delta dismiss_progress.value = int(elapsed * 100.0 / _auto_dismiss_delay) -func set_auto_dismiss(delay: float): +func set_auto_dismiss(delay: float) -> void: _auto_dismissed = true _auto_dismiss_delay = delay dismiss_progress.visible = true @@ -42,20 +42,20 @@ func set_auto_dismiss(delay: float): $DismissTimer.start(_auto_dismiss_delay) dismiss_progress.value = 0 -func _on_dismiss_timer_timeout(): +func _on_dismiss_timer_timeout() -> void: dismiss() -func _on_dismiss_pressed(): +func _on_dismiss_pressed() -> void: dismiss() -func dismiss(): +func dismiss() -> void: queue_free() -func set_error_text(text: String): +func set_error_text(text: String) -> void: message = text msg_txt.text = "[color=#f99]%s[/color]" % msg_txt.text -func _on_show_all_pressed(): +func _on_show_all_pressed() -> void: $DismissTimer.stop() dismiss_progress.visible = false full_popup.popup_centered_ratio(0.7) diff --git a/ui/client/DeviceAuthUI.gd b/ui/client/DeviceAuthUI.gd index 94b598f..240e734 100644 --- a/ui/client/DeviceAuthUI.gd +++ b/ui/client/DeviceAuthUI.gd @@ -1,15 +1,18 @@ @tool +class_name DeviceAuthUI extends MarginContainer -class_name DeviceAuthUI +signal errored(msg: String) +signal active_auth(active: bool) +signal has_token(token: String) @onready var login_api: JamLoginApi = $JamLoginApi @onready var notes: RichTextLabel = $Waiting/Notes @onready var user_code_label: Label = $Waiting/PC/M/UserCode - @onready var busy_scope: ScopeLocker = $BusyScope @onready var waiting_scope: ScopeLocker = $WaitingScope @onready var active_scope: ScopeLocker = $ActiveScope +@export var auth_mode: AUTH_MODE = AUTH_MODE.DEVELOPER var cancel_auth: bool = false var device_auth_url: String @@ -20,53 +23,50 @@ enum AUTH_MODE { DEVELOPER } -@export var auth_mode: AUTH_MODE = AUTH_MODE.DEVELOPER - -signal errored(msg: String) -signal active_auth(active: bool) -signal has_token(token: String) - -func _ready(): +func _ready() -> void: $Base.visible = true $Busy.visible = false $Waiting.visible = false - - active_scope.lock_changed.connect(func(x): active_auth.emit(x)) - - var dir := (self.get_script() as Script).get_path().get_base_dir() - var settings = ConfigFile.new() - var err = settings.load(dir + "/../../settings.cfg") - if err != OK: + active_scope.lock_changed.connect(func(x:bool)->void: active_auth.emit(x)) + var dir: String = (self.get_script() as Script).get_path().get_base_dir() + var settings: ConfigFile = ConfigFile.new() + var err: Error = settings.load(dir + "/../../settings.cfg") + if not err == OK: printerr("Failed to load auth settings") return + device_auth_url = settings.get_value("auth", "url") - - var deployment = ConfigFile.new() + var deployment: ConfigFile = ConfigFile.new() err = deployment.load(dir + "/../../deployment.cfg") - if err != OK: + if not err == OK: printerr("Failed to load deployment settings") return + game_id = deployment.get_value("game", "id") -func _err(msg: String): + +func _err(msg: String) -> void: errored.emit(msg) -func _on_notes_meta_hover_started(_meta): + +func _on_notes_meta_hover_started(_meta: String) -> void: mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND -func _on_notes_meta_hover_ended(_meta): + +func _on_notes_meta_hover_ended(_meta: String) -> void: mouse_default_cursor_shape = Control.CURSOR_ARROW -func _on_notes_meta_clicked(meta: String): + +func _on_notes_meta_clicked(meta: String) -> void: OS.shell_open(meta) -func _on_login_button_pressed(): - var _active_lock = active_scope.get_lock() - var _lock = busy_scope.get_lock() +func _on_login_button_pressed() -> void: + var _active_lock: ScopeLocker.ScopeLock = active_scope.get_lock() + var _lock: ScopeLocker.ScopeLock = busy_scope.get_lock() var res: JamLoginApi.Result if auth_mode == AUTH_MODE.USER: - if len(game_id) > 0: + if not game_id.is_empty(): res = await login_api.request_user_auth(game_id) elif OS.is_debug_build(): res = await login_api.request_developer_auth() @@ -78,13 +78,12 @@ func _on_login_button_pressed(): _err(res.error_msg) return - var userCode := res.data["userCode"] as String - var deviceCode := res.data["deviceCode"] as String - var authUrl := "%s?user_code=%s" % [device_auth_url, userCode] - + var userCode: String = res.data["userCode"] as String + var deviceCode: String = res.data["deviceCode"] as String + var authUrl: String = "%s?user_code=%s" % [device_auth_url, userCode] OS.shell_open(authUrl) user_code_label.text = userCode - notes.parse_bbcode("[center][color=#eeeeee][bgcolor=#00000000]Confirm the following code at + notes.parse_bbcode("[center][color=#eeeeee][bgcolor=#00000000]Confirm the following code at [url]%s[/url][/bgcolor][/color][/center]" % device_auth_url) _lock = waiting_scope.get_lock() @@ -93,7 +92,7 @@ func _on_login_button_pressed(): cancel_auth = false return await get_tree().create_timer(1).timeout - var authRes := await login_api.check_auth(userCode, deviceCode) + var authRes: JamHttpBase.Result = await login_api.check_auth(userCode, deviceCode) if authRes.errored: _err(authRes.error_msg) elif authRes.data["state"] == "pending": @@ -104,13 +103,16 @@ func _on_login_button_pressed(): _err("Access was not granted") break -func _on_busy_scope_lock_changed(locked): + +func _on_busy_scope_lock_changed(locked: bool) -> void: $Busy.visible = locked $Base.visible = not (locked||waiting_scope.is_locked()) -func _on_waiting_scope_lock_changed(locked): + +func _on_waiting_scope_lock_changed(locked: bool) -> void: $Waiting.visible = locked $Base.visible = not (locked||busy_scope.is_locked()) -func _on_cancel_auth_pressed(): + +func _on_cancel_auth_pressed() -> void: cancel_auth = true diff --git a/ui/client/ExampleClientUI.gd b/ui/client/ExampleClientUI.gd index 2471dfc..53c795b 100644 --- a/ui/client/ExampleClientUI.gd +++ b/ui/client/ExampleClientUI.gd @@ -6,37 +6,27 @@ extends JamClientUI @onready var host_page: Control = $CC/M/M/PageStack/HostGame @onready var join_code_page: Control = $CC/M/M/PageStack/JoinGameCode @onready var session_page: Control = $CC/M/M/PageStack/Session - @onready var errors: Control = $Bottom/ErrorArea/Errors @onready var version_info: Label = $Bottom/M/VersionInfo - @onready var device_auth: DeviceAuthUI = $CC/M/M/PageStack/GjwtEntry/Entry/DeviceAuth @onready var manual_auth: Control = $CC/M/M/PageStack/GjwtEntry/Entry/Manual @onready var gjwt_entry: Control = $CC/M/M/PageStack/GjwtEntry/Entry @onready var gjwt_busy: Control = $CC/M/M/PageStack/GjwtEntry/Busy - @onready var start_join: Button = $CC/M/M/PageStack/Home/VB/StartJoin @onready var start_host: Button = $CC/M/M/PageStack/Home/VB/StartHost @onready var no_deploy_lbl: Label = $CC/M/M/PageStack/Home/NoDeployment @onready var dev_tools: MenuButton = $CC/M/M/PageStack/Home/VB/DevTools @onready var logged_in: Label = $CC/M/M/PageStack/Home/VB/LoggedIn - @onready var join_busy: Control = $CC/M/M/PageStack/JoinGameCode/Busy @onready var join_busy_lock: ScopeLocker = $CC/M/M/PageStack/JoinGameCode/JoinBusy @onready var join_code_edit: LineEdit = $CC/M/M/PageStack/JoinGameCode/Entry/EnterCode/JoinCode @onready var join_btn: Button = $CC/M/M/PageStack/JoinGameCode/Entry/EnterCode/JoinWithCode - @onready var host_busy: Control = $CC/M/M/PageStack/HostGame/Busy @onready var host_busy_lock: ScopeLocker = $CC/M/M/PageStack/HostGame/HostBusy @onready var host_region_select: OptionButton = $CC/M/M/PageStack/HostGame/G/RegionSelect @onready var host_btn: Button = $CC/M/M/PageStack/HostGame/HB/Host - @onready var guest_auth_ui: VBoxContainer = $CC/M/M/PageStack/GjwtEntry/Entry/Manual/Guest @onready var local_launch_ui: VBoxContainer = $CC/M/M/PageStack/GjwtEntry/Entry/Manual/Local - -const REFRESH_NORMAL = 4.0 -const REFRESH_FAST = 2.0 -const REFRESH_SLOW = 5.0 @onready var session_refresh_timer: Timer = $CC/M/M/PageStack/Session/SessionRefresh @onready var join_code_btn: Button = $CC/M/M/PageStack/Session/M/VB/JoinInfo/JoinCodeCopy @onready var start_game_btn: Button = $CC/M/M/PageStack/Session/M/VB/StartBox/StartGame @@ -44,8 +34,12 @@ const REFRESH_SLOW = 5.0 @onready var player_grid: GridContainer = $CC/M/M/PageStack/Session/M/VB/Players/M/VB/Grid var JOIN_ID_MIN_LEN: int = 4 - var did_join_game: bool = false +var joined_players: Dictionary = {} + +const REFRESH_NORMAL = 4.0 +const REFRESH_FAST = 2.0 +const REFRESH_SLOW = 5.0 var session_token: String = "" var session_result: JamClientApi.GameSessionResult = null: @@ -84,9 +78,10 @@ var session_result: JamClientApi.GameSessionResult = null: else: start_game_btn.visible = true -func _ready(): - var gid_parts = game_id.split("-") - var version_number = gid_parts[len(gid_parts) - 1] + +func _ready() -> void: + var gid_parts: PackedStringArray = game_id.split("-") + var version_number: String = gid_parts[len(gid_parts) - 1] version_info.text = "version %s" % version_number version_info.text += " - jam launch %s" % client_api.addon_version if OS.is_debug_build() and OS.get_name() != "Android": @@ -96,17 +91,15 @@ func _ready(): else: dev_tools.visible = false local_launch_ui.visible = false - - var allow_guests = await client_api.check_guests_allowed(jam_connect.game_id) + + var allow_guests: JamHttpBase.Result = await client_api.check_guests_allowed(jam_connect.game_id) jam_connect.allow_guests = not allow_guests.errored guest_auth_ui.visible = jam_connect.allow_guests - device_auth.active_auth.connect(_on_active_device_auth) device_auth.has_token.connect(_set_gjwt) jam_connect.gjwt_acquired.connect(_on_gjwt_acquired) jam_client.fetching_test_gjwt.connect(_on_gjwt_fetch_busy) _on_gjwt_fetch_busy(jam_client.gjwt_fetch_busy) - if jam_client.jwt.has_token(): _on_gjwt_acquired() else: @@ -118,93 +111,98 @@ func _ready(): jam_connect.local_player_joining.connect(_on_joining_game) jam_connect.local_player_left.connect(_on_leaving_game) -func _on_active_device_auth(active: bool): + +func _on_active_device_auth(active: bool) -> void: manual_auth.visible = !active - -func _on_gjwt_fetch_busy(busy: bool): + + +func _on_gjwt_fetch_busy(busy: bool) -> void: gjwt_entry.visible = not busy gjwt_busy.visible = busy if busy: pages.show_page_node(gjwt_page, false) -func _on_gjwt_acquired(): + +func _on_gjwt_acquired() -> void: pages.show_page_node(home_page, false) logged_in.text = "Logged in as\n%s" % jam_client.jwt.username -var joined_players = {} -func update_player_grid(): +func update_player_grid() -> void: while player_grid.get_child_count() > 0: var c := player_grid.get_child(0) player_grid.remove_child(c) c.queue_free() -func _on_joining_game(): + +func _on_joining_game() -> void: start_game_btn.disabled = true did_join_game = true -func _on_leaving_game(): + +func _on_leaving_game() -> void: if did_join_game: start_game_btn.disabled = false session_result = null session_token = "" did_join_game = false -func _on_devtools_pressed(id: int): + +func _on_devtools_pressed(id: int) -> void: if id == 0: jam_connect.start_as_dev_server.call_deferred() elif id == 1: jam_client.client_session_request("localhost", 7437, "localdev") elif id >= 3 and id <= 7: - var client_num = id - 2 + var client_num: int = id - 2 jam_client.test_client_number = client_num - var pop = dev_tools.get_popup() + var pop: PopupMenu = dev_tools.get_popup() for x in range(3, 8): pop.set_item_checked(pop.get_item_index(x), x == id) -func set_enable_deployments(enable: bool): + +func set_enable_deployments(enable: bool) -> void: start_host.disabled = !enable start_join.disabled = !enable no_deploy_lbl.visible = !enable -func _notification(what): + +func _notification(what: int) -> void: if what == NOTIFICATION_WM_CLOSE_REQUEST: print("got quit notification") if session_result != null: - var res = await client_api.leave_game_session(session_result.session_id) + var res: JamHttpBase.Result = await client_api.leave_game_session(session_result.session_id) if res.errored: push_error(res.error_msg) else: print("left game session") get_tree().quit() + func enter_session(session_id: String, token: String) -> bool: - var res := await client_api.get_game_session(session_id) + var res: JamHttpBase.Result = await client_api.get_game_session(session_id) if res.errored: show_error(res.error_msg) return false session_result = res - if len(session_result.join_id): join_code_btn.text = session_result.join_id join_code_btn.get_parent().visible = true else: join_code_btn.get_parent().visible = false - session_token = token session_refresh_timer.start(REFRESH_NORMAL) pages.show_page_node(session_page) return true + func exit_session() -> bool: session_refresh_timer.stop() session_token = "" - if session_result != null: var res := await client_api.leave_game_session(session_result.session_id) session_result = null - if res.errored: show_error(res.error_msg) return false @@ -213,32 +211,37 @@ func exit_session() -> bool: else: return false -func leave_game_session(): + +func leave_game_session() -> void: exit_session() -func _on_page_stack_tab_changed(_tab): + +func _on_page_stack_tab_changed(_tab: int) -> void: if not pages: return if pages.get_current_tab_control() != session_page: exit_session() -func _on_start_join_pressed(): + +func _on_start_join_pressed() -> void: pages.show_page_node(join_code_page) -func _on_start_host_pressed(): + +func _on_start_host_pressed() -> void: pages.show_page_node(host_page) -func _on_host_pressed(): + +func _on_host_pressed() -> void: var region := "us-east-2" - var region_id = host_region_select.get_item_id(host_region_select.selected) + var region_id: int = host_region_select.get_item_id(host_region_select.selected) if region_id == 0: region = "us-east-2" elif region_id == 1: region = "eu-west-2" - var _lock = host_busy_lock.get_lock() + var _lock: ScopeLocker.ScopeLock = host_busy_lock.get_lock() - var res := await client_api.create_game_session(region) + var res: JamHttpBase.Result = await client_api.create_game_session(region) if res.errored: show_error(res.error_msg) return @@ -248,19 +251,20 @@ func _on_host_pressed(): if res.errored: show_error("failed to leave game session after failing to enter: %s" % res.error_msg) -func _on_host_busy_lock_changed(locked): + +func _on_host_busy_lock_changed(locked: bool) -> void: host_btn.get_parent().visible = not locked host_region_select.get_parent().visible = not locked host_busy.visible = locked - start_host.get_parent().visible = not locked -func _on_join_with_code_pressed(): + +func _on_join_with_code_pressed() -> void: if join_busy_lock.is_locked(): show_error("cannot trigger join while join is already in progress") return - var _lock = join_busy_lock.get_lock() - var res := await client_api.join_game_session(join_code_edit.text) + var _lock: ScopeLocker.ScopeLock = join_busy_lock.get_lock() + var res: JamHttpBase.Result = await client_api.join_game_session(join_code_edit.text) if res.errored: show_error(res.error_msg) return @@ -268,25 +272,30 @@ func _on_join_with_code_pressed(): return join_code_edit.clear() -func _on_paste_code_pressed(): + +func _on_paste_code_pressed() -> void: join_code_edit.text = DisplayServer.clipboard_get() _on_join_with_code_pressed() -func _on_join_code_text_submitted(_new_text): + +func _on_join_code_text_submitted(_new_text: String) -> void: _on_join_with_code_pressed() -func _on_join_code_text_changed(new_text): - var upper = new_text.to_upper() + +func _on_join_code_text_changed(new_text: String) -> void: + var upper: String = new_text.to_upper() if upper != new_text: join_code_edit.text = upper join_code_edit.caret_column = len(upper) join_btn.disabled = len(upper) < JOIN_ID_MIN_LEN -func _on_join_busy_lock_changed(locked): + +func _on_join_busy_lock_changed(locked: bool) -> void: join_btn.get_parent().get_parent().visible = not locked join_busy.visible = locked -func _on_session_refresh_timeout(): + +func _on_session_refresh_timeout() -> void: if session_result == null or session_result.has_unusable_status(): return @@ -302,7 +311,8 @@ func _on_session_refresh_timeout(): session_refresh_timer.start(REFRESH_FAST) session_result = res -func show_error(msg: String, auto_dismiss_delay: float=0.0): + +func show_error(msg: String, auto_dismiss_delay: float=0.0) -> void: printerr(msg) var msg_panel: MessagePanel = preload ("../MessagePanel.tscn").instantiate() errors.add_child(msg_panel) @@ -311,23 +321,29 @@ func show_error(msg: String, auto_dismiss_delay: float=0.0): if auto_dismiss_delay > 0.0: msg_panel.set_auto_dismiss(auto_dismiss_delay) -func clear_errors(): + +func clear_errors() -> void: for msg in errors.get_children(): msg.dismiss() -func _on_join_code_copy_pressed(): + +func _on_join_code_copy_pressed() -> void: DisplayServer.clipboard_set(join_code_btn.text) -func _on_leave_session_pressed(): + +func _on_leave_session_pressed() -> void: pages.go_back() -func _on_host_back_pressed(): + +func _on_host_back_pressed() -> void: pages.go_back() -func _on_join_code_back_pressed(): + +func _on_join_code_back_pressed() -> void: pages.go_back() -func _on_start_game_pressed(): + +func _on_start_game_pressed() -> void: if session_result and session_result.busy_progress() == 1.0: session_refresh_timer.stop() var addr := session_result.address @@ -340,27 +356,32 @@ func _on_start_game_pressed(): show_error("cannot start game without a session that is ready", 5.0) return -func _set_gjwt(gjwt: String): + +func _set_gjwt(gjwt: String) -> void: jam_client.set_gjwt(gjwt) if !OS.is_debug_build() or OS.get_name() == "Android": jam_client.persist_gjwt() -func _on_client_pressed(): + +func _on_client_pressed() -> void: jam_client.client_session_request("localhost", 7437, "localdev") -func _on_server_pressed(): + +func _on_server_pressed() -> void: jam_connect.start_as_dev_server.call_deferred() -func _on_device_auth_errored(msg: String): + +func _on_device_auth_errored(msg: String) -> void: show_error(msg) + func _on_guest_auth_pressed() -> void: _on_gjwt_fetch_busy(true) - var res = await jam_client.api.get_guest_jwt(jam_connect.game_id) + var res: JamHttpBase.Result = await jam_client.api.get_guest_jwt(jam_connect.game_id) if res.errored: show_error(res.error_msg) else: - jam_client.set_gjwt(res.data["token"]) + jam_client.set_gjwt(res.data["token"] as String) _on_gjwt_fetch_busy.call_deferred(false) diff --git a/ui/client/JamClientUI.gd b/ui/client/JamClientUI.gd index 306f256..0e2f22f 100644 --- a/ui/client/JamClientUI.gd +++ b/ui/client/JamClientUI.gd @@ -1,19 +1,21 @@ -extends Control class_name JamClientUI +extends Control var jam_connect: JamConnect var jam_client: JamClient var client_api: JamClientApi var game_id: String -func client_ui_initialization(jc: JamConnect): +func client_ui_initialization(jc: JamConnect) -> void: jam_connect = jc jam_client = jc.client client_api = jam_client.api game_id = client_api.game_id -func leave_game_session(): - pass -func show_error(msg: String, _auto_dismiss_delay: float = 0.0): +func show_error(msg: String, _auto_dismiss_delay: float = 0.0) -> void: printerr(msg) + + +func leave_game_session() -> void: + pass diff --git a/ui/pages/PageStack.gd b/ui/pages/PageStack.gd index 5cbc6f1..56e6aec 100644 --- a/ui/pages/PageStack.gd +++ b/ui/pages/PageStack.gd @@ -6,10 +6,10 @@ var stack: Array[int] = [] signal go_back_enabled(enabled: bool) -func _ready(): +func _ready() -> void: tabs_visible = false -func go_back(): +func go_back() -> void: if len(stack) < 2: return stack.pop_back() @@ -25,7 +25,7 @@ func show_page_node(page_node: Node, push_to_stack: bool = true) -> bool: printerr("Failed to show page node ", page_node) return false -func show_page(idx: int, push_to_stack: bool = true): +func show_page(idx: int, push_to_stack: bool = true) -> void: current_tab = idx if push_to_stack: stack.push_back(idx) diff --git a/util/JamConfig.gd b/util/JamConfig.gd index 608697b..1272ce6 100644 --- a/util/JamConfig.gd +++ b/util/JamConfig.gd @@ -1,21 +1,23 @@ @tool -extends RefCounted class_name JamConfig +extends RefCounted static var default_path: String: get: return OS.get_data_dir().path_join("jam-launch/jam-config.cfg") + static func set_value(key: String, value: Variant, section: String="core") -> bool: - var c := get_cache() + var c: CacheResult = get_cache() if c.errored: printerr("error storing key in Jam Config: %s" % c.error_msg) return false c.cache.set_value(section, key, value) return not write_cache(c.cache).errored + static func clear(key: String, section: String="core") -> bool: - var c := get_cache() + var c: CacheResult = get_cache() if c.errored: printerr("error clearing key in Jam Config: %s" % c.error_msg) return false @@ -26,8 +28,9 @@ static func clear(key: String, section: String="core") -> bool: c.cache.erase_section_key(section, key) return not write_cache(c.cache).errored + static func get_value(key: String, section: String="core", default: Variant=null) -> Variant: - var c := get_cache() + var c: CacheResult = get_cache() if c.errored: printerr("error getting key from Jam Config: %s" % c.error_msg) return default @@ -35,30 +38,62 @@ static func get_value(key: String, section: String="core", default: Variant=null return default return c.cache.get_value(section, key, default) + +static func get_cache(path_override: String="") -> CacheResult: + var path: String = default_path + if not path_override.is_empty(): + path = path_override + + if !FileAccess.file_exists(path): + return CacheResult.not_found(path) + var cfg: ConfigFile = ConfigFile.new() + var result: int = cfg.load(path) + if not result == OK: + return CacheResult.err("failed to parse Jam Config file '%s'" % path) + return CacheResult.result(cfg, path) + + +static func write_cache(cache: ConfigFile, path_override: String="") -> CacheResult: + var path: String = default_path + if not path_override.is_empty(): + path = path_override + + var base_dir: String = path.get_base_dir() + if not FileAccess.file_exists(base_dir): + var err: Error = DirAccess.make_dir_recursive_absolute(base_dir) + if not err == OK: + return CacheResult.err("failed to create directories for Jam Config file '%s' - err %d" % [path, err]) + + var result: Error = cache.save(path) + if not result == OK: + return CacheResult.err("failed to write Jam Config file to path '%s' - error code %d" % [path, result]) + + return CacheResult.result(cache, path) + + class CacheResult: var cache: ConfigFile var errored: bool = false var error_msg: String = "" var exists: bool = true var path: String = "" - var _dirty: bool = false static func err(msg: String) -> CacheResult: - var r := CacheResult.new() + var r: CacheResult = CacheResult.new() r.errored = true r.error_msg = msg return r static func not_found(value_path: String) -> CacheResult: - var r := CacheResult.new() + var r: CacheResult = CacheResult.new() r.cache = ConfigFile.new() r.exists = false r.path = value_path return r static func result(cfg: ConfigFile, value_path: String) -> CacheResult: - var r := CacheResult.new() + var r: CacheResult = CacheResult.new() r.cache = cfg r.path = value_path return r @@ -70,39 +105,9 @@ class CacheResult: _dirty = true return true - func _notification(what): + func _notification(what: int) -> void: if what == NOTIFICATION_PREDELETE: if _dirty: var r := JamConfig.write_cache(cache, path) if r.errored: printerr("error writing back Jam Config: %s" % r.error_msg) - -static func get_cache(path_override: String="") -> CacheResult: - var path := default_path - if not path_override.is_empty(): - path = path_override - - if !FileAccess.file_exists(path): - return CacheResult.not_found(path) - var cfg := ConfigFile.new() - var result = cfg.load(path) - if result != OK: - return CacheResult.err("failed to parse Jam Config file '%s'" % path) - return CacheResult.result(cfg, path) - -static func write_cache(cache: ConfigFile, path_override: String="") -> CacheResult: - var path := default_path - if not path_override.is_empty(): - path = path_override - - var base_dir := path.get_base_dir() - if not FileAccess.file_exists(base_dir): - var err = DirAccess.make_dir_recursive_absolute(base_dir) - if err != OK: - return CacheResult.err("failed to create directories for Jam Config file '%s' - err %d" % [path, err]) - - var result := cache.save(path) - if result != OK: - return CacheResult.err("failed to write Jam Config file to path '%s' - error code %d" % [path, result]) - - return CacheResult.result(cache, path) diff --git a/util/JamError.gd b/util/JamError.gd index 88163ac..d4897e3 100644 --- a/util/JamError.gd +++ b/util/JamError.gd @@ -11,8 +11,9 @@ var error_msg: String = "" static func ok() -> JamError: return JamError.new() + static func err(msg: String) -> JamError: - var e = JamError.new() + var e: JamError = JamError.new() e.errored = true e.error_msg = msg return e diff --git a/util/JamResult.gd b/util/JamResult.gd index eeba8e7..a4847fd 100644 --- a/util/JamResult.gd +++ b/util/JamResult.gd @@ -9,12 +9,13 @@ var error_msg: String = "" var value: Variant = null static func ok(val: Variant=null) -> JamResult: - var r = JamResult.new() + var r: JamResult = JamResult.new() r.value = val return r + static func err(msg: String, val: Variant=null) -> JamResult: - var e = JamResult.new() + var e: JamResult = JamResult.new() e.errored = true e.error_msg = msg e.value = val diff --git a/util/KeyValCache.gd b/util/KeyValCache.gd index 3e6bfaf..f6352dd 100644 --- a/util/KeyValCache.gd +++ b/util/KeyValCache.gd @@ -1,11 +1,11 @@ -extends RefCounted class_name KeyValCache +extends RefCounted var old_default_cache_path: String = "user://jamlaunchcache.json" var cache_path: String = OS.get_data_dir().path_join("jam-launch/data.json") -func store(key: String, val: String): - var c := get_cache() +func store(key: String, val: String) -> bool: + var c: CacheResult = get_cache() if c.errored: if not c.no_file: printerr("error storing key in cache: %s" % c.error_msg) @@ -14,8 +14,9 @@ func store(key: String, val: String): c.cache[key] = val return write_cache(c.cache) -func clear(key: String): - var c := get_cache() + +func clear(key: String) -> bool: + var c: CacheResult = get_cache() if c.errored: if not c.no_file: printerr("error clearing key in cache: %s" % c.error_msg) @@ -25,66 +26,68 @@ func clear(key: String): else: return false -func get_val(key: String): - var c := get_cache() + +func get_val(key: String) -> Variant: + var c: CacheResult = get_cache() if c.errored: return null return c.cache.get(key) -class CacheResult: - var cache: Dictionary = {} - var errored: bool = false - var error_msg: String = "" - var no_file: bool = false - - static func err(msg: String) -> CacheResult: - var r := CacheResult.new() - r.errored = true - r.error_msg = msg - return r - - static func result(data: Dictionary) -> CacheResult: - var r := CacheResult.new() - r.cache = data - return r func get_cache(path_override: String = "") -> CacheResult: - var path := cache_path + var path: String = cache_path if not path_override.is_empty(): path = path_override if !FileAccess.file_exists(path): if FileAccess.file_exists(old_default_cache_path) and path_override.is_empty(): - var res := get_cache(old_default_cache_path) + var res: CacheResult = get_cache(old_default_cache_path) if not res.errored: write_cache(res.cache, cache_path) return res - var e = CacheResult.err("cache file '%s' does not exist" % path) + var e: CacheResult = CacheResult.err("cache file '%s' does not exist" % path) e.no_file = true return e - var cache_string := FileAccess.get_file_as_string(path) - var data = JSON.parse_string(cache_string) + var cache_string: String = FileAccess.get_file_as_string(path) + var data: Variant = JSON.parse_string(cache_string) if data == null: return CacheResult.err("failed to parse cache file '%s' as JSON" % path) return CacheResult.result(data as Dictionary) -func write_cache(cache: Dictionary, path_override: String = ""): + +func write_cache(cache: Dictionary, path_override: String = "") -> bool: if path_override.is_empty(): path_override = cache_path - - var s := JSON.stringify(cache) - - var base_dir := path_override.get_base_dir() + + var s: String = JSON.stringify(cache) + var base_dir: String = path_override.get_base_dir() if not FileAccess.file_exists(base_dir): - var err = DirAccess.make_dir_recursive_absolute(base_dir) - if err != OK: + var err: Error = DirAccess.make_dir_recursive_absolute(base_dir) + if not err == OK: printerr("failed to create directories for cache file '%s' - err %d" % [path_override, err]) return false - - var f := FileAccess.open(path_override, FileAccess.WRITE) + + var f: FileAccess = FileAccess.open(path_override, FileAccess.WRITE) if f == null: printerr("failed to open cache for writing at '%s'" % path_override) return false f.store_string(s) f.close() - return true + + +class CacheResult: + var cache: Dictionary = {} + var errored: bool = false + var error_msg: String = "" + var no_file: bool = false + + static func err(msg: String) -> CacheResult: + var r: CacheResult = CacheResult.new() + r.errored = true + r.error_msg = msg + return r + + static func result(data: Dictionary) -> CacheResult: + var r: CacheResult = CacheResult.new() + r.cache = data + return r \ No newline at end of file diff --git a/util/ScopeLocker.gd b/util/ScopeLocker.gd index 970a567..5094220 100644 --- a/util/ScopeLocker.gd +++ b/util/ScopeLocker.gd @@ -8,39 +8,31 @@ signal lock_changed(locked: bool) var _lock_ops: Array[Callable] = [] var _unlock_ops: Array[Callable] = [] +var _lock_count: int = 0 var _is_locked: bool: get: return _lock_count > 0 -var _lock_count: int = 0 -class ScopeLock: - extends RefCounted - var _locker: ScopeLocker - - func _init(locker: ScopeLocker): - _locker = locker - _locker._lock() - - func _notification(what): - if what == NOTIFICATION_PREDELETE: - _locker._unlock() func get_lock() -> ScopeLock: return ScopeLock.new(self) -func add_lock_ops(do_lock: Callable, do_unlock: Callable): + +func add_lock_ops(do_lock: Callable, do_unlock: Callable) -> void: _lock_ops.append(do_lock) _unlock_ops.append(do_unlock) -func _lock(): + +func _lock() -> void: _lock_count += 1 locked.emit() lock_changed.emit(true) for op in _lock_ops: op.call() -func _unlock(): + +func _unlock() -> void: _lock_count -= 1 if _lock_count < 0: _lock_count = 0 @@ -50,5 +42,19 @@ func _unlock(): for op in _unlock_ops: op.call() + func is_locked() -> bool: return _is_locked + + +class ScopeLock: + extends RefCounted + var _locker: ScopeLocker + + func _init(locker: ScopeLocker) -> void: + _locker = locker + _locker._lock() + + func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE: + _locker._unlock() \ No newline at end of file diff --git a/util/StreamingUpload.gd b/util/StreamingUpload.gd index af7d302..81a1df7 100644 --- a/util/StreamingUpload.gd +++ b/util/StreamingUpload.gd @@ -1,58 +1,22 @@ @tool -extends Object class_name StreamingUpload +extends Object -class StringReader: - extends RefCounted - var filename: String - var data_length: int - var data: String - var idx: int = 0 - - func get_data(max_bytes: int) -> PackedByteArray: - var toTake := min(max_bytes, len(data)) as int - var buf = data.substr(idx, toTake).to_utf8_buffer() - idx += toTake - return buf - -class FileReader: - extends RefCounted - var filename: String - var data_length: int - var data_reader: FileAccess - - static func from_path(path: String) -> JamResult: - var r = FileReader.new() - r.filename = path.get_file() - r.data_reader = FileAccess.open(path, FileAccess.READ) - if r.data_reader == null: - return JamResult.err("failed to open file '%s' - error code %d" % [path, FileAccess.get_open_error()]) - r.data_length = r.data_reader.get_length() - return JamResult.ok(r) - - func get_data(max_bytes: int) -> PackedByteArray: - return data_reader.get_buffer(max_bytes) - - func _notification(what): - if what == NOTIFICATION_PREDELETE: - if data_reader != null: - data_reader.close() - -static func streaming_upload(url: String, fields: Dictionary, reader) -> JamError: - var url_no_proto = url.substr(7) - var split_url = url_no_proto.split("/", false, 1) +static func streaming_upload(url: String, fields: Dictionary, reader: Variant) -> JamError: + var url_no_proto: String = url.substr(7) + var split_url: PackedStringArray = url_no_proto.split("/", false, 1) var host: String = split_url[0] - var path = "/" + var path: String = "/" if len(split_url) > 1: path += split_url[1] var host_ip := IP.resolve_hostname(host) # prepare request data - var bound = "----BodyBoundary%d" % (randi() % 100000) + var bound: String = "----BodyBoundary%d" % (randi() % 100000) var upload_body_start := PackedByteArray() upload_body_start.append_array("--{0}\r\n".format([bound]).to_utf8_buffer()) - for key in fields: + for key: Variant in fields: upload_body_start.append_array(("Content-Disposition: form-data; name=\"{0}\"\r\n\r\n".format([key])).to_utf8_buffer()) upload_body_start.append_array(("{0}".format([fields[key]])).to_utf8_buffer()) upload_body_start.append_array("\r\n--{0}\r\n".format([bound]).to_utf8_buffer()) @@ -71,9 +35,9 @@ static func streaming_upload(url: String, fields: Dictionary, reader) -> JamErro first_chunk.append_array(upload_body_start) # Set up StreamPeers - var tcp_peer := StreamPeerTCP.new() - var err := tcp_peer.connect_to_host(host_ip, 443) - if err != OK: + var tcp_peer: StreamPeerTCP = StreamPeerTCP.new() + var err: int = tcp_peer.connect_to_host(host_ip, 443) + if not err == OK: return JamError.err("Failed to connect to upload host for {0} upload".format([reader.filename])) while true: tcp_peer.poll() @@ -82,65 +46,102 @@ static func streaming_upload(url: String, fields: Dictionary, reader) -> JamErro elif tcp_peer.get_status() != StreamPeerTCP.STATUS_CONNECTING: return JamError.err("Bad TCP peer status %d for %s export" % [tcp_peer.get_status(), reader.filename]) OS.delay_msec(50) - var tls_peer := StreamPeerTLS.new() + var tls_peer: StreamPeerTLS = StreamPeerTLS.new() err = tls_peer.connect_to_stream(tcp_peer, host) - if err != OK: + if not err == OK: return JamError.err("Failed TLS to upload host for %s export" % [reader.filename]) while true: tls_peer.poll() if tls_peer.get_status() == StreamPeerTLS.STATUS_CONNECTED: break - elif tls_peer.get_status() != StreamPeerTLS.STATUS_HANDSHAKING: + elif tls_peer.get_status() == StreamPeerTLS.STATUS_HANDSHAKING: return JamError.err("Bad TLS peer status %d for %s export" % [tls_peer.get_status(), reader.filename]) OS.delay_msec(50) # Send data err = tls_peer.put_data(first_chunk) - if err != OK: + if not err == OK: return JamError.err("Failed to put first chunk of data for %s export upload" % [reader.filename]) - var to_write = reader.data_length + var to_write: int = reader.data_length while to_write > 0: tls_peer.poll() - if tls_peer.get_status() != StreamPeerTLS.STATUS_CONNECTED: + if not tls_peer.get_status() == StreamPeerTLS.STATUS_CONNECTED: return JamError.err("Bad TLS peer status %d for %s export (mid-upload)" % [tls_peer.get_status(), reader.filename]) - var maxBytes = min(to_write, 16384) + var maxBytes: int = min(to_write, 16384) var buf: PackedByteArray = reader.get_data(maxBytes) if buf.size() < 1: printerr("unexpected empty read %d (supposedly %d left...)" % [buf.size(), to_write]) return JamError.err("Bad export read with %d bytes left for %s export (mid-upload)" % [to_write, reader.filename]) to_write -= buf.size() err = tls_peer.put_data(buf) - if err != OK: + if not err == OK: return JamError.err("Failed to write archive data for %s export upload - code: %d" % [reader.filename, err]) err = tls_peer.put_data(last_chunk) - if err != OK: + if not err == OK: return JamError.err("Failed to put last chunk of data for %s export upload" % [reader.filename]) # Get and parse response - var http_resp_re := RegEx.new() + var http_resp_re: RegEx = RegEx.new() http_resp_re.compile("HTTP/1.1 ([0-9]+) (.*)") - var full_resp := PackedByteArray() - for x in range(200 * 60 * 3): + var full_resp: PackedByteArray = PackedByteArray() + for _x in range(200 * 60 * 3): OS.delay_msec(50) tls_peer.poll() - if tls_peer.get_status() != StreamPeerTLS.STATUS_CONNECTED: + if not tls_peer.get_status() == StreamPeerTLS.STATUS_CONNECTED: return JamError.err("Failed to get response for %s export upload before connection closed" % [reader.filename]) if tls_peer.get_available_bytes() > 0: - var resp = tls_peer.get_data(tls_peer.get_available_bytes()) - if resp[0] != 0: + var resp: Array = tls_peer.get_data(tls_peer.get_available_bytes()) + if not resp[0] == 0: return JamError.err("Failure receiving HTTP response bytes from %s export upload" % [reader.filename]) full_resp.append_array(resp[1] as PackedByteArray) - var resp_string := full_resp.get_string_from_utf8() - var m := http_resp_re.search(resp_string) - if m != null: - var code = int(m.get_string(1)) - var reason = m.get_string(2) + var resp_string: String = full_resp.get_string_from_utf8() + var m: RegExMatch = http_resp_re.search(resp_string) + if not m == null: + var code: int = int(m.get_string(1)) + var reason: String = m.get_string(2) if code < 200 or code > 299: return JamError.err("Received HTTP error during %s upload - %d: %s" % [reader.filename, code, reason]) else: return JamError.ok() return JamError.err("HTTP response for %s upload timed out" % [reader.filename]) + + +class StringReader: + extends RefCounted + var filename: String + var data_length: int + var data: String + var idx: int = 0 + + func get_data(max_bytes: int) -> PackedByteArray: + var toTake: int = min(max_bytes, len(data)) as int + var buf: PackedByteArray = data.substr(idx, toTake).to_utf8_buffer() + idx += toTake + return buf + +class FileReader: + extends RefCounted + var filename: String + var data_length: int + var data_reader: FileAccess + + static func from_path(path: String) -> JamResult: + var r: FileReader = FileReader.new() + r.filename = path.get_file() + r.data_reader = FileAccess.open(path, FileAccess.READ) + if r.data_reader == null: + return JamResult.err("failed to open file '%s' - error code %d" % [path, FileAccess.get_open_error()]) + r.data_length = r.data_reader.get_length() + return JamResult.ok(r) + + func get_data(max_bytes: int) -> PackedByteArray: + return data_reader.get_buffer(max_bytes) + + func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE: + if not data_reader == null: + data_reader.close() \ No newline at end of file