From ee141c46c6b71ba1d9c0a5c01ea76b4b3f970efc Mon Sep 17 00:00:00 2001 From: Harmony Honey Date: Sun, 26 Nov 2023 21:58:12 -0500 Subject: [PATCH] add silent wolf (: --- addons/silent_wolf/.DS_Store | Bin 0 -> 10244 bytes addons/silent_wolf/Auth/Auth.gd | 652 ++++++++++++++++++ addons/silent_wolf/Auth/ConfirmEmail.gd | 60 ++ addons/silent_wolf/Auth/ConfirmEmail.tscn | 143 ++++ addons/silent_wolf/Auth/Login.gd | 41 ++ addons/silent_wolf/Auth/Login.tscn | 208 ++++++ addons/silent_wolf/Auth/Register.gd | 81 +++ addons/silent_wolf/Auth/Register.tscn | 285 ++++++++ .../Auth/RegisterUsernamePassword.tscn | 266 +++++++ addons/silent_wolf/Auth/ResetPassword.gd | 63 ++ addons/silent_wolf/Auth/ResetPassword.tscn | 395 +++++++++++ addons/silent_wolf/Multiplayer/.DS_Store | Bin 0 -> 6148 bytes addons/silent_wolf/Multiplayer/Multiplayer.gd | 39 ++ addons/silent_wolf/Multiplayer/ws/WSClient.gd | 81 +++ addons/silent_wolf/Players/Players.gd | 199 ++++++ addons/silent_wolf/Scores/.DS_Store | Bin 0 -> 6148 bytes addons/silent_wolf/Scores/Leaderboard.gd | 126 ++++ addons/silent_wolf/Scores/Leaderboard.tscn | 88 +++ addons/silent_wolf/Scores/ScoreItem.tscn | 48 ++ addons/silent_wolf/Scores/Scores.gd | 528 ++++++++++++++ addons/silent_wolf/Scores/assets/.DS_Store | Bin 0 -> 6148 bytes .../Scores/assets/fonts/Comfortaa-Bold.ttf | Bin 0 -> 137696 bytes addons/silent_wolf/SilentWolf.gd | 178 +++++ addons/silent_wolf/assets/.DS_Store | Bin 0 -> 6148 bytes .../assets/fonts/Comfortaa-Bold.ttf | Bin 0 -> 137696 bytes addons/silent_wolf/assets/gfx/.DS_Store | Bin 0 -> 6148 bytes .../assets/gfx/checkbox_checked.png | Bin 0 -> 813 bytes .../assets/gfx/checkbox_unchecked.png | Bin 0 -> 215 bytes .../assets/gfx/dummy_info_icon_small.png | Bin 0 -> 158 bytes addons/silent_wolf/assets/gfx/info_icon.png | Bin 0 -> 23374 bytes .../assets/gfx/info_icon_small.png | Bin 0 -> 15579 bytes addons/silent_wolf/common/CommonErrors.gd | 17 + addons/silent_wolf/common/SWButton.tscn | 49 ++ addons/silent_wolf/examples/.DS_Store | Bin 0 -> 6148 bytes .../CustomLeaderboards/ReverseLeaderboard.gd | 87 +++ .../ReverseLeaderboard.tscn | 119 ++++ .../CustomLeaderboards/SmallScoreItem.tscn | 14 + .../CustomLeaderboards/TimeBasedLboards.gd | 81 +++ .../CustomLeaderboards/TimeBasedLboards.tscn | 228 ++++++ addons/silent_wolf/plugin.cfg | 7 + addons/silent_wolf/silent_wolf.gd | 8 + addons/silent_wolf/utils/SWHashing.gd | 9 + .../silent_wolf/utils/SWLocalFileStorage.gd | 45 ++ addons/silent_wolf/utils/SWLogger.gd | 21 + addons/silent_wolf/utils/UUID.gd | 47 ++ 45 files changed, 4213 insertions(+) create mode 100644 addons/silent_wolf/.DS_Store create mode 100644 addons/silent_wolf/Auth/Auth.gd create mode 100644 addons/silent_wolf/Auth/ConfirmEmail.gd create mode 100644 addons/silent_wolf/Auth/ConfirmEmail.tscn create mode 100644 addons/silent_wolf/Auth/Login.gd create mode 100644 addons/silent_wolf/Auth/Login.tscn create mode 100644 addons/silent_wolf/Auth/Register.gd create mode 100644 addons/silent_wolf/Auth/Register.tscn create mode 100644 addons/silent_wolf/Auth/RegisterUsernamePassword.tscn create mode 100644 addons/silent_wolf/Auth/ResetPassword.gd create mode 100644 addons/silent_wolf/Auth/ResetPassword.tscn create mode 100644 addons/silent_wolf/Multiplayer/.DS_Store create mode 100644 addons/silent_wolf/Multiplayer/Multiplayer.gd create mode 100644 addons/silent_wolf/Multiplayer/ws/WSClient.gd create mode 100644 addons/silent_wolf/Players/Players.gd create mode 100644 addons/silent_wolf/Scores/.DS_Store create mode 100644 addons/silent_wolf/Scores/Leaderboard.gd create mode 100644 addons/silent_wolf/Scores/Leaderboard.tscn create mode 100644 addons/silent_wolf/Scores/ScoreItem.tscn create mode 100644 addons/silent_wolf/Scores/Scores.gd create mode 100644 addons/silent_wolf/Scores/assets/.DS_Store create mode 100644 addons/silent_wolf/Scores/assets/fonts/Comfortaa-Bold.ttf create mode 100644 addons/silent_wolf/SilentWolf.gd create mode 100644 addons/silent_wolf/assets/.DS_Store create mode 100644 addons/silent_wolf/assets/fonts/Comfortaa-Bold.ttf create mode 100644 addons/silent_wolf/assets/gfx/.DS_Store create mode 100644 addons/silent_wolf/assets/gfx/checkbox_checked.png create mode 100644 addons/silent_wolf/assets/gfx/checkbox_unchecked.png create mode 100644 addons/silent_wolf/assets/gfx/dummy_info_icon_small.png create mode 100644 addons/silent_wolf/assets/gfx/info_icon.png create mode 100644 addons/silent_wolf/assets/gfx/info_icon_small.png create mode 100644 addons/silent_wolf/common/CommonErrors.gd create mode 100644 addons/silent_wolf/common/SWButton.tscn create mode 100644 addons/silent_wolf/examples/.DS_Store create mode 100644 addons/silent_wolf/examples/CustomLeaderboards/ReverseLeaderboard.gd create mode 100644 addons/silent_wolf/examples/CustomLeaderboards/ReverseLeaderboard.tscn create mode 100644 addons/silent_wolf/examples/CustomLeaderboards/SmallScoreItem.tscn create mode 100644 addons/silent_wolf/examples/CustomLeaderboards/TimeBasedLboards.gd create mode 100644 addons/silent_wolf/examples/CustomLeaderboards/TimeBasedLboards.tscn create mode 100644 addons/silent_wolf/plugin.cfg create mode 100644 addons/silent_wolf/silent_wolf.gd create mode 100644 addons/silent_wolf/utils/SWHashing.gd create mode 100644 addons/silent_wolf/utils/SWLocalFileStorage.gd create mode 100644 addons/silent_wolf/utils/SWLogger.gd create mode 100644 addons/silent_wolf/utils/UUID.gd diff --git a/addons/silent_wolf/.DS_Store b/addons/silent_wolf/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..2d80e47465ca6fe6e8d0954e450996b1baf5e41c GIT binary patch literal 10244 zcmeHM&1(}u6n~SX$=2H1gI^b6(St>THdTsJlr%;}K@cPMppxdxB)HiP`DnDIuoo{1 z{tcc451s_^;7J89ir~SE9=v$=&_6)md^DYz&06pvc9{vYZ!`0IGdu4$Z)V>t0FYe2 zngOr@z`{)B^lrS2NVJ~jK#GZyvnUDb1N5MK$?+;>$14ZWrodRhSio4oSio4oSm1xK z0Pfi|Src5EtFeHwfU&^P0(5<_F;kfp(&v&b9e80O0LlawvxN5;2QVF5$gGe)mrNDg zawG^ZIVJ%XAC8!Wy^6 zi_-_!)=ti)?M!BVJ#DXL=1!kV+w++->+9pzz9T0Vt`+Y(E!TU!7$-yBmM8NOtXH@oJ~q8& zZ;gi}RKSB4{ycD1F317Yleti^5#d7_lfnt4e&G0(dW{$RMN9aKWJ8Uckr;&^u(2Dp zAB`gmSF(G!rnR6A4&KXWlISz!0qV;<1@IB!DljgUuXBgd4grkHnSXJHb2b7e`&2ki z% z4M(cfLo)s0E#2Be+SMF)8?-tNY-~2HU2QU?;3`f9F$Dsyyn*e=uB@RWLcTUCmC-}{ z(FPhqWAf|9<2RJa07`aY2tkctPHRVFY!`FdKGe~DT{w;#<{hZ3&KoOfyd|N)Hr7sX zxsHAM6E-3YE92jEY8*H2bCO{c;$y3y^?I51=O3qj`i|Idr8Z=@4Z|pu$DRb9LnC|& zV}~Z&X#iX5KfjECmo1Xt) z2><@SGX`Ux(pbP);IAw|68STQn literal 0 HcmV?d00001 diff --git a/addons/silent_wolf/Auth/Auth.gd b/addons/silent_wolf/Auth/Auth.gd new file mode 100644 index 0000000..9dc7857 --- /dev/null +++ b/addons/silent_wolf/Auth/Auth.gd @@ -0,0 +1,652 @@ +extends Node + +const CommonErrors = preload("res://addons/silent_wolf/common/CommonErrors.gd") +const SWLocalFileStorage = preload("res://addons/silent_wolf/utils/SWLocalFileStorage.gd") +const SWLogger = preload("res://addons/silent_wolf/utils/SWLogger.gd") +const UUID = preload("res://addons/silent_wolf/utils/UUID.gd") + +signal sw_login_succeeded +signal sw_login_failed +signal sw_logout_succeeded +signal sw_registration_succeeded +signal sw_registration_user_pwd_succeeded +signal sw_registration_failed +signal sw_email_verif_succeeded +signal sw_email_verif_failed +signal sw_resend_conf_code_succeeded +signal sw_resend_conf_code_failed +signal sw_session_check_complete +signal sw_request_password_reset_succeeded +signal sw_request_password_reset_failed +signal sw_reset_password_succeeded +signal sw_reset_password_failed +signal sw_get_player_details_succeeded +signal sw_get_player_details_failed + +var tmp_username = null +var logged_in_player = null +var logged_in_player_email = null +var logged_in_anon = false +var token = null +var id_token = null + +var RegisterPlayer = null +var VerifyEmail = null +var ResendConfCode = null +var LoginPlayer = null +var ValidateSession = null +var RequestPasswordReset = null +var ResetPassword = null +var GetPlayerDetails = null + +# wekrefs +var wrRegisterPlayer = null +var wrVerifyEmail = null +var wrResendConfCode = null +var wrLoginPlayer = null +var wrValidateSession = null +var wrRequestPasswordReset = null +var wrResetPassword = null +var wrGetPlayerDetails = null + +var login_timeout = 0 +var login_timer = null + +var complete_session_check_wait_timer + +func _ready(): + pass + + +func set_player_logged_in(player_name): + logged_in_player = player_name + SWLogger.info("SilentWolf - player logged in as " + str(player_name)) + if SilentWolf.auth_config.has("session_duration_seconds") and typeof(SilentWolf.auth_config.session_duration_seconds) == 2: + login_timeout = SilentWolf.auth_config.session_duration_seconds + else: + login_timeout = 0 + SWLogger.info("SilentWolf login timeout: " + str(login_timeout)) + if login_timeout != 0: + setup_login_timer() + + +func get_anon_user_id() -> String: + var anon_user_id = OS.get_unique_id() + if anon_user_id == '': + anon_user_id = UUID.generate_uuid_v4() + print("anon_user_id: " + str(anon_user_id)) + return anon_user_id + + +func logout_player(): + logged_in_player = null + # remove any player data if present + SilentWolf.Players.clear_player_data() + # remove stored session if any + remove_stored_session() + emit_signal("sw_logout_succeeded") + + +func register_player_anon(player_name=null) -> Node: + var user_local_id: String = get_anon_user_id() + RegisterPlayer = HTTPRequest.new() + wrRegisterPlayer = weakref(RegisterPlayer) + if OS.get_name() != "HTML5": + RegisterPlayer.set_use_threads(true) + get_tree().get_root().add_child(RegisterPlayer) + RegisterPlayer.connect("request_completed", self, "_on_RegisterPlayer_request_completed") + SWLogger.info("Calling SilentWolf to register an anonymous player") + var game_id = SilentWolf.config.game_id + var game_version = SilentWolf.config.game_version + var api_key = SilentWolf.config.api_key + var payload = { "game_id": game_id, "anon": true, "player_name": player_name, "user_local_id": user_local_id } + var query = JSON.print(payload) + var headers = [ + "Content-Type: application/json", + "x-api-key: " + api_key, + "x-sw-plugin-version: " + SilentWolf.version, + "x-sw-game-id: " + SilentWolf.config.game_id, + "x-sw-godot-version: " + SilentWolf.godot_version + ] + #print("register_player headers: " + str(headers)) + RegisterPlayer.request("https://api.silentwolf.com/create_new_player", headers, true, HTTPClient.METHOD_POST, query) + return self + + +func register_player(player_name, email, password, confirm_password): + tmp_username = player_name + RegisterPlayer = HTTPRequest.new() + wrRegisterPlayer = weakref(RegisterPlayer) + if OS.get_name() != "HTML5": + RegisterPlayer.set_use_threads(true) + get_tree().get_root().add_child(RegisterPlayer) + RegisterPlayer.connect("request_completed", self, "_on_RegisterPlayer_request_completed") + SWLogger.info("Calling SilentWolf to register a player") + var game_id = SilentWolf.config.game_id + var game_version = SilentWolf.config.game_version + var api_key = SilentWolf.config.api_key + var payload = { "game_id": game_id, "anon": false, "player_name": player_name, "email": email, "password": password, "confirm_password": confirm_password } + var query = JSON.print(payload) + var headers = [ + "Content-Type: application/json", + "x-api-key: " + api_key, + "x-sw-plugin-version: " + SilentWolf.version, + "x-sw-game-id: " + SilentWolf.config.game_id, + "x-sw-godot-version: " + SilentWolf.godot_version + ] + #print("register_player headers: " + str(headers)) + RegisterPlayer.request("https://api.silentwolf.com/create_new_player", headers, true, HTTPClient.METHOD_POST, query) + return self + + +func register_player_user_password(player_name, password, confirm_password): + tmp_username = player_name + RegisterPlayer = HTTPRequest.new() + wrRegisterPlayer = weakref(RegisterPlayer) + if OS.get_name() != "HTML5": + RegisterPlayer.set_use_threads(true) + get_tree().get_root().add_child(RegisterPlayer) + RegisterPlayer.connect("request_completed", self, "_on_RegisterPlayerUserPassword_request_completed") + SWLogger.info("Calling SilentWolf to register a player") + var game_id = SilentWolf.config.game_id + var game_version = SilentWolf.config.game_version + var api_key = SilentWolf.config.api_key + var payload = { "game_id": game_id, "player_name": player_name, "password": password, "confirm_password": confirm_password } + var query = JSON.print(payload) + var headers = [ + "Content-Type: application/json", + "x-api-key: " + api_key, + "x-sw-plugin-version: " + SilentWolf.version, + "x-sw-game-id: " + SilentWolf.config.game_id, + "x-sw-godot-version: " + SilentWolf.godot_version + ] + #print("register_player headers: " + str(headers)) + RegisterPlayer.request("https://api.silentwolf.com/create_new_player", headers, true, HTTPClient.METHOD_POST, query) + return self + + +func verify_email(player_name, code): + tmp_username = player_name + VerifyEmail = HTTPRequest.new() + wrVerifyEmail = weakref(VerifyEmail) + if OS.get_name() != "HTML5": + VerifyEmail.set_use_threads(true) + get_tree().get_root().add_child(VerifyEmail) + VerifyEmail.connect("request_completed", self, "_on_VerifyEmail_request_completed") + SWLogger.info("Calling SilentWolf to verify email address for: " + str(player_name)) + var game_id = SilentWolf.config.game_id + var game_version = SilentWolf.config.game_version + var api_key = SilentWolf.config.api_key + var payload = { "game_id": game_id, "username": player_name, "code": code } + var query = JSON.print(payload) + var headers = [ + "Content-Type: application/json", + "x-api-key: " + api_key, + "x-sw-plugin-version: " + SilentWolf.version, + "x-sw-game-id: " + SilentWolf.config.game_id, + "x-sw-godot-version: " + SilentWolf.godot_version + ] + #print("register_player headers: " + str(headers)) + VerifyEmail.request("https://api.silentwolf.com/confirm_verif_code", headers, true, HTTPClient.METHOD_POST, query) + return self + + +func resend_conf_code(player_name): + ResendConfCode = HTTPRequest.new() + wrResendConfCode = weakref(ResendConfCode) + if OS.get_name() != "HTML5": + ResendConfCode.set_use_threads(true) + get_tree().get_root().add_child(ResendConfCode) + ResendConfCode.connect("request_completed", self, "_on_ResendConfCode_request_completed") + SWLogger.info("Calling SilentWolf to resend confirmation code for: " + str(player_name)) + var game_id = SilentWolf.config.game_id + var game_version = SilentWolf.config.game_version + var api_key = SilentWolf.config.api_key + var payload = { "game_id": game_id, "username": player_name } + var query = JSON.print(payload) + var headers = [ + "Content-Type: application/json", + "x-api-key: " + api_key, + "x-sw-plugin-version: " + SilentWolf.version, + "x-sw-game-id: " + SilentWolf.config.game_id, + "x-sw-godot-version: " + SilentWolf.godot_version + ] + #print("register_player headers: " + str(headers)) + ResendConfCode.request("https://api.silentwolf.com/resend_conf_code", headers, true, HTTPClient.METHOD_POST, query) + return self + + +func login_player(username, password, remember_me=false): + tmp_username = username + LoginPlayer = HTTPRequest.new() + wrLoginPlayer = weakref(LoginPlayer) + print("OS name: " + str(OS.get_name())) + if OS.get_name() != "HTML5": + LoginPlayer.set_use_threads(true) + print("get_tree().get_root(): " + str(get_tree().get_root())) + get_tree().get_root().add_child(LoginPlayer) + LoginPlayer.connect("request_completed", self, "_on_LoginPlayer_request_completed") + SWLogger.info("Calling SilentWolf to log in a player") + var game_id = SilentWolf.config.game_id + var api_key = SilentWolf.config.api_key + var payload = { "game_id": game_id, "username": username, "password": password, "remember_me": str(remember_me) } + if SilentWolf.auth_config.has("saved_session_expiration_days") and typeof(SilentWolf.auth_config.saved_session_expiration_days) == 2: + payload["remember_me_expires_in"] = str(SilentWolf.auth_config.saved_session_expiration_days) + SWLogger.debug("SilentWolf login player payload: " + str(payload)) + var query = JSON.print(payload) + var headers = [ + "Content-Type: application/json", + "x-api-key: " + api_key, + "x-sw-plugin-version: " + SilentWolf.version, + "x-sw-game-id: " + SilentWolf.config.game_id, + "x-sw-godot-version: " + SilentWolf.godot_version + ]#print("login_player headers: " + str(headers)) + LoginPlayer.request("https://api.silentwolf.com/login_player", headers, true, HTTPClient.METHOD_POST, query) + return self + + +func request_player_password_reset(player_name): + RequestPasswordReset = HTTPRequest.new() + wrRequestPasswordReset = weakref(RequestPasswordReset) + print("OS name: " + str(OS.get_name())) + if OS.get_name() != "HTML5": + RequestPasswordReset.set_use_threads(true) + get_tree().get_root().add_child(RequestPasswordReset) + RequestPasswordReset.connect("request_completed", self, "_on_RequestPasswordReset_request_completed") + SWLogger.info("Calling SilentWolf to request a password reset for: " + str(player_name)) + var game_id = SilentWolf.config.game_id + var api_key = SilentWolf.config.api_key + var payload = { "game_id": game_id, "player_name": player_name } + SWLogger.debug("SilentWolf request player password reset payload: " + str(payload)) + var query = JSON.print(payload) + var headers = [ + "Content-Type: application/json", + "x-api-key: " + api_key, + "x-sw-plugin-version: " + SilentWolf.version, + "x-sw-game-id: " + SilentWolf.config.game_id, + "x-sw-godot-version: " + SilentWolf.godot_version + ] + RequestPasswordReset.request("https://api.silentwolf.com/request_player_password_reset", headers, true, HTTPClient.METHOD_POST, query) + return self + + +func reset_player_password(player_name, conf_code, new_password, confirm_password): + ResetPassword = HTTPRequest.new() + wrResetPassword = weakref(ResetPassword) + if OS.get_name() != "HTML5": + ResetPassword.set_use_threads(true) + get_tree().get_root().add_child(ResetPassword) + ResetPassword.connect("request_completed", self, "_on_ResetPassword_request_completed") + SWLogger.info("Calling SilentWolf to reset password for: " + str(player_name)) + var game_id = SilentWolf.config.game_id + var api_key = SilentWolf.config.api_key + var payload = { "game_id": game_id, "player_name": player_name, "conf_code": conf_code, "password": new_password, "confirm_password": confirm_password } + SWLogger.debug("SilentWolf request player password reset payload: " + str(payload)) + var query = JSON.print(payload) + var headers = [ + "Content-Type: application/json", + "x-api-key: " + api_key, + "x-sw-plugin-version: " + SilentWolf.version, + "x-sw-game-id: " + SilentWolf.config.game_id, + "x-sw-godot-version: " + SilentWolf.godot_version + ] + ResetPassword.request("https://api.silentwolf.com/reset_player_password", headers, true, HTTPClient.METHOD_POST, query) + return self + + +func get_player_details(player_name): + GetPlayerDetails = HTTPRequest.new() + wrRegisterPlayer = weakref(GetPlayerDetails) + if OS.get_name() != "HTML5": + GetPlayerDetails.set_use_threads(true) + get_tree().get_root().add_child(GetPlayerDetails) + GetPlayerDetails.connect("request_completed", self, "_on_GetPlayerDetails_request_completed") + SWLogger.info("Calling SilentWolf to get player details") + var game_id = SilentWolf.config.game_id + var game_version = SilentWolf.config.game_version + var api_key = SilentWolf.config.api_key + var payload = { "game_id": game_id, "player_name": player_name } + var query = JSON.print(payload) + var headers = [ + "Content-Type: application/json", + "x-api-key: " + api_key, + "x-sw-plugin-version: " + SilentWolf.version, + "x-sw-game-id: " + SilentWolf.config.game_id, + "x-sw-godot-version: " + SilentWolf.godot_version + ] + #print("register_player headers: " + str(headers)) + GetPlayerDetails.request("https://api.silentwolf.com/get_player_details", headers, true, HTTPClient.METHOD_POST, query) + return self + + +func _on_LoginPlayer_request_completed( result, response_code, headers, body ): + SWLogger.info("LoginPlayer request completed") + var status_check = CommonErrors.check_status_code(response_code) + #LoginPlayer.queue_free() + SilentWolf.free_request(wrLoginPlayer, LoginPlayer) + SWLogger.debug("response headers: " + str(response_code)) + SWLogger.debug("response headers: " + str(headers)) + #SWLogger.debug("response body: " + str(body.get_string_from_utf8())) + + if status_check: + var json = JSON.parse(body.get_string_from_utf8()) + var response = json.result + if "message" in response.keys() and response.message == "Forbidden": + SWLogger.error("You are not authorized to call the SilentWolf API - check your API key configuration: https://silentwolf.com/leaderboard") + else: + if "lookup" in response.keys(): + print("remember me lookup: " + str(response.lookup)) + save_session(response.lookup, response.validator) + if "validator" in response.keys(): + print("remember me validator: " + str(response.validator)) + SWLogger.info("SilentWolf login player success? : " + str(response.success)) + # TODO: get JWT token and store it + # send a different signal depending on login success or failure + if response.success: + token = response.swtoken + #id_token = response.swidtoken + SWLogger.debug("token: " + token) + set_player_logged_in(tmp_username) + emit_signal("sw_login_succeeded") + else: + emit_signal("sw_login_failed", response.error) + + +func _on_RegisterPlayer_request_completed( result, response_code, headers, body ): + SWLogger.info("RegisterPlayer request completed") + var status_check = CommonErrors.check_status_code(response_code) + #RegisterPlayer.queue_free() + SilentWolf.free_request(wrRegisterPlayer, RegisterPlayer) + SWLogger.debug("response headers: " + str(response_code)) + SWLogger.debug("response headers: " + str(headers)) + SWLogger.debug("response body: " + str(body.get_string_from_utf8())) + + if status_check: + var json = JSON.parse(body.get_string_from_utf8()) + var response = json.result + SWLogger.debug("reponse: " + str(response)) + if "message" in response.keys() and response.message == "Forbidden": + SWLogger.error("You are not authorized to call the SilentWolf API - check your API key configuration: https://silentwolf.com/leaderboard") + else: + SWLogger.info("SilentWolf create new player success? : " + str(response.success)) + # also get a JWT token here + # send a different signal depending on registration success or failure + if response.success: + var anon = response.anon + if anon: + SWLogger.info("Anonymous Player registration succeeded") + logged_in_anon = true + if 'player_name' in response: + logged_in_player = response.player_name + elif 'player_local_id' in response: + logged_in_player = str("anon##" + response.player_local_id) + else: + logged_in_player = "anon##unknown" + print("Anon registration, logged in player: " + str(logged_in_player)) + else: + # if email confirmation is enabled for the game, we can't log in the player just yet + var email_conf_enabled = response.email_conf_enabled + if email_conf_enabled: + SWLogger.info("Player registration succeeded, but player still needs to verify email address") + else: + SWLogger.info("Player registration succeeded, email verification is disabled") + logged_in_player = tmp_username + emit_signal("sw_registration_succeeded") + else: + emit_signal("sw_registration_failed", response.error) + + +func _on_RegisterPlayerUserPassword_request_completed( result, response_code, headers, body ): + SWLogger.info("RegisterPlayerUserPassword request completed") + var status_check = CommonErrors.check_status_code(response_code) + #RegisterPlayer.queue_free() + SilentWolf.free_request(wrRegisterPlayer, RegisterPlayer) + SWLogger.debug("response headers: " + str(response_code)) + SWLogger.debug("response headers: " + str(headers)) + SWLogger.debug("response body: " + str(body.get_string_from_utf8())) + + if status_check: + var json = JSON.parse(body.get_string_from_utf8()) + var response = json.result + SWLogger.debug("reponse: " + str(response)) + if "message" in response.keys() and response.message == "Forbidden": + SWLogger.error("You are not authorized to call the SilentWolf API - check your API key configuration: https://silentwolf.com/leaderboard") + else: + SWLogger.info("SilentWolf create new player success? : " + str(response.success)) + # also get a JWT token here + # send a different signal depending on registration success or failure + if response.success: + # if email confirmation is enabled for the game, we can't log in the player just yet + var email_conf_enabled = response.email_conf_enabled + SWLogger.info("Player registration with username/password succeeded, player account autoconfirmed") + logged_in_player = tmp_username + emit_signal("sw_registration_user_pwd_succeeded") + else: + emit_signal("sw_registration_failed", response.error) + + +func _on_VerifyEmail_request_completed( result, response_code, headers, body ): + SWLogger.info("VerifyEmail request completed") + var status_check = CommonErrors.check_status_code(response_code) + SilentWolf.free_request(wrVerifyEmail, VerifyEmail) + SWLogger.debug("response headers: " + str(response_code)) + SWLogger.debug("response headers: " + str(headers)) + SWLogger.debug("response body: " + str(body.get_string_from_utf8())) + + if status_check: + var json = JSON.parse(body.get_string_from_utf8()) + var response = json.result + SWLogger.debug("reponse: " + str(response)) + if "message" in response.keys() and response.message == "Forbidden": + SWLogger.error("You are not authorized to call the SilentWolf API - check your API key configuration: https://silentwolf.com/playerauth") + else: + SWLogger.info("SilentWolf verify email success? : " + str(response.success)) + # also get a JWT token here + # send a different signal depending on registration success or failure + if response.success: + logged_in_player = tmp_username + emit_signal("sw_email_verif_succeeded") + else: + emit_signal("sw_email_verif_failed", response.error) + + +func _on_ResendConfCode_request_completed( result, response_code, headers, body ): + SWLogger.info("ResendConfCode request completed") + var status_check = CommonErrors.check_status_code(response_code) + SilentWolf.free_request(wrResendConfCode, ResendConfCode) + SWLogger.debug("response headers: " + str(response_code)) + SWLogger.debug("response headers: " + str(headers)) + SWLogger.debug("response body: " + str(body.get_string_from_utf8())) + + if status_check: + var json = JSON.parse(body.get_string_from_utf8()) + var response = json.result + SWLogger.debug("reponse: " + str(response)) + if "message" in response.keys() and response.message == "Forbidden": + SWLogger.error("You are not authorized to call the SilentWolf API - check your API key configuration: https://silentwolf.com/playerauth") + else: + SWLogger.info("SilentWolf resend conf code success? : " + str(response.success)) + # also get a JWT token here + # send a different signal depending on registration success or failure + if response.success: + emit_signal("sw_resend_conf_code_succeeded") + else: + emit_signal("sw_resend_conf_code_failed", response.error) + + +func _on_RequestPasswordReset_request_completed( result, response_code, headers, body ): + SWLogger.info("RequestPasswordReset request completed") + var status_check = CommonErrors.check_status_code(response_code) + SilentWolf.free_request(wrRequestPasswordReset, RequestPasswordReset) + SWLogger.debug("response headers: " + str(response_code)) + SWLogger.debug("response headers: " + str(headers)) + SWLogger.debug("response body: " + str(body.get_string_from_utf8())) + + if status_check: + var json = JSON.parse(body.get_string_from_utf8()) + var response = json.result + SWLogger.debug("reponse: " + str(response)) + if "message" in response.keys() and response.message == "Forbidden": + SWLogger.error("You are not authorized to call the SilentWolf API - check your API key configuration: https://silentwolf.com/playerauth") + else: + SWLogger.info("SilentWolf request player password reset success? : " + str(response.success)) + if response.success: + emit_signal("sw_request_password_reset_succeeded") + else: + emit_signal("sw_request_password_reset_failed", response.error) + + +func _on_ResetPassword_request_completed( result, response_code, headers, body ): + SWLogger.info("ResetPassword request completed") + var status_check = CommonErrors.check_status_code(response_code) + SilentWolf.free_request(wrResetPassword, ResetPassword) + SWLogger.debug("response headers: " + str(response_code)) + SWLogger.debug("response headers: " + str(headers)) + SWLogger.debug("response body: " + str(body.get_string_from_utf8())) + + if status_check: + var json = JSON.parse(body.get_string_from_utf8()) + var response = json.result + SWLogger.debug("reponse: " + str(response)) + if "message" in response.keys() and response.message == "Forbidden": + SWLogger.error("You are not authorized to call the SilentWolf API - check your API key configuration: https://silentwolf.com/playerauth") + else: + SWLogger.info("SilentWolf reset player password success? : " + str(response.success)) + if response.success: + emit_signal("sw_reset_password_succeeded") + else: + emit_signal("sw_reset_password_failed", response.error) + + +func setup_login_timer(): + login_timer = Timer.new() + login_timer.set_one_shot(true) + login_timer.set_wait_time(login_timeout) + login_timer.connect("timeout", self, "on_login_timeout_complete") + add_child(login_timer) + + +func on_login_timeout_complete(): + logout_player() + + +# store lookup (not logged in player name) and validator in local file +func save_session(lookup, validator): + var path = "user://swsession.save" + var session_data = { + "lookup": lookup, + "validator": validator + } + SWLocalFileStorage.save_data("user://swsession.save", session_data, "Saving SilentWolf session: ") + + +func remove_stored_session(): + var path = "user://swsession.save" + SWLocalFileStorage.remove_data(path, "Removing SilentWolf session if any: " ) + + +# reload lookup and validator and send them back to the server to auto-login user +func load_session() -> Dictionary: + var sw_session_data = null + var path = "user://swsession.save" + sw_session_data = SWLocalFileStorage.get_data(path) + if sw_session_data == null: + SWLogger.debug("No local SilentWolf session stored, or session data stored in incorrect format") + SWLogger.info("Found session data: " + str(sw_session_data)) + return sw_session_data + + +func auto_login_player(): + var sw_session_data = load_session() + if sw_session_data: + SWLogger.debug("Found saved SilentWolf session data, attempting autologin...") + var lookup = sw_session_data.lookup + var validator = sw_session_data.validator + # whether successful or not, in the end the "sw_session_check_complete" signal will be emitted + validate_player_session(lookup, validator) + else: + SWLogger.debug("No saved SilentWolf session data, so no autologin will be performed") + # the following is needed to delay the emission of the signal just a little bit, otherwise the signal is never received! + setup_complete_session_check_wait_timer() + complete_session_check_wait_timer.start() + return self + + +# Signal can't be emitted directly from auto_login_player() function +# otherwise it won't connect back to calling script +func complete_session_check(return_value=null): + SWLogger.debug("emitting signal....") + emit_signal("sw_session_check_complete", return_value) + + +func validate_player_session(lookup, validator, scene=get_tree().get_current_scene()): + ValidateSession = HTTPRequest.new() + wrValidateSession = weakref(ValidateSession) + if OS.get_name() != "HTML5": + ValidateSession.set_use_threads(true) + scene.add_child(ValidateSession) + ValidateSession.connect("request_completed", self, "_on_ValidateSession_request_completed") + SWLogger.info("Calling SilentWolf to validate an existing player session") + var game_id = SilentWolf.config.game_id + var api_key = SilentWolf.config.api_key + var payload = { "game_id": game_id, "lookup": lookup, "validator": validator } + SWLogger.debug("Validate session payload: " + str(payload)) + var query = JSON.print(payload) + var headers = ["Content-Type: application/json", "x-api-key: " + api_key, "x-sw-plugin-version: " + SilentWolf.version] + ValidateSession.request("https://api.silentwolf.com/validate_remember_me", headers, true, HTTPClient.METHOD_POST, query) + return self + + +func _on_ValidateSession_request_completed( result, response_code, headers, body ): + SWLogger.info("SilentWolf - ValidateSession request completed") + var status_check = CommonErrors.check_status_code(response_code) + #ValidateSession.queue_free() + SilentWolf.free_request(wrValidateSession, ValidateSession) + SWLogger.debug("response headers: " + str(response_code)) + SWLogger.debug("response headers: " + str(headers)) + SWLogger.debug("response body: " + str(body.get_string_from_utf8())) + + if status_check: + var json = JSON.parse(body.get_string_from_utf8()) + var response = json.result + SWLogger.debug("reponse: " + str(response)) + if "message" in response.keys() and response.message == "Forbidden": + SWLogger.error("You are not authorized to call the SilentWolf API - check your API key configuration: https://silentwolf.com/leaderboard") + else: + SWLogger.info("SilentWolf validate session success? : " + str(response.success)) + if response.success: + set_player_logged_in(response.player_name) + complete_session_check(logged_in_player) + else: + complete_session_check(response.error) + + +func _on_GetPlayerDetails_request_completed( result, response_code, headers, body ): + SWLogger.info("SilentWolf - GetPlayerDetails request completed") + var status_check = CommonErrors.check_status_code(response_code) + #ValidateSession.queue_free() + SilentWolf.free_request(wrGetPlayerDetails, GetPlayerDetails) + SWLogger.debug("response headers: " + str(response_code)) + SWLogger.debug("response headers: " + str(headers)) + SWLogger.debug("response body: " + str(body.get_string_from_utf8())) + + if status_check: + var json = JSON.parse(body.get_string_from_utf8()) + var response = json.result + SWLogger.debug("reponse: " + str(response)) + if "message" in response.keys() and response.message == "Forbidden": + SWLogger.error("You are not authorized to call the SilentWolf API - check your API key configuration: https://silentwolf.com/playerauth") + else: + SWLogger.info("SilentWolf get player details success? : " + str(response.success)) + if response.success: + emit_signal("sw_get_player_details_succeeded", response.player_details) + else: + emit_signal("sw_get_player_details_failed") + + +func setup_complete_session_check_wait_timer(): + complete_session_check_wait_timer = Timer.new() + complete_session_check_wait_timer.set_one_shot(true) + complete_session_check_wait_timer.set_wait_time(0.01) + complete_session_check_wait_timer.connect("timeout", self, "complete_session_check") + add_child(complete_session_check_wait_timer) diff --git a/addons/silent_wolf/Auth/ConfirmEmail.gd b/addons/silent_wolf/Auth/ConfirmEmail.gd new file mode 100644 index 0000000..92e6bf6 --- /dev/null +++ b/addons/silent_wolf/Auth/ConfirmEmail.gd @@ -0,0 +1,60 @@ +extends TextureRect + + +const SWLogger = preload("res://addons/silent_wolf/utils/SWLogger.gd") + + +func _ready(): + SilentWolf.Auth.connect("sw_email_verif_succeeded", self, "_on_confirmation_succeeded") + SilentWolf.Auth.connect("sw_email_verif_failed", self, "_on_confirmation_failed") + SilentWolf.Auth.connect("sw_resend_conf_code_succeeded", self, "_on_resend_code_succeeded") + SilentWolf.Auth.connect("sw_resend_conf_code_failed", self, "_on_resend_code_failed") + + +func _on_confirmation_succeeded(): + SWLogger.info("email verification succeeded: " + str(SilentWolf.Auth.logged_in_player)) + # redirect to configured scene (user is logged in after registration) + var scene_name = SilentWolf.auth_config.redirect_to_scene + get_tree().change_scene(scene_name) + + +func _on_confirmation_failed(error): + hide_processing_label() + SWLogger.info("email verification failed: " + str(error)) + $"FormContainer/ErrorMessage".text = error + $"FormContainer/ErrorMessage".show() + + +func _on_resend_code_succeeded(): + SWLogger.info("Code resend succeeded for player: " + str(SilentWolf.Auth.tmp_username)) + $"FormContainer/ErrorMessage".text = "Confirmation code was resent to your email address. Please check your inbox (and your spam)." + $"FormContainer/ErrorMessage".show() + + +func _on_resend_code_failed(): + SWLogger.info("Code resend failed for player: " + str(SilentWolf.Auth.tmp_username)) + $"FormContainer/ErrorMessage".text = "Confirmation code could not be resent" + $"FormContainer/ErrorMessage".show() + + +func show_processing_label(): + $"FormContainer/ProcessingLabel".show() + + +func hide_processing_label(): + $"FormContainer/ProcessingLabel".hide() + + +func _on_ConfirmButton_pressed(): + var username = SilentWolf.Auth.tmp_username + var code = $"FormContainer/CodeContainer/VerifCode".text + SWLogger.debug("Email verification form submitted, code: " + str(code)) + SilentWolf.Auth.verify_email(username, code) + show_processing_label() + + +func _on_ResendConfCodeButton_pressed(): + var username = SilentWolf.Auth.tmp_username + SWLogger.debug("Requesting confirmation code resend") + SilentWolf.Auth.resend_conf_code(username) + show_processing_label() diff --git a/addons/silent_wolf/Auth/ConfirmEmail.tscn b/addons/silent_wolf/Auth/ConfirmEmail.tscn new file mode 100644 index 0000000..ce87602 --- /dev/null +++ b/addons/silent_wolf/Auth/ConfirmEmail.tscn @@ -0,0 +1,143 @@ +[gd_scene load_steps=13 format=2] + +[ext_resource path="res://addons/silent_wolf/Auth/ConfirmEmail.gd" type="Script" id=1] +[ext_resource path="res://addons/silent_wolf/common/SWButton.tscn" type="PackedScene" id=2] +[ext_resource path="res://addons/silent_wolf/assets/fonts/Comfortaa-Bold.ttf" type="DynamicFontData" id=3] + +[sub_resource type="DynamicFontData" id=13] +font_path = "res://addons/silent_wolf/assets/fonts/Comfortaa-Bold.ttf" + +[sub_resource type="DynamicFont" id=1] +size = 64 +font_data = SubResource( 13 ) + +[sub_resource type="DynamicFont" id=2] +size = 48 +font_data = SubResource( 13 ) + +[sub_resource type="DynamicFont" id=3] +size = 32 +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=4] +size = 32 +font_data = SubResource( 13 ) + +[sub_resource type="StyleBoxFlat" id=5] +content_margin_left = 23.0 +content_margin_right = 23.0 +content_margin_top = 23.0 +content_margin_bottom = 23.0 +bg_color = Color( 0.831373, 0.415686, 0.415686, 1 ) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="StyleBoxFlat" id=6] +content_margin_left = 23.0 +content_margin_right = 23.0 +content_margin_top = 23.0 +content_margin_bottom = 23.0 +bg_color = Color( 0.831373, 0.415686, 0.415686, 1 ) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="StyleBoxFlat" id=7] +content_margin_left = 23.0 +content_margin_right = 23.0 +content_margin_top = 23.0 +content_margin_bottom = 23.0 +bg_color = Color( 0.482353, 0.458824, 0.458824, 1 ) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="DynamicFont" id=8] +size = 64 +font_data = ExtResource( 3 ) + +[node name="ConfirmEmail" type="TextureRect"] +margin_right = 40.0 +margin_bottom = 40.0 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="FormContainer" type="VBoxContainer" parent="."] +margin_left = 565.0 +margin_top = 197.0 +margin_right = 1500.0 +margin_bottom = 798.0 +custom_constants/separation = 100 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Label" type="Label" parent="FormContainer"] +margin_right = 935.0 +margin_bottom = 72.0 +custom_fonts/font = SubResource( 1 ) +text = "Confirm your email address" +align = 1 + +[node name="CodeContainer" type="HBoxContainer" parent="FormContainer"] +margin_top = 172.0 +margin_right = 935.0 +margin_bottom = 247.0 +custom_constants/separation = 20 + +[node name="Label" type="Label" parent="FormContainer/CodeContainer"] +margin_top = 10.0 +margin_right = 133.0 +margin_bottom = 65.0 +custom_fonts/font = SubResource( 2 ) +text = "Code" + +[node name="VerifCode" type="LineEdit" parent="FormContainer/CodeContainer"] +margin_left = 153.0 +margin_right = 483.0 +margin_bottom = 75.0 +rect_min_size = Vector2( 330, 75 ) +custom_fonts/font = SubResource( 3 ) +max_length = 30 + +[node name="ErrorMessage" type="Label" parent="FormContainer"] +visible = false +margin_top = 522.0 +margin_right = 648.0 +margin_bottom = 559.0 +custom_fonts/font = SubResource( 4 ) +custom_colors/font_color = Color( 0.866667, 0.101961, 0.101961, 1 ) +autowrap = true + +[node name="ConfirmButton" parent="FormContainer" instance=ExtResource( 2 )] +margin_top = 347.0 +margin_right = 935.0 +margin_bottom = 465.0 +text = "Submit" + +[node name="ResendConfCodeButton" parent="FormContainer" instance=ExtResource( 2 )] +margin_top = 565.0 +margin_right = 935.0 +margin_bottom = 683.0 +custom_styles/hover = SubResource( 5 ) +custom_styles/pressed = SubResource( 6 ) +custom_styles/normal = SubResource( 7 ) +custom_fonts/font = SubResource( 8 ) +text = "Resend code" + +[node name="ProcessingLabel" type="Label" parent="FormContainer"] +visible = false +margin_top = 740.0 +margin_right = 648.0 +margin_bottom = 812.0 +custom_fonts/font = SubResource( 1 ) +text = "Processing..." +align = 1 +[connection signal="pressed" from="FormContainer/ConfirmButton" to="." method="_on_ConfirmButton_pressed"] +[connection signal="pressed" from="FormContainer/ResendConfCodeButton" to="." method="_on_ResendConfCodeButton_pressed"] diff --git a/addons/silent_wolf/Auth/Login.gd b/addons/silent_wolf/Auth/Login.gd new file mode 100644 index 0000000..3015d60 --- /dev/null +++ b/addons/silent_wolf/Auth/Login.gd @@ -0,0 +1,41 @@ +extends TextureRect + +const SWLogger = preload("res://addons/silent_wolf/utils/SWLogger.gd") + +func _ready(): + #var auth_node = get_tree().get_root().get_node("res://addons/silent_wolf/Auth/Auth") + SilentWolf.Auth.connect("sw_login_succeeded", self, "_on_login_succeeded") + SilentWolf.Auth.connect("sw_login_failed", self, "_on_login_failed") + +func _on_LoginButton_pressed(): + var username = $"FormContainer/UsernameContainer/Username".text + var password = $"FormContainer/PasswordContainer/Password".text + var remember_me = $"FormContainer/RememberMeCheckBox".is_pressed() + SWLogger.debug("Login form submitted, remember_me: " + str(remember_me)) + SilentWolf.Auth.login_player(username, password, remember_me) + show_processing_label() + +func _on_login_succeeded(): + var scene_name = SilentWolf.auth_config.redirect_to_scene + SWLogger.info("logged in as: " + str(SilentWolf.Auth.logged_in_player)) + get_tree().change_scene(scene_name) + +func _on_login_failed(error): + hide_processing_label() + SWLogger.info("log in failed: " + str(error)) + $"FormContainer/ErrorMessage".text = error + $"FormContainer/ErrorMessage".show() + +func _on_BackButton_pressed(): + get_tree().change_scene(SilentWolf.auth_config.redirect_to_scene) + +func show_processing_label(): + $"FormContainer/ProcessingLabel".show() + $"FormContainer/ProcessingLabel".show() + +func hide_processing_label(): + $"FormContainer/ProcessingLabel".hide() + + +func _on_LinkButton_pressed(): + get_tree().change_scene(SilentWolf.auth_config.reset_password_scene) diff --git a/addons/silent_wolf/Auth/Login.tscn b/addons/silent_wolf/Auth/Login.tscn new file mode 100644 index 0000000..fa7eef2 --- /dev/null +++ b/addons/silent_wolf/Auth/Login.tscn @@ -0,0 +1,208 @@ +[gd_scene load_steps=19 format=2] + +[ext_resource path="res://addons/silent_wolf/Auth/Login.gd" type="Script" id=1] +[ext_resource path="res://addons/silent_wolf/common/SWButton.tscn" type="PackedScene" id=2] +[ext_resource path="res://addons/silent_wolf/assets/fonts/Comfortaa-Bold.ttf" type="DynamicFontData" id=3] +[ext_resource path="res://addons/silent_wolf/assets/gfx/checkbox_unchecked.png" type="Texture" id=5] +[ext_resource path="res://addons/silent_wolf/assets/gfx/checkbox_checked.png" type="Texture" id=6] + +[sub_resource type="StyleBoxFlat" id=1] +content_margin_left = 23.0 +content_margin_right = 23.0 +content_margin_top = 23.0 +content_margin_bottom = 23.0 +bg_color = Color( 0.831373, 0.415686, 0.415686, 1 ) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="StyleBoxFlat" id=2] +content_margin_left = 23.0 +content_margin_right = 23.0 +content_margin_top = 23.0 +content_margin_bottom = 23.0 +bg_color = Color( 0.831373, 0.415686, 0.415686, 1 ) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="StyleBoxFlat" id=3] +content_margin_left = 23.0 +content_margin_right = 23.0 +content_margin_top = 23.0 +content_margin_bottom = 23.0 +bg_color = Color( 0.482353, 0.458824, 0.458824, 1 ) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="DynamicFont" id=4] +size = 64 +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFontData" id=13] +font_path = "res://addons/silent_wolf/assets/fonts/Comfortaa-Bold.ttf" + +[sub_resource type="DynamicFont" id=5] +size = 64 +font_data = SubResource( 13 ) + +[sub_resource type="DynamicFont" id=6] +size = 48 +font_data = SubResource( 13 ) + +[sub_resource type="DynamicFont" id=7] +size = 32 +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=8] +size = 32 +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=9] +size = 32 +outline_color = Color( 0.211765, 0.25098, 0.937255, 1 ) +font_data = SubResource( 13 ) + +[sub_resource type="StyleBoxFlat" id=10] +content_margin_left = 5.0 +bg_color = Color( 0.6, 0.6, 0.6, 0 ) + +[sub_resource type="DynamicFont" id=11] +size = 32 +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=12] +size = 32 +font_data = SubResource( 13 ) + +[node name="Login" type="TextureRect"] +margin_right = 40.0 +margin_bottom = 40.0 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="BackButton" parent="." instance=ExtResource( 2 )] +margin_left = 339.0 +margin_top = 117.0 +margin_right = 667.0 +margin_bottom = 235.0 +custom_styles/hover = SubResource( 1 ) +custom_styles/pressed = SubResource( 2 ) +custom_styles/normal = SubResource( 3 ) +custom_fonts/font = SubResource( 4 ) +text = "← Back" + +[node name="FormContainer" type="VBoxContainer" parent="."] +margin_left = 679.0 +margin_top = 196.75 +margin_right = 1327.0 +margin_bottom = 933.75 +custom_constants/separation = 80 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Label" type="Label" parent="FormContainer"] +margin_right = 648.0 +margin_bottom = 72.0 +custom_fonts/font = SubResource( 5 ) +text = "Log in" +align = 1 + +[node name="UsernameContainer" type="HBoxContainer" parent="FormContainer"] +margin_top = 152.0 +margin_right = 648.0 +margin_bottom = 227.0 +custom_constants/separation = 20 + +[node name="Label" type="Label" parent="FormContainer/UsernameContainer"] +margin_top = 10.0 +margin_right = 269.0 +margin_bottom = 65.0 +custom_fonts/font = SubResource( 6 ) +text = "Username:" + +[node name="Username" type="LineEdit" parent="FormContainer/UsernameContainer"] +margin_left = 289.0 +margin_right = 619.0 +margin_bottom = 75.0 +rect_min_size = Vector2( 330, 75 ) +custom_fonts/font = SubResource( 7 ) +max_length = 30 + +[node name="PasswordContainer" type="HBoxContainer" parent="FormContainer"] +margin_top = 307.0 +margin_right = 648.0 +margin_bottom = 382.0 +custom_constants/separation = 40 + +[node name="Label" type="Label" parent="FormContainer/PasswordContainer"] +margin_top = 10.0 +margin_right = 247.0 +margin_bottom = 65.0 +custom_fonts/font = SubResource( 6 ) +text = "Password:" + +[node name="Password" type="LineEdit" parent="FormContainer/PasswordContainer"] +margin_left = 287.0 +margin_right = 617.0 +margin_bottom = 75.0 +rect_min_size = Vector2( 330, 75 ) +custom_fonts/font = SubResource( 8 ) +max_length = 30 +secret = true + +[node name="LinkButton" type="LinkButton" parent="FormContainer"] +margin_top = 462.0 +margin_right = 648.0 +margin_bottom = 499.0 +custom_fonts/font = SubResource( 9 ) +custom_colors/font_color = Color( 0.321569, 0.360784, 0.92549, 1 ) +text = "Forgot Password?" + +[node name="RememberMeCheckBox" type="CheckBox" parent="FormContainer"] +margin_top = 579.0 +margin_right = 648.0 +margin_bottom = 629.0 +rect_min_size = Vector2( 50, 50 ) +focus_mode = 0 +custom_icons/checked = ExtResource( 6 ) +custom_icons/unchecked = ExtResource( 5 ) +custom_styles/normal = SubResource( 10 ) +custom_fonts/font = SubResource( 11 ) +custom_constants/hseparation = 15 +text = "Stay signed in for 30 days" +expand_icon = true + +[node name="ErrorMessage" type="Label" parent="FormContainer"] +visible = false +margin_top = 522.0 +margin_right = 648.0 +margin_bottom = 559.0 +custom_fonts/font = SubResource( 12 ) +custom_colors/font_color = Color( 0.866667, 0.101961, 0.101961, 1 ) +autowrap = true + +[node name="LoginButton" parent="FormContainer" instance=ExtResource( 2 )] +margin_top = 709.0 +margin_right = 648.0 +margin_bottom = 827.0 +text = "Submit" + +[node name="ProcessingLabel" type="Label" parent="FormContainer"] +visible = false +margin_top = 740.0 +margin_right = 648.0 +margin_bottom = 812.0 +custom_fonts/font = SubResource( 5 ) +text = "Processing..." +align = 1 +[connection signal="pressed" from="BackButton" to="." method="_on_BackButton_pressed"] +[connection signal="pressed" from="FormContainer/LinkButton" to="." method="_on_LinkButton_pressed"] +[connection signal="pressed" from="FormContainer/LoginButton" to="." method="_on_LoginButton_pressed"] diff --git a/addons/silent_wolf/Auth/Register.gd b/addons/silent_wolf/Auth/Register.gd new file mode 100644 index 0000000..48a54c2 --- /dev/null +++ b/addons/silent_wolf/Auth/Register.gd @@ -0,0 +1,81 @@ +extends TextureRect + +const SWLogger = preload("res://addons/silent_wolf/utils/SWLogger.gd") + +func _ready(): + SilentWolf.check_auth_ready() + SilentWolf.Auth.connect("sw_registration_succeeded", self, "_on_registration_succeeded") + SilentWolf.Auth.connect("sw_registration_user_pwd_succeeded", self, "_on_registration_succeeded") + SilentWolf.Auth.connect("sw_registration_failed", self, "_on_registration_failed") + + +func _on_RegisterButton_pressed(): + var player_name = $"FormContainer/MainFormContainer/FormInputFields/PlayerName".text + var email = $"FormContainer/MainFormContainer/FormInputFields/Email".text + var password = $"FormContainer/MainFormContainer/FormInputFields/Password".text + var confirm_password = $"FormContainer/MainFormContainer/FormInputFields/ConfirmPassword".text + SilentWolf.Auth.register_player(player_name, email, password, confirm_password) + show_processing_label() + + +func _on_RegisterUPButton_pressed(): + var player_name = $"FormContainer/MainFormContainer/FormInputFields/PlayerName".text + var password = $"FormContainer/MainFormContainer/FormInputFields/Password".text + var confirm_password = $"FormContainer/MainFormContainer/FormInputFields/ConfirmPassword".text + SilentWolf.Auth.register_player_user_password(player_name, password, confirm_password) + show_processing_label() + + +func _on_registration_succeeded(): + #get_tree().change_scene("res://addons/silent_wolf/Auth/Login.tscn") + # redirect to configured scene (user is logged in after registration) + var scene_name = SilentWolf.auth_config.redirect_to_scene + # if doing email verification, open scene to confirm email address + if ("email_confirmation_scene" in SilentWolf.auth_config) and (SilentWolf.auth_config.email_confirmation_scene) != "": + SWLogger.info("registration succeeded, waiting for email verification...") + scene_name = SilentWolf.auth_config.email_confirmation_scene + else: + SWLogger.info("registration succeeded, logged in player: " + str(SilentWolf.Auth.logged_in_player)) + get_tree().change_scene(scene_name) + + +func _on_registration_user_pwd_succeeded(): + var scene_name = SilentWolf.auth_config.redirect_to_scene + get_tree().change_scene(scene_name) + + +func _on_registration_failed(error): + hide_processing_label() + SWLogger.info("registration failed: " + str(error)) + $"FormContainer/ErrorMessage".text = error + $"FormContainer/ErrorMessage".show() + + +func _on_BackButton_pressed(): + get_tree().change_scene(SilentWolf.auth_config.redirect_to_scene) + + +func show_processing_label(): + $"FormContainer/ProcessingLabel".show() + + +func hide_processing_label(): + $"FormContainer/ProcessingLabel".hide() + + +func _on_UsernameToolButton_mouse_entered(): + $"FormContainer/InfoBox".text = "Username should contain at least 6 characters (letters or numbers) and no spaces." + $"FormContainer/InfoBox".show() + + +func _on_UsernameToolButton_mouse_exited(): + $"FormContainer/InfoBox".hide() + + +func _on_PasswordToolButton_mouse_entered(): + $"FormContainer/InfoBox".text = "Password should contain at least 8 characters including uppercase and lowercase letters, numbers and (optionally) special characters." + $"FormContainer/InfoBox".show() + + +func _on_PasswordToolButton_mouse_exited(): + $"FormContainer/InfoBox".hide() diff --git a/addons/silent_wolf/Auth/Register.tscn b/addons/silent_wolf/Auth/Register.tscn new file mode 100644 index 0000000..9703000 --- /dev/null +++ b/addons/silent_wolf/Auth/Register.tscn @@ -0,0 +1,285 @@ +[gd_scene load_steps=21 format=2] + +[ext_resource path="res://addons/silent_wolf/Auth/Register.gd" type="Script" id=1] +[ext_resource path="res://addons/silent_wolf/common/SWButton.tscn" type="PackedScene" id=2] +[ext_resource path="res://addons/silent_wolf/assets/fonts/Comfortaa-Bold.ttf" type="DynamicFontData" id=3] +[ext_resource path="res://assets/fonts/Comfortaa-Bold.ttf" type="DynamicFontData" id=4] +[ext_resource path="res://addons/silent_wolf/assets/gfx/info_icon_small.png" type="Texture" id=5] +[ext_resource path="res://addons/silent_wolf/assets/gfx/dummy_info_icon_small.png" type="Texture" id=6] + +[sub_resource type="StyleBoxFlat" id=1] +content_margin_left = 23.0 +content_margin_right = 23.0 +content_margin_top = 23.0 +content_margin_bottom = 23.0 +bg_color = Color( 0.831373, 0.415686, 0.415686, 1 ) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="StyleBoxFlat" id=2] +content_margin_left = 23.0 +content_margin_right = 23.0 +content_margin_top = 23.0 +content_margin_bottom = 23.0 +bg_color = Color( 0.831373, 0.415686, 0.415686, 1 ) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="StyleBoxFlat" id=3] +content_margin_left = 23.0 +content_margin_right = 23.0 +content_margin_top = 23.0 +content_margin_bottom = 23.0 +bg_color = Color( 0.482353, 0.458824, 0.458824, 1 ) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="DynamicFont" id=4] +size = 64 +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=5] +size = 64 +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=6] +size = 64 +font_data = ExtResource( 4 ) + +[sub_resource type="DynamicFont" id=7] +size = 32 +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=8] +size = 32 +font_data = ExtResource( 4 ) + +[sub_resource type="DynamicFont" id=9] +size = 32 +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=10] +size = 32 +font_data = ExtResource( 3 ) + +[sub_resource type="StyleBoxFlat" id=11] +content_margin_left = 30.0 +content_margin_right = 30.0 +content_margin_top = 30.0 +content_margin_bottom = 30.0 +bg_color = Color( 1, 1, 1, 1 ) +border_width_left = 3 +border_width_top = 3 +border_width_right = 3 +border_width_bottom = 3 +border_color = Color( 0.666667, 0.223529, 0.223529, 1 ) +corner_radius_top_left = 10 +corner_radius_top_right = 10 +corner_radius_bottom_right = 10 +corner_radius_bottom_left = 10 + +[sub_resource type="DynamicFont" id=12] +size = 32 +outline_color = Color( 0.666667, 0.223529, 0.223529, 1 ) +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=13] +size = 32 +outline_color = Color( 0.854902, 0.0901961, 0.0901961, 1 ) +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=14] +size = 32 +font_data = ExtResource( 3 ) + +[node name="Register" type="TextureRect"] +margin_right = 40.0 +margin_bottom = 40.0 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="BackButton" parent="." instance=ExtResource( 2 )] +margin_left = 241.0 +margin_top = 54.0 +margin_right = 529.0 +margin_bottom = 172.0 +custom_styles/hover = SubResource( 1 ) +custom_styles/pressed = SubResource( 2 ) +custom_styles/normal = SubResource( 3 ) +custom_fonts/font = SubResource( 4 ) +text = "← Back" + +[node name="FormContainer" type="VBoxContainer" parent="."] +margin_left = 396.0 +margin_top = 200.0 +margin_right = 1665.0 +margin_bottom = 1060.0 +grow_horizontal = 0 +grow_vertical = 0 +custom_constants/separation = 60 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Label" type="Label" parent="FormContainer"] +margin_right = 1269.0 +margin_bottom = 72.0 +custom_fonts/font = SubResource( 5 ) +text = "Sign up" +align = 1 + +[node name="MainFormContainer" type="HBoxContainer" parent="FormContainer"] +margin_top = 132.0 +margin_right = 1269.0 +margin_bottom = 525.0 +custom_constants/separation = 30 + +[node name="FormLabels" type="VBoxContainer" parent="FormContainer/MainFormContainer"] +margin_right = 625.0 +margin_bottom = 393.0 +custom_constants/separation = 30 + +[node name="PlayerNameLabel" type="Label" parent="FormContainer/MainFormContainer/FormLabels"] +margin_right = 625.0 +margin_bottom = 80.0 +rect_min_size = Vector2( 0, 80 ) +custom_fonts/font = SubResource( 6 ) +text = "Username:" + +[node name="EmailLabel" type="Label" parent="FormContainer/MainFormContainer/FormLabels"] +margin_top = 110.0 +margin_right = 625.0 +margin_bottom = 182.0 +custom_fonts/font = SubResource( 6 ) +text = "Email:" + +[node name="PasswordLabel" type="Label" parent="FormContainer/MainFormContainer/FormLabels"] +margin_top = 212.0 +margin_right = 625.0 +margin_bottom = 284.0 +custom_fonts/font = SubResource( 6 ) +text = "Password:" + +[node name="ConfirmPasswordLabel" type="Label" parent="FormContainer/MainFormContainer/FormLabels"] +margin_top = 314.0 +margin_right = 625.0 +margin_bottom = 386.0 +custom_fonts/font = SubResource( 6 ) +text = "Confirm password:" + +[node name="FormInputFields" type="VBoxContainer" parent="FormContainer/MainFormContainer"] +margin_left = 655.0 +margin_right = 1155.0 +margin_bottom = 393.0 +custom_constants/separation = 30 + +[node name="PlayerName" type="LineEdit" parent="FormContainer/MainFormContainer/FormInputFields"] +margin_right = 500.0 +margin_bottom = 78.0 +rect_min_size = Vector2( 500, 78 ) +custom_fonts/font = SubResource( 7 ) +max_length = 30 + +[node name="Email" type="LineEdit" parent="FormContainer/MainFormContainer/FormInputFields"] +margin_top = 108.0 +margin_right = 500.0 +margin_bottom = 183.0 +rect_min_size = Vector2( 360, 75 ) +custom_fonts/font = SubResource( 8 ) +max_length = 50 + +[node name="Password" type="LineEdit" parent="FormContainer/MainFormContainer/FormInputFields"] +margin_top = 213.0 +margin_right = 500.0 +margin_bottom = 288.0 +rect_min_size = Vector2( 360, 75 ) +custom_fonts/font = SubResource( 9 ) +max_length = 30 +secret = true + +[node name="ConfirmPassword" type="LineEdit" parent="FormContainer/MainFormContainer/FormInputFields"] +margin_top = 318.0 +margin_right = 500.0 +margin_bottom = 393.0 +rect_min_size = Vector2( 360, 75 ) +custom_fonts/font = SubResource( 10 ) +max_length = 30 +secret = true + +[node name="InfoLabels" type="VBoxContainer" parent="FormContainer/MainFormContainer"] +margin_left = 1185.0 +margin_right = 1269.0 +margin_bottom = 393.0 +rect_min_size = Vector2( 40, 0 ) +custom_constants/separation = 24 + +[node name="UsernameToolButton" type="ToolButton" parent="FormContainer/MainFormContainer/InfoLabels"] +margin_right = 84.0 +margin_bottom = 80.0 +icon = ExtResource( 5 ) + +[node name="DummyToolButton1" type="ToolButton" parent="FormContainer/MainFormContainer/InfoLabels"] +margin_top = 104.0 +margin_right = 84.0 +margin_bottom = 184.0 +icon = ExtResource( 6 ) + +[node name="PasswordToolButton" type="ToolButton" parent="FormContainer/MainFormContainer/InfoLabels"] +margin_top = 208.0 +margin_right = 84.0 +margin_bottom = 288.0 +icon = ExtResource( 5 ) + +[node name="DummyToolButton2" type="ToolButton" parent="FormContainer/MainFormContainer/InfoLabels"] +margin_top = 312.0 +margin_right = 84.0 +margin_bottom = 392.0 +icon = ExtResource( 6 ) + +[node name="InfoBox" type="Label" parent="FormContainer"] +visible = false +margin_right = 1269.0 +margin_bottom = 137.0 +rect_min_size = Vector2( 250, 0 ) +custom_styles/normal = SubResource( 11 ) +custom_fonts/font = SubResource( 12 ) +custom_colors/font_color = Color( 0.666667, 0.223529, 0.223529, 1 ) +custom_colors/font_outline_modulate = Color( 0.937255, 0.917647, 0.917647, 1 ) +text = "Password should contain at least 8 characters including uppercase and lowercase letters, numbers and (optionally) special characters." +autowrap = true + +[node name="ErrorMessage" type="Label" parent="FormContainer"] +visible = false +margin_right = 1189.0 +margin_bottom = 37.0 +custom_fonts/font = SubResource( 13 ) +custom_colors/font_color = Color( 0.866667, 0.101961, 0.101961, 1 ) +autowrap = true + +[node name="RegisterButton" parent="FormContainer" instance=ExtResource( 2 )] +margin_top = 585.0 +margin_right = 1269.0 +margin_bottom = 703.0 +text = "Submit" + +[node name="ProcessingLabel" type="Label" parent="FormContainer"] +visible = false +margin_right = 1189.0 +margin_bottom = 72.0 +custom_fonts/font = SubResource( 14 ) +text = "Processing..." +align = 1 +[connection signal="pressed" from="BackButton" to="." method="_on_BackButton_pressed"] +[connection signal="mouse_entered" from="FormContainer/MainFormContainer/InfoLabels/UsernameToolButton" to="." method="_on_UsernameToolButton_mouse_entered"] +[connection signal="mouse_exited" from="FormContainer/MainFormContainer/InfoLabels/UsernameToolButton" to="." method="_on_UsernameToolButton_mouse_exited"] +[connection signal="mouse_entered" from="FormContainer/MainFormContainer/InfoLabels/PasswordToolButton" to="." method="_on_PasswordToolButton_mouse_entered"] +[connection signal="mouse_exited" from="FormContainer/MainFormContainer/InfoLabels/PasswordToolButton" to="." method="_on_PasswordToolButton_mouse_exited"] +[connection signal="pressed" from="FormContainer/RegisterButton" to="." method="_on_RegisterButton_pressed"] diff --git a/addons/silent_wolf/Auth/RegisterUsernamePassword.tscn b/addons/silent_wolf/Auth/RegisterUsernamePassword.tscn new file mode 100644 index 0000000..099c342 --- /dev/null +++ b/addons/silent_wolf/Auth/RegisterUsernamePassword.tscn @@ -0,0 +1,266 @@ +[gd_scene load_steps=20 format=2] + +[ext_resource path="res://addons/silent_wolf/Auth/Register.gd" type="Script" id=1] +[ext_resource path="res://addons/silent_wolf/common/SWButton.tscn" type="PackedScene" id=2] +[ext_resource path="res://addons/silent_wolf/assets/fonts/Comfortaa-Bold.ttf" type="DynamicFontData" id=3] +[ext_resource path="res://assets/fonts/Comfortaa-Bold.ttf" type="DynamicFontData" id=4] +[ext_resource path="res://addons/silent_wolf/assets/gfx/info_icon_small.png" type="Texture" id=5] +[ext_resource path="res://addons/silent_wolf/assets/gfx/dummy_info_icon_small.png" type="Texture" id=6] + +[sub_resource type="StyleBoxFlat" id=1] +content_margin_left = 23.0 +content_margin_right = 23.0 +content_margin_top = 23.0 +content_margin_bottom = 23.0 +bg_color = Color( 0.831373, 0.415686, 0.415686, 1 ) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="StyleBoxFlat" id=2] +content_margin_left = 23.0 +content_margin_right = 23.0 +content_margin_top = 23.0 +content_margin_bottom = 23.0 +bg_color = Color( 0.831373, 0.415686, 0.415686, 1 ) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="StyleBoxFlat" id=3] +content_margin_left = 23.0 +content_margin_right = 23.0 +content_margin_top = 23.0 +content_margin_bottom = 23.0 +bg_color = Color( 0.482353, 0.458824, 0.458824, 1 ) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="DynamicFont" id=4] +size = 64 +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=5] +size = 64 +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=6] +size = 64 +font_data = ExtResource( 4 ) + +[sub_resource type="DynamicFont" id=7] +size = 32 +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=8] +size = 32 +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=9] +size = 32 +font_data = ExtResource( 3 ) + +[sub_resource type="StyleBoxFlat" id=10] +content_margin_left = 30.0 +content_margin_right = 30.0 +content_margin_top = 30.0 +content_margin_bottom = 30.0 +bg_color = Color( 1, 1, 1, 1 ) +border_width_left = 3 +border_width_top = 3 +border_width_right = 3 +border_width_bottom = 3 +border_color = Color( 0.666667, 0.223529, 0.223529, 1 ) +corner_radius_top_left = 10 +corner_radius_top_right = 10 +corner_radius_bottom_right = 10 +corner_radius_bottom_left = 10 + +[sub_resource type="DynamicFont" id=11] +size = 32 +outline_color = Color( 0.666667, 0.223529, 0.223529, 1 ) +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=12] +size = 32 +outline_color = Color( 0.854902, 0.0901961, 0.0901961, 1 ) +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=13] +size = 32 +font_data = ExtResource( 3 ) + +[node name="Register" type="TextureRect"] +margin_right = 40.0 +margin_bottom = 40.0 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="BackButton" parent="." instance=ExtResource( 2 )] +margin_left = 241.0 +margin_top = 54.0 +margin_right = 529.0 +margin_bottom = 172.0 +custom_styles/hover = SubResource( 1 ) +custom_styles/pressed = SubResource( 2 ) +custom_styles/normal = SubResource( 3 ) +custom_fonts/font = SubResource( 4 ) +text = "← Back" + +[node name="FormContainer" type="VBoxContainer" parent="."] +margin_left = 396.0 +margin_top = 200.0 +margin_right = 1665.0 +margin_bottom = 1060.0 +grow_horizontal = 0 +grow_vertical = 0 +custom_constants/separation = 60 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Label" type="Label" parent="FormContainer"] +margin_right = 1269.0 +margin_bottom = 72.0 +custom_fonts/font = SubResource( 5 ) +text = "Sign up" +align = 1 + +[node name="MainFormContainer" type="HBoxContainer" parent="FormContainer"] +margin_top = 132.0 +margin_right = 1269.0 +margin_bottom = 524.0 +custom_constants/separation = 30 + +[node name="FormLabels" type="VBoxContainer" parent="FormContainer/MainFormContainer"] +margin_right = 625.0 +margin_bottom = 392.0 +custom_constants/separation = 30 + +[node name="PlayerNameLabel" type="Label" parent="FormContainer/MainFormContainer/FormLabels"] +margin_right = 625.0 +margin_bottom = 80.0 +rect_min_size = Vector2( 0, 80 ) +custom_fonts/font = SubResource( 6 ) +text = "Username:" + +[node name="PasswordLabel" type="Label" parent="FormContainer/MainFormContainer/FormLabels"] +margin_top = 110.0 +margin_right = 625.0 +margin_bottom = 182.0 +custom_fonts/font = SubResource( 6 ) +text = "Password:" + +[node name="ConfirmPasswordLabel" type="Label" parent="FormContainer/MainFormContainer/FormLabels"] +margin_top = 212.0 +margin_right = 625.0 +margin_bottom = 284.0 +custom_fonts/font = SubResource( 6 ) +text = "Confirm password:" + +[node name="FormInputFields" type="VBoxContainer" parent="FormContainer/MainFormContainer"] +margin_left = 655.0 +margin_right = 1155.0 +margin_bottom = 392.0 +custom_constants/separation = 30 + +[node name="PlayerName" type="LineEdit" parent="FormContainer/MainFormContainer/FormInputFields"] +margin_right = 500.0 +margin_bottom = 78.0 +rect_min_size = Vector2( 500, 78 ) +custom_fonts/font = SubResource( 7 ) +max_length = 30 + +[node name="Password" type="LineEdit" parent="FormContainer/MainFormContainer/FormInputFields"] +margin_top = 108.0 +margin_right = 500.0 +margin_bottom = 183.0 +rect_min_size = Vector2( 360, 75 ) +custom_fonts/font = SubResource( 8 ) +max_length = 30 +secret = true + +[node name="ConfirmPassword" type="LineEdit" parent="FormContainer/MainFormContainer/FormInputFields"] +margin_top = 213.0 +margin_right = 500.0 +margin_bottom = 288.0 +rect_min_size = Vector2( 360, 75 ) +custom_fonts/font = SubResource( 9 ) +max_length = 30 +secret = true + +[node name="InfoLabels" type="VBoxContainer" parent="FormContainer/MainFormContainer"] +margin_left = 1185.0 +margin_right = 1269.0 +margin_bottom = 392.0 +rect_min_size = Vector2( 40, 0 ) +custom_constants/separation = 24 + +[node name="UsernameToolButton" type="ToolButton" parent="FormContainer/MainFormContainer/InfoLabels"] +margin_right = 84.0 +margin_bottom = 80.0 +icon = ExtResource( 5 ) + +[node name="DummyToolButton1" type="ToolButton" parent="FormContainer/MainFormContainer/InfoLabels"] +margin_top = 104.0 +margin_right = 84.0 +margin_bottom = 184.0 +icon = ExtResource( 6 ) + +[node name="PasswordToolButton" type="ToolButton" parent="FormContainer/MainFormContainer/InfoLabels"] +margin_top = 208.0 +margin_right = 84.0 +margin_bottom = 288.0 +icon = ExtResource( 5 ) + +[node name="DummyToolButton2" type="ToolButton" parent="FormContainer/MainFormContainer/InfoLabels"] +margin_top = 312.0 +margin_right = 84.0 +margin_bottom = 392.0 +icon = ExtResource( 6 ) + +[node name="InfoBox" type="Label" parent="FormContainer"] +visible = false +margin_right = 1269.0 +margin_bottom = 137.0 +rect_min_size = Vector2( 250, 0 ) +custom_styles/normal = SubResource( 10 ) +custom_fonts/font = SubResource( 11 ) +custom_colors/font_color = Color( 0.666667, 0.223529, 0.223529, 1 ) +custom_colors/font_outline_modulate = Color( 0.937255, 0.917647, 0.917647, 1 ) +text = "Password should contain at least 8 characters including uppercase and lowercase letters, numbers and (optionally) special characters." +autowrap = true + +[node name="ErrorMessage" type="Label" parent="FormContainer"] +visible = false +margin_right = 1189.0 +margin_bottom = 37.0 +custom_fonts/font = SubResource( 12 ) +custom_colors/font_color = Color( 0.866667, 0.101961, 0.101961, 1 ) +autowrap = true + +[node name="RegisterUPButton" parent="FormContainer" instance=ExtResource( 2 )] +margin_top = 584.0 +margin_right = 1269.0 +margin_bottom = 702.0 +text = "Submit" + +[node name="ProcessingLabel" type="Label" parent="FormContainer"] +visible = false +margin_right = 1189.0 +margin_bottom = 72.0 +custom_fonts/font = SubResource( 13 ) +text = "Processing..." +align = 1 +[connection signal="pressed" from="BackButton" to="." method="_on_BackButton_pressed"] +[connection signal="mouse_entered" from="FormContainer/MainFormContainer/InfoLabels/UsernameToolButton" to="." method="_on_UsernameToolButton_mouse_entered"] +[connection signal="mouse_exited" from="FormContainer/MainFormContainer/InfoLabels/UsernameToolButton" to="." method="_on_UsernameToolButton_mouse_exited"] +[connection signal="mouse_entered" from="FormContainer/MainFormContainer/InfoLabels/PasswordToolButton" to="." method="_on_PasswordToolButton_mouse_entered"] +[connection signal="mouse_exited" from="FormContainer/MainFormContainer/InfoLabels/PasswordToolButton" to="." method="_on_PasswordToolButton_mouse_exited"] +[connection signal="pressed" from="FormContainer/RegisterUPButton" to="." method="_on_RegisterUPButton_pressed"] diff --git a/addons/silent_wolf/Auth/ResetPassword.gd b/addons/silent_wolf/Auth/ResetPassword.gd new file mode 100644 index 0000000..1c75f51 --- /dev/null +++ b/addons/silent_wolf/Auth/ResetPassword.gd @@ -0,0 +1,63 @@ +extends TextureRect + +var player_name = null +var login_scene = "res://addons/silent_wolf/Auth/Login.tscn" + +# Called when the node enters the scene tree for the first time. +func _ready(): + $"RequestFormContainer/ProcessingLabel".hide() + $"PwdResetFormContainer/ProcessingLabel".hide() + $"PasswordChangedContainer".hide() + $"PwdResetFormContainer".hide() + $"RequestFormContainer".show() + SilentWolf.Auth.connect("sw_request_password_reset_succeeded", self, "_on_send_code_succeeded") + SilentWolf.Auth.connect("sw_request_password_reset_failed", self, "_on_send_code_failed") + SilentWolf.Auth.connect("sw_reset_password_succeeded", self, "_on_reset_succeeded") + SilentWolf.Auth.connect("sw_reset_password_failed", self, "_on_reset_failed") + if "login_scene" in SilentWolf.Auth: + login_scene = SilentWolf.Auth.login_scene + +func _on_BackButton_pressed(): + get_tree().change_scene(login_scene) + + +func _on_PlayerNameSubmitButton_pressed(): + player_name = $"RequestFormContainer/FormContainer/FormInputFields/PlayerName".text + SilentWolf.Auth.request_player_password_reset(player_name) + $"RequestFormContainer/ProcessingLabel".show() + + +func _on_send_code_succeeded(): + $"RequestFormContainer/ProcessingLabel".hide() + $"RequestFormContainer".hide() + $"PwdResetFormContainer".show() + + +func _on_send_code_failed(error): + $"RequestFormContainer/ProcessingLabel".hide() + $"RequestFormContainer/ErrorMessage".text = "Could not send confirmation code. " + str(error) + $"RequestFormContainer/ErrorMessage".show() + + +func _on_NewPasswordSubmitButton_pressed(): + var code = $"PwdResetFormContainer/FormContainer/FormInputFields/Code".text + var password = $"PwdResetFormContainer/FormContainer/FormInputFields/Password".text + var confirm_password = $"PwdResetFormContainer/FormContainer/FormInputFields/ConfirmPassword".text + SilentWolf.Auth.reset_player_password(player_name, code, password, confirm_password) + $"PwdResetFormContainer/ProcessingLabel".show() + + +func _on_reset_succeeded(): + $"PwdResetFormContainer/ProcessingLabel".hide() + $"PwdResetFormContainer".hide() + $"PasswordChangedContainer".show() + + +func _on_reset_failed(error): + $"PwdResetFormContainer/ProcessingLabel".hide() + $"PwdResetFormContainer/ErrorMessage".text = "Could not reset password. " + str(error) + $"PwdResetFormContainer/ErrorMessage".show() + + +func _on_CloseButton_pressed(): + get_tree().change_scene(login_scene) diff --git a/addons/silent_wolf/Auth/ResetPassword.tscn b/addons/silent_wolf/Auth/ResetPassword.tscn new file mode 100644 index 0000000..1996d65 --- /dev/null +++ b/addons/silent_wolf/Auth/ResetPassword.tscn @@ -0,0 +1,395 @@ +[gd_scene load_steps=27 format=2] + +[ext_resource path="res://addons/silent_wolf/Auth/ResetPassword.gd" type="Script" id=1] +[ext_resource path="res://addons/silent_wolf/common/SWButton.tscn" type="PackedScene" id=2] +[ext_resource path="res://addons/silent_wolf/assets/fonts/Comfortaa-Bold.ttf" type="DynamicFontData" id=3] +[ext_resource path="res://assets/fonts/Comfortaa-Bold.ttf" type="DynamicFontData" id=6] + +[sub_resource type="StyleBoxFlat" id=1] +content_margin_left = 23.0 +content_margin_right = 23.0 +content_margin_top = 23.0 +content_margin_bottom = 23.0 +bg_color = Color( 0.831373, 0.415686, 0.415686, 1 ) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="StyleBoxFlat" id=2] +content_margin_left = 23.0 +content_margin_right = 23.0 +content_margin_top = 23.0 +content_margin_bottom = 23.0 +bg_color = Color( 0.831373, 0.415686, 0.415686, 1 ) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="StyleBoxFlat" id=3] +content_margin_left = 23.0 +content_margin_right = 23.0 +content_margin_top = 23.0 +content_margin_bottom = 23.0 +bg_color = Color( 0.482353, 0.458824, 0.458824, 1 ) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="DynamicFont" id=4] +size = 64 +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=5] +size = 64 +font_data = ExtResource( 6 ) + +[sub_resource type="DynamicFont" id=6] +size = 32 +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=7] +size = 64 +font_data = ExtResource( 6 ) + +[sub_resource type="DynamicFont" id=8] +size = 32 +font_data = ExtResource( 6 ) + +[sub_resource type="DynamicFont" id=9] +size = 32 +font_data = ExtResource( 6 ) + +[sub_resource type="StyleBoxFlat" id=10] +content_margin_left = 23.0 +content_margin_right = 23.0 +content_margin_top = 23.0 +content_margin_bottom = 23.0 +bg_color = Color( 0.831373, 0.415686, 0.415686, 1 ) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="StyleBoxFlat" id=11] +content_margin_left = 23.0 +content_margin_right = 23.0 +content_margin_top = 23.0 +content_margin_bottom = 23.0 +bg_color = Color( 0.831373, 0.415686, 0.415686, 1 ) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="StyleBoxFlat" id=12] +content_margin_left = 23.0 +content_margin_right = 23.0 +content_margin_top = 23.0 +content_margin_bottom = 23.0 +bg_color = Color( 0.666667, 0.223529, 0.223529, 1 ) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="DynamicFont" id=13] +size = 64 +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=14] +size = 64 +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=15] +size = 32 +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=16] +size = 64 +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=17] +size = 64 +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=18] +size = 64 +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=19] +size = 32 +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=20] +size = 32 +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=21] +size = 32 +outline_color = Color( 0.854902, 0.0901961, 0.0901961, 1 ) +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=22] +size = 32 +font_data = ExtResource( 3 ) + +[node name="ResetPassword" type="TextureRect"] +margin_right = 40.0 +margin_bottom = 40.0 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="BackButton" parent="." instance=ExtResource( 2 )] +margin_left = 339.0 +margin_top = 117.0 +margin_right = 667.0 +margin_bottom = 235.0 +custom_styles/hover = SubResource( 1 ) +custom_styles/pressed = SubResource( 2 ) +custom_styles/normal = SubResource( 3 ) +custom_fonts/font = SubResource( 4 ) +text = "← Back" + +[node name="RequestFormContainer" type="VBoxContainer" parent="."] +visible = false +margin_left = 679.0 +margin_top = 196.75 +margin_right = 1327.0 +margin_bottom = 933.75 +custom_constants/separation = 65 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Label" type="Label" parent="RequestFormContainer"] +margin_right = 1035.0 +margin_bottom = 72.0 +custom_fonts/font = SubResource( 5 ) +text = "Reset password" +align = 1 + +[node name="LabelExplainer" type="Label" parent="RequestFormContainer"] +margin_top = 137.0 +margin_right = 1035.0 +margin_bottom = 174.0 +custom_fonts/font = SubResource( 6 ) +text = "Please enter your player name below." +autowrap = true + +[node name="FormContainer" type="HBoxContainer" parent="RequestFormContainer"] +margin_top = 239.0 +margin_right = 1035.0 +margin_bottom = 314.0 +custom_constants/separation = 50 + +[node name="FormLabels" type="VBoxContainer" parent="RequestFormContainer/FormContainer"] +margin_right = 426.0 +margin_bottom = 75.0 +custom_constants/separation = 30 + +[node name="PlayerNameLabel" type="Label" parent="RequestFormContainer/FormContainer/FormLabels"] +margin_right = 426.0 +margin_bottom = 72.0 +custom_fonts/font = SubResource( 7 ) +text = "Player name:" + +[node name="FormInputFields" type="VBoxContainer" parent="RequestFormContainer/FormContainer"] +margin_left = 476.0 +margin_right = 836.0 +margin_bottom = 75.0 +custom_constants/separation = 30 + +[node name="PlayerName" type="LineEdit" parent="RequestFormContainer/FormContainer/FormInputFields"] +margin_right = 360.0 +margin_bottom = 75.0 +rect_min_size = Vector2( 360, 75 ) +custom_fonts/font = SubResource( 8 ) +max_length = 50 + +[node name="ErrorMessage" type="Label" parent="RequestFormContainer"] +visible = false +margin_top = 522.0 +margin_right = 648.0 +margin_bottom = 559.0 +custom_fonts/font = SubResource( 9 ) +custom_colors/font_color = Color( 0.866667, 0.101961, 0.101961, 1 ) +autowrap = true + +[node name="PlayerNameSubmitButton" parent="RequestFormContainer" instance=ExtResource( 2 )] +margin_top = 379.0 +margin_right = 1035.0 +margin_bottom = 497.0 +custom_styles/hover = SubResource( 10 ) +custom_styles/pressed = SubResource( 11 ) +custom_styles/normal = SubResource( 12 ) +custom_fonts/font = SubResource( 13 ) +text = "Submit" + +[node name="ProcessingLabel" type="Label" parent="RequestFormContainer"] +visible = false +margin_top = 740.0 +margin_right = 648.0 +margin_bottom = 812.0 +custom_fonts/font = SubResource( 5 ) +text = "Processing..." +align = 1 + +[node name="PwdResetFormContainer" type="VBoxContainer" parent="."] +margin_left = 679.0 +margin_top = 196.75 +margin_right = 1714.0 +margin_bottom = 973.75 +custom_constants/separation = 70 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Label" type="Label" parent="PwdResetFormContainer"] +margin_right = 1035.0 +margin_bottom = 72.0 +custom_fonts/font = SubResource( 14 ) +text = "Reset password" +align = 1 + +[node name="LabelExplainer" type="Label" parent="PwdResetFormContainer"] +margin_top = 142.0 +margin_right = 1035.0 +margin_bottom = 219.0 +custom_fonts/font = SubResource( 15 ) +text = "Please enter below the code we sent you by email and your new password twice." +autowrap = true + +[node name="FormContainer" type="HBoxContainer" parent="PwdResetFormContainer"] +margin_top = 289.0 +margin_right = 1035.0 +margin_bottom = 574.0 +custom_constants/separation = 50 + +[node name="FormLabels" type="VBoxContainer" parent="PwdResetFormContainer/FormContainer"] +margin_right = 625.0 +margin_bottom = 285.0 +custom_constants/separation = 30 + +[node name="CodeLabel" type="Label" parent="PwdResetFormContainer/FormContainer/FormLabels"] +margin_right = 625.0 +margin_bottom = 72.0 +custom_fonts/font = SubResource( 16 ) +text = "Code:" + +[node name="PasswordLabel" type="Label" parent="PwdResetFormContainer/FormContainer/FormLabels"] +margin_top = 102.0 +margin_right = 625.0 +margin_bottom = 174.0 +custom_fonts/font = SubResource( 17 ) +text = "Password:" + +[node name="ConfirmPasswordLabel" type="Label" parent="PwdResetFormContainer/FormContainer/FormLabels"] +margin_top = 204.0 +margin_right = 625.0 +margin_bottom = 276.0 +custom_fonts/font = SubResource( 18 ) +text = "Confirm password:" + +[node name="FormInputFields" type="VBoxContainer" parent="PwdResetFormContainer/FormContainer"] +margin_left = 675.0 +margin_right = 1035.0 +margin_bottom = 285.0 +custom_constants/separation = 30 + +[node name="Code" type="LineEdit" parent="PwdResetFormContainer/FormContainer/FormInputFields"] +margin_right = 360.0 +margin_bottom = 75.0 +rect_min_size = Vector2( 360, 75 ) +custom_fonts/font = SubResource( 8 ) +max_length = 50 + +[node name="Password" type="LineEdit" parent="PwdResetFormContainer/FormContainer/FormInputFields"] +margin_top = 105.0 +margin_right = 360.0 +margin_bottom = 180.0 +rect_min_size = Vector2( 360, 75 ) +custom_fonts/font = SubResource( 19 ) +max_length = 30 +secret = true + +[node name="ConfirmPassword" type="LineEdit" parent="PwdResetFormContainer/FormContainer/FormInputFields"] +margin_top = 210.0 +margin_right = 360.0 +margin_bottom = 285.0 +rect_min_size = Vector2( 360, 75 ) +custom_fonts/font = SubResource( 20 ) +max_length = 30 +secret = true + +[node name="ErrorMessage" type="Label" parent="PwdResetFormContainer"] +visible = false +margin_top = 522.0 +margin_right = 648.0 +margin_bottom = 559.0 +custom_fonts/font = SubResource( 21 ) +custom_colors/font_color = Color( 0.866667, 0.101961, 0.101961, 1 ) +autowrap = true + +[node name="NewPasswordSubmitButton" parent="PwdResetFormContainer" instance=ExtResource( 2 )] +margin_top = 644.0 +margin_right = 1035.0 +margin_bottom = 762.0 +custom_styles/hover = SubResource( 10 ) +custom_styles/pressed = SubResource( 11 ) +custom_styles/normal = SubResource( 12 ) +custom_fonts/font = SubResource( 13 ) +text = "Submit" + +[node name="ProcessingLabel" type="Label" parent="PwdResetFormContainer"] +visible = false +margin_top = 740.0 +margin_right = 648.0 +margin_bottom = 812.0 +custom_fonts/font = SubResource( 22 ) +text = "Processing..." +align = 1 + +[node name="PasswordChangedContainer" type="VBoxContainer" parent="."] +visible = false +margin_left = 679.0 +margin_top = 312.0 +margin_right = 1327.0 +margin_bottom = 933.0 +custom_constants/separation = 100 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="PwdChanedLabel" type="Label" parent="PasswordChangedContainer"] +margin_right = 524.0 +margin_bottom = 72.0 +custom_fonts/font = SubResource( 5 ) +text = "Password reset" +align = 1 + +[node name="PasswordChangedLabelExplainer" type="Label" parent="PasswordChangedContainer"] +margin_top = 76.0 +margin_right = 524.0 +margin_bottom = 153.0 +custom_fonts/font = SubResource( 15 ) +text = "Your password was changed successfully." +autowrap = true + +[node name="CloseButton" parent="PasswordChangedContainer" instance=ExtResource( 2 )] +margin_top = 157.0 +margin_right = 524.0 +margin_bottom = 275.0 +custom_styles/hover = SubResource( 10 ) +custom_styles/pressed = SubResource( 11 ) +custom_styles/normal = SubResource( 12 ) +custom_fonts/font = SubResource( 13 ) +text = "Close" +[connection signal="pressed" from="BackButton" to="." method="_on_BackButton_pressed"] +[connection signal="pressed" from="RequestFormContainer/PlayerNameSubmitButton" to="." method="_on_PlayerNameSubmitButton_pressed"] +[connection signal="pressed" from="PwdResetFormContainer/NewPasswordSubmitButton" to="." method="_on_NewPasswordSubmitButton_pressed"] +[connection signal="pressed" from="PasswordChangedContainer/CloseButton" to="." method="_on_CloseButton_pressed"] diff --git a/addons/silent_wolf/Multiplayer/.DS_Store b/addons/silent_wolf/Multiplayer/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..936f46063ea34a080f9819adfbe579017317fa44 GIT binary patch literal 6148 zcmeHKQA@)x5Kgw~GKSCxg*^s*9k@;ziZ7+kKVU^4RAx(u7Hc!s&OVGmpY;#im+JZemK8D>r$ySs`W6sN~Yz+ z**#NP5hmGmt`m}Y3L&@GNfxWpSBospb#7oBq9ZyJr?*@l4*Sw|N2|VEy2In6z8twH zt5rwr?H`<8OrFx`OuZU1IdH9I$6^g{py<841oJFa*#p>hY#l>L3=jjv05L!e*xDev zLS5Z-NTn^J@x6nb3nTCk~76fYsx7cim+m70*C!I&*gY7V84v%Zi|;`2DO zyEz04-bCz7+5KkcXE*af_lGgY-DPMq)@O`a&=5H)8wAZ8T@4eA$kiM<%Yq`C2Z;6sPL$R{Q zC&xoEu}|0QuC;$~cy>8^PF_;^ritXhxsqLj6}*E|tLfESq=`%)!CvK5v4q3`F+dCu z1KY=dISZ`b_LWW*69dG+4-DY`AfO>S21|`<>wpfg&**O7FNF<5HU>5QwDVIH$`^?2cGb+8K+&bXtIdSZYWs58*i!#7x0(a z_{guP(1;iy2L2fXyfyWvE)-?X)^FwESu3DDKtsW}5)}~8YnK2R;65@?PVE<{L!4u< W)QGd7U8MukML-ck9Wn3=4156@M@&-y literal 0 HcmV?d00001 diff --git a/addons/silent_wolf/Scores/Leaderboard.gd b/addons/silent_wolf/Scores/Leaderboard.gd new file mode 100644 index 0000000..b6cee45 --- /dev/null +++ b/addons/silent_wolf/Scores/Leaderboard.gd @@ -0,0 +1,126 @@ +tool +extends Node2D + +const ScoreItem = preload("ScoreItem.tscn") +const SWLogger = preload("res://addons/silent_wolf/utils/SWLogger.gd") + +var list_index = 0 +# Replace the leaderboard name if you're not using the default leaderboard +var ld_name = "main" +var max_scores = 10 + +func _ready(): + print("SilentWolf.Scores.leaderboards: " + str(SilentWolf.Scores.leaderboards)) + print("SilentWolf.Scores.ldboard_config: " + str(SilentWolf.Scores.ldboard_config)) + #var scores = SilentWolf.Scores.scores + var scores = [] + if ld_name in SilentWolf.Scores.leaderboards: + scores = SilentWolf.Scores.leaderboards[ld_name] + var local_scores = SilentWolf.Scores.local_scores + + if len(scores) > 0: + render_board(scores, local_scores) + else: + # use a signal to notify when the high scores have been returned, and show a "loading" animation until it's the case... + add_loading_scores_message() + yield(SilentWolf.Scores.get_high_scores(), "sw_scores_received") + hide_message() + render_board(SilentWolf.Scores.scores, local_scores) + + +func render_board(scores, local_scores): + var all_scores = scores + if ld_name in SilentWolf.Scores.ldboard_config and is_default_leaderboard(SilentWolf.Scores.ldboard_config[ld_name]): + all_scores = merge_scores_with_local_scores(scores, local_scores, max_scores) + if !scores and !local_scores: + add_no_scores_message() + else: + if !scores: + add_no_scores_message() + if !all_scores: + for score in scores: + add_item(score.player_name, str(int(score.score))) + else: + for score in all_scores: + add_item(score.player_name, str(int(score.score))) + + +func is_default_leaderboard(ld_config): + var default_insert_opt = (ld_config.insert_opt == "keep") + var not_time_based = !("time_based" in ld_config) + return default_insert_opt and not_time_based + + +func merge_scores_with_local_scores(scores, local_scores, max_scores=10): + if local_scores: + for score in local_scores: + var in_array = score_in_score_array(scores, score) + if !in_array: + scores.append(score) + scores.sort_custom(self, "sort_by_score"); + var return_scores = scores + if scores.size() > max_scores: + return_scores = scores.resize(max_scores) + return return_scores + + +func sort_by_score(a, b): + if a.score > b.score: + return true; + else: + if a.score < b.score: + return false; + else: + return true; + + +func score_in_score_array(scores, new_score): + var in_score_array = false + if new_score and scores: + for score in scores: + if score.score_id == new_score.score_id: # score.player_name == new_score.player_name and score.score == new_score.score: + in_score_array = true + return in_score_array + + +func add_item(player_name, score): + var item = ScoreItem.instance() + list_index += 1 + item.get_node("PlayerName").text = str(list_index) + str(". ") + player_name + item.get_node("Score").text = score + item.margin_top = list_index * 100 + $"Board/HighScores/ScoreItemContainer".add_child(item) + + +func add_no_scores_message(): + var item = $"Board/MessageContainer/TextMessage" + item.text = "No scores yet!" + $"Board/MessageContainer".show() + item.margin_top = 135 + + +func add_loading_scores_message(): + var item = $"Board/MessageContainer/TextMessage" + item.text = "Loading scores..." + $"Board/MessageContainer".show() + item.margin_top = 135 + + +func hide_message(): + $"Board/MessageContainer".hide() + + +func clear_leaderboard(): + var score_item_container = $"Board/HighScores/ScoreItemContainer" + if score_item_container.get_child_count() > 0: + var children = score_item_container.get_children() + for c in children: + score_item_container.remove_child(c) + c.queue_free() + + +func _on_CloseButton_pressed(): + var scene_name = SilentWolf.scores_config.open_scene_on_close + SWLogger.info("Closing SilentWolf leaderboard, switching to scene: " + str(scene_name)) + #global.reset() + get_tree().change_scene(scene_name) diff --git a/addons/silent_wolf/Scores/Leaderboard.tscn b/addons/silent_wolf/Scores/Leaderboard.tscn new file mode 100644 index 0000000..1065998 --- /dev/null +++ b/addons/silent_wolf/Scores/Leaderboard.tscn @@ -0,0 +1,88 @@ +[gd_scene load_steps=8 format=2] + +[ext_resource path="res://addons/silent_wolf/Scores/Leaderboard.gd" type="Script" id=1] +[ext_resource path="res://addons/silent_wolf/Scores/assets/fonts/Comfortaa-Bold.ttf" type="DynamicFontData" id=2] +[ext_resource path="res://addons/silent_wolf/assets/fonts/Comfortaa-Bold.ttf" type="DynamicFontData" id=3] +[ext_resource path="res://addons/silent_wolf/common/SWButton.tscn" type="PackedScene" id=4] + +[sub_resource type="DynamicFont" id=1] +size = 76 +font_data = ExtResource( 2 ) + +[sub_resource type="DynamicFont" id=2] +size = 32 +font_data = ExtResource( 3 ) + +[sub_resource type="Theme" id=3] + +[node name="Leaderboard" type="Node2D"] +script = ExtResource( 1 ) + +[node name="OldBoard" type="MarginContainer" parent="."] +visible = false +margin_left = 50.0 +margin_right = 50.0 +margin_bottom = 40.0 + +[node name="HighScores" type="TextureRect" parent="OldBoard"] +margin_bottom = 40.0 + +[node name="Board" type="VBoxContainer" parent="."] +margin_left = 20.0 +margin_top = 20.0 +margin_right = 1884.0 +margin_bottom = 1071.0 +custom_constants/separation = 0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="TitleContainer" type="CenterContainer" parent="Board"] +margin_right = 1864.0 +margin_bottom = 85.0 + +[node name="Label" type="Label" parent="Board/TitleContainer"] +margin_left = 667.0 +margin_right = 1196.0 +margin_bottom = 85.0 +custom_fonts/font = SubResource( 1 ) +text = "Leaderboard" + +[node name="MessageContainer" type="CenterContainer" parent="Board"] +visible = false +margin_top = 89.0 +margin_right = 1864.0 +margin_bottom = 126.0 + +[node name="TextMessage" type="Label" parent="Board/MessageContainer"] +margin_left = 789.0 +margin_right = 1075.0 +margin_bottom = 37.0 +custom_fonts/font = SubResource( 2 ) +text = "Loading scores..." +valign = 1 + +[node name="HighScores" type="CenterContainer" parent="Board"] +margin_top = 85.0 +margin_right = 1864.0 +margin_bottom = 185.0 +rect_min_size = Vector2( 0, 100 ) +theme = SubResource( 3 ) + +[node name="ScoreItemContainer" type="VBoxContainer" parent="Board/HighScores"] +margin_left = 932.0 +margin_top = 50.0 +margin_right = 932.0 +margin_bottom = 50.0 + +[node name="CloseButtonContainer" type="CenterContainer" parent="Board"] +margin_top = 185.0 +margin_right = 1864.0 +margin_bottom = 303.0 + +[node name="CloseButton" parent="Board/CloseButtonContainer" instance=ExtResource( 4 )] +margin_left = 582.0 +margin_right = 1281.0 +margin_bottom = 118.0 +text = "Close Leaderboard" +[connection signal="pressed" from="Board/CloseButtonContainer/CloseButton" to="." method="_on_CloseButton_pressed"] diff --git a/addons/silent_wolf/Scores/ScoreItem.tscn b/addons/silent_wolf/Scores/ScoreItem.tscn new file mode 100644 index 0000000..67f91fd --- /dev/null +++ b/addons/silent_wolf/Scores/ScoreItem.tscn @@ -0,0 +1,48 @@ +[gd_scene load_steps=5 format=2] + +[ext_resource path="res://addons/silent_wolf/assets/fonts/Comfortaa-Bold.ttf" type="DynamicFontData" id=2] + +[sub_resource type="StyleBoxFlat" id=1] +bg_color = Color( 0.831373, 0.415686, 0.415686, 1 ) + +[sub_resource type="DynamicFont" id=2] +size = 32 +font_data = ExtResource( 2 ) + +[sub_resource type="DynamicFont" id=3] +size = 40 +use_mipmaps = true +use_filter = true +font_data = ExtResource( 2 ) + +[node name="ScoreItem" type="Panel"] +margin_right = 782.0 +margin_bottom = 76.0 +grow_horizontal = 0 +grow_vertical = 0 +rect_min_size = Vector2( 782, 76 ) +custom_styles/panel = SubResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="PlayerName" type="RichTextLabel" parent="."] +margin_left = 22.0 +margin_top = 18.0 +margin_right = 589.0 +margin_bottom = 73.0 +custom_fonts/normal_font = SubResource( 2 ) +text = "1. Godzilla" + +[node name="Score" type="Label" parent="."] +margin_left = 671.0 +margin_top = 17.0 +margin_right = 756.0 +margin_bottom = 65.0 +custom_fonts/font = SubResource( 3 ) +text = "13" +align = 2 +valign = 1 +__meta__ = { +"_edit_use_anchors_": false +} diff --git a/addons/silent_wolf/Scores/Scores.gd b/addons/silent_wolf/Scores/Scores.gd new file mode 100644 index 0000000..b47262d --- /dev/null +++ b/addons/silent_wolf/Scores/Scores.gd @@ -0,0 +1,528 @@ +extends Node + +const CommonErrors = preload("res://addons/silent_wolf/common/CommonErrors.gd") +const SWLogger = preload("res://addons/silent_wolf/utils/SWLogger.gd") +const UUID = preload("res://addons/silent_wolf/utils/UUID.gd") +const SWHashing = preload("res://addons/silent_wolf/utils/SWHashing.gd") + +# legacy signals +signal scores_received +signal position_received +signal score_posted + +# new signals +signal sw_scores_received +signal sw_player_scores_received +signal sw_top_player_score_received +signal sw_position_received +signal sw_scores_around_received +signal sw_score_posted +signal sw_leaderboard_wiped +signal sw_score_deleted + +# leaderboard scores by leaderboard name +var leaderboards = {} +# leaderboard scores from past periods by leaderboard name and period_offset (negative integers) +var leaderboards_past_periods = {} +# leaderboard configurations by leaderboard name +var ldboard_config = {} + +# contains only the scores from one leaderboard at a time +var scores = [] +var player_scores = [] +var player_top_score = null +var local_scores = [] +#var custom_local_scores = [] +var score_id = "" +var position = 0 +var scores_above = [] +var scores_below = [] + +#var request_timeout = 3 +#var request_timer = null + +# latest number of scores to be fetched from the backend +var latest_max = 10 + +var ScorePosition = null +var ScoresAround = null +var HighScores = null +var ScoresByPlayer = null +var TopScoreByPlayer = null +var PostScore = null +var WipeLeaderboard = null +var DeleteScore = null + +# wekrefs +var wrScorePosition = null +var wrScoresAround = null +var wrHighScores = null +var wrScoresByPlayer = null +var wrTopScoreByPlayer = null +var wrPostScore = null +var wrWipeLeaderboard = null +var wrDeleteScore = null + +# Called when the node enters the scene tree for the first time. +func _ready(): + pass + #connect("request_completed", self, "_on_Scores_request_completed") + #setup_request_timer() + +func get_score_position(score, ldboard_name="main"): + var score_id = null + var score_value = null + print("score: " + str(score)) + if UUID.is_uuid(str(score)): + score_id = score + else: + score_value = score + ScorePosition = HTTPRequest.new() + wrScorePosition = weakref(ScorePosition) + if OS.get_name() != "HTML5": + ScorePosition.set_use_threads(true) + get_tree().get_root().call_deferred("add_child", ScorePosition) + ScorePosition.connect("request_completed", self, "_on_GetScorePosition_request_completed") + SWLogger.info("Calling SilentWolf to get score position") + var game_id = SilentWolf.config.game_id + var game_version = SilentWolf.config.game_version + var payload = { "game_id": game_id, "game_version": game_version, "ldboard_name": ldboard_name } + if score_id: + payload["score_id"] = score_id + if score_value: + payload["score_value"] = score_value + var request_url = "https://api.silentwolf.com/get_score_position" + send_post_request(ScorePosition, request_url, payload) + return self + + +func get_scores_around(score, scores_to_fetch=3, ldboard_name="main"): + var score_id = null + var score_value = null + print("score: " + str(score)) + if UUID.is_uuid(str(score)): + score_id = score + else: + score_value = score + ScoresAround = HTTPRequest.new() + wrScoresAround = weakref(ScoresAround) + if OS.get_name() != "HTML5": + ScoresAround.set_use_threads(true) + get_tree().get_root().call_deferred("add_child",ScoresAround) + ScoresAround.connect("request_completed", self, "_on_ScoresAround_request_completed") + SWLogger.info("Calling SilentWolf backend to scores above and below a certain score...") + # resetting the latest_number value in case the first requests times out, we need to request the same amount of top scores in the retry + #latest_max = maximum + var game_id = SilentWolf.config.game_id + var game_version = SilentWolf.config.game_version + var request_url = "https://api.silentwolf.com/get_scores_around/" + str(game_id) + "?version=" + str(game_version) + "&scores_to_fetch=" + str(scores_to_fetch) + "&ldboard_name=" + str(ldboard_name) + "&score_id=" + str(score_id) + "&score_value=" + str(score_value) + send_get_request(ScoresAround, request_url) + return self + +func get_high_scores(maximum=10, ldboard_name="main", period_offset=0): + HighScores = HTTPRequest.new() + wrHighScores = weakref(HighScores) + if OS.get_name() != "HTML5": + HighScores.set_use_threads(true) + get_tree().get_root().call_deferred("add_child", HighScores) + HighScores.connect("request_completed", self, "_on_GetHighScores_request_completed") + SWLogger.info("Calling SilentWolf backend to get scores...") + # resetting the latest_number value in case the first requests times out, we need to request the same amount of top scores in the retry + latest_max = maximum + var game_id = SilentWolf.config.game_id + var game_version = SilentWolf.config.game_version + var request_url = "https://api.silentwolf.com/get_top_scores/" + str(game_id) + "?version=" + str(game_version) + "&max=" + str(maximum) + "&ldboard_name=" + str(ldboard_name) + "&period_offset=" + str(period_offset) + send_get_request(HighScores, request_url) + return self + + +func get_scores_by_player(player_name, maximum=10, ldboard_name="main", period_offset=0): + print("get_scores_by_player, player_name = " + str(player_name)) + if player_name == null: + SWLogger.error("Error in SilentWolf.Scores.get_scores_by_player: provided player_name is null") + else: + ScoresByPlayer = HTTPRequest.new() + wrScoresByPlayer = weakref(ScoresByPlayer) + if OS.get_name() != "HTML5": + ScoresByPlayer.set_use_threads(true) + get_tree().get_root().call_deferred("add_child", ScoresByPlayer) + ScoresByPlayer.connect("request_completed", self, "_on_GetScoresByPlayer_request_completed") + SWLogger.info("Calling SilentWolf backend to get scores for player: " + str(player_name)) + # resetting the latest_number value in case the first requests times out, we need to request the same amount of top scores in the retry + latest_max = maximum + var game_id = SilentWolf.config.game_id + var game_version = SilentWolf.config.game_version + print("ldboard_name: " + str(ldboard_name)) + var request_url = "https://api.silentwolf.com/get_scores_by_player/" + str(game_id) + "?version=" + str(game_version) + "&max=" + str(maximum) + "&ldboard_name=" + str(ldboard_name.percent_encode()) + "&player_name=" + str(player_name.percent_encode()) + "&period_offset=" + str(period_offset) + send_get_request(ScoresByPlayer, request_url) + return self + + +func get_top_score_by_player(player_name, maximum=10, ldboard_name="main", period_offset=0): + SWLogger.info("get_top_score_by_player, player_name = " + str(player_name)) + if player_name == null: + SWLogger.error("Error in SilentWolf.Scores.get_top_score_by_player: provided player_name is null") + else: + TopScoreByPlayer = HTTPRequest.new() + wrTopScoreByPlayer = weakref(TopScoreByPlayer) + if OS.get_name() != "HTML5": + TopScoreByPlayer.set_use_threads(true) + get_tree().get_root().call_deferred("add_child", TopScoreByPlayer) + TopScoreByPlayer.connect("request_completed", self, "_on_GetTopScoreByPlayer_request_completed") + SWLogger.info("Calling SilentWolf backend to get scores for player: " + str(player_name)) + # resetting the latest_number value in case the first requests times out, we need to request the same amount of top scores in the retry + latest_max = maximum + var game_id = SilentWolf.config.game_id + var game_version = SilentWolf.config.game_version + var request_url = "https://api.silentwolf.com/get_top_score_by_player/" + str(game_id) + "?version=" + str(game_version) + "&max=" + str(maximum) + "&ldboard_name=" + str(ldboard_name.percent_encode()) + "&player_name=" + str(player_name.percent_encode()) + "&period_offset=" + str(period_offset) + send_get_request(TopScoreByPlayer, request_url) + return self + + +func add_to_local_scores(game_result, ld_name="main"): + var local_score = { "score_id": game_result.score_id, "game_id_version" : game_result.game_id + ";" + game_result.game_version, "player_name": game_result.player_name, "score": game_result.score } + local_scores.append(local_score) + #if ld_name == "main": + # TODO: even here, since the main leader board can be customized, we can't just blindly write to the local_scores variable and pull up the scores later + # we need to know what type of leader board it is, or local caching is useless + #local_scores.append(local_score) + #else: + #if ld_name in custom_local_scores: + # TODO: problem: can't just append here - what if it's a highest/latest/accumulator/time-based leaderboard? + # maybe don't use local scores for these special cases? performance? + #custom_local_scores[ld_name].append(local_score) + #else: + #custom_local_scores[ld_name] = [local_score] + SWLogger.debug("local scores: " + str(local_scores)) + + +# metadata, if included should be a dictionary +func persist_score(player_name, score, ldboard_name="main", metadata={}): + # player_name must be present + if player_name == null or player_name == "": + SWLogger.error("ERROR in SilentWolf.Scores.persist_score - please enter a valid player name") + elif typeof(ldboard_name) != TYPE_STRING: + # check that ldboard_name, if present is a String + SWLogger.error("ERROR in ilentWolf.Scores.persist_score - leaderboard name must be a String") + elif typeof(metadata) != TYPE_DICTIONARY: + # check that metadata, if present, is a dictionary + SWLogger.error("ERROR in SilentWolf.Scores.persist_score - metadata must be a dictionary") + else: + PostScore = HTTPRequest.new() + wrPostScore = weakref(PostScore) + if OS.get_name() != "HTML5": + PostScore.set_use_threads(true) + get_tree().get_root().call_deferred("add_child", PostScore) + PostScore.connect("request_completed", self, "_on_PostNewScore_request_completed") + SWLogger.info("Calling SilentWolf backend to post new score...") + var game_id = SilentWolf.config.game_id + var game_version = SilentWolf.config.game_version + + var score_uuid = UUID.generate_uuid_v4() + score_id = score_uuid + var payload = { + "score_id" : score_id, + "player_name" : player_name, + "game_id": game_id, + "game_version": game_version, + "score": score, + "ldboard_name": ldboard_name + } + print("!metadata.empty(): " + str(!metadata.empty())) + if !metadata.empty(): + print("metadata: " + str(metadata)) + payload["metadata"] = metadata + SWLogger.debug("payload: " + str(payload)) + # also add to local scores + add_to_local_scores(payload) + var request_url = "https://api.silentwolf.com/post_new_score" + send_post_request(PostScore, request_url, payload) + return self + +# Deletes all your scores for your game and version +# Scores are permanently deleted, no going back! +func wipe_leaderboard(ldboard_name='main'): + WipeLeaderboard = HTTPRequest.new() + wrWipeLeaderboard = weakref(WipeLeaderboard) + if OS.get_name() != "HTML5": + WipeLeaderboard.set_use_threads(true) + get_tree().get_root().call_deferred("add_child", WipeLeaderboard) + WipeLeaderboard.connect("request_completed", self, "_on_WipeLeaderboard_request_completed") + SWLogger.info("Calling SilentWolf backend to wipe leaderboard...") + var game_id = SilentWolf.config.game_id + var game_version = SilentWolf.config.game_version + var payload = { "game_id": game_id, "game_version": game_version, "ldboard_name": ldboard_name } + var request_url = "https://api.silentwolf.com/wipe_leaderboard" + send_post_request(WipeLeaderboard, request_url, payload) + return self + + +func delete_score(score_id): + DeleteScore = HTTPRequest.new() + wrDeleteScore = weakref(DeleteScore) + if OS.get_name() != "HTML5": + DeleteScore.set_use_threads(true) + get_tree().get_root().call_deferred("add_child", DeleteScore) + DeleteScore.connect("request_completed", self, "_on_DeleteScore_request_completed") + SWLogger.info("Calling SilentWolf to delete a score") + var game_id = SilentWolf.config.game_id + var game_version = SilentWolf.config.game_version + var request_url = "https://api.silentwolf.com/delete_score?game_id=" + str(game_id) + "&game_version=" + str(game_version) + "&score_id=" + str(score_id) + send_get_request(DeleteScore, request_url) + return self + + +func _on_GetScoresByPlayer_request_completed(result, response_code, headers, body): + SWLogger.info("GetScoresByPlayer request completed") + var status_check = CommonErrors.check_status_code(response_code) + #print("client status: " + str(HighScores.get_http_client_status())) + #HighScores.queue_free() + SilentWolf.free_request(wrScoresByPlayer, ScoresByPlayer) + SWLogger.debug("response code: " + str(response_code)) + SWLogger.debug("response headers: " + str(headers)) + SWLogger.debug("response body: " + str(body.get_string_from_utf8())) + + if status_check: + var json = JSON.parse(body.get_string_from_utf8()) + print("json: " + str(json)) + var response = json.result + if response == null: + SWLogger.error("No data returned in GetScoresByPlayer response. Leaderboard may be empty") + emit_signal("sw_player_scores_received", 'No Leaderboard found', scores) + elif "message" in response.keys() and response.message == "Forbidden": + SWLogger.error("You are not authorized to call the SilentWolf API - check your API key configuration: https://silentwolf.com/leaderboard") + else: + SWLogger.info("SilentWolf get scores by player success") + if "top_scores" in response: + player_scores = response.top_scores + SWLogger.debug("scores: " + str(scores)) + var ld_name = response.ld_name + #print("ld_name: " + str(ld_name)) + var ld_config = response.ld_config + var player_name = response.player_name + #print("latest_scores: " + str(leaderboards)) + emit_signal("sw_player_scores_received", player_scores) + + +func _on_GetTopScoreByPlayer_request_completed(result, response_code, headers, body): + SWLogger.info("GetTopScoreByPlayer request completed") + var status_check = CommonErrors.check_status_code(response_code) + SilentWolf.free_request(wrTopScoreByPlayer, TopScoreByPlayer) + SWLogger.debug("response code: " + str(response_code)) + SWLogger.debug("response headers: " + str(headers)) + SWLogger.debug("response body: " + str(body.get_string_from_utf8())) + + if status_check: + var json = JSON.parse(body.get_string_from_utf8()) + print("json: " + str(json)) + var response = json.result + if response == null: + SWLogger.error("No data returned in GetTopScoreByPlayer response. There was a problem getting the response from the backend.") + emit_signal("sw_player_scores_received", 'No Leaderboard found', scores) + elif "message" in response.keys() and response.message == "Forbidden": + SWLogger.error("You are not authorized to call the SilentWolf API - check your API key configuration: https://silentwolf.com/leaderboard") + else: + SWLogger.info("SilentWolf get top score by player success") + if "top_score" in response: + print("top score from response: " + str(response.top_score)) + if response.top_score == {}: + player_top_score = {} + else: + player_top_score = response.top_score + SWLogger.debug("top score: " + str(player_top_score)) + var ld_name = response.ld_name + #print("ld_name: " + str(ld_name)) + var ld_config = response.ld_config + var player_name = response.player_name + #print("latest_scores: " + str(leaderboards)) + emit_signal("sw_top_player_score_received", player_top_score) + + + +func _on_GetHighScores_request_completed(result, response_code, headers, body): + SWLogger.info("GetHighScores request completed") + var status_check = CommonErrors.check_status_code(response_code) + #print("client status: " + str(HighScores.get_http_client_status())) + #HighScores.queue_free() + SilentWolf.free_request(wrHighScores, HighScores) + SWLogger.debug("response code: " + str(response_code)) + SWLogger.debug("response headers: " + str(headers)) + SWLogger.debug("response body: " + str(body.get_string_from_utf8())) + + if status_check: + var json = JSON.parse(body.get_string_from_utf8()) + var response = json.result + if response == null: + SWLogger.error("No data returned in GetHighScores response. Leaderboard may be empty") + emit_signal("sw_scores_received", 'No Leaderboard found', scores) + emit_signal("scores_received", scores) + elif "message" in response.keys() and response.message == "Forbidden": + SWLogger.error("You are not authorized to call the SilentWolf API - check your API key configuration: https://silentwolf.com/leaderboard") + else: + SWLogger.info("SilentWolf get high score success") + if "top_scores" in response: + scores = response.top_scores + SWLogger.debug("scores: " + str(scores)) + var ld_name = response.ld_name + #print("ld_name: " + str(ld_name)) + var ld_config = response.ld_config + #print("ld_config: " + str(ld_config)) + if "period_offset" in response: + var period_offset = str(response["period_offset"]) + leaderboards_past_periods[ld_name + ";" + period_offset] = scores + else: + leaderboards[ld_name] = scores + ldboard_config[ld_name] = ld_config + #print("latest_scores: " + str(leaderboards)) + emit_signal("sw_scores_received", ld_name, scores) + emit_signal("scores_received", scores) + #var retries = 0 + #request_timer.stop() + +func _on_DeleteScore_request_completed(result, response_code, headers, body): + SWLogger.info("DeleteScore request completed") + var status_check = CommonErrors.check_status_code(response_code) + SilentWolf.free_request(wrDeleteScore, DeleteScore) + SWLogger.debug("response headers: " + str(response_code)) + SWLogger.debug("response headers: " + str(headers)) + SWLogger.debug("response body: " + str(body.get_string_from_utf8())) + + if status_check: + var json = JSON.parse(body.get_string_from_utf8()) + var response = json.result + if "message" in response.keys() and response.message == "Forbidden": + SWLogger.error("You are not authorized to call the SilentWolf API - check your API key configuration: https://silentwolf.com/leaderboard") + else: + SWLogger.info("SilentWolf delete score success") + emit_signal("sw_score_deleted") + + +func _on_PostNewScore_request_completed(result, response_code, headers, body): + SWLogger.info("PostNewScore request completed") + var status_check = CommonErrors.check_status_code(response_code) + #PostScore.queue_free() + SilentWolf.free_request(wrPostScore, PostScore) + SWLogger.debug("response headers: " + str(response_code)) + SWLogger.debug("response headers: " + str(headers)) + SWLogger.debug("response body: " + str(body.get_string_from_utf8())) + + if status_check: + var json = JSON.parse(body.get_string_from_utf8()) + var response = json.result + if "message" in response.keys() and response.message == "Forbidden": + SWLogger.error("You are not authorized to call the SilentWolf API - check your API key configuration: https://silentwolf.com/leaderboard") + else: + SWLogger.info("SilentWolf post score success: " + str(response_code)) + if "score_id" in response: + emit_signal("sw_score_posted", response["score_id"]) + else: + emit_signal("sw_score_posted") + emit_signal("score_posted") + + +func _on_GetScorePosition_request_completed(result, response_code, headers, body): + SWLogger.info("GetScorePosition request completed") + var status_check = CommonErrors.check_status_code(response_code) + #ScorePosition.queue_free() + SilentWolf.free_request(wrScorePosition, ScorePosition) + SWLogger.debug("response headers: " + str(response_code)) + SWLogger.debug("response headers: " + str(headers)) + SWLogger.debug("response body: " + str(body.get_string_from_utf8())) + + if status_check: + var json = JSON.parse(body.get_string_from_utf8()) + var response = json.result + if "message" in response.keys() and response.message == "Forbidden": + SWLogger.error("You are not authorized to call the SilentWolf API - check your API key configuration: https://silentwolf.com/leaderboard") + else: + SWLogger.info("SilentWolf find score position success.") + position = int(response.position) + emit_signal("sw_position_received", position) + emit_signal("position_received", position) + + +func _on_ScoresAround_request_completed(result, response_code, headers, body): + SWLogger.info("ScoresAround request completed") + var status_check = CommonErrors.check_status_code(response_code) + + SilentWolf.free_request(wrScoresAround, ScoresAround) + SWLogger.debug("response headers: " + str(response_code)) + SWLogger.debug("response headers: " + str(headers)) + SWLogger.debug("response body: " + str(body.get_string_from_utf8())) + + if status_check: + var json = JSON.parse(body.get_string_from_utf8()) + var response = json.result + if "message" in response.keys() and response.message == "Forbidden": + SWLogger.error("You are not authorized to call the SilentWolf API - check your API key configuration: https://silentwolf.com/leaderboard") + else: + SWLogger.info("SilentWolf get scores around success") + if "scores_above" in response: + scores_above = response.scores_above + scores_below = response.scores_below + var ld_name = response.ld_name + #print("ld_name: " + str(ld_name)) + var ld_config = response.ld_config + #print("ld_config: " + str(ld_config)) + ldboard_config[ld_name] = ld_config + if "score_position" in response: + position = response.score_position + + emit_signal("sw_scores_around_received", scores_above, scores_below, position) + + +func _on_WipeLeaderboard_request_completed(result, response_code, headers, body): + SWLogger.info("WipeLeaderboard request completed") + var status_check = CommonErrors.check_status_code(response_code) + #WipeLeaderboard.queue_free() + SilentWolf.free_request(wrWipeLeaderboard, WipeLeaderboard) + SWLogger.debug("response headers: " + str(response_code)) + SWLogger.debug("response headers: " + str(headers)) + SWLogger.debug("response body: " + str(body.get_string_from_utf8())) + + if status_check: + var json = JSON.parse(body.get_string_from_utf8()) + var response = json.result + if "message" in response.keys() and response.message == "Forbidden": + SWLogger.error("You are not authorized to call the SilentWolf API - check your API key configuration: https://silentwolf.com/leaderboard") + else: + SWLogger.info("SilentWolf wipe leaderboard success.") + emit_signal("sw_leaderboard_wiped") + + +func send_get_request(http_node, request_url): + var headers = ["x-api-key: " + SilentWolf.config.api_key, "x-sw-plugin-version: " + SilentWolf.version] + if !http_node.is_inside_tree(): + yield(get_tree().create_timer(0.01), "timeout") + SWLogger.debug("Method: GET") + SWLogger.debug("request_url: " + str(request_url)) + SWLogger.debug("headers: " + str(headers)) + http_node.request(request_url, headers) + + +func send_post_request(http_node, request_url, payload): + var headers = [ + "Content-Type: application/json", + "x-api-key: " + SilentWolf.config.api_key, + "x-sw-plugin-version: " + SilentWolf.version + ] + if "post_new_score" in request_url: + SWLogger.info("We're doing a post score") + var player_name = payload["player_name"] + var player_score = payload["score"] + var timestamp = OS.get_system_time_msecs() + var to_be_hashed = [player_name, player_score, timestamp] + SWLogger.debug("send_post_request to_be_hashed: " + str(to_be_hashed)) + var hashed = SWHashing.hash_values(to_be_hashed) + SWLogger.debug("send_post_request hashed: " + str(hashed)) + headers.append("x-sw-act-tmst: " + str(timestamp)) + headers.append("x-sw-act-dig: " + hashed) + var use_ssl = true + if !http_node.is_inside_tree(): + yield(get_tree().create_timer(0.01), "timeout") + var query = JSON.print(payload) + SWLogger.info("Method: POST") + SWLogger.info("request_url: " + str(request_url)) + SWLogger.info("headers: " + str(headers)) + SWLogger.info("query: " + str(query)) + http_node.request(request_url, headers, use_ssl, HTTPClient.METHOD_POST, query) diff --git a/addons/silent_wolf/Scores/assets/.DS_Store b/addons/silent_wolf/Scores/assets/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..3232312723b5ba83cda4423a54f6d35c5d65589d GIT binary patch literal 6148 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 ZScLzAQ@Hdr(!!Vbl%O*HAm9Um{{nVbme2qI literal 0 HcmV?d00001 diff --git a/addons/silent_wolf/SilentWolf.gd b/addons/silent_wolf/SilentWolf.gd new file mode 100644 index 0000000..7acb1ce --- /dev/null +++ b/addons/silent_wolf/SilentWolf.gd @@ -0,0 +1,178 @@ +extends Node + +const version = "0.6.20" +var godot_version = Engine.get_version_info().string + +const SWHashing = preload("res://addons/silent_wolf/utils/SWHashing.gd") + +onready var Auth = Node.new() +onready var Scores = Node.new() +onready var Players = Node.new() +onready var Multiplayer = Node.new() + +# +# SILENTWOLF CONFIG: THE CONFIG VARIABLES BELOW WILL BE OVERRIDED THE +# NEXT TIME YOU UPDATE YOUR PLUGIN! +# +# As a best practice, use SilentWolf.configure from your game's +# code instead to set the SilentWolf configuration. +# +# See https://silentwolf.com for more details +# +var config = { + "api_key": "FmKF4gtm0Z2RbUAEU62kZ2OZoYLj4PYOURAPIKEY", + "game_id": "YOURGAMEID", + "game_version": "0.0.0", + "log_level": 0 +} + +var scores_config = { + "open_scene_on_close": "res://scenes/Splash.tscn" +} + +var auth_config = { + "redirect_to_scene": "res://scenes/Splash.tscn", + "login_scene": "res://addons/silent_wolf/Auth/Login.tscn", + "email_confirmation_scene": "res://addons/silent_wolf/Auth/ConfirmEmail.tscn", + "reset_password_scene": "res://addons/silent_wolf/Auth/ResetPassword.tscn", + "session_duration_seconds": 0, + "saved_session_expiration_days": 30 +} + +var SWLogger = load("res://addons/silent_wolf/utils/SWLogger.gd") + +var auth_script = load("res://addons/silent_wolf/Auth/Auth.gd") +var scores_script = load("res://addons/silent_wolf/Scores/Scores.gd") +var players_script = load("res://addons/silent_wolf/Players/Players.gd") +var multiplayer_script = load("res://addons/silent_wolf/Multiplayer/Multiplayer.gd") + +func _init(): + print("SW Init timestamp: " + str(OS.get_time())) + +func _ready(): + # The following line would keep SilentWolf working even if the game tree is paused. + #pause_mode = Node.PAUSE_MODE_PROCESS + print("SW ready start timestamp: " + str(OS.get_time())) + Auth.set_script(auth_script) + add_child(Auth) + Scores.set_script(scores_script) + add_child(Scores) + Players.set_script(players_script) + add_child(Players) + Multiplayer.set_script(multiplayer_script) + add_child(Multiplayer) + print("SW ready end timestamp: " + str(OS.get_time())) + +func configure(json_config): + config = json_config + +func configure_api_key(api_key): + config.apiKey = api_key + +func configure_game_id(game_id): + config.game_id = game_id + +func configure_game_version(game_version): + config.game_version = game_version + +################################################################## +# Log levels: +# 0 - error (only log errors) +# 1 - info (log errors and the main actions taken by the SilentWolf plugin) - default setting +# 2 - debug (detailed logs, including the above and much more, to be used when investigating a problem). This shouldn't be the default setting in production. +################################################################## +func configure_log_level(log_level): + config.log_level = log_level + +func configure_scores(json_scores_config): + scores_config = json_scores_config + +func configure_scores_open_scene_on_close(scene): + scores_config.open_scene_on_close = scene + +func configure_auth(json_auth_config): + auth_config = json_auth_config + +func configure_auth_redirect_to_scene(scene): + auth_config.open_scene_on_close = scene + +func configure_auth_session_duration(duration): + auth_config.session_duration = duration + + +func free_request(weak_ref, object): + if (weak_ref.get_ref()): + object.queue_free() + + +func send_get_request(http_node, request_url): + var headers = [ + "x-api-key: " + SilentWolf.config.api_key, + "x-sw-game-id: " + SilentWolf.config.game_id, + "x-sw-plugin-version: " + SilentWolf.version, + "x-sw-godot-version: " + godot_version + ] + if !http_node.is_inside_tree(): + yield(get_tree().create_timer(0.01), "timeout") + SWLogger.debug("Method: GET") + SWLogger.debug("request_url: " + str(request_url)) + SWLogger.debug("headers: " + str(headers)) + http_node.request(request_url, headers) + + +func send_post_request(http_node, request_url, payload): + var headers = [ + "Content-Type: application/json", + "x-api-key: " + SilentWolf.config.api_key, + "x-sw-game-id: " + SilentWolf.config.game_id, + "x-sw-plugin-version: " + SilentWolf.version, + "x-sw-godot-version: " + godot_version + ] + # TODO: this os specific to post_new_score - should be made generic + # or make a section for each type of post request with inetgrity check + # (e.g. also push player data) + if "post_new_score" in request_url: + SWLogger.info("We're doing a post score") + var player_name = payload["player_name"] + var player_score = payload["score"] + var timestamp = OS.get_system_time_msecs() + var to_be_hashed = [player_name, player_score, timestamp] + SWLogger.debug("send_post_request to_be_hashed: " + str(to_be_hashed)) + var hashed = SWHashing.hash_values(to_be_hashed) + SWLogger.debug("send_post_request hashed: " + str(hashed)) + headers.append("x-sw-act-tmst: " + str(timestamp)) + headers.append("x-sw-act-dig: " + hashed) + var use_ssl = true + if !http_node.is_inside_tree(): + yield(get_tree().create_timer(0.01), "timeout") + var query = JSON.print(payload) + SWLogger.info("Method: POST") + SWLogger.info("request_url: " + str(request_url)) + SWLogger.info("headers: " + str(headers)) + SWLogger.info("query: " + str(query)) + http_node.request(request_url, headers, use_ssl, HTTPClient.METHOD_POST, query) + + +func check_auth_ready(): + if !Auth: + yield(get_tree().create_timer(0.01), "timeout") + + +func check_scores_ready(): + if !Scores: + yield(get_tree().create_timer(0.01), "timeout") + + +func check_players_ready(): + if !Players: + yield(get_tree().create_timer(0.01), "timeout") + + +func check_multiplayer_ready(): + if !Multiplayer: + yield(get_tree().create_timer(0.01), "timeout") + + +func check_sw_ready(): + if !Auth or !Scores or !Players or !Multiplayer: + yield(get_tree().create_timer(0.01), "timeout") diff --git a/addons/silent_wolf/assets/.DS_Store b/addons/silent_wolf/assets/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..1bc0903ff14ae004ff1985825d22cf3c1c79d6b3 GIT binary patch literal 6148 zcmeHKL5tHs6n@io-PA=X=)!s%@ZzE1W|c+RORV*vM8}k=;%8VU+4<&DRY$1{?$b9|N>@H{dyFP>@4;{nD0jfO_S-5%cLK&uEs7 zd8L1f#%Yq}ySraSt=`x;v*~SmTi(;~NYBE&pHH(+fAW}~9%!9L%YHw4m<(pU*2UXe z<^4nrCMF<>2k7$TQKDi!>*%S9GZPzHgV*$$z1Gfrer>NUeSd$^mUDma#`U(`_irv1 zO>g`1)r0%J7wJgpSLT~R;NE299ODvRW2cl+I`qRsJsKNx?b5fZb)>sp&KEbs;=_eE zpKq;t#DDM@AC+g27+#+LWjIl(Rzn=6FRsH7>vCM$QK=&DP|FozuM<)Y&Zbfcc>ne% z8`Ubn$9w7vRe|G1x?F)(YlVFwF>D#P!aP|{gYdfWwoXH`E=R|JW8kkCp#4Fj5(bQg zMYDBaW3B**4IDOteLAKnMluGBg+;WW2pfuMLj_+kgbl}W$$kN2VbO+z;LC^LnFZfa zgqa=RFO55hz@lp%1CD_z19kh@qVxaG@9+QBAot`La15L%21KJ9b~~67pRH@j(OD}| spP`bFUt!UNV8h3;>CjQUgK7kAk~)Y1V_^|TP~4Azp}{qdfq%-t4_LC$ivR!s literal 0 HcmV?d00001 diff --git a/addons/silent_wolf/assets/fonts/Comfortaa-Bold.ttf b/addons/silent_wolf/assets/fonts/Comfortaa-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..9c42b2f33a9a418cb63148f716ea784905050bdf GIT binary patch literal 137696 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 ZScLzAQ@Hdr(!!Vbl%O*HAm9Um{{nVbme2qI literal 0 HcmV?d00001 diff --git a/addons/silent_wolf/assets/gfx/.DS_Store b/addons/silent_wolf/assets/gfx/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T06HKg02y>e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00NasL_t(o!|j(#Z__{+MbFIG zj$WhZ7q!E!w+yDPBy67fKRJB2sNClJ#h;jU$VPRPC6iFH=!dLRjvV3&E$IOLF zl`8#G;%6_6G3JlM2=N_{eCQ5VfED2X1GwT+ur|)6b@>zpLMeol2q_;3pfMD2O#1#k zc^G0hn>brrDA)b_fTk$O!;r;vO8DXh$#jZ87|=RBMLN!JyJ&67xhxF%cy-16;)3+` zYjhA`ZEd0Z{iXVGBY--~h+n;8esMuG8i5el2M4%Mo}gTJsUDOYaW4o6ClivJ8%&ab zQn=kN-q8`Z?}HE!{*)?N| zf+9)CqX@Ld^1QNaN|o+?_&|7lO?rEa$ud-{h1c)nbvme8Z7KIw0xXiGoXqQMOrGO) zyZGHMR--`?$1J8(qR9jk#|X>9ZnyDzJ)B36mg>Eg0FBlq;LQ!e`8hg@NZ-E20RGdb zB(oVG$78ZLZ$N9*_BQ@tK)u~wy0mLgvB@%Y7!pq=2+JZ50@B%xXgo%TA*$KLJ3XcO z{5f{BxpZk)0u(}E)$7y`4@vrcqRUHk5U>~y$!0V3VnGoE2;0W)bZDHM;O_4ut<|^L za$gF8bR4`+2b1RjM57TpipXBR#Asa(&dv`0V6gIFtW{zmC3d4hqt`=7iF6zm<1xj% zcjaPjweZf)%F?!4Yr(DFgF>L{b^POFq~jo6mvA^lhaqmSNAu(acXt=5)L%|@#!#dw z@!cKKbV?osxa~In(a}nS@uO6?e(W>`o#zxug2{8F*1($>YMK}vQ zB8wRq^pruEv0|xx8BmbD#M9T6{RtzlAd|JgC5IhAA;}Wgh!W@g+}zZ>5(ej@)Wnk1 z6ovB4k_-iRPv3y>Mm}+%B7094$B>A_Z!c`*Wl-Q@KG^cN+>BX4Be3iDwX-KfC(Zks p>oMv5jCY4OFSF-B2O60<((5!?Y@C;!Zv`5~;OXk;vd$@?2>>&+J1PJG literal 0 HcmV?d00001 diff --git a/addons/silent_wolf/assets/gfx/dummy_info_icon_small.png b/addons/silent_wolf/assets/gfx/dummy_info_icon_small.png new file mode 100644 index 0000000000000000000000000000000000000000..368c4b7734bf278b871b4256f08db13103f630db GIT binary patch literal 158 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!Y)RhkE)4%caKYZ?lYt_f1s;*b z3=De8Ak0{?)V>TT$X?><>&pI^hnZ7AOY~)8Hc&|0)5S5Q;?~=PjJ!bJp#|UlbNS2{ c0GSxTjwjHAfl)zX(>jopr>mdKI;Vst0NB$as{jB1 literal 0 HcmV?d00001 diff --git a/addons/silent_wolf/assets/gfx/info_icon.png b/addons/silent_wolf/assets/gfx/info_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..151a92333149c430cf9cfbf61938068d1eaf19c9 GIT binary patch literal 23374 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 literal 0 HcmV?d00001 diff --git a/addons/silent_wolf/assets/gfx/info_icon_small.png b/addons/silent_wolf/assets/gfx/info_icon_small.png new file mode 100644 index 0000000000000000000000000000000000000000..6bf137734504bb9caa30b85328a5648bacfbd093 GIT binary patch literal 15579 zcmV<1JS4-3P) zaB^>EX>4U6ba`-PAZ2)IW&i+q+TEO4wiL?_WdD5>ZwTHu!|`^lnn4Y}ws|Cn<^d;x z_f>E&9I}bVC7H}5d6@lw|Iac1`OklxciEblO3f{2%dgmC^PL~6eSSTEI~(tx_gDP# zp8NiF^YMn~rNH0e`Dfm*&v#x=U%yb|>-F*d>!!@taq89<{4V^T__^HQjo+19oLFpO&B5vSx$fQDGcJyYfaN# z-}~Eq<2Uvj!v-vv?lM}>JAILA6x?9;h&AEF;vz@_AuGY8v*kTFj6Iyc`b?ta!M|GW&eV2BiQD42!-!dNjT z{HMf<5A_sMG%2N=N~)=)o)D%b+pkZ?la>|GtV;XY_l)F!Xo@vUd61s+UnbF zTxrLhcHU*z-F83t+6gC~bn+>uo_6|&)m~Qp-fHd#%l+PJ?qxMuEZ)zRuU6ybQobC* z2~Lu1hQ)kzSiHyrIJB3|Y;`euSxz>yO>mVwR&zaj{C6PSIhm@ZYIS) zvYY>v<%~@Czp$K<>0XxmZ@c}(YOCLj60U(1g<4Y^4#0N&qXCCqlBi@UL+cw~(x;3%gT;42Paw`-|l8Q0Dc&Yrvvxj3)wS*YM0SB`7g=zY`` zftRDr3=)`8N?2#4+|!t8^L9*jW2Kg>GZ`}b9M__LsAU{USDD3rM|SilgIU1n0>T*eC@Tyy!@yemp>RDOIk}z z4Sp{5bPncB=c|qT!s4aj`nIHVu16G(-8qcxJxF($cFoo2!u97Nc=n#$<=&etnamD7 z#l|v@lh@$d)+u`~MZ|Eph^^r)&)4tK@X^LG*4V2I-gG_GRh0ou+uTeeh3I-6-Z9SN z3WMX^z*(NH+3ke>6@H>f;o3vpczKBupDbq+~{1G zt$Vh+LN&OWH=o&QV5$@g$isn2N0GJ2Bek)I$Fui>r&yvS@1?NjK#Hukr!BnzF_tWc z1Mtc0aQEto_XFM-F4(QuP6!xypNeZ}l>i|i&k<*^5dP*rOK<}QwRImwKnejsh}0>D zl+E%p=E8b`AfdSk(qo;(<@W)jSqFOjD<^cq)5(x+`ki5WO>49 z-z5eXXFAUk+f8`cyTWE8=UCzjeoQ{NhImhDJrrDq58?8FrJXYXC=qb*oP*Pr#~*wy z@%jUPRF`irhv1`V6l5=a39SQ0L6b7hUzecZB2K{3So-p;M@AaScbx$)FxYioluL`( zc@L`rN8s?eEtldJp<#how?NStyF=_Um@ZjcP zL%0^mts9Fh#R4f@2RqgPig(Td$1%7Ax58=XA^7Ep90MOZR8l!AZUP!}>#p4|BhvhL zNRroTTFC885R}yh(E^&J4VJyQFLz(opMN|oF;3zRqMT_nl(r3K(dSVCk| z0Y-{sPOcGu*OjoQ;E(Wzu=)Tn)vfB2{4l?G+>cqy{3dMyXC%Mvro#Zl< z3u_wf5$Pg7oM<*4Gi@EHb5aWg!S9R=94mV?Er$;`amiD;?CLM_A@>j_5Au0^u~{SN zBf(igpPY0d*y0J8FQKnkIIsnNCP7+xuW04d)_QV3-()TG>$LSCHi>H>#EzZZL&%By z4l4*n#XROBU^D8fk!jI=xRA0bS3>j_k9ZVrlE%1H?8(|?)eTjuz#fExf;;*x%y6kgC{0v=3xEfil4){L@uv zYxM$V0C+*DS!Ea-j)%`D10{+=FXu%@ZOzkNUVjFPVJs(p0|j29)|e+l%M=}f!vl_v z1Hm*^hXgkPFc6U}wU00xyJ4)Qz`|Ql3fC0?KYP&_2kKF@#gkoH9Rr$*IyWl<7koi6 z1RMb9aML_?Z(yArSVqhhhy-0&UhIR10t?pz$ZIf&)K*LeitjHKnyxN#AQr-WPN)P= z!3U5=M2Rbr6)06A24bE@>6mwuGVnh|5$XthYKoK+nmX$H(^z~zImfJVTl$Q2yb<#{}dNG63i)V9fuTA{R)N;r$uW_79i zTck1R6G8W#o|Cc9%@wP9|Ky5lRSPNRNfxU7MVx3TJm-f_5?j*-ktka6XZH zoW`OLIP#f#(ec!3}oL>*v+PY@}5kTg{05a~Y{UkBUAk;P5urQqdWLxl72 zHE1N{tOJco?-$zi~_*ih6H|3|9-d(#vZkk=m=>fF5a+7zy?wRx$R^v43@b@Afq3Ax!0R02m+*tgkAb{bP=7`;392jWEGDOgVy-n2)VYjfOSl6wx#*`dn>5;6~RTPM)DXo_*jla);7=lc{VT0x;bns0!2z4C=YA!NprztNs z7?rA^<5|2A2ZP%?7+uDtlEc`FCpZrpCuSE;1`HQ>LnL>&jc+DYRX&RX3#jdVFDFgu z>w-RoT;PosYY&NfVt_ou0uIvNQ6mO=gasDfZXhQ}L&CtzFE9haqGCoO-0>>E8+S`e zaL9h(6Y8XN|1CRI9C*yn%BF&2LQ ziI7PJzz7^3RBz}7BoFY_Jf72ozm?m4#mtbB5IY7+;{e&Ajl?S}OyC6&a+MlP#raYo z_Rqx|SQE6^qdw|XiNg+ZoMNG4c1Vu`JhY;Wqf$NL-&ADk%M>7=xSxa;4&20!o4cL0 zqz)@Yd*Du8;=>Uvgh8^XR;2JIl1Qdir{a;1e|qr1fo%#|3VYElzvW)XUY1fv2@NUSA-%L6$%D%}z4AY=tL`@?Q- z`BO!VwhW7hk&L9~^s zieZP}5If(;-@TwNg#f9k4yX_&NI-KEgktTisi1i+KUhj za~JyDa_B9#Rx82MC0GU|GMBEfKnlP~p^^mhZ9Z(|gv0Mdl?EgMm5oo3>X0*@8{od` zbX~I{>YN<*+OQwqPl8N{om)nQ?K})IDN_t9L8J-~G$56y zHG_^LiqH~+gaQb|Ic6RmhT{PLx-#@x$%8wA4dZwxitrUkXF>&$ugIoTy}u1*#JoQt zeO6kH36WSc2?P|p$_nKa9{(vXb>1w zHSn9Id4wKS58OC1Y_5nfsgq9Rz=8ldw+Z;YB)yPOt8pNVDwVG+ClZ0`(L4ex0%T3r z#rd5$5z-G?FM$k7vEp4b0+ILzyaw585Zq{w;GjF5yELfBAL@5+m+xGZZs)?ABOQTC zpvJBt7Of~+=BOrJJZ_FWly*Yc9s)_@3%L>rO|<~%Ek=zYQIdhrsw$43sa7uSsQ#jIAuQw7L(BjhuBY-p9)uW1X~RlGQ5h>_5R2GT z+Xf$GomOk9IWbqVgoyi%I&L|8!_NuGhoQp{_*RJRiTv!j9=wk9jR@s3TgF!C$R0H> zM|{FFQLmNj-UKa~++h?lBl!v7;WW1ojolpoK2j08+m!GyaI72^pU2q*R8~y_fPx0s zY0fIj9mq5oRun{WkU>OQyP%!(VkX=G$R_El`@_92?hE`>kASF&fUM~8wU+v?Ff71t zviJZCMgb9VTu%t0OiovO8TFY|B!Dq&vWNv{luv?~|5W`74mZ;cCa)QikUTgA>{!+{ z6zNm3b@JOF3)qeC23Re^UrkPkk|nPly*EV=*W+ooO;cKA>A8b?`bN|X$G z!^E!c_mVb4FkyiE098T$Y)O3;?Ir3L!A%~m^mW(DmEaVEH?KF~HI3 zS)-_y;HsHq8;d2V08tR-XoN;;KppjbI91}(R!0;z4b^zRAiQeEVc+VEe3VONDFHuH zD-6NI&hViT?WJAh`$}R7en`^`SO{0o^k)|oZw5w@YVi3YRK@-rQ2prZ!lcuo8 z1&Q5|5_K}=g@XAW;VF=>RNIAl%VM7yq1q6#T-6{@dgd-+>u>VSHz)y5GID{~Pn zyjgu$vDOcdPjMAS4M>+8YKjC7DSv(9Owc_xRfNjZU>3mm}0o( zs$_^L3^|F}0W*W6;@qOh_B2p2CuCG|R9OTn! zkR8D2b66vgR(Gs1P9VqvjYU=adOL#wNYc;^FOnjNpdKAOid=a=O_UWhlsGw7ODc%s zxC@h#+tVVLU152ER;U%%SsQ7ZY?Xn{uKG${#RnBY;ta3|1PV~A)qppH)}R?MW#=8{ zRZg;Mx;C;v{DYwF8p1>DM?6r$;`pB5!}3Z~xiqT)UvPUTn;p9!8= zyp>Z51BrsyDLukXXC!0jxGj(kwz|goVv12uTN1OYpF`gE>Jb&puQG?yDx?KMfd#lQ zP+D#z<(vt5*uOLn(qwM~E6M6>S4>RC9VE_`)Nw^wAn2y%I&Uc{sM$>>g7s^#G2ogI z%)mOhj=nb}U~U|tf@?U)6EuDzMYj?Qd4Fok3OabztOhm?GIH`lfIA%s2+QM^7eXE^ z%)h+MoNxctq0Am9m-X6yIXR~#9v>&KkhU*Qj?chR&7btEf5%bHFDsOP$WhHNE0n+D zsPG2Vos(}>U5I*&Ta$ifefgm7*yR#@b2_D`wJ*Yrv zL7$pvBNsa)6sA;K(bSDfB3R9PL4=C&YRZD{FiL0!FRSQ*N`=?Fswc2D0xpFq=v4It zw;fUjT+vd)$*XWh4u*`PJ(b&zh)C?3dwe5egN9E9{O{K<;YfHK{x}?F)%1RsE^yI& z+9FUJ?-nq&c#Fi;$UH;EqY7D^c@7Ir}F0 z0WDH5aB0>*v?I4%20=;!MARy^sT!=D)-tBT#gp2OC>dy6ohVv#E?B-nNrbp2Rm$~1 z;Kcxh<%ofLHW{xcHs;`_Jj)GpsgSDi0>pFGj;cmsIk7dh-~4KZb~5NOu6 z2$G`;QX8<<7)OQJid_%b1#ZN#5X=|spxWx|BaXEVgcS-~15h4KxOLT88QBw#nXJGa z!?lpJq$Y6$ycxe%lX1%r3Sv)NVYp$dDCQPa&eookthxdcHT;Epa$I9%1MlQ0xsfI>k!h5 z6q!NNJ4;0Vd1i^wMkg9D=)tzuZ*MSh{Kd%|D3R33KqzU7ffdwvr8=#55-lpb-N+r9 zy0=E$jQjLSpufZnQ(T8sdn%-I#~^t)6$SQ|fypCri`ITmYNPodk=knhN2J!8OKPpn ze-o?UR5yUysRHWImM`Bg<%|{fhg5^V9iq?o!-ssc)V zsY|d`$+G0teZ#+yyU?+oVYNdfU2&6P%&NaEoL9SARN@!DY~bzAvfz(3KcjGW3PC-2 zE+T7!+^8wv=XB>b<<$j4^X(+mZ8wVuM}a522_NK;cPU_ygFWFK>xW)%_QccYfV+ffy2U|qH(OKy^@h+rT(zlno7Q{ z9F<=2Ce6W|J=*mbxe8KX%_4^fINDMg^7V8LY(&v`8wbh4Z1E741T@=As)mKarMFj^ zp^D0oO~&CXF2wUQwUrv)(x)MQ9UcvB;~od;&3(|e{Hkn77iw%nfy}k zMk%70;+AyJ$Tr1d*VG%S9?dbax$lEpxTQ;Q0JZ-G6_bIL9MN#+o;$ajDTF1Ee4&EU;ry3jSCn;-}7RcO%|7C9jA&*u7n9P2lK zJJwh6#yQ4{G+tnT);ARA46-htJC zqCc3!{#Co%{OKI_uiD+_jehrA?QZjmF(|J}J5RB*go(=6+}6^JFxU_O(-f{MokhOD zNix)@^9CT8$Y5lVRki<6+(z{Yt7M~bUdiH>R7-`F!VngopCJgIg^OH28F=3|!`5_S z7l*7TMbM@kyw;I7h*^K^VrAmu+DnQ}gAiz4%@s`zf|Br141hD=?6^SkChgeOmz;hb z?P7=a-EqL1H1Cd?Q;FY3k_ldF6=Yd;n*(J)xQtrnA4yR(v-Y#S=*ZwqhRaie84 zulsJ##=H^gZ}n`<*QU+C^=!;f;{30AHs)*7=G~snfCMddRkykkwvlS5iw5XX`%_iT zs=YR79cCho zrsjUU_7nx2WkNSee0p%j(Qvu{8ouWD<0@TS;WbFzRr5L8>Z0Mjcn5wpIt2FxN8scp z_CMGl%*9X5=SI;9l(>EEq5=KP9NI<$_d0h_GDw%PGTfDb3YgRk+8pq^n6I^`(CTiD zyAxBHKhz#FxoPrNsuYH;BGgNiW-nAhlA9X9o(*68n=UsXowo=R0r4Ht!V0siW;y=2wjcEX*vMs zE85kg4!Y=&TnL`o#TzwohYZ6_HRBx5ZFle9G&i#DMh%aIo+Nn;0wvLK1{@wD0ZZ;H zfkhr@tFJLp6u{~tq4rL39X(7T`mBlZA`KSqz=2#B7OgyliQXN$HmG&3l~?Z^11qS} zBG?{a=`x~rso1em?_WvijH@ijVV4gpoAk(NK6cT zox374swUL&Q_$a+sbX6gYNwU5@D3~vu>Ipc+ArH^&A;xW{j!bL{MkNQQGO&$zfb;| z0#IR?1e|Gj?U@Vw*P!NY1nHq1$^tBcwljb#dWg;xbz^<3dJiYnhl@oGWU>z4RIdV^ z!e^mWT$jFDxFpE!Cf^|&s3VDUs2fp^M)xI^27%5}Xi&Cmp2na#A&|~#uu-#QcU#BO zh7rV|eHBNX)uU;m6xFLt5D>^xVj4Hr!#tKDDM_85JCZh3T(4SMuJ&Um&>I_phWa9` zOS)Bwv?Q!F4ZtB$RpLVg^n7dgx4GNDeN2L_^5C1gP7wAoRrVNnFA3Is;lIS1Yms-< z&<*O)Yv&nq9rdg^fwb@|?MBzLyYOwejJBDrEWUer&85olgoDsyl^5}?0enmFqah#> zr&+uhYxZoPS8pwp*VGObrA*;cmN{^y@vjxzr__W@@S7>Qb&0Y z)sd@jNl$o!Ki+VD?O6}IT6Hmf6&Kpyql4)N?{0&NP^T$}G$TK!`?+}#K4 zNQ?n#fNHIXuD;BGt^q4Wr7xKE7zs`5s&=R-XtpX;Wd>5f5*~vSp2cvOPW$xKhAW5T zY|}-F{8SGBgI`wwVg7jHh7h^i_N@Ks#=Sq-w`YF4aqkcI?U~<tG0^O z%u7M~tNEb8pj${t{V4F_P`h7mki^l@K;>;Isn4I}ud(H9NF8LryJdpZ`gCM=x{==j zOMF9hq@s`kC=;2adG!=Xw?$gq-iDoekdTlJ^Ts;JR0)(WZIjhdP;*aRkS)>SNNTy> zbUWbCng>sFkRKYrF{)V_R{%yeO`*qX0J1Bc1Q2Jvf%!o4f_8xcUYSF_B?;Z`UQolw zU%FMyyricnsi29A9^KLF(mpDUu4l!?^#A}qt0Sd{Cd$c9x?ADBIwnXmPs(onST0FV zBZ~_yO@#MzOU9nYqkUxbe`D~ zOCSh6G+RRI>Zs+bF( zhSfej6tC?H>Y)TzZx1DqVUr#s1G(}C=eSx~tDdpGS`Z6VKK&6gB`32_k&eX3%6y zmayR7A+fdV=_lG)gchrqPxWlfJ4Oe1RxMzSeo7<%;^Y zB0j3AKy=m^F_?TagL+i_b)T=hG#=uiQ%fCp3GXZNzC7U}e0{=0`{oG`dOGY6Pk12R zAG@>_K#!XkbAH(Xm-%05M!OF2$7h;(cRioSbu;c3H@N+6UM@ZRM9+ZTuK)ZUJ@cXy zpf*25_04=q_nH1+ z7ZLKn%;lH;_&!{pT;r4{;0j~}(jCeZ7)G?=B`B`FusF16d_U@Jel_b1v-!HI=r?`Y^Jd=qusi2aMCQ{X#%LyJM^F{v0)7k`YS?NDEAVfDjz*NZI#8$s~o7X?|P~% zZ}$mW*(&+$SP5wxFe|#}5h-MTKQ%*{NxD%)OOt=BQCUKQZ{{ffYLFS`|NHLy+s$|L zefK>M7%7KRsYz5mp%CrH6F6Bw6>=WN{b$UAkqHQlCtg~>N* z5<&?M!OF3eBw|g&b(NF{{FL?%lhREIHWn1IxBv2HOy@v5kx@V=e3@O(+9Rz)2gY zTgczE|A6I(P4`C1oc1YyZ&M?{~iQopXNY_kG_o007gBF#uqRM3PK1h5#TFiKKKt z6OlwjE~ONVF$MsbNF+s`aU1~1^H~6Z_kaE4AmBR$5kZl*rwlU@0dodK5{YESG-I0c zMOo8~MF4=Lls0253IJIEkN^+`fV4B=h?FwT7~=q-Io}a+j9R7{ivoZJ00aOL=d%QW zsFZTd8K)-_$+T(4bjMMaX~qlyu${@NY@=t~bYezyyqR&l#yy6l$B^YS1OQ-o4Cx+2 z;58(r6aZiZI49z94s*su06;j-DG71TbYmvwY%$F-3ui;yTbb-z>eA{EpG-Hu{`wm>$*LS`l649&M z+B(p(Y+)D?Zrg&01eKMQolU1RZL!$;4?WJ&J;%ayW6ro+%zH?c06^9^z;8$|!cY_d z(&x^d*Lr$-?;IQ)YDQ$M0oWQMVu%P*N{~{5_cP}RLJ%; zb&iE;#S8$z+%4ui+{kw*YEsIKYX3?pv%YbLXL}f}t?j0i@=qCKUm~JehztN2IM;U} z&Hpz=VxN{J3fUC}+;BxNT@? z_&zD+mpDgVh>Olq9qNcIqwD&2GMVhJCqZ0MbAVk+>y7hlEU3$1s%wq$oV!A{UXn;bDW62VVbc$zJ0H{DI)>SDVPNA z+_`7{z`)@1h*+z-ubj+#u$sXwzh91 zqGy642N7Q&qB~48cFH4J7yw`^IF{Er?u%M?Y+F9tC5mg91Aunu(9vUcb=5Bq4i3*J zq7?xL`5*Q5ojbU2;e!3%@2Ib@jd+}6_%I7hB$5>Li1V|}j*e|t64BE^&S4CHE`+$( zG-FwhWDyN;j(=O_I46v_`Z3ql)eZ? zxO4slzHnVa_5S;ZA_D_MuOVVhfOCE(g!pTPXl3Pbj#umob523Tb>YGVZ}s%_MiH?| zK|l>6(RloL@AGwaHQ5~>3S4*igG{Ca?Mo)kj-z*z;3uF$(0pCj)790BwtCddAk3EcH6KW*T3g%e8H4Ts68MI5=0tpkG9gM} zj@;>uoJ1n|8${fsAc030FJ2h$?LB!q409@#g08MT9hPM^Dlm@_(Mr>doeaS&(E%$1 zp8pvDkeuUBR9u5GcAkjtQLIAR_jDQGR*p=e`AFcnb6Q&3R&)NDb9@o7w72)iK{Gz;+a)UJeWly@7~H#e0zHy7s*g z%*@?l%2h}vI~D`LR}_&0RaKY&oq_~Pj~2}GFl2A!WSuNR6adoe*EgP-F{A8m)kBvt zc)_x)7egY)&ye-mvu8c1QUq*U{K>9ed!qrEH2~n6o78hAnX?K+wDIEAnEUY5b?uGi z%a?Y02$dDWbHHi|q5deU`zH>)3O?yKKal210iKf%(M*-mR0L&J;`^^EHvkHu{FRSz%&iP~0O#)Y~ zTKS1(S^s33v7fA8-}rbcHMT_|lV(+sqauOPsPVW;pc@-Y-ZJjIojP@TZX%KVW=l)k z`-6i+edo{jw;-~C&6~IUS0a%t4^slYXAWa5;)#G9`XQzKokG22-Q6F4RfRc`ND6cX zGk-c55m%^W(z$ah-_1h;$2{p^B?Y% zFI?yso^)_FB!!(5IbQl9rF>0A9XRLLsW6*nj3WRrpGY=lnz0BXyIOJb+t*(E`E8d< z0=5K+zU-TNB-Ojm{3_bQ)20^{-KZ9cH6d?Q&v`bLq$dTExFFgJxnVrXO~V@3Y#(3 z<=4xFpzBnaiHOkin$KAUL@ZUv2InS20s$c1+S(omfUQD^V+$6{U(wjuuy=59=!%OZ z5ENn;PiM2)WWE%(PwdJ|Ts2J_P;jgnQpyRK5m9rCmvUC2s_OD;Rf;7Ni5!^>2}~wC zY8ivqiD>_V1@q0Tu3DB#B$63K+!(9`(r+6YR%e1pV4er7lDJxywNI64X=!PjrNDeq zF!M={#=yYfJXQWFo6V#Gl)!>WV0n4j5+P`JRn_HRaGaB_tejO3Aa7D71BD>aH2%+d zNTB!3u|kMzL`s?FoM%+Iwz+dF>lB#B%U^Q#@91bMsshV7pA-ok9Zelvw5aN~<;$0j zx!ZAQXym&p=SV4mF?OQ4dHvxq5?G9SscBsEiukm`@|XAG@#eFIlE5N}Ypv={HZU-BRDqeJuV69_0CMHZ zYfuQ13PM#|2nh^GVdpq!?bALnxF(j~1;%eLWBE?N98yl~;6qs#v zCLdK0szVAwwQQI?&u3l6_BNb=IAMZ$>ACe4XgJP(OEyCvTbo=N5{5Vg`|Un zQrPKq=D`5xKqL}*%X{W*-`=(6@Zs(UCaYd<-rPRRjapw4;_MAKTz@10v&2LqDT0(h z&iOlvHkDVGOThD0LT%kK8*?Cfe79v*$Gw6sKI`nQB2 z&2JBpza50xea$lu5z8Tg(Wvpd>H(QfXTEdz@X?YG5~xgJXENDMs(M5xowRvmB-Lq} zF=NM$UDu9`jQ$_z{1d}4el;2O^4z)em!;F0@2W}zZQFh}2s0vbx0xlaQUd49seGMt ze$am{0>He1fx-Iy)q5?v8PCzt)Gzk$KRB@7(tt!HAmYQVt?m0#sj)XX=N}{T<;~4aXC|XwzV}{dIgxx!6}6?MCC}xro{loo)O%B3h@y7G)^+RF?#j~`;%*Or16^1hqn@vhQH42~ z+_I8LzC%QF6bqV`5?06K&BqIAWXLzq@(! zmM<2X4lYIsEJVF*ZEgP|k$jhksuYFgKqOx>&Dcvtq_A^d^93n^D^^^+iAe4ZkV$6& z$loOr$%ie=no=b&pL#hwJd$Z?Y5N=JY?&3{oIPvTuK9KmDQq18_$4*7)YL4#ytnsc zo0Rg3phh^(d8cjLe{PzwUVrucRMauLPE95|me{uaD@0tEr{cf6zP>hATU-6<@2#1o zrKPQsb8NS5ab?iM^I~B+f8Mt3ADL#Xe=0kgk01Z2wx_4}Z>5y?1~jv{k#l(I(j_-9 zTC}k5Qkq$q`-=mY(#*1bdslRLc=Ue}u_2__%L0H`iRkC189Ss9yS!JqiUf{d@N#!pgSX_ z++0>xy34lhmv6jrU0+Bu%j}Yp=-0DZt3e3S%sE~j)|2MkR{z%A-1La9>zSaA=4oqY zQLV!E?Ok^b50Adc7>g9rCFq%?Afkg88+94lwjhVhRT9y1=l3@XZT1BK*-s?DX_~RO ziy^K-%`D6`V<@GhNp=V8Qp)t9Lr1DV{q)>pLWsL3(b0U#;~0Y%h~$sr@#g+QF^3RW ztEgs{FcNt6)yvNcA?}$uv*K0&`K~JeRLJ%y);XG{wL~KN%|g(<3g=uhag|aME3}y< zq&rwwHHLI`?Y?7pc;wHdly@LvbP^rSs&Q!|dcC5e{KbZb)rqOX>Q2?QXHWKM@l~sHUT% z`BI7N6l2cc%rX@ zOEPgx!Gnm5_aOPDFYhJf-;y){AbP{d1!w_)gN0aWVD*JjKb1MFSPxO?F%&$&3B(XU zG#AZ||EK`HyHz-ZA>>fN=l2)G07CQx$O!Uqls|^43NijQd`y$P=yu;krCM8Bt~+(7 z>0JAFYUCF|G0c0x;F6xsluE;CI|$E{c+_vL9jdGdk}MwRgd~bFXRX9?gN2OZ nO2dy7Z0MsHW9cYvW6_3wNe!fDSsJ1Tjei8R3|z4Sf2zPchfh=9 literal 0 HcmV?d00001 diff --git a/addons/silent_wolf/examples/CustomLeaderboards/ReverseLeaderboard.gd b/addons/silent_wolf/examples/CustomLeaderboards/ReverseLeaderboard.gd new file mode 100644 index 0000000..2da5bf8 --- /dev/null +++ b/addons/silent_wolf/examples/CustomLeaderboards/ReverseLeaderboard.gd @@ -0,0 +1,87 @@ +tool +extends Node2D + +const ScoreItem = preload("res://addons/silent_wolf/Scores/ScoreItem.tscn") +const SWLogger = preload("res://addons/silent_wolf/utils/SWLogger.gd") + +var list_index = 0 +var ld_name = "main" + +func _ready(): + var scores = [] + if ld_name in SilentWolf.Scores.leaderboards: + scores = SilentWolf.Scores.leaderboards[ld_name] + + if len(scores) > 0: + render_board(scores) + else: + # use a signal to notify when the high scores have been returned, and show a "loading" animation until it's the case... + add_loading_scores_message() + yield(SilentWolf.Scores.get_high_scores(0), "sw_scores_received") + hide_message() + render_board(SilentWolf.Scores.scores) + +func render_board(scores): + if !scores: + add_no_scores_message() + else: + if len(scores) > 1 and scores[0].score > scores[-1].score: + scores.invert() + for i in range(len(scores)): + var score = scores[i] + add_item(score.player_name, str(int(score.score))) + + #var time = display_time(scores[i].score) + #add_item(score.player_name, time) + +#func display_time(time_in_millis): +# var minutes = int(floor(time_in_millis / 60000)) +# var seconds = int(floor((time_in_millis % 60000) / 1000)) +# var millis = time_in_millis - minutes*60000 - seconds*1000 +# var displayable_time = str(minutes) + ":" + str(seconds) + ":" + str(millis) +# return displayable_time + + +func reverse_order(scores): + var reverted_scores = scores + if len(scores) > 1 and scores[0].score > scores[-1].score: + reverted_scores = scores.invert() + return reverted_scores + + +func sort_by_score(a, b): + if a.score > b.score: + return true; + else: + if a.score < b.score: + return false; + else: + return true; + +func add_item(player_name, score): + var item = ScoreItem.instance() + list_index += 1 + item.get_node("PlayerName").text = str(list_index) + str(". ") + player_name + item.get_node("Score").text = score + item.margin_top = list_index * 100 + $"Board/HighScores/ScoreItemContainer".add_child(item) + +func add_no_scores_message(): + var item = $"Board/MessageContainer/TextMessage" + item.text = "No scores yet!" + $"Board/MessageContainer".show() + item.margin_top = 135 + +func add_loading_scores_message(): + var item = $"Board/MessageContainer/TextMessage" + item.text = "Loading scores..." + $"Board/MessageContainer".show() + item.margin_top = 135 + +func hide_message(): + $"Board/MessageContainer".hide() + +func _on_CloseButton_pressed(): + var scene_name = SilentWolf.scores_config.open_scene_on_close + print("scene_name: " + str(scene_name)) + get_tree().change_scene(scene_name) diff --git a/addons/silent_wolf/examples/CustomLeaderboards/ReverseLeaderboard.tscn b/addons/silent_wolf/examples/CustomLeaderboards/ReverseLeaderboard.tscn new file mode 100644 index 0000000..cfb1170 --- /dev/null +++ b/addons/silent_wolf/examples/CustomLeaderboards/ReverseLeaderboard.tscn @@ -0,0 +1,119 @@ +[gd_scene load_steps=12 format=2] + +[ext_resource path="res://addons/silent_wolf/assets/fonts/Comfortaa-Bold.ttf" type="DynamicFontData" id=2] +[ext_resource path="res://addons/silent_wolf/Scores/assets/fonts/Comfortaa-Bold.ttf" type="DynamicFontData" id=3] +[ext_resource path="res://addons/silent_wolf/common/SWButton.tscn" type="PackedScene" id=4] +[ext_resource path="res://addons/silent_wolf/examples/CustomLeaderboards/ReverseLeaderboard.gd" type="Script" id=5] + +[sub_resource type="DynamicFont" id=1] +size = 76 +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=7] +size = 32 +font_data = ExtResource( 2 ) + +[sub_resource type="Theme" id=2] + +[sub_resource type="StyleBoxFlat" id=3] +content_margin_left = 23.0 +content_margin_right = 23.0 +content_margin_top = 23.0 +content_margin_bottom = 23.0 +bg_color = Color( 0.831373, 0.415686, 0.415686, 1 ) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="StyleBoxFlat" id=4] +content_margin_left = 23.0 +content_margin_right = 23.0 +content_margin_top = 23.0 +content_margin_bottom = 23.0 +bg_color = Color( 0.831373, 0.415686, 0.415686, 1 ) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="StyleBoxFlat" id=5] +content_margin_left = 23.0 +content_margin_right = 23.0 +content_margin_top = 23.0 +content_margin_bottom = 23.0 +bg_color = Color( 0.666667, 0.223529, 0.223529, 1 ) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="DynamicFont" id=6] +size = 64 +font_data = ExtResource( 2 ) + +[node name="ReverseLeaderboard" type="Node2D"] +script = ExtResource( 5 ) + +[node name="Board" type="VBoxContainer" parent="."] +margin_left = 20.0 +margin_top = 20.0 +margin_right = 1884.0 +margin_bottom = 1071.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="TitleContainer" type="CenterContainer" parent="Board"] +margin_right = 1864.0 +margin_bottom = 85.0 + +[node name="Label" type="Label" parent="Board/TitleContainer"] +margin_left = 502.0 +margin_right = 1362.0 +margin_bottom = 85.0 +custom_fonts/font = SubResource( 1 ) +text = "Reverse Leaderboard" + +[node name="MessageContainer" type="CenterContainer" parent="Board"] +visible = false +margin_top = 89.0 +margin_right = 1864.0 +margin_bottom = 126.0 + +[node name="TextMessage" type="Label" parent="Board/MessageContainer"] +margin_left = 789.0 +margin_right = 1075.0 +margin_bottom = 37.0 +custom_fonts/font = SubResource( 7 ) +text = "Loading scores..." +valign = 1 + +[node name="HighScores" type="CenterContainer" parent="Board"] +margin_top = 89.0 +margin_right = 1864.0 +margin_bottom = 189.0 +rect_min_size = Vector2( 0, 100 ) +theme = SubResource( 2 ) + +[node name="ScoreItemContainer" type="VBoxContainer" parent="Board/HighScores"] +margin_left = 932.0 +margin_top = 50.0 +margin_right = 932.0 +margin_bottom = 50.0 + +[node name="CloseButtonContainer" type="CenterContainer" parent="Board"] +margin_top = 193.0 +margin_right = 1864.0 +margin_bottom = 311.0 + +[node name="CloseButton" parent="Board/CloseButtonContainer" instance=ExtResource( 4 )] +margin_left = 582.0 +margin_right = 1281.0 +margin_bottom = 118.0 +custom_styles/hover = SubResource( 3 ) +custom_styles/pressed = SubResource( 4 ) +custom_styles/normal = SubResource( 5 ) +custom_fonts/font = SubResource( 6 ) +text = "Close Leaderboard" +[connection signal="pressed" from="Board/CloseButtonContainer/CloseButton" to="." method="_on_CloseButton_pressed"] diff --git a/addons/silent_wolf/examples/CustomLeaderboards/SmallScoreItem.tscn b/addons/silent_wolf/examples/CustomLeaderboards/SmallScoreItem.tscn new file mode 100644 index 0000000..8c1316f --- /dev/null +++ b/addons/silent_wolf/examples/CustomLeaderboards/SmallScoreItem.tscn @@ -0,0 +1,14 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://addons/silent_wolf/Scores/ScoreItem.tscn" type="PackedScene" id=1] + +[sub_resource type="StyleBoxFlat" id=1] +bg_color = Color( 0.831373, 0.415686, 0.415686, 1 ) + +[node name="ScoreItem" index="0" instance=ExtResource( 1 )] +rect_min_size = Vector2( 434, 64 ) +custom_styles/panel = SubResource( 1 ) + +[node name="Score" parent="." index="1"] +margin_left = 360.0 +margin_right = 427.0 diff --git a/addons/silent_wolf/examples/CustomLeaderboards/TimeBasedLboards.gd b/addons/silent_wolf/examples/CustomLeaderboards/TimeBasedLboards.gd new file mode 100644 index 0000000..fc7328c --- /dev/null +++ b/addons/silent_wolf/examples/CustomLeaderboards/TimeBasedLboards.gd @@ -0,0 +1,81 @@ +extends Node2D + +const ScoreItem = preload("SmallScoreItem.tscn") +const SWLogger = preload("res://addons/silent_wolf/utils/SWLogger.gd") + +var ld_names = ["Weekly", "Monthly", "main"] + +func _ready(): + SilentWolf.Scores.connect("sw_scores_received", self, "_on_scores_received") + #var scores = SilentWolf.Scores.scores + add_loading_scores_message() + SilentWolf.Scores.get_high_scores(10, "main") + # the other leaderboard scores will be called once the main call in finished + # (see signal connected above and _on_scores_received function below) + # when all the scores are loaded the leaderboard scene can be opened + + +func render_boards(leaderboards): + #print("leaderboards: " + str(leaderboards)) + var board_number = 0 + for board in leaderboards: + var list_index = 1 + #print("ld name: " + str(ld_names[board_number])) + #print("ld scores: " + str(board)) + for score in board: + add_item(ld_names[board_number], score.player_name, str(int(score.score)), list_index) + list_index += 1 + board_number += 1 + + +func add_item(ld_name, player_name, score, list_index): + var item = ScoreItem.instance() + item.get_node("PlayerName").text = str(list_index) + str(". ") + player_name + item.get_node("Score").text = score + item.margin_top = list_index * 100 + get_node("MainContainer/Boards/" + ld_name + "/HighScores/ScoreItemContainer").add_child(item) + + +func add_no_scores_message(): + var item = $"MainContainer/MessageContainer/TextMessage" + item.text = "No scores yet!" + $"MainContainer/MessageContainer".show() + item.margin_top = 135 + + +func add_loading_scores_message(): + var item = $"MainContainer/MessageContainer/TextMessage" + item.text = "Loading scores..." + $"MainContainer/MessageContainer".show() + item.margin_top = 135 + + +func hide_message(): + $"MainContainer/MessageContainer".hide() + + +func _on_CloseButton_pressed(): + var scene_name = SilentWolf.scores_config.open_scene_on_close + SWLogger.info("Closing SilentWolf leaderboard, switching to scene: " + str(scene_name)) + #global.reset() + get_tree().change_scene(scene_name) + + +func _on_scores_received(ld_name, scores): + if ld_name == "main": + SilentWolf.Scores.get_high_scores(10, "Weekly") + #SilentWolf.Scores.get_high_scores(10, "Weekly", -1) + elif ld_name == "Weekly": + SilentWolf.Scores.get_high_scores(10, "Monthly") + else: + #print("SilentWolf.Scores.leaderboards: " + str(SilentWolf.Scores.leaderboards)) + var ld_scores = [] + for i in [0, 1, 2]: + if ld_names[i] in SilentWolf.Scores.leaderboards: + ld_scores.append(SilentWolf.Scores.leaderboards[ld_names[i]]) + #elif (ld_names[i] + ";-1") in SilentWolf.Scores.leaderboards_past_periods: + # ld_scores.append(SilentWolf.Scores.leaderboards_past_periods[(ld_names[i] + ";-1")]) + else: + ld_scores.append([]) + hide_message() + render_boards(ld_scores) diff --git a/addons/silent_wolf/examples/CustomLeaderboards/TimeBasedLboards.tscn b/addons/silent_wolf/examples/CustomLeaderboards/TimeBasedLboards.tscn new file mode 100644 index 0000000..af4f978 --- /dev/null +++ b/addons/silent_wolf/examples/CustomLeaderboards/TimeBasedLboards.tscn @@ -0,0 +1,228 @@ +[gd_scene load_steps=13 format=2] + +[ext_resource path="res://addons/silent_wolf/assets/fonts/Comfortaa-Bold.ttf" type="DynamicFontData" id=2] +[ext_resource path="res://addons/silent_wolf/Scores/assets/fonts/Comfortaa-Bold.ttf" type="DynamicFontData" id=3] +[ext_resource path="res://addons/silent_wolf/common/SWButton.tscn" type="PackedScene" id=4] +[ext_resource path="res://addons/silent_wolf/examples/CustomLeaderboards/TimeBasedLboards.gd" type="Script" id=5] + +[sub_resource type="DynamicFont" id=1] +size = 120 +font_data = ExtResource( 3 ) + +[sub_resource type="DynamicFont" id=2] +size = 76 +font_data = ExtResource( 3 ) + +[sub_resource type="Theme" id=3] + +[sub_resource type="DynamicFont" id=8] +size = 32 +font_data = ExtResource( 2 ) + +[sub_resource type="StyleBoxFlat" id=4] +content_margin_left = 23.0 +content_margin_right = 23.0 +content_margin_top = 23.0 +content_margin_bottom = 23.0 +bg_color = Color( 0.831373, 0.415686, 0.415686, 1 ) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="StyleBoxFlat" id=5] +content_margin_left = 23.0 +content_margin_right = 23.0 +content_margin_top = 23.0 +content_margin_bottom = 23.0 +bg_color = Color( 0.831373, 0.415686, 0.415686, 1 ) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="StyleBoxFlat" id=6] +content_margin_left = 23.0 +content_margin_right = 23.0 +content_margin_top = 23.0 +content_margin_bottom = 23.0 +bg_color = Color( 0.666667, 0.223529, 0.223529, 1 ) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="DynamicFont" id=7] +size = 64 +font_data = ExtResource( 2 ) + +[node name="TimeBasedLBoards" type="Node2D"] +script = ExtResource( 5 ) + +[node name="MainContainer" type="VBoxContainer" parent="."] +margin_right = 1918.0 +margin_bottom = 1078.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="TitleContainer" type="CenterContainer" parent="MainContainer"] +margin_right = 1918.0 +margin_bottom = 135.0 + +[node name="Label2" type="Label" parent="MainContainer/TitleContainer"] +margin_left = 540.0 +margin_right = 1378.0 +margin_bottom = 135.0 +custom_fonts/font = SubResource( 1 ) +text = "Leaderboard" + +[node name="Boards" type="HBoxContainer" parent="MainContainer"] +margin_top = 139.0 +margin_right = 1918.0 +margin_bottom = 328.0 +custom_constants/separation = 160 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Weekly" type="VBoxContainer" parent="MainContainer/Boards"] +margin_right = 532.0 +margin_bottom = 189.0 +size_flags_horizontal = 3 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="TitleContainer" type="CenterContainer" parent="MainContainer/Boards/Weekly"] +margin_right = 532.0 +margin_bottom = 85.0 + +[node name="Label" type="Label" parent="MainContainer/Boards/Weekly/TitleContainer"] +margin_left = 76.0 +margin_right = 456.0 +margin_bottom = 85.0 +size_flags_horizontal = 4 +custom_fonts/font = SubResource( 2 ) +text = "This week" + +[node name="HighScores" type="CenterContainer" parent="MainContainer/Boards/Weekly"] +margin_left = 266.0 +margin_top = 89.0 +margin_right = 266.0 +margin_bottom = 189.0 +rect_min_size = Vector2( 0, 100 ) +size_flags_horizontal = 4 +theme = SubResource( 3 ) + +[node name="ScoreItemContainer" type="VBoxContainer" parent="MainContainer/Boards/Weekly/HighScores"] +margin_top = 50.0 +margin_bottom = 50.0 + +[node name="Monthly" type="VBoxContainer" parent="MainContainer/Boards"] +margin_left = 692.0 +margin_right = 1224.0 +margin_bottom = 189.0 +size_flags_horizontal = 3 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="TitleContainer" type="CenterContainer" parent="MainContainer/Boards/Monthly"] +margin_right = 532.0 +margin_bottom = 85.0 + +[node name="Label" type="Label" parent="MainContainer/Boards/Monthly/TitleContainer"] +margin_left = 44.0 +margin_right = 488.0 +margin_bottom = 85.0 +custom_fonts/font = SubResource( 2 ) +text = "This month" + +[node name="HighScores" type="CenterContainer" parent="MainContainer/Boards/Monthly"] +margin_left = 266.0 +margin_top = 89.0 +margin_right = 266.0 +margin_bottom = 189.0 +rect_min_size = Vector2( 0, 100 ) +size_flags_horizontal = 4 +theme = SubResource( 3 ) + +[node name="ScoreItemContainer" type="VBoxContainer" parent="MainContainer/Boards/Monthly/HighScores"] +margin_top = 50.0 +margin_bottom = 50.0 + +[node name="main" type="VBoxContainer" parent="MainContainer/Boards"] +margin_left = 1384.0 +margin_right = 1918.0 +margin_bottom = 189.0 +size_flags_horizontal = 3 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="TitleContainer" type="CenterContainer" parent="MainContainer/Boards/main"] +margin_right = 534.0 +margin_bottom = 85.0 + +[node name="Label" type="Label" parent="MainContainer/Boards/main/TitleContainer"] +margin_left = 121.0 +margin_right = 413.0 +margin_bottom = 85.0 +custom_fonts/font = SubResource( 2 ) +text = "All time" +align = 1 + +[node name="HighScores" type="CenterContainer" parent="MainContainer/Boards/main"] +margin_left = 267.0 +margin_top = 89.0 +margin_right = 267.0 +margin_bottom = 189.0 +rect_min_size = Vector2( 0, 100 ) +size_flags_horizontal = 4 +theme = SubResource( 3 ) + +[node name="ScoreItemContainer" type="VBoxContainer" parent="MainContainer/Boards/main/HighScores"] +margin_top = 50.0 +margin_bottom = 50.0 + +[node name="MessageContainer" type="CenterContainer" parent="MainContainer"] +visible = false +margin_left = 1144.0 +margin_top = 228.0 +margin_right = 3008.0 +margin_bottom = 265.0 + +[node name="TextMessage" type="Label" parent="MainContainer/MessageContainer"] +margin_left = 789.0 +margin_right = 1075.0 +margin_bottom = 37.0 +custom_fonts/font = SubResource( 8 ) +text = "Loading scores..." +valign = 1 + +[node name="CenterContainer" type="CenterContainer" parent="MainContainer"] +margin_top = 332.0 +margin_right = 1918.0 +margin_bottom = 450.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="CloseButtonContainer" type="CenterContainer" parent="MainContainer/CenterContainer"] +margin_left = 609.0 +margin_right = 1308.0 +margin_bottom = 118.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="CloseButton" parent="MainContainer/CenterContainer/CloseButtonContainer" instance=ExtResource( 4 )] +margin_right = 699.0 +margin_bottom = 118.0 +custom_styles/hover = SubResource( 4 ) +custom_styles/pressed = SubResource( 5 ) +custom_styles/normal = SubResource( 6 ) +custom_fonts/font = SubResource( 7 ) +text = "Close Leaderboard" +[connection signal="pressed" from="MainContainer/CenterContainer/CloseButtonContainer/CloseButton" to="." method="_on_CloseButton_pressed"] diff --git a/addons/silent_wolf/plugin.cfg b/addons/silent_wolf/plugin.cfg new file mode 100644 index 0000000..ecef28c --- /dev/null +++ b/addons/silent_wolf/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Silent Wolf plugin" +description="Backend services for Godot Engine." +author="Brass Harpooner" +version="0.6.6" +script="silent_wolf.gd" diff --git a/addons/silent_wolf/silent_wolf.gd b/addons/silent_wolf/silent_wolf.gd new file mode 100644 index 0000000..ed9e19d --- /dev/null +++ b/addons/silent_wolf/silent_wolf.gd @@ -0,0 +1,8 @@ +tool +extends EditorPlugin + +func _enter_tree(): + add_autoload_singleton("SilentWolf", "res://addons/silent_wolf/SilentWolf.gd") + +func _exit_tree(): + remove_autoload_singleton("SilentWolf") diff --git a/addons/silent_wolf/utils/SWHashing.gd b/addons/silent_wolf/utils/SWHashing.gd new file mode 100644 index 0000000..d6d74b8 --- /dev/null +++ b/addons/silent_wolf/utils/SWHashing.gd @@ -0,0 +1,9 @@ +extends Node + +static func hash_values(values: Array) -> String: + var to_be_hashed = "" + for value in values: + to_be_hashed = to_be_hashed + str(value) + var hashed = to_be_hashed.md5_text() + #print("Computed hashed: " + str(hashed)) + return hashed diff --git a/addons/silent_wolf/utils/SWLocalFileStorage.gd b/addons/silent_wolf/utils/SWLocalFileStorage.gd new file mode 100644 index 0000000..b72137e --- /dev/null +++ b/addons/silent_wolf/utils/SWLocalFileStorage.gd @@ -0,0 +1,45 @@ +extends Node + +const SWLogger = preload("res://addons/silent_wolf/utils/SWLogger.gd") + +# Retrieves data stored as JSON in local storage +# example path: "user://swsession.save" + +# store lookup (not logged in player name) and validator in local file +static func save_data(path: String, data: Dictionary, debug_message: String='Saving data to file in local storage: ') -> void: + var local_file = File.new() + local_file.open(path, File.WRITE) + SWLogger.debug(debug_message + str(data)) + local_file.store_line(to_json(data)) + local_file.close() + + +static func remove_data(path: String, debug_message: String='Removing data from file in local storage: ') -> void: + var local_file = File.new() + local_file.open(path, File.WRITE) + var data = {} + SWLogger.debug(debug_message + str(data)) + local_file.store_line(to_json(data)) + local_file.close() + + +static func does_file_exist(path: String, file: File=null) -> bool: + var local_file = file + if local_file == null: + local_file = File.new() + return local_file.file_exists(path) + + +static func get_data(path: String) -> Dictionary: + var local_file = File.new() + var return_data = null + if does_file_exist(path, local_file): + local_file.open(path, File.READ) + var data = parse_json(local_file.get_as_text()) + if typeof(data) == TYPE_DICTIONARY: + return_data = data + else: + SWLogger.debug("Invalid data in local storage") + else: + SWLogger.debug("Could not find any data at: " + str(path)) + return return_data diff --git a/addons/silent_wolf/utils/SWLogger.gd b/addons/silent_wolf/utils/SWLogger.gd new file mode 100644 index 0000000..708bdd2 --- /dev/null +++ b/addons/silent_wolf/utils/SWLogger.gd @@ -0,0 +1,21 @@ +extends Node + +static func get_log_level(): + var log_level = 1 + if SilentWolf.config.has('log_level'): + log_level = SilentWolf.config.log_level + else: + error("Couldn't find SilentWolf.config.log_level, defaulting to 1") + return log_level + +static func error(text): + printerr(str(text)) + push_error(str(text)) + +static func info(text): + if get_log_level() > 0: + print(str(text)) + +static func debug(text): + if get_log_level() > 1: + print(str(text)) diff --git a/addons/silent_wolf/utils/UUID.gd b/addons/silent_wolf/utils/UUID.gd new file mode 100644 index 0000000..bde2f5d --- /dev/null +++ b/addons/silent_wolf/utils/UUID.gd @@ -0,0 +1,47 @@ +static func getRandomInt(max_value): + randomize() + return randi() % max_value + +static func randomBytes(n): + var r = [] + for index in range(0, n): + r.append(getRandomInt(256)) + return r + +static func uuidbin(): + var b = randomBytes(16) + + b[6] = (b[6] & 0x0f) | 0x40 + b[8] = (b[8] & 0x3f) | 0x80 + return b + +static func generate_uuid_v4(): + var b = uuidbin() + + var low = '%02x%02x%02x%02x' % [b[0], b[1], b[2], b[3]] + var mid = '%02x%02x' % [b[4], b[5]] + var hi = '%02x%02x' % [b[6], b[7]] + var clock = '%02x%02x' % [b[8], b[9]] + var node = '%02x%02x%02x%02x%02x%02x' % [b[10], b[11], b[12], b[13], b[14], b[15]] + return '%s-%s-%s-%s-%s' % [low, mid, hi, clock, node] + + +# argument must be of type string! +static func is_uuid(test_string): + # if length of string is 36 and contains exactly 4 dashes, it's a UUID + return test_string.length() == 36 and test_string.count("-") == 4 + + +# MIT License + +# Copyright (c) 2018 Xavier Sellier + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software.