From adcc9c937d5a04112c66ef6c54f228e4c0c891bf Mon Sep 17 00:00:00 2001 From: TechieDamien Date: Sat, 2 Aug 2025 13:52:19 +0100 Subject: [PATCH] Adds SilentWolf plugin in preparation for leaderboard support --- .gitignore | 3 + Scripts/game_manager.gd | 16 + addons/silent_wolf/.DS_Store | Bin 0 -> 10244 bytes addons/silent_wolf/Auth/Auth.gd | 459 ++++++++++++++++++ addons/silent_wolf/Auth/Auth.gd.uid | 1 + addons/silent_wolf/Auth/ConfirmEmail.gd | 71 +++ addons/silent_wolf/Auth/ConfirmEmail.gd.uid | 1 + addons/silent_wolf/Auth/ConfirmEmail.tscn | 71 +++ addons/silent_wolf/Auth/Login.gd | 54 +++ addons/silent_wolf/Auth/Login.gd.uid | 1 + addons/silent_wolf/Auth/Login.tscn | 124 +++++ addons/silent_wolf/Auth/Register.gd | 93 ++++ addons/silent_wolf/Auth/Register.gd.uid | 1 + addons/silent_wolf/Auth/Register.tscn | 157 ++++++ .../Auth/RegisterUsernamePassword.tscn | 133 +++++ addons/silent_wolf/Auth/ResetPassword.gd | 77 +++ addons/silent_wolf/Auth/ResetPassword.gd.uid | 1 + addons/silent_wolf/Auth/ResetPassword.tscn | 200 ++++++++ addons/silent_wolf/Muliplayer/.DS_Store | Bin 0 -> 6148 bytes addons/silent_wolf/Muliplayer/Multiplayer.gd | 39 ++ .../silent_wolf/Muliplayer/Multiplayer.gd.uid | 1 + addons/silent_wolf/Muliplayer/ws/WSClient.gd | 82 ++++ .../silent_wolf/Muliplayer/ws/WSClient.gd.uid | 1 + addons/silent_wolf/Multiplayer/.DS_Store | Bin 0 -> 6148 bytes addons/silent_wolf/Multiplayer/Multiplayer.gd | 39 ++ .../Multiplayer/Multiplayer.gd.uid | 1 + addons/silent_wolf/Multiplayer/ws/WSClient.gd | 82 ++++ .../Multiplayer/ws/WSClient.gd.uid | 1 + addons/silent_wolf/Players/Players.gd | 171 +++++++ addons/silent_wolf/Players/Players.gd.uid | 1 + addons/silent_wolf/Scores/Leaderboard.gd | 127 +++++ addons/silent_wolf/Scores/Leaderboard.gd.uid | 1 + addons/silent_wolf/Scores/Leaderboard.tscn | 63 +++ addons/silent_wolf/Scores/ScoreItem.tscn | 25 + addons/silent_wolf/Scores/Scores.gd | 389 +++++++++++++++ addons/silent_wolf/Scores/Scores.gd.uid | 1 + addons/silent_wolf/SilentWolf.gd | 248 ++++++++++ addons/silent_wolf/SilentWolf.gd.uid | 1 + addons/silent_wolf/assets/.DS_Store | Bin 0 -> 8196 bytes .../assets/fonts/Comfortaa-Bold.ttf | Bin 0 -> 137696 bytes .../assets/fonts/Comfortaa-Bold.ttf.import | 35 ++ .../assets/gfx/checkbox_checked.png | Bin 0 -> 813 bytes .../assets/gfx/checkbox_checked.png.import | 34 ++ .../assets/gfx/checkbox_unchecked.png | Bin 0 -> 215 bytes .../assets/gfx/checkbox_unchecked.png.import | 34 ++ .../assets/gfx/dummy_info_icon_small.png | Bin 0 -> 158 bytes .../gfx/dummy_info_icon_small.png.import | 34 ++ addons/silent_wolf/assets/gfx/info_icon.png | Bin 0 -> 23374 bytes .../assets/gfx/info_icon.png.import | 34 ++ .../assets/gfx/info_icon_small.png | Bin 0 -> 15579 bytes .../assets/gfx/info_icon_small.png.import | 34 ++ .../silent_wolf/assets/themes/sw_theme.tres | 38 ++ addons/silent_wolf/common/SWButton.tscn | 4 + addons/silent_wolf/examples/.DS_Store | Bin 0 -> 6148 bytes .../CustomLeaderboards/ReverseLeaderboard.gd | 92 ++++ .../ReverseLeaderboard.gd.uid | 1 + .../ReverseLeaderboard.tscn | 51 ++ .../CustomLeaderboards/SmallScoreItem.tscn | 5 + .../CustomLeaderboards/TimeBasedLboards.gd | 87 ++++ .../TimeBasedLboards.gd.uid | 1 + .../CustomLeaderboards/TimeBasedLboards.tscn | 114 +++++ addons/silent_wolf/plugin.cfg | 7 + addons/silent_wolf/silent_wolf.gd | 8 + addons/silent_wolf/silent_wolf.gd.uid | 1 + addons/silent_wolf/utils/SWHashing.gd | 9 + addons/silent_wolf/utils/SWHashing.gd.uid | 1 + addons/silent_wolf/utils/SWHttpUtils.gd | 5 + addons/silent_wolf/utils/SWHttpUtils.gd.uid | 1 + .../silent_wolf/utils/SWLocalFileStorage.gd | 49 ++ .../utils/SWLocalFileStorage.gd.uid | 1 + addons/silent_wolf/utils/SWLogger.gd | 32 ++ addons/silent_wolf/utils/SWLogger.gd.uid | 1 + addons/silent_wolf/utils/SWUtils.gd | 35 ++ addons/silent_wolf/utils/SWUtils.gd.uid | 1 + addons/silent_wolf/utils/UUID.gd | 46 ++ addons/silent_wolf/utils/UUID.gd.uid | 1 + project.godot | 2 + 77 files changed, 3534 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/Auth.gd.uid create mode 100644 addons/silent_wolf/Auth/ConfirmEmail.gd create mode 100644 addons/silent_wolf/Auth/ConfirmEmail.gd.uid 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.gd.uid 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.gd.uid 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.gd.uid create mode 100644 addons/silent_wolf/Auth/ResetPassword.tscn create mode 100644 addons/silent_wolf/Muliplayer/.DS_Store create mode 100644 addons/silent_wolf/Muliplayer/Multiplayer.gd create mode 100644 addons/silent_wolf/Muliplayer/Multiplayer.gd.uid create mode 100644 addons/silent_wolf/Muliplayer/ws/WSClient.gd create mode 100644 addons/silent_wolf/Muliplayer/ws/WSClient.gd.uid 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/Multiplayer.gd.uid create mode 100644 addons/silent_wolf/Multiplayer/ws/WSClient.gd create mode 100644 addons/silent_wolf/Multiplayer/ws/WSClient.gd.uid create mode 100644 addons/silent_wolf/Players/Players.gd create mode 100644 addons/silent_wolf/Players/Players.gd.uid create mode 100644 addons/silent_wolf/Scores/Leaderboard.gd create mode 100644 addons/silent_wolf/Scores/Leaderboard.gd.uid 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/Scores.gd.uid create mode 100644 addons/silent_wolf/SilentWolf.gd create mode 100644 addons/silent_wolf/SilentWolf.gd.uid 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/fonts/Comfortaa-Bold.ttf.import create mode 100644 addons/silent_wolf/assets/gfx/checkbox_checked.png create mode 100644 addons/silent_wolf/assets/gfx/checkbox_checked.png.import create mode 100644 addons/silent_wolf/assets/gfx/checkbox_unchecked.png create mode 100644 addons/silent_wolf/assets/gfx/checkbox_unchecked.png.import create mode 100644 addons/silent_wolf/assets/gfx/dummy_info_icon_small.png create mode 100644 addons/silent_wolf/assets/gfx/dummy_info_icon_small.png.import create mode 100644 addons/silent_wolf/assets/gfx/info_icon.png create mode 100644 addons/silent_wolf/assets/gfx/info_icon.png.import create mode 100644 addons/silent_wolf/assets/gfx/info_icon_small.png create mode 100644 addons/silent_wolf/assets/gfx/info_icon_small.png.import create mode 100644 addons/silent_wolf/assets/themes/sw_theme.tres 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.gd.uid 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.gd.uid 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/silent_wolf.gd.uid create mode 100644 addons/silent_wolf/utils/SWHashing.gd create mode 100644 addons/silent_wolf/utils/SWHashing.gd.uid create mode 100644 addons/silent_wolf/utils/SWHttpUtils.gd create mode 100644 addons/silent_wolf/utils/SWHttpUtils.gd.uid create mode 100644 addons/silent_wolf/utils/SWLocalFileStorage.gd create mode 100644 addons/silent_wolf/utils/SWLocalFileStorage.gd.uid create mode 100644 addons/silent_wolf/utils/SWLogger.gd create mode 100644 addons/silent_wolf/utils/SWLogger.gd.uid create mode 100644 addons/silent_wolf/utils/SWUtils.gd create mode 100644 addons/silent_wolf/utils/SWUtils.gd.uid create mode 100644 addons/silent_wolf/utils/UUID.gd create mode 100644 addons/silent_wolf/utils/UUID.gd.uid diff --git a/.gitignore b/.gitignore index f0b5c41..c17e954 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ mono_crash.*.json # Export /Export/ + +# No Secrets +secrets.json diff --git a/Scripts/game_manager.gd b/Scripts/game_manager.gd index 9e97be0..4f7fb94 100644 --- a/Scripts/game_manager.gd +++ b/Scripts/game_manager.gd @@ -1,3 +1,19 @@ extends Node var score : int = 0 + +const secrets_file_path : String = "res://secrets.json" +var initialised_silent_wolf : bool = false + +func _ready(): + var json : JSON = JSON.new() + var file : FileAccess = FileAccess.open(secrets_file_path, FileAccess.READ) + var contents : String = file.get_as_text() + + if json.parse(contents) == OK: + if json.data is Dictionary: + SilentWolf.configure(json.data) + + SilentWolf.configure_scores({ + "open_scene_on_close": "res://Scenes/main_menu.tscn" + }) diff --git a/addons/silent_wolf/.DS_Store b/addons/silent_wolf/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..aea63aaa790d0c0bc0b7bac2c3549665715fc2ef GIT binary patch literal 10244 zcmeHMPiqrF6o1pE>DDUrpx{MV5kawFw-pt=Y}0rY#1#eqP_s>Fmu_}KHZe-6O)T5ya2nLHz_Cy?XJ@pC+@jljKswt}|ixZFYWdChzx~ot-xU08;7JJpdN~ z96Xp^IEp7jVfF0Dl__}#+W~_903ArfUfK#{8s^aUhGoDqU>UFsSOzQu{{jQ}&Su4# z7uKFF1C{~Hz<>cZKe%`>>r2}gmQNjc;wb>y65i$ouQ3ncKCZOBw0&W@E4C@J2O?dG zOfiUX$9k8+(fZQ%g%$2VggX$~Gm#kz;k~1u%i%zLVeQ#6U>V3Wz;gF8)F8!AL(AVk zgsnJAJ4;Jn$<%adrabG+I`ht>V2k#GPOZ~TE4B6m_U#U(akzh73-3ma-ulAvHJWs4 zQPOC0hp65_$isV4Qm4HN?I!h<`v#)InRDjW7f$Z%Tw1xj=z8AnqPydHE6a=SRd0EB zcg{I+=Hm6+>)UZNp|3<3L{0^8ObhF^9#cKth(r91%pntOyy*oSwAoS`$kaD!wJJ9= zobukD|M};aRlSWStxv?Xw#N7KBxolwO*XM@_8UvE3SAUqOW900%wi1UG4MU8Q@MWr z{l_)QwNgLi*;H@fc;Kt?TZRpw(8Ldgq#zt;VlwcYsaLsv@$JHu!MIj8MMf?rRi}5ZsI%> zL&Wu{!86pPU6AL&7@I*CBTm(3>do)-QkxOjp+1wa291nuYC6rrEmTSBQ<$eli^`yl zhRCS$cI~WGC5KY<7>P0{p|zuFP=i)O0@BUb~COV?0=4xW2I51y6iC9xdOF z$1iL<{u~|*yTmLjcv9-%bvG`rSJ%|zaE6s$u1{^f gXUsiq{tsK_jCEnoe3~dLRn@0XSc~ivR!s 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..d968e99 --- /dev/null +++ b/addons/silent_wolf/Auth/Auth.gd @@ -0,0 +1,459 @@ +extends Node + +const SWLocalFileStorage = preload("res://addons/silent_wolf/utils/SWLocalFileStorage.gd") +const SWUtils = preload("res://addons/silent_wolf/utils/SWUtils.gd") +const SWLogger = preload("res://addons/silent_wolf/utils/SWLogger.gd") +const UUID = preload("res://addons/silent_wolf/utils/UUID.gd") + +signal sw_login_complete +signal sw_logout_complete +signal sw_registration_complete +signal sw_registration_user_pwd_complete +signal sw_email_verif_complete +signal sw_resend_conf_code_complete +signal sw_session_check_complete +signal sw_request_password_reset_complete +signal sw_reset_password_complete +signal sw_get_player_details_complete + +var tmp_username = null +var logged_in_player = null +var logged_in_player_email = null +var logged_in_anon = false +var sw_access_token = null +var sw_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 register_player_anon(player_name = null) -> Node: + var user_local_id: String = get_anon_user_id() + var prepared_http_req = SilentWolf.prepare_http_request() + RegisterPlayer = prepared_http_req.request + wrRegisterPlayer = prepared_http_req.weakref + RegisterPlayer.request_completed.connect(_on_RegisterPlayer_request_completed) + SWLogger.info("Calling SilentWolf to register an anonymous player") + var payload = { "game_id": SilentWolf.config.game_id, "anon": true, "player_name": player_name, "user_local_id": user_local_id } + var request_url = "https://api.silentwolf.com/create_new_player" + SilentWolf.send_post_request(RegisterPlayer, request_url, payload) + return self + + +func register_player(player_name: String, email: String, password: String, confirm_password: String) -> Node: + tmp_username = player_name + var prepared_http_req = SilentWolf.prepare_http_request() + RegisterPlayer = prepared_http_req.request + wrRegisterPlayer = prepared_http_req.weakref + RegisterPlayer.request_completed.connect(_on_RegisterPlayer_request_completed) + SWLogger.info("Calling SilentWolf to register a player") + var payload = { "game_id": SilentWolf.config.game_id, "anon": false, "player_name": player_name, "email": email, "password": password, "confirm_password": confirm_password } + var request_url = "https://api.silentwolf.com/create_new_player" + SilentWolf.send_post_request(RegisterPlayer, request_url, payload) + return self + + +func _on_RegisterPlayer_request_completed(result, response_code, headers, body) -> void: + var status_check = SWUtils.check_http_response(response_code, headers, body) + SilentWolf.free_request(wrRegisterPlayer, RegisterPlayer) + + if status_check: + var json_body = JSON.parse_string(body.get_string_from_utf8()) + # also get a JWT token here, when available in backend + # send a different signal depending on registration success or failure + var sw_result: Dictionary = SilentWolf.build_result(json_body) + if json_body.success: + SWLogger.info("SilentWolf register player success, player_name: " + str(json_body.player_name)) + #sw_token = json_body.swtoken + var anon = json_body.anon + if anon: + SWLogger.info("Anonymous Player registration succeeded") + logged_in_anon = true + if 'player_name' in json_body: + logged_in_player = json_body.player_name + elif 'player_local_id' in json_body: + logged_in_player = str("anon##" + json_body.player_local_id) + else: + logged_in_player = "anon##unknown" + SWLogger.info("Anonymous 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 = json_body.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 + else: + SWLogger.error("SilentWolf player registration failure: " + str(json_body.error)) + sw_registration_complete.emit(sw_result) + + +func register_player_user_password(player_name: String, password: String, confirm_password: String) -> Node: + var prepared_http_req = SilentWolf.prepare_http_request() + RegisterPlayer = prepared_http_req.request + wrRegisterPlayer = prepared_http_req.weakref + RegisterPlayer.request_completed.connect(_on_RegisterPlayerUserPassword_request_completed) + SWLogger.info("Calling SilentWolf to register a player") + var payload = { "game_id": SilentWolf.config.game_id, "player_name": player_name, "password": password, "confirm_password": confirm_password } + var request_url = "https://api.silentwolf.com/create_new_player" + SilentWolf.send_post_request(RegisterPlayer, request_url, payload) + return self + + +func _on_RegisterPlayerUserPassword_request_completed(result, response_code, headers, body) -> void: + var status_check = SWUtils.check_http_response(response_code, headers, body) + #RegisterPlayer.queue_free() + SilentWolf.free_request(wrRegisterPlayer, RegisterPlayer) + + if status_check: + var json_body = JSON.parse_string(body.get_string_from_utf8()) + # also get a JWT token here + # send a different signal depending on registration success or failure + var sw_result: Dictionary = SilentWolf.build_result(json_body) + if json_body.success: + # if email confirmation is enabled for the game, we can't log in the player just yet + var email_conf_enabled = json_body.email_conf_enabled + SWLogger.info("Player registration with username/password succeeded, player account autoconfirmed.") + logged_in_player = tmp_username + else: + SWLogger.error("SilentWolf username/password player registration failure: " + str(json_body.error)) + sw_registration_user_pwd_complete.emit(sw_result) + + +func verify_email(player_name: String, code: String) -> Node: + var prepared_http_req = SilentWolf.prepare_http_request() + VerifyEmail = prepared_http_req.request + wrVerifyEmail = prepared_http_req.weakref + VerifyEmail.request_completed.connect(_on_VerifyEmail_request_completed) + SWLogger.info("Calling SilentWolf to verify email address for: " + str(player_name)) + var payload = { "game_id": SilentWolf.config.game_id, "username": player_name, "code": code } + var request_url = "https://api.silentwolf.com/confirm_verif_code" + SilentWolf.send_post_request(VerifyEmail, request_url, payload) + return self + + +func _on_VerifyEmail_request_completed(result, response_code, headers, body) -> void: + var status_check = SWUtils.check_http_response(response_code, headers, body) + SilentWolf.free_request(wrVerifyEmail, VerifyEmail) + + if status_check: + var json_body = JSON.parse_string(body.get_string_from_utf8()) + SWLogger.info("SilentWolf verify email success? : " + str(json_body.success)) + # also get a JWT token here + # send a different signal depending on registration success or failure + var sw_result: Dictionary = SilentWolf.build_result(json_body) + if json_body.success: + SWLogger.info("SilentWolf email verification success.") + logged_in_player = tmp_username + else: + SWLogger.error("SilentWolf email verification failure: " + str(json_body.error)) + sw_email_verif_complete.emit(sw_result) + + +func resend_conf_code(player_name: String) -> Node: + var prepared_http_req = SilentWolf.prepare_http_request() + ResendConfCode = prepared_http_req.request + wrResendConfCode = prepared_http_req.weakref + ResendConfCode.request_completed.connect(_on_ResendConfCode_request_completed) + SWLogger.info("Calling SilentWolf to resend confirmation code for: " + str(player_name)) + var payload = { "game_id": SilentWolf.config.game_id, "username": player_name } + var request_url = "https://api.silentwolf.com/resend_conf_code" + SilentWolf.send_post_request(ResendConfCode, request_url, payload) + return self + + +func _on_ResendConfCode_request_completed(result, response_code, headers, body) -> void: + SWLogger.info("ResendConfCode request completed") + var status_check = SWUtils.check_http_response(response_code, headers, body) + SilentWolf.free_request(wrResendConfCode, ResendConfCode) + + if status_check: + var json_body = JSON.parse_string(body.get_string_from_utf8()) + # also get a JWT token here + # send a different signal depending on registration success or failure + var sw_result: Dictionary = SilentWolf.build_result(json_body) + if json_body.success: + SWLogger.info("SilentWolf resend conf code success.") + else: + SWLogger.error("SilentWolf resend conf code failure: " + str(json_body.error)) + sw_resend_conf_code_complete.emit(sw_result) + + +func login_player(username: String, password: String, remember_me:bool=false) -> Node: + tmp_username = username + var prepared_http_req = SilentWolf.prepare_http_request() + LoginPlayer = prepared_http_req.request + wrLoginPlayer = prepared_http_req.weakref + LoginPlayer.request_completed.connect(_on_LoginPlayer_request_completed) + SWLogger.info("Calling SilentWolf to log in a player") + var payload = { "game_id": SilentWolf.config.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) + var payload_for_logging = payload + var obfuscated_password = SWUtils.obfuscate_string(payload["password"]) + print("obfuscated password: " + str(obfuscated_password)) + payload_for_logging["password"] = obfuscated_password + SWLogger.debug("SilentWolf login player payload: " + str(payload_for_logging)) + var request_url = "https://api.silentwolf.com/login_player" + SilentWolf.send_post_request(LoginPlayer, request_url, payload) + return self + + +func _on_LoginPlayer_request_completed(result, response_code, headers, body) -> void: + var status_check = SWUtils.check_http_response(response_code, headers, body) + SilentWolf.free_request(wrLoginPlayer, LoginPlayer) + + if status_check: + var json_body = JSON.parse_string(body.get_string_from_utf8()) + if "lookup" in json_body.keys(): + SWLogger.debug("remember me lookup: " + str(json_body.lookup)) + save_session(json_body.lookup, json_body.validator) + if "validator" in json_body.keys(): + SWLogger.debug("remember me validator: " + str(json_body.validator)) + # send a different signal depending on login success or failure + var sw_result: Dictionary = SilentWolf.build_result(json_body) + if json_body.success: + SWLogger.info("SilentWolf resend conf code success.") + sw_access_token = json_body.swtoken + sw_id_token = json_body.swidtoken + set_player_logged_in(tmp_username) + else: + SWLogger.error("SilentWolf login player failure: " + str(json_body.error)) + sw_login_complete.emit(sw_result) + + +func logout_player() -> void: + logged_in_player = null + # remove any player data if present + SilentWolf.Players.clear_player_data() + # remove stored session if any + var delete_success = remove_stored_session() + print("delete_success: " + str(delete_success)) + sw_access_token = null + sw_id_token = null + sw_logout_complete.emit(true, "") + + +func request_player_password_reset(player_name: String) -> Node: + var prepared_http_req = SilentWolf.prepare_http_request() + RequestPasswordReset = prepared_http_req.request + wrRequestPasswordReset = prepared_http_req.weakref + RequestPasswordReset.request_completed.connect(_on_RequestPasswordReset_request_completed) + SWLogger.info("Calling SilentWolf to request a password reset for: " + str(player_name)) + var payload = { "game_id": SilentWolf.config.game_id, "player_name": player_name } + SWLogger.debug("SilentWolf request player password reset payload: " + str(payload)) + var request_url = "https://api.silentwolf.com/request_player_password_reset" + SilentWolf.send_post_request(RequestPasswordReset, request_url, payload) + return self + + +func _on_RequestPasswordReset_request_completed(result, response_code, headers, body) -> void: + SWLogger.info("RequestPasswordReset request completed") + var status_check = SWUtils.check_http_response(response_code, headers, body) + SilentWolf.free_request(wrRequestPasswordReset, RequestPasswordReset) + + if status_check: + var json_body = JSON.parse_string(body.get_string_from_utf8()) + var sw_result: Dictionary = SilentWolf.build_result(json_body) + if json_body.success: + SWLogger.info("SilentWolf request player password reset success.") + else: + SWLogger.error("SilentWolf request password reset failure: " + str(json_body.error)) + sw_request_password_reset_complete.emit(sw_result) + + +func reset_player_password(player_name: String, conf_code: String, new_password: String, confirm_password: String) -> Node: + var prepared_http_req = SilentWolf.prepare_http_request() + ResetPassword = prepared_http_req.request + wrResetPassword = prepared_http_req.weakref + ResetPassword.request_completed.connect(_on_ResetPassword_completed) + SWLogger.info("Calling SilentWolf to reset password for: " + str(player_name)) + var payload = { "game_id": SilentWolf.config.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 request_url = "https://api.silentwolf.com/reset_player_password" + SilentWolf.send_post_request(ResetPassword, request_url, payload) + return self + + +func _on_ResetPassword_completed(result, response_code, headers, body) -> void: + var status_check = SWUtils.check_http_response(response_code, headers, body) + SilentWolf.free_request(wrResetPassword, ResetPassword) + + if status_check: + var json_body = JSON.parse_string(body.get_string_from_utf8()) + var sw_result: Dictionary = SilentWolf.build_result(json_body) + if json_body.success: + SWLogger.info("SilentWolf reset player password success.") + else: + SWLogger.error("SilentWolf reset password failure: " + str(json_body.error)) + sw_reset_password_complete.emit(sw_result) + + +func get_player_details(player_name: String) -> Node: + var prepared_http_req = SilentWolf.prepare_http_request() + GetPlayerDetails = prepared_http_req.request + wrGetPlayerDetails = prepared_http_req.weakref + GetPlayerDetails.request_completed.connect(_on_GetPlayerDetails_request_completed) + SWLogger.info("Calling SilentWolf to get player details") + var payload = { "game_id": SilentWolf.config.game_id, "player_name": player_name } + var request_url = "https://api.silentwolf.com/get_player_details" + SilentWolf.send_post_request(GetPlayerDetails, request_url, payload) + return self + + +func _on_GetPlayerDetails_request_completed(result, response_code, headers, body) -> void: + var status_check = SWUtils.check_http_response(response_code, headers, body) + SilentWolf.free_request(wrGetPlayerDetails, GetPlayerDetails) + + if status_check: + var json_body = JSON.parse_string(body.get_string_from_utf8()) + var sw_result: Dictionary = SilentWolf.build_result(json_body) + if json_body.success: + SWLogger.info("SilentWolf get player details success: " + str(json_body.player_details)) + sw_result["player_details"] = json_body.player_details + else: + SWLogger.error("SilentWolf get player details failure: " + str(json_body.error)) + sw_get_player_details_complete.emit(sw_result) + + +func validate_player_session(lookup: String, validator: String, scene: Node=get_tree().get_current_scene()) -> Node: + var prepared_http_req = SilentWolf.prepare_http_request() + ValidateSession = prepared_http_req.request + wrValidateSession = prepared_http_req.weakref + ValidateSession.request_completed.connect(_on_ValidateSession_request_completed) + SWLogger.info("Calling SilentWolf to validate an existing player session") + var payload = { "game_id": SilentWolf.config.game_id, "lookup": lookup, "validator": validator } + SWLogger.debug("Validate session payload: " + str(payload)) + var request_url = "https://api.silentwolf.com/validate_remember_me" + SilentWolf.send_post_request(ValidateSession, request_url, payload) + return self + + +func _on_ValidateSession_request_completed(result, response_code, headers, body) -> void: + var status_check = SWUtils.check_http_response(response_code, headers, body) + SilentWolf.free_request(wrValidateSession, ValidateSession) + + if status_check: + var json_body = JSON.parse_string(body.get_string_from_utf8()) + var sw_result: Dictionary = SilentWolf.build_result(json_body) + if json_body.success: + SWLogger.info("SilentWolf validate session success.") + set_player_logged_in(json_body.player_name) + sw_result["logged_in_player"] = logged_in_player + else: + SWLogger.error("SilentWolf validate session failure: " + str(json_body.error)) + complete_session_check(sw_result) + + +func auto_login_player() -> Node: + var sw_session_data = load_session() + SWLogger.debug("SW session data " + str(sw_session_data)) + 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 + + +func set_player_logged_in(player_name: String) -> void: + 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 remove_stored_session() -> bool: + var path = "user://swsession.save" + var delete_success = SWLocalFileStorage.remove_data(path, "Removing SilentWolf session if any: " ) + return delete_success + + +# Signal can't be emitted directly from auto_login_player() function +# otherwise it won't connect back to calling script +func complete_session_check(sw_result=null) -> void: + SWLogger.debug("SilentWolf: completing session check") + sw_session_check_complete.emit(sw_result) + + +func setup_complete_session_check_wait_timer() -> void: + 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.timeout.connect(complete_session_check) + add_child(complete_session_check_wait_timer) + + +func setup_login_timer() -> void: + login_timer = Timer.new() + login_timer.set_one_shot(true) + login_timer.set_wait_time(login_timeout) + login_timer.timeout.connect(on_login_timeout_complete) + add_child(login_timer) + + +func on_login_timeout_complete() -> void: + logout_player() + + +# store lookup (not logged in player name) and validator in local file +func save_session(lookup: String, validator: String) -> void: + SWLogger.debug("Saving session, lookup: " + str(lookup) + ", validator: " + str(validator)) + var path = "user://swsession.save" + var session_data: Dictionary = { + "lookup": lookup, + "validator": validator + } + SWLocalFileStorage.save_data("user://swsession.save", session_data, "Saving SilentWolf session: ") + + +# 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 diff --git a/addons/silent_wolf/Auth/Auth.gd.uid b/addons/silent_wolf/Auth/Auth.gd.uid new file mode 100644 index 0000000..3154a3d --- /dev/null +++ b/addons/silent_wolf/Auth/Auth.gd.uid @@ -0,0 +1 @@ +uid://cpvig1whdvguk diff --git a/addons/silent_wolf/Auth/ConfirmEmail.gd b/addons/silent_wolf/Auth/ConfirmEmail.gd new file mode 100644 index 0000000..38a1b74 --- /dev/null +++ b/addons/silent_wolf/Auth/ConfirmEmail.gd @@ -0,0 +1,71 @@ +extends TextureRect + +const SWLogger = preload("res://addons/silent_wolf/utils/SWLogger.gd") + + +func _ready(): + SilentWolf.Auth.sw_email_verif_complete.connect(_on_confirmation_complete) + SilentWolf.Auth.sw_resend_conf_code_complete.connect(_on_resend_code_complete) + + +func _on_confirmation_complete(sw_result: Dictionary) -> void: + if sw_result.success: + confirmation_success() + else: + confirmation_failure(sw_result.error) + + +func confirmation_success() -> void: + 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_to_file(scene_name) + + +func confirmation_failure(error: String) -> void: + hide_processing_label() + SWLogger.info("email verification failed: " + str(error)) + $"FormContainer/ErrorMessage".text = error + $"FormContainer/ErrorMssage".show() + + +func _on_resend_code_complete(sw_result: Dictionary) -> void: + if sw_result.success: + resend_code_success() + else: + resend_code_failure() + + +func resend_code_success() -> void: + 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 resend_code_failure() -> void: + 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() -> void: + $"FormContainer/ProcessingLabel".show() + + +func hide_processing_label() -> void: + $"FormContainer/ProcessingLabel".hide() + + +func _on_ConfirmButton_pressed() -> void: + 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() -> void: + 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.gd.uid b/addons/silent_wolf/Auth/ConfirmEmail.gd.uid new file mode 100644 index 0000000..ff1df67 --- /dev/null +++ b/addons/silent_wolf/Auth/ConfirmEmail.gd.uid @@ -0,0 +1 @@ +uid://dlaymvnl1b3ur diff --git a/addons/silent_wolf/Auth/ConfirmEmail.tscn b/addons/silent_wolf/Auth/ConfirmEmail.tscn new file mode 100644 index 0000000..ab6babf --- /dev/null +++ b/addons/silent_wolf/Auth/ConfirmEmail.tscn @@ -0,0 +1,71 @@ +[gd_scene load_steps=4 format=3 uid="uid://bsh33b4p03ayd"] + +[ext_resource type="Script" uid="uid://dlaymvnl1b3ur" path="res://addons/silent_wolf/Auth/ConfirmEmail.gd" id="1"] +[ext_resource type="Theme" uid="uid://d2eakbmaefnt6" path="res://addons/silent_wolf/assets/themes/sw_theme.tres" id="1_wyfy3"] +[ext_resource type="PackedScene" uid="uid://clllbf6am8qdf" path="res://addons/silent_wolf/common/SWButton.tscn" id="2"] + +[node name="ConfirmEmail" type="TextureRect"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_right = -1920.0 +offset_bottom = -1080.0 +theme = ExtResource("1_wyfy3") +script = ExtResource("1") + +[node name="FormContainer" type="VBoxContainer" parent="."] +layout_mode = 0 +offset_left = 562.0 +offset_top = 221.0 +offset_right = 1096.0 +offset_bottom = 804.0 +theme_override_constants/separation = 24 + +[node name="Label" type="Label" parent="FormContainer"] +layout_mode = 2 +theme_override_font_sizes/font_size = 72 +text = "Confirm your email address" +horizontal_alignment = 1 + +[node name="CodeContainer" type="HBoxContainer" parent="FormContainer"] +layout_mode = 2 +size_flags_horizontal = 4 +size_flags_vertical = 6 + +[node name="Label" type="Label" parent="FormContainer/CodeContainer"] +custom_minimum_size = Vector2(160, 0) +layout_mode = 2 +text = "Code" + +[node name="VerifCode" type="LineEdit" parent="FormContainer/CodeContainer"] +custom_minimum_size = Vector2(220, 80) +layout_mode = 2 +max_length = 30 + +[node name="ErrorMessage" type="Label" parent="FormContainer"] +visible = false +layout_mode = 2 +theme_override_colors/font_color = Color(0.92549, 0, 0, 1) +autowrap_mode = 1 + +[node name="ConfirmButton" parent="FormContainer" instance=ExtResource("2")] +custom_minimum_size = Vector2(200, 80) +layout_mode = 2 +size_flags_horizontal = 4 +text = "Submit" + +[node name="ResendConfCodeButton" parent="FormContainer" instance=ExtResource("2")] +custom_minimum_size = Vector2(320, 80) +layout_mode = 2 +size_flags_horizontal = 4 +text = "Resend code" + +[node name="ProcessingLabel" type="Label" parent="FormContainer"] +visible = false +layout_mode = 2 +theme_override_font_sizes/font_size = 36 +text = "Processing..." +horizontal_alignment = 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..07567c5 --- /dev/null +++ b/addons/silent_wolf/Auth/Login.gd @@ -0,0 +1,54 @@ +extends TextureRect + +const SWLogger = preload("res://addons/silent_wolf/utils/SWLogger.gd") + + +func _ready(): + SilentWolf.Auth.sw_login_complete.connect(_on_login_complete) + + +func _on_LoginButton_pressed() -> void: + 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_complete(sw_result: Dictionary) -> void: + if sw_result.success: + login_success() + else: + login_failure(sw_result.error) + + +func login_success() -> void: + var scene_name = SilentWolf.auth_config.redirect_to_scene + SWLogger.info("logged in as: " + str(SilentWolf.Auth.logged_in_player)) + get_tree().change_scene_to_file(scene_name) + + +func login_failure(error: String) -> void: + hide_processing_label() + SWLogger.info("log in failed: " + str(error)) + $"FormContainer/ErrorMessage".text = error + $"FormContainer/ErrorMessage".show() + + +func show_processing_label() -> void: + $"FormContainer/ProcessingLabel".show() + $"FormContainer/ProcessingLabel".show() + + +func hide_processing_label() -> void: + $"FormContainer/ProcessingLabel".hide() + + +func _on_LinkButton_pressed() -> void: + get_tree().change_scene_to_file(SilentWolf.auth_config.reset_password_scene) + + +func _on_back_button_pressed(): + print("Back button pressed") + get_tree().change_scene_to_file(SilentWolf.auth_config.redirect_to_scene) diff --git a/addons/silent_wolf/Auth/Login.gd.uid b/addons/silent_wolf/Auth/Login.gd.uid new file mode 100644 index 0000000..9939140 --- /dev/null +++ b/addons/silent_wolf/Auth/Login.gd.uid @@ -0,0 +1 @@ +uid://srgdira4bqf2 diff --git a/addons/silent_wolf/Auth/Login.tscn b/addons/silent_wolf/Auth/Login.tscn new file mode 100644 index 0000000..b60c1a4 --- /dev/null +++ b/addons/silent_wolf/Auth/Login.tscn @@ -0,0 +1,124 @@ +[gd_scene load_steps=9 format=3 uid="uid://bi7lxcglf2tnc"] + +[ext_resource type="Script" uid="uid://srgdira4bqf2" path="res://addons/silent_wolf/Auth/Login.gd" id="1"] +[ext_resource type="Theme" uid="uid://d2eakbmaefnt6" path="res://addons/silent_wolf/assets/themes/sw_theme.tres" id="1_bbh8k"] +[ext_resource type="PackedScene" uid="uid://clllbf6am8qdf" path="res://addons/silent_wolf/common/SWButton.tscn" id="2"] +[ext_resource type="Texture2D" uid="uid://bpn62aabunxva" path="res://addons/silent_wolf/assets/gfx/checkbox_checked.png" id="4_ak74a"] +[ext_resource type="Texture2D" uid="uid://bgs8a50ilk5cj" path="res://addons/silent_wolf/assets/gfx/checkbox_unchecked.png" id="5_3asm1"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_oxxi6"] +bg_color = Color(0.301961, 0.301961, 0.301961, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qt8ss"] +bg_color = Color(0.301961, 0.301961, 0.301961, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_o52ik"] +bg_color = Color(0.301961, 0.301961, 0.301961, 1) + +[node name="Login" type="TextureRect"] +anchors_preset = -1 +anchor_right = 0.989 +anchor_bottom = 0.984 +offset_right = -1898.88 +offset_bottom = -1062.72 +mouse_filter = 2 +theme = ExtResource("1_bbh8k") +script = ExtResource("1") + +[node name="BackButton" parent="." instance=ExtResource("2")] +process_priority = 1 +layout_mode = 0 +offset_left = 595.0 +offset_top = 148.0 +offset_right = 740.0 +offset_bottom = 197.0 +focus_mode = 1 +text = "← Back" + +[node name="FormContainer" type="VBoxContainer" parent="."] +layout_mode = 0 +offset_left = 435.0 +offset_top = 226.0 +offset_right = 1562.0 +offset_bottom = 990.0 +theme_override_constants/separation = 24 +alignment = 1 + +[node name="Label" type="Label" parent="FormContainer"] +layout_mode = 2 +theme_override_constants/line_spacing = 16 +theme_override_font_sizes/font_size = 72 +text = "Log in" +horizontal_alignment = 1 + +[node name="UsernameContainer" type="HBoxContainer" parent="FormContainer"] +layout_mode = 2 +alignment = 1 + +[node name="Label" type="Label" parent="FormContainer/UsernameContainer"] +layout_mode = 2 +text = "Username: " +horizontal_alignment = 1 + +[node name="Username" type="LineEdit" parent="FormContainer/UsernameContainer"] +custom_minimum_size = Vector2(250, 0) +layout_mode = 2 +max_length = 30 + +[node name="PasswordContainer" type="HBoxContainer" parent="FormContainer"] +layout_mode = 2 +alignment = 1 + +[node name="Label" type="Label" parent="FormContainer/PasswordContainer"] +layout_mode = 2 +text = "Password: " + +[node name="Password" type="LineEdit" parent="FormContainer/PasswordContainer"] +custom_minimum_size = Vector2(250, 0) +layout_mode = 2 +max_length = 30 +secret = true + +[node name="LinkButton" type="LinkButton" parent="FormContainer"] +layout_mode = 2 +size_flags_horizontal = 4 +theme_override_constants/outline_size = 0 +text = "Forgot Password?" + +[node name="RememberMeCheckBox" type="CheckBox" parent="FormContainer"] +layout_mode = 2 +size_flags_horizontal = 4 +focus_mode = 0 +theme_override_colors/font_color = Color(1, 1, 1, 1) +theme_override_colors/font_pressed_color = Color(1, 1, 1, 1) +theme_override_colors/font_hover_color = Color(1, 1, 1, 1) +theme_override_icons/checked = ExtResource("4_ak74a") +theme_override_icons/unchecked = ExtResource("5_3asm1") +theme_override_styles/normal = SubResource("StyleBoxFlat_oxxi6") +theme_override_styles/pressed = SubResource("StyleBoxFlat_qt8ss") +theme_override_styles/hover = SubResource("StyleBoxFlat_o52ik") +text = "Stay signed in for 30 days" +expand_icon = true + +[node name="ErrorMessage" type="Label" parent="FormContainer"] +custom_minimum_size = Vector2(0, 48) +layout_mode = 2 +theme_override_colors/font_color = Color(0.92549, 0, 0, 1) +autowrap_mode = 1 + +[node name="LoginButton" parent="FormContainer" instance=ExtResource("2")] +custom_minimum_size = Vector2(280, 120) +layout_mode = 2 +size_flags_horizontal = 4 +theme_override_font_sizes/font_size = 48 +text = "Submit" + +[node name="ProcessingLabel" type="Label" parent="FormContainer"] +visible = false +layout_mode = 2 +text = "Processing..." +horizontal_alignment = 1 + +[connection signal="pressed" from="BackButton" to="." method="_on_back_button_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..2f6117b --- /dev/null +++ b/addons/silent_wolf/Auth/Register.gd @@ -0,0 +1,93 @@ +extends TextureRect + +const SWLogger = preload("res://addons/silent_wolf/utils/SWLogger.gd") + + +func _ready(): + SilentWolf.check_auth_ready() + SilentWolf.Auth.sw_registration_complete.connect(_on_registration_complete) + SilentWolf.Auth.sw_registration_user_pwd_complete.connect(_on_registration_user_pwd_complete) + + +func _on_RegisterButton_pressed() -> void: + 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() -> void: + 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_complete(sw_result: Dictionary) -> void: + if sw_result.success: + registration_success() + else: + registration_failure(sw_result.error) + + +func _on_registration_user_pwd_complete(sw_result: Dictionary) -> void: + if sw_result.success: + registration_user_pwd_success() + else: + registration_failure(sw_result.error) + + +func registration_success() -> void: + # 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_to_file(scene_name) + + +func registration_user_pwd_success() -> void: + var scene_name = SilentWolf.auth_config.redirect_to_scene + get_tree().change_scene_to_file(scene_name) + + +func registration_failure(error: String) -> void: + hide_processing_label() + $"FormContainer/ErrorMessage".text = error + $"FormContainer/ErrorMessage".show() + + +func _on_BackButton_pressed() -> void: + get_tree().change_scene_to_file(SilentWolf.auth_config.redirect_to_scene) + + +func show_processing_label() -> void: + $"FormContainer/ProcessingLabel".show() + + +func hide_processing_label() -> void: + $"FormContainer/ProcessingLabel".hide() + + +func _on_UsernameToolButton_mouse_entered() -> void: + $"FormContainer/InfoBox".text = "Username should contain at least 6 characters (letters or numbers) and no spaces." + $"FormContainer/InfoBox".show() + + +func _on_UsernameToolButton_mouse_exited() -> void: + $"FormContainer/InfoBox".hide() + + +func _on_PasswordToolButton_mouse_entered() -> void: + $"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() -> void: + $"FormContainer/InfoBox".hide() diff --git a/addons/silent_wolf/Auth/Register.gd.uid b/addons/silent_wolf/Auth/Register.gd.uid new file mode 100644 index 0000000..492acb7 --- /dev/null +++ b/addons/silent_wolf/Auth/Register.gd.uid @@ -0,0 +1 @@ +uid://dvf4mw8y68sa diff --git a/addons/silent_wolf/Auth/Register.tscn b/addons/silent_wolf/Auth/Register.tscn new file mode 100644 index 0000000..968d595 --- /dev/null +++ b/addons/silent_wolf/Auth/Register.tscn @@ -0,0 +1,157 @@ +[gd_scene load_steps=6 format=3 uid="uid://chxe78yjqftkl"] + +[ext_resource type="Script" uid="uid://dvf4mw8y68sa" path="res://addons/silent_wolf/Auth/Register.gd" id="1"] +[ext_resource type="Theme" uid="uid://d2eakbmaefnt6" path="res://addons/silent_wolf/assets/themes/sw_theme.tres" id="1_dsws8"] +[ext_resource type="PackedScene" uid="uid://clllbf6am8qdf" path="res://addons/silent_wolf/common/SWButton.tscn" id="2"] +[ext_resource type="Texture2D" uid="uid://dh8fem1lgom04" path="res://addons/silent_wolf/assets/gfx/info_icon_small.png" id="5"] +[ext_resource type="Texture2D" uid="uid://gdw18po2h7hb" path="res://addons/silent_wolf/assets/gfx/dummy_info_icon_small.png" id="6"] + +[node name="Register" type="TextureRect"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_right = -1920.0 +offset_bottom = -1080.0 +grow_horizontal = 2 +grow_vertical = 2 +theme = ExtResource("1_dsws8") +script = ExtResource("1") + +[node name="BackButton" parent="." instance=ExtResource("2")] +layout_mode = 0 +offset_left = 215.0 +offset_top = 51.0 +offset_right = 281.0 +offset_bottom = 82.0 +theme_override_font_sizes/font_size = 36 +text = "← Back" + +[node name="FormContainer" type="VBoxContainer" parent="."] +layout_mode = 0 +offset_left = 449.0 +offset_top = 194.0 +offset_right = 1436.0 +offset_bottom = 998.0 +theme_override_constants/separation = 36 + +[node name="Label" type="Label" parent="FormContainer"] +layout_mode = 2 +theme_override_font_sizes/font_size = 72 +text = "Sign up" +horizontal_alignment = 1 + +[node name="MainFormContainer" type="HBoxContainer" parent="FormContainer"] +layout_mode = 2 +theme_override_constants/separation = 36 + +[node name="FormLabels" type="VBoxContainer" parent="FormContainer/MainFormContainer"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_constants/separation = 32 + +[node name="PlayerNameLabel" type="Label" parent="FormContainer/MainFormContainer/FormLabels"] +layout_mode = 2 +theme_override_font_sizes/font_size = 36 +text = "Username:" + +[node name="EmailLabel" type="Label" parent="FormContainer/MainFormContainer/FormLabels"] +layout_mode = 2 +theme_override_font_sizes/font_size = 36 +text = "Email:" + +[node name="PasswordLabel" type="Label" parent="FormContainer/MainFormContainer/FormLabels"] +layout_mode = 2 +theme_override_font_sizes/font_size = 36 +text = "Password:" + +[node name="ConfirmPasswordLabel" type="Label" parent="FormContainer/MainFormContainer/FormLabels"] +layout_mode = 2 +theme_override_font_sizes/font_size = 36 +text = "Confirm password:" + +[node name="FormInputFields" type="VBoxContainer" parent="FormContainer/MainFormContainer"] +custom_minimum_size = Vector2(460, 0) +layout_mode = 2 + +[node name="PlayerName" type="LineEdit" parent="FormContainer/MainFormContainer/FormInputFields"] +custom_minimum_size = Vector2(0, 80) +layout_mode = 2 +max_length = 30 + +[node name="Email" type="LineEdit" parent="FormContainer/MainFormContainer/FormInputFields"] +custom_minimum_size = Vector2(0, 80) +layout_mode = 2 +max_length = 50 + +[node name="Password" type="LineEdit" parent="FormContainer/MainFormContainer/FormInputFields"] +custom_minimum_size = Vector2(0, 80) +layout_mode = 2 +max_length = 30 +secret = true + +[node name="ConfirmPassword" type="LineEdit" parent="FormContainer/MainFormContainer/FormInputFields"] +custom_minimum_size = Vector2(0, 80) +layout_mode = 2 +max_length = 30 +secret = true + +[node name="InfoLabels" type="VBoxContainer" parent="FormContainer/MainFormContainer"] +layout_direction = 3 +layout_mode = 2 +theme = ExtResource("1_dsws8") + +[node name="UsernameToolButton" type="Button" parent="FormContainer/MainFormContainer/InfoLabels"] +layout_mode = 2 +icon = ExtResource("5") +flat = true + +[node name="DummyToolButton1" type="Button" parent="FormContainer/MainFormContainer/InfoLabels"] +modulate = Color(1, 1, 1, 0) +layout_mode = 2 +icon = ExtResource("6") + +[node name="PasswordToolButton" type="Button" parent="FormContainer/MainFormContainer/InfoLabels"] +layout_mode = 2 +icon = ExtResource("5") +flat = true + +[node name="DummyToolButton2" type="Button" parent="FormContainer/MainFormContainer/InfoLabels"] +modulate = Color(1, 1, 1, 0) +layout_mode = 2 +icon = ExtResource("6") + +[node name="InfoBox" type="Label" parent="FormContainer"] +visible = false +layout_mode = 2 +text = "Password should contain at least 8 characters including uppercase and lowercase letters, numbers and (optionally) special characters." +autowrap_mode = 1 + +[node name="ErrorMessage" type="Label" parent="FormContainer"] +visible = false +custom_minimum_size = Vector2(36, 0) +layout_mode = 2 +theme = ExtResource("1_dsws8") +theme_override_colors/font_color = Color(0.92549, 0, 0, 1) +horizontal_alignment = 1 +autowrap_mode = 1 + +[node name="RegisterButton" parent="FormContainer" instance=ExtResource("2")] +custom_minimum_size = Vector2(280, 80) +layout_mode = 2 +size_flags_horizontal = 4 +theme_override_font_sizes/font_size = 48 +text = "Submit" + +[node name="ProcessingLabel" type="Label" parent="FormContainer"] +visible = false +layout_mode = 2 +size_flags_horizontal = 4 +theme_override_font_sizes/font_size = 36 +text = "Processing..." + +[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..8fa78ad --- /dev/null +++ b/addons/silent_wolf/Auth/RegisterUsernamePassword.tscn @@ -0,0 +1,133 @@ +[gd_scene load_steps=6 format=3 uid="uid://smtpyjhmn308"] + +[ext_resource type="Script" uid="uid://dvf4mw8y68sa" path="res://addons/silent_wolf/Auth/Register.gd" id="1"] +[ext_resource type="Theme" uid="uid://d2eakbmaefnt6" path="res://addons/silent_wolf/assets/themes/sw_theme.tres" id="1_ksggb"] +[ext_resource type="PackedScene" uid="uid://clllbf6am8qdf" path="res://addons/silent_wolf/common/SWButton.tscn" id="2"] +[ext_resource type="Texture2D" uid="uid://dh8fem1lgom04" path="res://addons/silent_wolf/assets/gfx/info_icon_small.png" id="5"] +[ext_resource type="Texture2D" uid="uid://gdw18po2h7hb" path="res://addons/silent_wolf/assets/gfx/dummy_info_icon_small.png" id="6"] + +[node name="Register" type="TextureRect"] +anchors_preset = -1 +anchor_right = 1.0 +anchor_bottom = 0.949 +offset_right = -1920.0 +offset_bottom = -1024.92 +theme = ExtResource("1_ksggb") +script = ExtResource("1") + +[node name="BackButton" parent="." instance=ExtResource("2")] +layout_mode = 0 +offset_left = 225.0 +offset_top = 92.0 +offset_right = 370.0 +offset_bottom = 141.0 +text = "← Back" + +[node name="FormContainer" type="VBoxContainer" parent="."] +layout_mode = 0 +offset_left = 408.0 +offset_top = 190.0 +offset_right = 1486.0 +offset_bottom = 859.0 +theme_override_constants/separation = 24 + +[node name="Label" type="Label" parent="FormContainer"] +layout_mode = 2 +theme_override_font_sizes/font_size = 72 +text = "Sign up" +horizontal_alignment = 1 + +[node name="MainFormContainer" type="HBoxContainer" parent="FormContainer"] +layout_mode = 2 +theme_override_constants/separation = 36 + +[node name="FormLabels" type="VBoxContainer" parent="FormContainer/MainFormContainer"] +layout_mode = 2 +size_flags_vertical = 5 +theme_override_constants/separation = 73 + +[node name="PlayerNameLabel" type="Label" parent="FormContainer/MainFormContainer/FormLabels"] +layout_mode = 2 +theme_override_font_sizes/font_size = 36 +text = "Username:" + +[node name="PasswordLabel" type="Label" parent="FormContainer/MainFormContainer/FormLabels"] +layout_mode = 2 +text = "Password:" + +[node name="ConfirmPasswordLabel" type="Label" parent="FormContainer/MainFormContainer/FormLabels"] +layout_mode = 2 +text = "Confirm password:" + +[node name="FormInputFields" type="VBoxContainer" parent="FormContainer/MainFormContainer"] +layout_mode = 2 +theme_override_constants/separation = 28 + +[node name="PlayerName" type="LineEdit" parent="FormContainer/MainFormContainer/FormInputFields"] +custom_minimum_size = Vector2(400, 80) +layout_mode = 2 +max_length = 30 + +[node name="Password" type="LineEdit" parent="FormContainer/MainFormContainer/FormInputFields"] +custom_minimum_size = Vector2(400, 80) +layout_mode = 2 +max_length = 30 +secret = true + +[node name="ConfirmPassword" type="LineEdit" parent="FormContainer/MainFormContainer/FormInputFields"] +custom_minimum_size = Vector2(400, 80) +layout_mode = 2 +max_length = 30 +secret = true + +[node name="InfoLabels" type="VBoxContainer" parent="FormContainer/MainFormContainer"] +layout_mode = 2 +theme_override_constants/separation = 32 + +[node name="UsernameToolButton" type="Button" parent="FormContainer/MainFormContainer/InfoLabels"] +layout_mode = 2 +icon = ExtResource("5") +flat = true + +[node name="PasswordToolButton" type="Button" parent="FormContainer/MainFormContainer/InfoLabels"] +layout_mode = 2 +icon = ExtResource("5") +flat = true + +[node name="DummyToolButton1" type="Button" parent="FormContainer/MainFormContainer/InfoLabels"] +modulate = Color(1, 1, 1, 0) +layout_mode = 2 +icon = ExtResource("6") + +[node name="InfoBox" type="Label" parent="FormContainer"] +visible = false +layout_mode = 2 +text = "Password should contain at least 8 characters including uppercase and lowercase letters, numbers and (optionally) special characters." +autowrap_mode = 1 + +[node name="ErrorMessage" type="Label" parent="FormContainer"] +visible = false +layout_mode = 2 +theme_override_colors/font_color = Color(0.92549, 0, 0, 1) +autowrap_mode = 1 + +[node name="RegisterUPButton" parent="FormContainer" instance=ExtResource("2")] +custom_minimum_size = Vector2(220, 80) +layout_mode = 2 +size_flags_horizontal = 4 +theme_override_font_sizes/font_size = 48 +text = "Submit" + +[node name="ProcessingLabel" type="Label" parent="FormContainer"] +visible = false +layout_mode = 2 +theme_override_font_sizes/font_size = 36 +text = "Processing..." +horizontal_alignment = 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..5db3f9f --- /dev/null +++ b/addons/silent_wolf/Auth/ResetPassword.gd @@ -0,0 +1,77 @@ +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.sw_request_password_reset_complete.connect(_on_send_code_complete) + SilentWolf.Auth.sw_reset_password_complete.connect(_on_reset_complete) + if "login_scene" in SilentWolf.Auth: + login_scene = SilentWolf.Auth.login_scene + + +func _on_BackButton_pressed() -> void: + get_tree().change_scene_to_file(login_scene) + + +func _on_PlayerNameSubmitButton_pressed() -> void: + player_name = $"RequestFormContainer/FormContainer/FormInputFields/PlayerName".text + SilentWolf.Auth.request_player_password_reset(player_name) + $"RequestFormContainer/ProcessingLabel".show() + + +func _on_send_code_complete(sw_result: Dictionary) -> void: + if sw_result.success: + send_code_success() + else: + send_code_failure(sw_result.error) + + +func send_code_success() -> void: + $"RequestFormContainer/ProcessingLabel".hide() + $"RequestFormContainer".hide() + $"PwdResetFormContainer".show() + + +func send_code_failure(error: String) -> void: + $"RequestFormContainer/ProcessingLabel".hide() + $"RequestFormContainer/ErrorMessage".text = "Could not send confirmation code. " + str(error) + $"RequestFormContainer/ErrorMessage".show() + + +func _on_NewPasswordSubmitButton_pressed() -> void: + 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_complete(sw_result: Dictionary) -> void: + if sw_result.success: + reset_success() + else: + reset_failure(sw_result.error) + + +func reset_success() -> void: + $"PwdResetFormContainer/ProcessingLabel".hide() + $"PwdResetFormContainer".hide() + $"PasswordChangedContainer".show() + + +func reset_failure(error: String) -> void: + $"PwdResetFormContainer/ProcessingLabel".hide() + $"PwdResetFormContainer/ErrorMessage".text = "Could not reset password. " + str(error) + $"PwdResetFormContainer/ErrorMessage".show() + + +func _on_CloseButton_pressed() -> void: + get_tree().change_scene_to_file(login_scene) diff --git a/addons/silent_wolf/Auth/ResetPassword.gd.uid b/addons/silent_wolf/Auth/ResetPassword.gd.uid new file mode 100644 index 0000000..ec9a7b5 --- /dev/null +++ b/addons/silent_wolf/Auth/ResetPassword.gd.uid @@ -0,0 +1 @@ +uid://cr0afxttlphm7 diff --git a/addons/silent_wolf/Auth/ResetPassword.tscn b/addons/silent_wolf/Auth/ResetPassword.tscn new file mode 100644 index 0000000..b1312b0 --- /dev/null +++ b/addons/silent_wolf/Auth/ResetPassword.tscn @@ -0,0 +1,200 @@ +[gd_scene load_steps=4 format=3 uid="uid://dlfgu138q4bat"] + +[ext_resource type="Script" uid="uid://cr0afxttlphm7" path="res://addons/silent_wolf/Auth/ResetPassword.gd" id="1"] +[ext_resource type="Theme" uid="uid://d2eakbmaefnt6" path="res://addons/silent_wolf/assets/themes/sw_theme.tres" id="1_qg4ch"] +[ext_resource type="PackedScene" uid="uid://clllbf6am8qdf" path="res://addons/silent_wolf/common/SWButton.tscn" id="2"] + +[node name="ResetPassword" type="TextureRect"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_right = -1920.0 +offset_bottom = -1080.0 +grow_horizontal = 2 +grow_vertical = 2 +theme = ExtResource("1_qg4ch") +script = ExtResource("1") + +[node name="BackButton" parent="." instance=ExtResource("2")] +layout_mode = 0 +offset_left = 137.0 +offset_top = 95.0 +offset_right = 282.0 +offset_bottom = 144.0 +text = "← Back" + +[node name="RequestFormContainer" type="VBoxContainer" parent="."] +visible = false +layout_mode = 0 +offset_left = 650.0 +offset_top = 212.0 +offset_right = 1346.0 +offset_bottom = 812.0 +theme_override_constants/separation = 48 + +[node name="Label" type="Label" parent="RequestFormContainer"] +layout_mode = 2 +theme_override_font_sizes/font_size = 72 +text = "Reset password" +horizontal_alignment = 1 + +[node name="LabelExplainer" type="Label" parent="RequestFormContainer"] +layout_mode = 2 +text = "Please enter your player name below." + +[node name="FormContainer" type="HBoxContainer" parent="RequestFormContainer"] +layout_mode = 2 +theme_override_constants/separation = 24 + +[node name="FormLabels" type="VBoxContainer" parent="RequestFormContainer/FormContainer"] +layout_mode = 2 + +[node name="PlayerNameLabel" type="Label" parent="RequestFormContainer/FormContainer/FormLabels"] +custom_minimum_size = Vector2(0, 80) +layout_mode = 2 +text = "Player name" +vertical_alignment = 1 + +[node name="FormInputFields" type="VBoxContainer" parent="RequestFormContainer/FormContainer"] +layout_mode = 2 + +[node name="PlayerName" type="LineEdit" parent="RequestFormContainer/FormContainer/FormInputFields"] +custom_minimum_size = Vector2(440, 80) +layout_mode = 2 +max_length = 50 + +[node name="ErrorMessage" type="Label" parent="RequestFormContainer"] +visible = false +modulate = Color(0.92549, 0, 0, 1) +layout_mode = 2 + +[node name="PlayerNameSubmitButton" parent="RequestFormContainer" instance=ExtResource("2")] +custom_minimum_size = Vector2(220, 80) +layout_mode = 2 +size_flags_horizontal = 4 +text = "Submit" + +[node name="ProcessingLabel" type="Label" parent="RequestFormContainer"] +visible = false +layout_mode = 2 +theme_override_font_sizes/font_size = 36 +text = "Processing..." +horizontal_alignment = 1 + +[node name="PwdResetFormContainer" type="VBoxContainer" parent="."] +layout_mode = 0 +offset_left = 277.0 +offset_top = 208.0 +offset_right = 1769.0 +offset_bottom = 870.0 +theme_override_constants/separation = 48 + +[node name="Label" type="Label" parent="PwdResetFormContainer"] +layout_mode = 2 +theme_override_font_sizes/font_size = 72 +text = "Reset password" +horizontal_alignment = 1 + +[node name="LabelExplainer" type="Label" parent="PwdResetFormContainer"] +layout_mode = 2 +text = "Please enter below the code we sent you by email and your new password twice." + +[node name="FormContainer" type="HBoxContainer" parent="PwdResetFormContainer"] +layout_mode = 2 +size_flags_horizontal = 4 +theme_override_constants/separation = 24 + +[node name="FormLabels" type="VBoxContainer" parent="PwdResetFormContainer/FormContainer"] +custom_minimum_size = Vector2(0, 80) +layout_mode = 2 +theme_override_constants/separation = 18 + +[node name="CodeLabel" type="Label" parent="PwdResetFormContainer/FormContainer/FormLabels"] +custom_minimum_size = Vector2(0, 80) +layout_mode = 2 +text = "Code" +vertical_alignment = 1 + +[node name="PasswordLabel" type="Label" parent="PwdResetFormContainer/FormContainer/FormLabels"] +custom_minimum_size = Vector2(0, 80) +layout_mode = 2 +text = "Password" +vertical_alignment = 1 + +[node name="ConfirmPasswordLabel" type="Label" parent="PwdResetFormContainer/FormContainer/FormLabels"] +custom_minimum_size = Vector2(0, 80) +layout_mode = 2 +text = "Confirm password" +vertical_alignment = 1 + +[node name="FormInputFields" type="VBoxContainer" parent="PwdResetFormContainer/FormContainer"] +custom_minimum_size = Vector2(440, 80) +layout_mode = 2 +theme_override_constants/separation = 18 + +[node name="Code" type="LineEdit" parent="PwdResetFormContainer/FormContainer/FormInputFields"] +custom_minimum_size = Vector2(0, 80) +layout_mode = 2 +max_length = 50 + +[node name="Password" type="LineEdit" parent="PwdResetFormContainer/FormContainer/FormInputFields"] +custom_minimum_size = Vector2(0, 80) +layout_mode = 2 +max_length = 30 +secret = true + +[node name="ConfirmPassword" type="LineEdit" parent="PwdResetFormContainer/FormContainer/FormInputFields"] +custom_minimum_size = Vector2(0, 80) +layout_mode = 2 +max_length = 30 +secret = true + +[node name="ErrorMessage" type="Label" parent="PwdResetFormContainer"] +visible = false +modulate = Color(0.92549, 0, 0, 1) +layout_mode = 2 +autowrap_mode = 1 + +[node name="NewPasswordSubmitButton" parent="PwdResetFormContainer" instance=ExtResource("2")] +custom_minimum_size = Vector2(220, 80) +layout_mode = 2 +size_flags_horizontal = 4 +text = "Submit" + +[node name="ProcessingLabel" type="Label" parent="PwdResetFormContainer"] +visible = false +layout_mode = 2 +text = "Processing..." +horizontal_alignment = 1 + +[node name="PasswordChangedContainer" type="VBoxContainer" parent="."] +visible = false +layout_mode = 0 +offset_left = 525.0 +offset_top = 269.0 +offset_right = 1469.0 +offset_bottom = 793.0 +theme_override_constants/separation = 60 + +[node name="PwdChanedLabel" type="Label" parent="PasswordChangedContainer"] +layout_mode = 2 +size_flags_horizontal = 4 +size_flags_vertical = 1 +theme_override_font_sizes/font_size = 72 +text = "Password reset" + +[node name="PasswordChangedLabelExplainer" type="Label" parent="PasswordChangedContainer"] +layout_mode = 2 +text = "Your password was changed successfully." +horizontal_alignment = 1 + +[node name="CloseButton" parent="PasswordChangedContainer" instance=ExtResource("2")] +custom_minimum_size = Vector2(320, 80) +layout_mode = 2 +size_flags_horizontal = 4 +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/Muliplayer/.DS_Store b/addons/silent_wolf/Muliplayer/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..c49b577728cdb30dba037d3aa81ef63a848528f1 GIT binary patch literal 6148 zcmeHK%}T>S5Z*X^(986ucOJBoMrr+6MEzzEwR$Qc@LLdabrD3qNa|t``LW7%wSyH{p;0OihkLvG z6>eFh3Lje5Zmq%(tlDT)U>jRIho_DEuoH<#O(2Eenvykx89afa&JwmQ_f~YenIp4z zgyWbqvhB~!=hu(7qaU9?*Vnz9J>P@di$W2#VUBE1V@sln3?Ku@z``0sw3 z&Ni58)aitp;)9x%si{z?S{>%+Dx6STBlgGuGBC-&k{*`i`+qXO{+~?35i)=b{3`}% z-f^8e9Lc<`Q-_mpEr)gkO^S?5HGZdnqMl-il}~XQni8;csQ}srQ;lE&!9N0u2JDc5 HUuED6a`R#U literal 0 HcmV?d00001 diff --git a/addons/silent_wolf/Muliplayer/Multiplayer.gd b/addons/silent_wolf/Muliplayer/Multiplayer.gd new file mode 100644 index 0000000..b7095cd --- /dev/null +++ b/addons/silent_wolf/Muliplayer/Multiplayer.gd @@ -0,0 +1,39 @@ +extends Node + +@onready var WSClient = Node.new() + +var mp_ws_ready = false +var mp_session_started = false + +var mp_player_name = "" + + +func _ready(): + mp_ws_ready = false + mp_session_started = false + var ws_client_script = load("res://addons/silent_wolf/Multiplayer/ws/WSClient.gd") + WSClient.set_script(ws_client_script) + add_child(WSClient) + + +func init_mp_session(player_name): + #mp_player_name = player_name + WSClient.init_mp_session(player_name) + # TODO: instead of waiting an arbitrary amount of time, yield on + # a function that guarantees that child ready() function has run + #yield(get_tree().create_timer(0.3), "timeout") + + +func _send_init_message(): + WSClient.init_mp_session(mp_player_name) + mp_ws_ready = true + mp_session_started = true + + +func send(data: Dictionary): + # First check that WSClient is in tree + print("Attempting to send data to web socket server") + if WSClient.is_inside_tree(): + # TODO: check if data is properly formatted (should be dictionary?) + print("Sending data to web socket server...") + WSClient.send_to_server("update", data) diff --git a/addons/silent_wolf/Muliplayer/Multiplayer.gd.uid b/addons/silent_wolf/Muliplayer/Multiplayer.gd.uid new file mode 100644 index 0000000..e572ffb --- /dev/null +++ b/addons/silent_wolf/Muliplayer/Multiplayer.gd.uid @@ -0,0 +1 @@ +uid://ci4ycv1aesr8m diff --git a/addons/silent_wolf/Muliplayer/ws/WSClient.gd b/addons/silent_wolf/Muliplayer/ws/WSClient.gd new file mode 100644 index 0000000..8c4cf1c --- /dev/null +++ b/addons/silent_wolf/Muliplayer/ws/WSClient.gd @@ -0,0 +1,82 @@ +extends Node + +const SWLogger = preload("res://addons/silent_wolf/utils/SWLogger.gd") + +# The URL we will connect to +@export var websocket_url = "wss://ws.silentwolfmp.com/server" +@export var ws_room_init_url = "wss://ws.silentwolfmp.com/init" + +signal ws_client_ready + +# Our WebSocketClient instance +#var _client = WebSocketClient.new() + +func _ready(): + SWLogger.debug("Entering MPClient _ready function") + # Connect base signals to get notified of connection open, close, and errors. + #_client.connect("connection_closed", self, "_closed") + #_client.connect("connection_error", self, "_closed") + #_client.connect("connection_established", self, "_connected") + # This signal is emitted when not using the Multiplayer API every time + # a full packet is received. + # Alternatively, you could check get_peer(1).get_available_packets() in a loop. + #_client.connect("data_received", self, "_on_data") + + # Initiate connection to the given URL. + #var err = _client.connect_to_url(websocket_url) + #if err != OK: + #SWLogger.debug("Unable to connect to WS server") + # print("Unable to connect to WS server") + # set_process(false) + #emit_signal("ws_client_ready") + +func _closed(was_clean = false): + # was_clean will tell you if the disconnection was correctly notified + # by the remote peer before closing the socket. + SWLogger.debug("WS connection closed, clean: " + str(was_clean)) + set_process(false) + +func _connected(proto = ""): + # This is called on connection, "proto" will be the selected WebSocket + # sub-protocol (which is optional) + #SWLogger.debug("Connected with protocol: " + str(proto)) + print("Connected with protocol: ", proto) + # You MUST always use get_peer(1).put_packet to send data to server, + # and not put_packet directly when not using the MultiplayerAPI. + #var test_packet = { "data": "Test packet" } + #send_to_server(test_packet) + #_client.get_peer(1).put_packet("Test packet".to_utf8()) + + +func _on_data(): + # Print the received packet, you MUST always use get_peer(1).get_packet + # to receive data from server, and not get_packet directly when not + # using the MultiplayerAPI. + #SWLogger.debug("Got data from WS server: " + str(_client.get_peer(1).get_packet().get_string_from_utf8())) + #print("Got data from WS server: ", _client.get_peer(1).get_packet().get_string_from_utf8()) + pass + +func _process(delta): + # Call this in _process or _physics_process. Data transfer, and signals + # emission will only happen when calling this function. + #_client.poll() + pass + + +# send arbitrary data to backend +func send_to_server(message_type, data): + data["message_type"] = message_type + print("Sending data to server: " + str(data)) + #_client.get_peer(1).put_packet(str(JSON.stringify(data)).to_utf8()) + + +func init_mp_session(player_name): + print("WSClient init_mp_session, sending initialisation packet to server") + var init_packet = { + "player_name": player_name + } + return send_to_server("init", init_packet) + + +func create_room(): + pass diff --git a/addons/silent_wolf/Muliplayer/ws/WSClient.gd.uid b/addons/silent_wolf/Muliplayer/ws/WSClient.gd.uid new file mode 100644 index 0000000..f8e3896 --- /dev/null +++ b/addons/silent_wolf/Muliplayer/ws/WSClient.gd.uid @@ -0,0 +1 @@ +uid://bwth2adqsbpey diff --git a/addons/silent_wolf/Multiplayer/.DS_Store b/addons/silent_wolf/Multiplayer/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..c49b577728cdb30dba037d3aa81ef63a848528f1 GIT binary patch literal 6148 zcmeHK%}T>S5Z*X^(986ucOJBoMrr+6MEzzEwR$Qc@LLdabrD3qNa|t``LW7%wSyH{p;0OihkLvG z6>eFh3Lje5Zmq%(tlDT)U>jRIho_DEuoH<#O(2Eenvykx89afa&JwmQ_f~YenIp4z zgyWbqvhB~!=hu(7qaU9?*Vnz9J>P@di$W2#VUBE1V@sln3?Ku@z``0sw3 z&Ni58)aitp;)9x%si{z?S{>%+Dx6STBlgGuGBC-&k{*`i`+qXO{+~?35i)=b{3`}% z-f^8e9Lc<`Q-_mpEr)gkO^S?5HGZdnqMl-il}~XQni8;csQ}srQ;lE&!9N0u2JDc5 HUuED6a`R#U literal 0 HcmV?d00001 diff --git a/addons/silent_wolf/Multiplayer/Multiplayer.gd b/addons/silent_wolf/Multiplayer/Multiplayer.gd new file mode 100644 index 0000000..b7095cd --- /dev/null +++ b/addons/silent_wolf/Multiplayer/Multiplayer.gd @@ -0,0 +1,39 @@ +extends Node + +@onready var WSClient = Node.new() + +var mp_ws_ready = false +var mp_session_started = false + +var mp_player_name = "" + + +func _ready(): + mp_ws_ready = false + mp_session_started = false + var ws_client_script = load("res://addons/silent_wolf/Multiplayer/ws/WSClient.gd") + WSClient.set_script(ws_client_script) + add_child(WSClient) + + +func init_mp_session(player_name): + #mp_player_name = player_name + WSClient.init_mp_session(player_name) + # TODO: instead of waiting an arbitrary amount of time, yield on + # a function that guarantees that child ready() function has run + #yield(get_tree().create_timer(0.3), "timeout") + + +func _send_init_message(): + WSClient.init_mp_session(mp_player_name) + mp_ws_ready = true + mp_session_started = true + + +func send(data: Dictionary): + # First check that WSClient is in tree + print("Attempting to send data to web socket server") + if WSClient.is_inside_tree(): + # TODO: check if data is properly formatted (should be dictionary?) + print("Sending data to web socket server...") + WSClient.send_to_server("update", data) diff --git a/addons/silent_wolf/Multiplayer/Multiplayer.gd.uid b/addons/silent_wolf/Multiplayer/Multiplayer.gd.uid new file mode 100644 index 0000000..4cdebec --- /dev/null +++ b/addons/silent_wolf/Multiplayer/Multiplayer.gd.uid @@ -0,0 +1 @@ +uid://dl2dueymuik76 diff --git a/addons/silent_wolf/Multiplayer/ws/WSClient.gd b/addons/silent_wolf/Multiplayer/ws/WSClient.gd new file mode 100644 index 0000000..8c4cf1c --- /dev/null +++ b/addons/silent_wolf/Multiplayer/ws/WSClient.gd @@ -0,0 +1,82 @@ +extends Node + +const SWLogger = preload("res://addons/silent_wolf/utils/SWLogger.gd") + +# The URL we will connect to +@export var websocket_url = "wss://ws.silentwolfmp.com/server" +@export var ws_room_init_url = "wss://ws.silentwolfmp.com/init" + +signal ws_client_ready + +# Our WebSocketClient instance +#var _client = WebSocketClient.new() + +func _ready(): + SWLogger.debug("Entering MPClient _ready function") + # Connect base signals to get notified of connection open, close, and errors. + #_client.connect("connection_closed", self, "_closed") + #_client.connect("connection_error", self, "_closed") + #_client.connect("connection_established", self, "_connected") + # This signal is emitted when not using the Multiplayer API every time + # a full packet is received. + # Alternatively, you could check get_peer(1).get_available_packets() in a loop. + #_client.connect("data_received", self, "_on_data") + + # Initiate connection to the given URL. + #var err = _client.connect_to_url(websocket_url) + #if err != OK: + #SWLogger.debug("Unable to connect to WS server") + # print("Unable to connect to WS server") + # set_process(false) + #emit_signal("ws_client_ready") + +func _closed(was_clean = false): + # was_clean will tell you if the disconnection was correctly notified + # by the remote peer before closing the socket. + SWLogger.debug("WS connection closed, clean: " + str(was_clean)) + set_process(false) + +func _connected(proto = ""): + # This is called on connection, "proto" will be the selected WebSocket + # sub-protocol (which is optional) + #SWLogger.debug("Connected with protocol: " + str(proto)) + print("Connected with protocol: ", proto) + # You MUST always use get_peer(1).put_packet to send data to server, + # and not put_packet directly when not using the MultiplayerAPI. + #var test_packet = { "data": "Test packet" } + #send_to_server(test_packet) + #_client.get_peer(1).put_packet("Test packet".to_utf8()) + + +func _on_data(): + # Print the received packet, you MUST always use get_peer(1).get_packet + # to receive data from server, and not get_packet directly when not + # using the MultiplayerAPI. + #SWLogger.debug("Got data from WS server: " + str(_client.get_peer(1).get_packet().get_string_from_utf8())) + #print("Got data from WS server: ", _client.get_peer(1).get_packet().get_string_from_utf8()) + pass + +func _process(delta): + # Call this in _process or _physics_process. Data transfer, and signals + # emission will only happen when calling this function. + #_client.poll() + pass + + +# send arbitrary data to backend +func send_to_server(message_type, data): + data["message_type"] = message_type + print("Sending data to server: " + str(data)) + #_client.get_peer(1).put_packet(str(JSON.stringify(data)).to_utf8()) + + +func init_mp_session(player_name): + print("WSClient init_mp_session, sending initialisation packet to server") + var init_packet = { + "player_name": player_name + } + return send_to_server("init", init_packet) + + +func create_room(): + pass diff --git a/addons/silent_wolf/Multiplayer/ws/WSClient.gd.uid b/addons/silent_wolf/Multiplayer/ws/WSClient.gd.uid new file mode 100644 index 0000000..0cca4a2 --- /dev/null +++ b/addons/silent_wolf/Multiplayer/ws/WSClient.gd.uid @@ -0,0 +1 @@ +uid://berbygfcg03k diff --git a/addons/silent_wolf/Players/Players.gd b/addons/silent_wolf/Players/Players.gd new file mode 100644 index 0000000..2f36751 --- /dev/null +++ b/addons/silent_wolf/Players/Players.gd @@ -0,0 +1,171 @@ +extends Node + +const SWUtils = preload("res://addons/silent_wolf/utils/SWUtils.gd") +const SWLogger = preload("res://addons/silent_wolf/utils/SWLogger.gd") + +signal sw_save_player_data_complete +signal sw_get_player_data_complete +signal sw_delete_player_data_complete + +var GetPlayerData = null +var SavePlayerData = null +var DeletePlayerData = null + +# wekrefs +var wrGetPlayerData = null +var wrSavePlayerData = null +var wrDeletePlayerData = null + +var player_name = null +var player_data = null + + +func save_player_data(player_name: String, player_data: Dictionary, overwrite: bool=true) -> Node: + if player_name == "": + SWLogger.error("player_name cannot be empty when calling SilentWolf.Players.save_player_data(player_name, player_data)") + elif typeof(player_data) != TYPE_DICTIONARY: + SWLogger.error("Player data should be of type Dictionary, instead it is of type: " + str(typeof(player_data))) + else: + var prepared_http_req = SilentWolf.prepare_http_request() + SavePlayerData = prepared_http_req.request + wrSavePlayerData = prepared_http_req.weakref + SavePlayerData.request_completed.connect(_on_SavePlayerData_request_completed) + SWLogger.info("Calling SilentWolf to post player data") + var payload = { "game_id": SilentWolf.config.game_id, "player_name": player_name, "player_data": player_data, "overwrite": overwrite } + var request_url = "https://api.silentwolf.com/push_player_data" + SilentWolf.send_post_request(SavePlayerData, request_url, payload) + return self + + +func _on_SavePlayerData_request_completed(result, response_code, headers, body) -> void: + var status_check = SWUtils.check_http_response(response_code, headers, body) + if is_instance_valid(SavePlayerData): + SilentWolf.free_request(wrSavePlayerData, SavePlayerData) + + if status_check: + var json_body = JSON.parse_string(body.get_string_from_utf8()) + var sw_result: Dictionary = SilentWolf.build_result(json_body) + if json_body.success: + SWLogger.info("SilentWolf save player data success for player: " + str(json_body.player_name)) + var player_name = json_body.player_name + #sw_result["player_name"] = player_name + else: + SWLogger.error("SilentWolf save player data failure: " + str(json_body.error)) + sw_save_player_data_complete.emit(sw_result) + + +func get_player_data(player_name: String) -> Node: + if player_name == "": + SWLogger.error("player_name cannot be empty when calling SilentWolf.Players.get_player_data(player_name)") + else: + var prepared_http_req = SilentWolf.prepare_http_request() + GetPlayerData = prepared_http_req.request + wrGetPlayerData = prepared_http_req.weakref + GetPlayerData.request_completed.connect(_on_GetPlayerData_request_completed) + SWLogger.info("Calling SilentWolf to get player data") + var request_url = "https://api.silentwolf.com/get_player_data/" + str(SilentWolf.config.game_id) + "/" + str(player_name) + SilentWolf.send_get_request(GetPlayerData, request_url) + return self + + +func _on_GetPlayerData_request_completed(result, response_code, headers, body) -> void: + var status_check = SWUtils.check_http_response(response_code, headers, body) + if is_instance_valid(GetPlayerData): + SilentWolf.free_request(wrGetPlayerData, GetPlayerData) + + if status_check: + var json_body = JSON.parse_string(body.get_string_from_utf8()) + var sw_result: Dictionary = SilentWolf.build_result(json_body) + if json_body.success: + SWLogger.info("SilentWolf get player data success for player: " + str(json_body.player_name)) + player_name = json_body.player_name + player_data = json_body.player_data + SWLogger.debug("Player data: " + str(player_data)) + sw_result["player_data"] = player_data + else: + SWLogger.error("SilentWolf get player data failure: " + str(json_body.error)) + sw_get_player_data_complete.emit(sw_result) + + +func delete_player_weapons(player_name: String) -> void: + var weapons = { "Weapons": [] } + delete_player_data(player_name, weapons) + + +func remove_player_money(player_name: String) -> void: + var money = { "Money": 0 } + delete_player_data(player_name, money) + + +func delete_player_items(player_name: String, item_name: String) -> void: + var item = { item_name: "" } + delete_player_data(player_name, item) + + +func delete_all_player_data(player_name: String) -> void: + delete_player_data(player_name, {}) + + +func delete_player_data(player_name: String, player_data: Dictionary) -> Node: + var prepared_http_req = SilentWolf.prepare_http_request() + DeletePlayerData = prepared_http_req.request + wrDeletePlayerData = prepared_http_req.weakref + DeletePlayerData.request_completed.connect(_on_DeletePlayerData_request_completed) + SWLogger.info("Calling SilentWolf to remove player data") + var payload = { "game_id": SilentWolf.config.game_id, "player_name": player_name, "player_data": player_data } + var query = JSON.stringify(payload) + var request_url = "https://api.silentwolf.com/remove_player_data" + SilentWolf.send_post_request(DeletePlayerData, request_url, payload) + return self + + +func _on_DeletePlayerData_request_completed(result, response_code, headers, body) -> void: + var status_check = SWUtils.check_http_response(response_code, headers, body) + if is_instance_valid(DeletePlayerData): + DeletePlayerData.queue_free() + + if status_check: + var json_body = JSON.parse_string(body.get_string_from_utf8()) + var sw_result: Dictionary = SilentWolf.build_result(json_body) + if json_body.success: + SWLogger.info("SilentWolf delete player data success for player: " + str(json_body.player_name)) + var player_name = json_body.player_name + # return player_data after (maybe partial) removal + var player_data = json_body.player_data + sw_result["player_name"] = player_name + sw_result["player_data"] = player_data + else: + SWLogger.error("SilentWolf delete player data failure: " + str(json_body.error)) + sw_delete_player_data_complete.emit(sw_result) + + +func get_stats() -> Dictionary: + var stats = null + if player_data: + stats = { + "strength": player_data.strength, + "speed": player_data.speed, + "reflexes": player_data.reflexes, + "max_health": player_data.max_health, + "career": player_data.career + } + return stats + + +func get_inventory() -> Dictionary: + var inventory = null + if player_data: + inventory = { + "weapons": player_data.weapons, + "gold": player_data.gold + } + return inventory + + +func set_player_data(new_player_data: Dictionary) -> void: + player_data = new_player_data + + +func clear_player_data() -> void: + player_name = null + player_data = null diff --git a/addons/silent_wolf/Players/Players.gd.uid b/addons/silent_wolf/Players/Players.gd.uid new file mode 100644 index 0000000..fcaf584 --- /dev/null +++ b/addons/silent_wolf/Players/Players.gd.uid @@ -0,0 +1 @@ +uid://fd0sa4db1qey diff --git a/addons/silent_wolf/Scores/Leaderboard.gd b/addons/silent_wolf/Scores/Leaderboard.gd new file mode 100644 index 0000000..e1bf2cd --- /dev/null +++ b/addons/silent_wolf/Scores/Leaderboard.gd @@ -0,0 +1,127 @@ +@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() + var sw_result = await SilentWolf.Scores.get_scores().sw_get_scores_complete + scores = sw_result.scores + hide_message() + render_board(scores, local_scores) + + +func render_board(scores: Array, local_scores: Array) -> void: + 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.is_empty() and local_scores.is_empty(): + add_no_scores_message() + else: + if scores.is_empty(): + add_no_scores_message() + if all_scores.is_empty(): + 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: Dictionary) -> bool: + 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: Array, local_scores: Array, max_scores: int=10) -> Array: + 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(sort_by_score); + if scores.size() > max_scores: + var new_size = scores.resize(max_scores) + return scores + + +func sort_by_score(a: Dictionary, b: Dictionary) -> bool: + if a.score > b.score: + return true; + else: + if a.score < b.score: + return false; + else: + return true; + + +func score_in_score_array(scores: Array, new_score: Dictionary) -> bool: + var in_score_array = false + if !new_score.is_empty() and !scores.is_empty(): + 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: String, score_value: String) -> void: + var item = ScoreItem.instantiate() + list_index += 1 + item.get_node("PlayerName").text = str(list_index) + str(". ") + player_name + item.get_node("Score").text = score_value + item.offset_top = list_index * 100 + $"Board/HighScores/ScoreItemContainer".add_child(item) + + +func add_no_scores_message() -> void: + var item = $"Board/MessageContainer/TextMessage" + item.text = "No scores yet!" + $"Board/MessageContainer".show() + item.offset_top = 135 + + +func add_loading_scores_message() -> void: + var item = $"Board/MessageContainer/TextMessage" + item.text = "Loading scores..." + $"Board/MessageContainer".show() + item.offset_top = 135 + + +func hide_message() -> void: + $"Board/MessageContainer".hide() + + +func clear_leaderboard() -> void: + 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() -> void: + 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_to_file(scene_name) diff --git a/addons/silent_wolf/Scores/Leaderboard.gd.uid b/addons/silent_wolf/Scores/Leaderboard.gd.uid new file mode 100644 index 0000000..91a401f --- /dev/null +++ b/addons/silent_wolf/Scores/Leaderboard.gd.uid @@ -0,0 +1 @@ +uid://bvcpbbvn2ydyc diff --git a/addons/silent_wolf/Scores/Leaderboard.tscn b/addons/silent_wolf/Scores/Leaderboard.tscn new file mode 100644 index 0000000..1fe0cbb --- /dev/null +++ b/addons/silent_wolf/Scores/Leaderboard.tscn @@ -0,0 +1,63 @@ +[gd_scene load_steps=5 format=3 uid="uid://b2h8ok8yfc7wb"] + +[ext_resource type="Script" uid="uid://bvcpbbvn2ydyc" path="res://addons/silent_wolf/Scores/Leaderboard.gd" id="1"] +[ext_resource type="Theme" uid="uid://d2eakbmaefnt6" path="res://addons/silent_wolf/assets/themes/sw_theme.tres" id="2_ixaq4"] +[ext_resource type="PackedScene" uid="uid://clllbf6am8qdf" path="res://addons/silent_wolf/common/SWButton.tscn" id="4"] + +[sub_resource type="Theme" id="Theme_j1gah"] + +[node name="Leaderboard" type="Node2D"] +script = ExtResource("1") + +[node name="OldBoard" type="MarginContainer" parent="."] +visible = false + +[node name="HighScores" type="TextureRect" parent="OldBoard"] +layout_mode = 2 + +[node name="Board" type="VBoxContainer" parent="."] +offset_left = 232.0 +offset_top = 3.0 +offset_right = 1738.0 +offset_bottom = 1070.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme = ExtResource("2_ixaq4") +theme_override_constants/separation = 48 +alignment = 1 + +[node name="TitleContainer" type="CenterContainer" parent="Board"] +layout_mode = 2 + +[node name="Label" type="Label" parent="Board/TitleContainer"] +layout_mode = 2 +theme_override_font_sizes/font_size = 72 +text = "Leaderboard" +horizontal_alignment = 1 + +[node name="MessageContainer" type="CenterContainer" parent="Board"] +visible = false +layout_mode = 2 + +[node name="TextMessage" type="Label" parent="Board/MessageContainer"] +layout_mode = 2 +theme_override_font_sizes/font_size = 36 +text = "Loading scores..." + +[node name="HighScores" type="CenterContainer" parent="Board"] +layout_mode = 2 +theme = SubResource("Theme_j1gah") + +[node name="ScoreItemContainer" type="VBoxContainer" parent="Board/HighScores"] +layout_mode = 2 + +[node name="CloseButtonContainer" type="CenterContainer" parent="Board"] +layout_mode = 2 + +[node name="CloseButton" parent="Board/CloseButtonContainer" instance=ExtResource("4")] +custom_minimum_size = Vector2(600, 80) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +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..27cb8fc --- /dev/null +++ b/addons/silent_wolf/Scores/ScoreItem.tscn @@ -0,0 +1,25 @@ +[gd_scene format=3 uid="uid://2wy4d8d5av0l"] + +[node name="ScoreItem" type="Panel"] +custom_minimum_size = Vector2(500, 50) +grow_horizontal = 0 +grow_vertical = 0 + +[node name="PlayerName" type="RichTextLabel" parent="."] +custom_minimum_size = Vector2(100, 25) +layout_mode = 0 +offset_left = 8.0 +offset_right = 359.0 +offset_bottom = 50.0 +theme_override_font_sizes/normal_font_size = 32 +text = "1. Godzilla" + +[node name="Score" type="Label" parent="."] +layout_mode = 0 +offset_left = 359.0 +offset_top = 1.0 +offset_right = 485.0 +offset_bottom = 49.0 +theme_override_font_sizes/font_size = 32 +text = "13" +horizontal_alignment = 2 diff --git a/addons/silent_wolf/Scores/Scores.gd b/addons/silent_wolf/Scores/Scores.gd new file mode 100644 index 0000000..edfa635 --- /dev/null +++ b/addons/silent_wolf/Scores/Scores.gd @@ -0,0 +1,389 @@ +extends Node + +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") +const SWUtils = preload("res://addons/silent_wolf/utils/SWUtils.gd") + +# new signals +signal sw_get_scores_complete +signal sw_get_player_scores_complete +signal sw_top_player_score_complete +signal sw_get_position_complete +signal sw_get_scores_around_complete +signal sw_save_score_complete +signal sw_wipe_leaderboard_complete +signal sw_delete_score_complete + +# 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 SaveScore = null +var GetScores = null +var ScorePosition = null +var ScoresAround = null +var ScoresByPlayer = null +var TopScoreByPlayer = null +var WipeLeaderboard = null +var DeleteScore = null + +# wekrefs +var wrSaveScore = null +var wrGetScores = null +var wrScorePosition = null +var wrScoresAround = null +var wrScoresByPlayer = null +var wrTopScoreByPlayer = null +var wrWipeLeaderboard = null +var wrDeleteScore = null + + +# metadata, if included should be a dictionary +# The score attribute could be either a score_value (int) or score_id (String) +func save_score(player_name: String, score, ldboard_name: String="main", metadata: Dictionary={}) -> Node: + # 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 SilentWolf.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: + var prepared_http_req = SilentWolf.prepare_http_request() + SaveScore = prepared_http_req.request + wrSaveScore = prepared_http_req.weakref + SaveScore.request_completed.connect(_on_SaveScore_request_completed) + SWLogger.info("Calling SilentWolf backend to post new score...") + var game_id = SilentWolf.config.game_id + + 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, + "score": score, + "ldboard_name": ldboard_name + } + print("!metadata.empty(): " + str(!metadata.is_empty())) + if !metadata.is_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/save_score" + SilentWolf.send_post_request(SaveScore, request_url, payload) + return self + + +func _on_SaveScore_request_completed(result, response_code, headers, body) -> void: + var status_check = SWUtils.check_http_response(response_code, headers, body) + SilentWolf.free_request(wrSaveScore, SaveScore) + + if status_check: + var json_body = JSON.parse_string(body.get_string_from_utf8()) + var sw_result: Dictionary = SilentWolf.build_result(json_body) + if json_body.success: + SWLogger.info("SilentWolf save score success.") + sw_result["score_id"] = json_body.score_id + else: + SWLogger.error("SilentWolf save score failure: " + str(json_body.error)) + sw_save_score_complete.emit(sw_result) + + +func get_scores(maximum: int=10, ldboard_name: String="main", period_offset: int=0) -> Node: + var prepared_http_req = SilentWolf.prepare_http_request() + GetScores = prepared_http_req.request + wrGetScores = prepared_http_req.weakref + GetScores.request_completed.connect(_on_GetScores_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 request_url = "https://api.silentwolf.com/get_scores/" + str(SilentWolf.config.game_id) + "?max=" + str(maximum) + "&ldboard_name=" + str(ldboard_name) + "&period_offset=" + str(period_offset) + SilentWolf.send_get_request(GetScores, request_url) + return self + + +func _on_GetScores_request_completed(result, response_code, headers, body) -> void: + var status_check = SWUtils.check_http_response(response_code, headers, body) + SilentWolf.free_request(wrGetScores, GetScores) + + if status_check: + var json_body = JSON.parse_string(body.get_string_from_utf8()) + var sw_result: Dictionary = SilentWolf.build_result(json_body) + if json_body.success: + SWLogger.info("SilentWolf get scores success, found " + str(json_body.top_scores.size()) + " scores.") + scores = translate_score_fields_in_array(json_body.top_scores) + SWLogger.debug("Scores: " + str(scores)) + var ld_name = json_body.ld_name + var ld_config = json_body.ld_config + if "period_offset" in json_body: + var period_offset = str(json_body["period_offset"]) + leaderboards_past_periods[ld_name + ";" + period_offset] = scores + else: + leaderboards[ld_name] = scores + ldboard_config[ld_name] = ld_config + sw_result["scores"] = scores + sw_result["ld_name"] = ld_name + else: + SWLogger.error("SilentWolf get scores failure: " + str(json_body.error)) + sw_get_scores_complete.emit(sw_result) + + +func get_scores_by_player(player_name: String, maximum: int=10, ldboard_name: String="main", period_offset: int=0) -> Node: + if player_name == null: + SWLogger.error("Error in SilentWolf.Scores.get_scores_by_player: provided player_name is null") + else: + var prepared_http_req = SilentWolf.prepare_http_request() + ScoresByPlayer = prepared_http_req.request + wrScoresByPlayer = prepared_http_req.weakref + ScoresByPlayer.request_completed.connect(_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 request_url = "https://api.silentwolf.com/get_scores_by_player/" + str(SilentWolf.config.game_id) + "?max=" + str(maximum) + "&ldboard_name=" + str(ldboard_name.uri_encode()) + "&player_name=" + str(player_name.uri_encode()) + "&period_offset=" + str(period_offset) + SilentWolf.send_get_request(ScoresByPlayer, request_url) + return self + + +func _on_GetScoresByPlayer_request_completed(result, response_code, headers, body) -> void: + var status_check = SWUtils.check_http_response(response_code, headers, body) + SilentWolf.free_request(wrScoresByPlayer, ScoresByPlayer) + + if status_check: + var json_body = JSON.parse_string(body.get_string_from_utf8()) + var sw_result: Dictionary = SilentWolf.build_result(json_body) + if json_body.success: + SWLogger.info("SilentWolf get scores by player success, found " + str(json_body.top_scores.size()) + " scores.") + player_scores = translate_score_fields_in_array(json_body.top_scores) + SWLogger.debug("Scores for " + json_body.player_name + ": " + str(player_scores)) + var ld_name = json_body.ld_name + var ld_config = json_body.ld_config + var player_name = json_body.player_name + sw_result["scores"] = player_scores + else: + SWLogger.error("SilentWolf get scores by player failure: " + str(json_body.error)) + sw_get_player_scores_complete.emit(sw_result) + + +func get_top_score_by_player(player_name: String, maximum: int=10, ldboard_name: String="main", period_offset: int=0) -> Node: + if player_name == null: + SWLogger.error("Error in SilentWolf.Scores.get_top_score_by_player: provided player_name is null") + else: + var prepared_http_req = SilentWolf.prepare_http_request() + TopScoreByPlayer = prepared_http_req.request + wrTopScoreByPlayer = prepared_http_req.weakref + TopScoreByPlayer.request_completed.connect(_on_GetTopScoreByPlayer_request_completed) + SWLogger.info("Calling SilentWolf backend to get top score 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 request_url = "https://api.silentwolf.com/get_top_score_by_player/" + str(SilentWolf.config.game_id) + "?max=" + str(maximum) + "&ldboard_name=" + str(ldboard_name.uri_encode()) + "&player_name=" + str(player_name.uri_encode()) + "&period_offset=" + str(period_offset) + SilentWolf.send_get_request(TopScoreByPlayer, request_url) + return self + + +func _on_GetTopScoreByPlayer_request_completed(result, response_code, headers, body) -> void: + var status_check = SWUtils.check_http_response(response_code, headers, body) + SilentWolf.free_request(wrTopScoreByPlayer, TopScoreByPlayer) + + if status_check: + var json_body = JSON.parse_string(body.get_string_from_utf8()) + var sw_result: Dictionary = SilentWolf.build_result(json_body) + if json_body.success: + SWLogger.info("SilentWolf get top score by player success, found top score? " + str(!json_body.top_score.is_empty())) + if !json_body.top_score.is_empty(): + player_top_score = translate_score_fields(json_body.top_score) + SWLogger.debug("Top score for " + json_body.player_name + ": " + str(player_top_score)) + var ld_name = json_body.ld_name + var ld_config = json_body.ld_config + var player_name = json_body.player_name + sw_result["top_score"] = player_top_score + else: + SWLogger.error("SilentWolf get top score by player failure: " + str(json_body.error)) + sw_top_player_score_complete.emit(sw_result) + + +# The score attribute could be either a score_value (int) or score_id (Sstring) +func get_score_position(score, ldboard_name: String="main") -> Node: + 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 + var prepared_http_req = SilentWolf.prepare_http_request() + ScorePosition = prepared_http_req.request + wrScorePosition = prepared_http_req.weakref + ScorePosition.request_completed.connect(_on_GetScorePosition_request_completed) + SWLogger.info("Calling SilentWolf to get score position") + var payload = { "game_id": SilentWolf.config.game_id, "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" + SilentWolf.send_post_request(ScorePosition, request_url, payload) + return self + + +func _on_GetScorePosition_request_completed(result, response_code, headers, body) -> void: + var status_check = SWUtils.check_http_response(response_code, headers, body) + SilentWolf.free_request(wrScorePosition, ScorePosition) + + if status_check: + var json_body = JSON.parse_string(body.get_string_from_utf8()) + var sw_result: Dictionary = SilentWolf.build_result(json_body) + if json_body.success: + SWLogger.info("SilentWolf get score position success: " + str(json_body.position)) + sw_result["position"] = int(json_body.position) + else: + SWLogger.error("SilentWolf get score position failure: " + str(json_body.error)) + sw_get_position_complete.emit(sw_result) + + +# The score attribute couldd be either a score_value (int) or score_id (Sstring) +func get_scores_around(score, scores_to_fetch=3, ldboard_name: String="main") -> Node: + 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 + var prepared_http_req = SilentWolf.prepare_http_request() + ScoresAround = prepared_http_req.request + wrScoresAround = prepared_http_req.weakref + ScoresAround.request_completed.connect(_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 request_url = "https://api.silentwolf.com/get_scores_around/" + str(SilentWolf.config.game_id) + "?scores_to_fetch=" + str(scores_to_fetch) + "&ldboard_name=" + str(ldboard_name) + "&score_id=" + str(score_id) + "&score_value=" + str(score_value) + SilentWolf.send_get_request(ScoresAround, request_url) + return self + + +func _on_ScoresAround_request_completed(result, response_code, headers, body) -> void: + var status_check = SWUtils.check_http_response(response_code, headers, body) + + SilentWolf.free_request(wrScoresAround, ScoresAround) + + if status_check: + var json_body = JSON.parse_string(body.get_string_from_utf8()) + var sw_result: Dictionary = SilentWolf.build_result(json_body) + if json_body.success: + SWLogger.info("SilentWolf get scores around success.") + sw_result["scores_above"] = translate_score_fields_in_array(json_body.scores_above) + sw_result["scores_below"] = translate_score_fields_in_array(json_body.scores_below) + if "score_position" in json_body: + sw_result["position"] = json_body.score_position + else: + SWLogger.error("SilentWolf get scores around failure: " + str(json_body.error)) + sw_get_scores_around_complete.emit(sw_result) + + + +func delete_score(score_id: String, ldboard_name: String='main') -> Node: + var prepared_http_req = SilentWolf.prepare_http_request() + DeleteScore = prepared_http_req.request + wrDeleteScore = prepared_http_req.weakref + DeleteScore.request_completed.connect(_on_DeleteScore_request_completed) + SWLogger.info("Calling SilentWolf to delete a score") + var request_url = "https://api.silentwolf.com/delete_score?game_id=" + str(SilentWolf.config.game_id) + "&ldboard_name=" + str(ldboard_name) + "&score_id=" + str(score_id) + SilentWolf.send_get_request(DeleteScore, request_url) + return self + + +func _on_DeleteScore_request_completed(result, response_code, headers, body) -> void: + var status_check = SWUtils.check_http_response(response_code, headers, body) + SilentWolf.free_request(wrDeleteScore, DeleteScore) + + if status_check: + var json_body = JSON.parse_string(body.get_string_from_utf8()) + var sw_result: Dictionary = SilentWolf.build_result(json_body) + if json_body.success: + SWLogger.info("SilentWolf delete score success") + else: + SWLogger.error("SilentWolf delete score failure: " + str(json_body.error)) + sw_delete_score_complete.emit(sw_result) + + +# Deletes all your scores for your game +# Scores are permanently deleted, no going back! +func wipe_leaderboard(ldboard_name: String='main') -> Node: + var prepared_http_req = SilentWolf.prepare_http_request() + WipeLeaderboard = prepared_http_req.request + wrWipeLeaderboard = prepared_http_req.weakref + WipeLeaderboard.request_completed.connect(_on_WipeLeaderboard_request_completed) + SWLogger.info("Calling SilentWolf backend to wipe leaderboard...") + var payload = { "game_id": SilentWolf.config.game_id, "ldboard_name": ldboard_name } + var request_url = "https://api.silentwolf.com/wipe_leaderboard" + SilentWolf.send_post_request(WipeLeaderboard, request_url, payload) + return self + + +func _on_WipeLeaderboard_request_completed(result, response_code, headers, body) -> void: + var status_check = SWUtils.check_http_response(response_code, headers, body) + SilentWolf.free_request(wrWipeLeaderboard, WipeLeaderboard) + + if status_check: + var json_body = JSON.parse_string(body.get_string_from_utf8()) + var sw_result: Dictionary = SilentWolf.build_result(json_body) + if json_body.success: + SWLogger.info("SilentWolf wipe leaderboard success.") + else: + SWLogger.error("SilentWolf wipe leaderboard failure: " + str(json_body.error)) + sw_wipe_leaderboard_complete.emit(sw_result) + + +func add_to_local_scores(game_result: Dictionary, ld_name: String="main") -> void: + var local_score = { "score_id": game_result.score_id, "game_id" : game_result.game_id, "player_name": game_result.player_name, "score": game_result.score } + local_scores.append(local_score) + SWLogger.debug("local scores: " + str(local_scores)) + + +func translate_score_fields_in_array(scores: Array) -> Array: + var translated_scores = [] + for score in scores: + var new_score = translate_score_fields(score) + translated_scores.append(new_score) + return translated_scores + + +func translate_score_fields(score: Dictionary) -> Dictionary: + var translated_score = {} + translated_score["score_id"] = score["sid"] + translated_score["score"] = score["s"] + translated_score["player_name"] = score["pn"] + if "md" in score: + translated_score["metadata"] = score["md"] + if "position" in score: + translated_score["position"] = score["position"] + if "t" in score: + translated_score["timestamp"] = score["t"] + return translated_score diff --git a/addons/silent_wolf/Scores/Scores.gd.uid b/addons/silent_wolf/Scores/Scores.gd.uid new file mode 100644 index 0000000..7779045 --- /dev/null +++ b/addons/silent_wolf/Scores/Scores.gd.uid @@ -0,0 +1 @@ +uid://cgoyt6qo5vgsq diff --git a/addons/silent_wolf/SilentWolf.gd b/addons/silent_wolf/SilentWolf.gd new file mode 100644 index 0000000..35b28a7 --- /dev/null +++ b/addons/silent_wolf/SilentWolf.gd @@ -0,0 +1,248 @@ +extends Node + +const version = "0.9.9" +var godot_version = Engine.get_version_info().string + +const SWUtils = preload("res://addons/silent_wolf/utils/SWUtils.gd") +const SWHashing = preload("res://addons/silent_wolf/utils/SWHashing.gd") +const SWLogger = preload("res://addons/silent_wolf/utils/SWLogger.gd") + +#var Auth = Node.new() +#var Scores = Node.new() +#var Players = Node.new() +#var Multiplayer = Node.new() + +@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", + "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 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(SWUtils.get_timestamp())) + + +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(SWUtils.get_timestamp())) + 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(SWUtils.get_timestamp())) + + +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 prepare_http_request() -> Dictionary: + var request = HTTPRequest.new() + var weakref = weakref(request) + if OS.get_name() != "Web": + request.set_use_threads(true) + request.process_mode = Node.PROCESS_MODE_ALWAYS + get_tree().get_root().call_deferred("add_child", request) + var return_dict = { + "request": request, + "weakref": weakref + } + return return_dict + + +func send_get_request(http_node: HTTPRequest, request_url: String): + 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 + ] + headers = add_jwt_token_headers(headers) + print("GET headers: " + str(headers)) + if !http_node.is_inside_tree(): + await 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 + ] + headers = add_jwt_token_headers(headers) + print("POST headers: " + str(headers)) + # TODO: This should in fact be the case for all POST requests, make the following code more generic + #var post_request_paths: Array[String] = ["post_new_score", "push_player_data"] + var paths_with_values_to_hash: Dictionary = { + "save_score": ["player_name", "score"], + "push_player_data": ["player_name", "player_data"] + } + for path in paths_with_values_to_hash: + var values_to_hash = [] + if check_string_in_url(path, request_url): + SWLogger.debug("Computing hash for " + str(path)) + var fields_to_hash = paths_with_values_to_hash[path] + for field in fields_to_hash: + var value = payload[field] + # if the data is a dictionary (e.g. player data, stringify it before hashing) + if typeof(payload[field]) == TYPE_DICTIONARY: + value = JSON.stringify(payload[field]) + values_to_hash = values_to_hash + [value] + var timestamp = SWUtils.get_timestamp() + values_to_hash = values_to_hash + [timestamp] + SWLogger.debug(str(path) + " to_be_hashed: " + str(values_to_hash)) + var hashed = SWHashing.hash_values(values_to_hash) + SWLogger.debug("hash value: " + str(hashed)) + headers.append("x-sw-act-tmst: " + str(timestamp)) + headers.append("x-sw-act-dig: " + hashed) + break + var use_ssl = true + if !http_node.is_inside_tree(): + await get_tree().create_timer(0.01).timeout + var query = JSON.stringify(payload) + SWLogger.debug("Method: POST") + SWLogger.debug("request_url: " + str(request_url)) + SWLogger.debug("headers: " + str(headers)) + SWLogger.debug("query: " + str(query)) + http_node.request(request_url, headers, HTTPClient.METHOD_POST, query) + + +func add_jwt_token_headers(headers: Array) -> Array: + if Auth.sw_id_token != null: + headers.append("x-sw-id-token: " + Auth.sw_id_token) + if Auth.sw_access_token != null: + headers.append("x-sw-access-token: " + Auth.sw_access_token) + return headers + + +func check_string_in_url(test_string: String, url: String) -> bool: + return test_string in url + + +func build_result(body: Dictionary) -> Dictionary: + var error = null + var success = false + if "error" in body: + error = body.error + if "success" in body: + success = body.success + return { + "success": success, + "error": error + } + + +func check_auth_ready(): + if !Auth: + await get_tree().create_timer(0.01).timeout + + +func check_scores_ready(): + if !Scores: + await get_tree().create_timer(0.01).timeout + + +func check_players_ready(): + if !Players: + await get_tree().create_timer(0.01).timeout + + +func check_multiplayer_ready(): + if !Multiplayer: + await get_tree().create_timer(0.01).timeout + + +func check_sw_ready(): + if !Auth or !Scores or !Players or !Multiplayer: + await get_tree().create_timer(0.01).timeout diff --git a/addons/silent_wolf/SilentWolf.gd.uid b/addons/silent_wolf/SilentWolf.gd.uid new file mode 100644 index 0000000..5af8370 --- /dev/null +++ b/addons/silent_wolf/SilentWolf.gd.uid @@ -0,0 +1 @@ +uid://6vtjmk436rp3 diff --git a/addons/silent_wolf/assets/.DS_Store b/addons/silent_wolf/assets/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..586af51ae05bcf41e484446d6cded462949af911 GIT binary patch literal 8196 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 ZScLzAQ@Hdr(!!Vbl%O*HAm9Um{{nVbme2qI literal 0 HcmV?d00001 diff --git a/addons/silent_wolf/assets/fonts/Comfortaa-Bold.ttf.import b/addons/silent_wolf/assets/fonts/Comfortaa-Bold.ttf.import new file mode 100644 index 0000000..607fdbc --- /dev/null +++ b/addons/silent_wolf/assets/fonts/Comfortaa-Bold.ttf.import @@ -0,0 +1,35 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://d4kabkagmfeq" +path="res://.godot/imported/Comfortaa-Bold.ttf-4fa420dde6e624fdca7fe038f7939233.fontdata" + +[deps] + +source_file="res://addons/silent_wolf/assets/fonts/Comfortaa-Bold.ttf" +dest_files=["res://.godot/imported/Comfortaa-Bold.ttf-4fa420dde6e624fdca7fe038f7939233.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +disable_embedded_bitmaps=true +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +hinting=1 +subpixel_positioning=1 +keep_rounding_remainders=true +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/silent_wolf/assets/gfx/checkbox_checked.png b/addons/silent_wolf/assets/gfx/checkbox_checked.png new file mode 100644 index 0000000000000000000000000000000000000000..8528d2c493736e4b2d8e9d7a479ac5baea98e8a4 GIT binary patch literal 813 zcmV+|1JeA7P)6HKg02y>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/checkbox_unchecked.png.import b/addons/silent_wolf/assets/gfx/checkbox_unchecked.png.import new file mode 100644 index 0000000..900733a --- /dev/null +++ b/addons/silent_wolf/assets/gfx/checkbox_unchecked.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bgs8a50ilk5cj" +path="res://.godot/imported/checkbox_unchecked.png-747244fa66895a7a282a8f1df36959ae.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/silent_wolf/assets/gfx/checkbox_unchecked.png" +dest_files=["res://.godot/imported/checkbox_unchecked.png-747244fa66895a7a282a8f1df36959ae.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/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/dummy_info_icon_small.png.import b/addons/silent_wolf/assets/gfx/dummy_info_icon_small.png.import new file mode 100644 index 0000000..0c4eacc --- /dev/null +++ b/addons/silent_wolf/assets/gfx/dummy_info_icon_small.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://gdw18po2h7hb" +path="res://.godot/imported/dummy_info_icon_small.png-dbbedcd89d38767ae4fbec73495e5426.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/silent_wolf/assets/gfx/dummy_info_icon_small.png" +dest_files=["res://.godot/imported/dummy_info_icon_small.png-dbbedcd89d38767ae4fbec73495e5426.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/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.png.import b/addons/silent_wolf/assets/gfx/info_icon.png.import new file mode 100644 index 0000000..8c588fa --- /dev/null +++ b/addons/silent_wolf/assets/gfx/info_icon.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bex7pcjbbiqh8" +path="res://.godot/imported/info_icon.png-75dcfef383fc397e1798b32243763c67.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/silent_wolf/assets/gfx/info_icon.png" +dest_files=["res://.godot/imported/info_icon.png-75dcfef383fc397e1798b32243763c67.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/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 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 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..efba07d --- /dev/null +++ b/addons/silent_wolf/examples/CustomLeaderboards/ReverseLeaderboard.gd @@ -0,0 +1,92 @@ +@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() + var sw_result = await SilentWolf.Scores.get_high_scores().sw_get_scores_complete + scores = sw_result["scores"] + hide_message() + render_board(scores) + + +func render_board(scores: Array) -> void: + if scores.is_empty(): + add_no_scores_message() + else: + if len(scores) > 1 and scores[0].score > scores[-1].score: + scores.reverse() + 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: Array) -> Array: + if len(scores) > 1 and scores[0].score > scores[-1].score: + scores.reverse() + return scores + + +func sort_by_score(a: Dictionary, b: Dictionary) -> bool: + if a.score > b.score: + return true; + else: + if a.score < b.score: + return false; + else: + return true; + + +func add_item(player_name: String, score_value: String) -> void: + var item = ScoreItem.instance() + list_index += 1 + item.get_node("PlayerName").text = str(list_index) + str(". ") + player_name + item.get_node("Score").text = score_value + item.offset_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.offset_top = 135 + + +func add_loading_scores_message() -> void: + var item = $"Board/MessageContainer/TextMessage" + item.text = "Loading scores..." + $"Board/MessageContainer".show() + item.offset_top = 135 + + +func hide_message() -> void: + $"Board/MessageContainer".hide() + + +func _on_CloseButton_pressed() -> void: + var scene_name = SilentWolf.scores_config.open_scene_on_close + print("scene_name: " + str(scene_name)) + get_tree().change_scene_to_file(scene_name) diff --git a/addons/silent_wolf/examples/CustomLeaderboards/ReverseLeaderboard.gd.uid b/addons/silent_wolf/examples/CustomLeaderboards/ReverseLeaderboard.gd.uid new file mode 100644 index 0000000..0f533bf --- /dev/null +++ b/addons/silent_wolf/examples/CustomLeaderboards/ReverseLeaderboard.gd.uid @@ -0,0 +1 @@ +uid://dwb4mjp33o4ye diff --git a/addons/silent_wolf/examples/CustomLeaderboards/ReverseLeaderboard.tscn b/addons/silent_wolf/examples/CustomLeaderboards/ReverseLeaderboard.tscn new file mode 100644 index 0000000..b42ab07 --- /dev/null +++ b/addons/silent_wolf/examples/CustomLeaderboards/ReverseLeaderboard.tscn @@ -0,0 +1,51 @@ +[gd_scene load_steps=5 format=3 uid="uid://de5nks52pxc80"] + +[ext_resource type="Theme" uid="uid://d2eakbmaefnt6" path="res://addons/silent_wolf/assets/themes/sw_theme.tres" id="2_1tgjj"] +[ext_resource type="PackedScene" uid="uid://clllbf6am8qdf" path="res://addons/silent_wolf/common/SWButton.tscn" id="4"] +[ext_resource type="Script" uid="uid://dwb4mjp33o4ye" path="res://addons/silent_wolf/examples/CustomLeaderboards/ReverseLeaderboard.gd" id="5"] + +[sub_resource type="Theme" id="2"] + +[node name="ReverseLeaderboard" type="Node2D"] +script = ExtResource("5") + +[node name="Board" type="VBoxContainer" parent="."] +offset_left = 722.0 +offset_top = 293.0 +offset_right = 1287.0 +offset_bottom = 669.0 +theme = ExtResource("2_1tgjj") +theme_override_constants/separation = 48 + +[node name="TitleContainer" type="CenterContainer" parent="Board"] +layout_mode = 2 + +[node name="Label" type="Label" parent="Board/TitleContainer"] +layout_mode = 2 +theme_override_font_sizes/font_size = 72 +text = "Reverse Leaderboard" + +[node name="MessageContainer" type="CenterContainer" parent="Board"] +visible = false +layout_mode = 2 + +[node name="TextMessage" type="Label" parent="Board/MessageContainer"] +layout_mode = 2 +text = "Loading scores..." + +[node name="HighScores" type="CenterContainer" parent="Board"] +layout_mode = 2 +theme = SubResource("2") + +[node name="ScoreItemContainer" type="VBoxContainer" parent="Board/HighScores"] +layout_mode = 2 + +[node name="CloseButtonContainer" type="CenterContainer" parent="Board"] +layout_mode = 2 + +[node name="CloseButton" parent="Board/CloseButtonContainer" instance=ExtResource("4")] +custom_minimum_size = Vector2(600, 80) +layout_mode = 2 +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..f4eef8a --- /dev/null +++ b/addons/silent_wolf/examples/CustomLeaderboards/SmallScoreItem.tscn @@ -0,0 +1,5 @@ +[gd_scene load_steps=2 format=3 uid="uid://bqv70cuvl5eel"] + +[ext_resource type="PackedScene" uid="uid://2wy4d8d5av0l" path="res://addons/silent_wolf/Scores/ScoreItem.tscn" id="1"] + +[node name="ScoreItem" instance=ExtResource("1")] diff --git a/addons/silent_wolf/examples/CustomLeaderboards/TimeBasedLboards.gd b/addons/silent_wolf/examples/CustomLeaderboards/TimeBasedLboards.gd new file mode 100644 index 0000000..0f169c5 --- /dev/null +++ b/addons/silent_wolf/examples/CustomLeaderboards/TimeBasedLboards.gd @@ -0,0 +1,87 @@ +extends Node2D + +const ScoreItem = preload("SmallScoreItem.tscn") +const SWLogger = preload("res://addons/silent_wolf/utils/SWLogger.gd") + +var ld_names = ["Weekly", "Monthly", "main"] + +var scores = [] + +func _ready(): + SilentWolf.Scores.sw_get_scores_complete.connect(_on_scores_received) + + #var scores = SilentWolf.Scores.scores + add_loading_scores_message() + var sw_result = SilentWolf.Scores.get_scores(10, "main") + scores = sw_result.scores + # 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: Array) -> void: + #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: String, player_name: String, score: String, list_index: int) -> void: + var item = ScoreItem.instantiate() + item.get_node("PlayerName").text = str(list_index) + str(". ") + player_name + item.get_node("Score").text = score + item.offset_top = list_index * 100 + get_node("MainContainer/Boards/" + ld_name + "/HighScores/ScoreItemContainer").add_child(item) + + +func add_no_scores_message() -> void: + var item = $"MainContainer/MessageContainer/TextMessage" + item.text = "No scores yet!" + $"MainContainer/MessageContainer".show() + item.offset_top = 135 + + +func add_loading_scores_message() -> void: + var item = $"MainContainer/MessageContainer/TextMessage" + item.text = "Loading scores..." + $"MainContainer/MessageContainer".show() + item.offset_top = 135 + + +func hide_message() -> void: + $"MainContainer/MessageContainer".hide() + + +func _on_CloseButton_pressed() -> void: + var scene_name = SilentWolf.scores_config.open_scene_on_close + SWLogger.info("Closing SilentWolf leaderboard, switching to scene: " + str(scene_name)) + get_tree().change_scene_to_file(scene_name) + + +func _on_scores_received(get_scores_result: Dictionary) -> void: + var ld_name: String = get_scores_result.ld_name + var scores: Array = get_scores_result.scores + + if ld_name == "main": + SilentWolf.Scores.get_scores(10, "Weekly") + #SilentWolf.Scores.get_scores(10, "Weekly", -1) + elif ld_name == "Weekly": + SilentWolf.Scores.get_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.gd.uid b/addons/silent_wolf/examples/CustomLeaderboards/TimeBasedLboards.gd.uid new file mode 100644 index 0000000..0ccd0c5 --- /dev/null +++ b/addons/silent_wolf/examples/CustomLeaderboards/TimeBasedLboards.gd.uid @@ -0,0 +1 @@ +uid://dvc1hed63vviq diff --git a/addons/silent_wolf/examples/CustomLeaderboards/TimeBasedLboards.tscn b/addons/silent_wolf/examples/CustomLeaderboards/TimeBasedLboards.tscn new file mode 100644 index 0000000..f146e52 --- /dev/null +++ b/addons/silent_wolf/examples/CustomLeaderboards/TimeBasedLboards.tscn @@ -0,0 +1,114 @@ +[gd_scene load_steps=5 format=3 uid="uid://bsxtktsci8a4t"] + +[ext_resource type="Theme" uid="uid://d2eakbmaefnt6" path="res://addons/silent_wolf/assets/themes/sw_theme.tres" id="2_xs0b2"] +[ext_resource type="PackedScene" uid="uid://clllbf6am8qdf" path="res://addons/silent_wolf/common/SWButton.tscn" id="4"] +[ext_resource type="Script" uid="uid://dvc1hed63vviq" path="res://addons/silent_wolf/examples/CustomLeaderboards/TimeBasedLboards.gd" id="5"] + +[sub_resource type="Theme" id="3"] + +[node name="TimeBasedLBoards" type="Node2D"] +script = ExtResource("5") + +[node name="MainContainer" type="VBoxContainer" parent="."] +offset_left = 412.0 +offset_top = 95.0 +offset_right = 1516.0 +offset_bottom = 734.0 +theme = ExtResource("2_xs0b2") +theme_override_constants/separation = 128 + +[node name="TitleContainer" type="CenterContainer" parent="MainContainer"] +layout_mode = 2 + +[node name="Label2" type="Label" parent="MainContainer/TitleContainer"] +layout_mode = 2 +theme_override_font_sizes/font_size = 72 +text = "Leaderboard" + +[node name="Boards" type="HBoxContainer" parent="MainContainer"] +layout_mode = 2 +theme_override_constants/separation = 192 +alignment = 1 + +[node name="Weekly" type="VBoxContainer" parent="MainContainer/Boards"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="TitleContainer" type="CenterContainer" parent="MainContainer/Boards/Weekly"] +layout_mode = 2 + +[node name="Label" type="Label" parent="MainContainer/Boards/Weekly/TitleContainer"] +layout_mode = 2 +size_flags_horizontal = 4 +theme_override_font_sizes/font_size = 36 +text = "This week" + +[node name="HighScores" type="CenterContainer" parent="MainContainer/Boards/Weekly"] +layout_mode = 2 +size_flags_horizontal = 4 +theme = SubResource("3") + +[node name="ScoreItemContainer" type="VBoxContainer" parent="MainContainer/Boards/Weekly/HighScores"] +layout_mode = 2 + +[node name="Monthly" type="VBoxContainer" parent="MainContainer/Boards"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="TitleContainer" type="CenterContainer" parent="MainContainer/Boards/Monthly"] +layout_mode = 2 + +[node name="Label" type="Label" parent="MainContainer/Boards/Monthly/TitleContainer"] +layout_mode = 2 +theme_override_font_sizes/font_size = 36 +text = "This month" + +[node name="HighScores" type="CenterContainer" parent="MainContainer/Boards/Monthly"] +layout_mode = 2 +size_flags_horizontal = 4 +theme = SubResource("3") + +[node name="ScoreItemContainer" type="VBoxContainer" parent="MainContainer/Boards/Monthly/HighScores"] +layout_mode = 2 + +[node name="main" type="VBoxContainer" parent="MainContainer/Boards"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="TitleContainer" type="CenterContainer" parent="MainContainer/Boards/main"] +layout_mode = 2 + +[node name="Label" type="Label" parent="MainContainer/Boards/main/TitleContainer"] +layout_mode = 2 +theme_override_font_sizes/font_size = 36 +text = "All time" + +[node name="HighScores" type="CenterContainer" parent="MainContainer/Boards/main"] +layout_mode = 2 +size_flags_horizontal = 4 +theme = SubResource("3") + +[node name="ScoreItemContainer" type="VBoxContainer" parent="MainContainer/Boards/main/HighScores"] +layout_mode = 2 + +[node name="MessageContainer" type="CenterContainer" parent="MainContainer"] +visible = false +layout_mode = 2 + +[node name="TextMessage" type="Label" parent="MainContainer/MessageContainer"] +layout_mode = 2 +text = "Loading scores..." + +[node name="CenterContainer" type="CenterContainer" parent="MainContainer"] +layout_mode = 2 + +[node name="CloseButtonContainer" type="CenterContainer" parent="MainContainer/CenterContainer"] +layout_mode = 2 + +[node name="CloseButton" parent="MainContainer/CenterContainer/CloseButtonContainer" instance=ExtResource("4")] +custom_minimum_size = Vector2(600, 80) +layout_mode = 2 +theme_override_font_sizes/font_size = 36 +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..2d666b2 --- /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.9.9" +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..425cb2f --- /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/silent_wolf.gd.uid b/addons/silent_wolf/silent_wolf.gd.uid new file mode 100644 index 0000000..90c5379 --- /dev/null +++ b/addons/silent_wolf/silent_wolf.gd.uid @@ -0,0 +1 @@ +uid://dd0nkn2y0uspg 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/SWHashing.gd.uid b/addons/silent_wolf/utils/SWHashing.gd.uid new file mode 100644 index 0000000..4a2f24a --- /dev/null +++ b/addons/silent_wolf/utils/SWHashing.gd.uid @@ -0,0 +1 @@ +uid://cf1c2aoqmrxf0 diff --git a/addons/silent_wolf/utils/SWHttpUtils.gd b/addons/silent_wolf/utils/SWHttpUtils.gd new file mode 100644 index 0000000..5567928 --- /dev/null +++ b/addons/silent_wolf/utils/SWHttpUtils.gd @@ -0,0 +1,5 @@ +extends Node + +const SWLogger = preload("res://addons/silent_wolf/utils/SWLogger.gd") + + diff --git a/addons/silent_wolf/utils/SWHttpUtils.gd.uid b/addons/silent_wolf/utils/SWHttpUtils.gd.uid new file mode 100644 index 0000000..171ad5e --- /dev/null +++ b/addons/silent_wolf/utils/SWHttpUtils.gd.uid @@ -0,0 +1 @@ +uid://2mllvd5kk6df diff --git a/addons/silent_wolf/utils/SWLocalFileStorage.gd b/addons/silent_wolf/utils/SWLocalFileStorage.gd new file mode 100644 index 0000000..7e02727 --- /dev/null +++ b/addons/silent_wolf/utils/SWLocalFileStorage.gd @@ -0,0 +1,49 @@ +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: ') -> bool: + var save_success = false + var file = FileAccess.open(path, FileAccess.WRITE) + file.store_string(str(data)) + save_success = true + SWLogger.debug(debug_message + str(data)) + return save_success + + +static func remove_data(path: String, debug_message: String='Removing data from file in local storage: ') -> bool: + var delete_success = false + if FileAccess.file_exists(path): + var file = FileAccess.open(path, FileAccess.WRITE) + var data = {} + file.store_var(data) + delete_success = true + SWLogger.debug(debug_message) + return delete_success + + +static func does_file_exist(path: String) -> bool: + return FileAccess.file_exists(path) + + +static func get_data(path: String) -> Dictionary: + var content = {} + print("path: " + str(path)) + if FileAccess.file_exists(path): + var file = FileAccess.open(path, FileAccess.READ) + var text_content = file.get_as_text() + if text_content == null or text_content == '' or text_content.length() == 0: + SWLogger.debug("Empty file in local storage at " + str(path)) + else: + var data = JSON.parse_string(text_content) + if typeof(data) == TYPE_DICTIONARY: + content = data + else: + SWLogger.debug("Invalid data in local storage at " + str(path)) + else: + SWLogger.debug("Could not find any data at: " + str(path)) + return content diff --git a/addons/silent_wolf/utils/SWLocalFileStorage.gd.uid b/addons/silent_wolf/utils/SWLocalFileStorage.gd.uid new file mode 100644 index 0000000..e79a7c5 --- /dev/null +++ b/addons/silent_wolf/utils/SWLocalFileStorage.gd.uid @@ -0,0 +1 @@ +uid://cvhnkacjrdi1b diff --git a/addons/silent_wolf/utils/SWLogger.gd b/addons/silent_wolf/utils/SWLogger.gd new file mode 100644 index 0000000..7f54969 --- /dev/null +++ b/addons/silent_wolf/utils/SWLogger.gd @@ -0,0 +1,32 @@ +extends Node + +const SWUtils = preload("res://addons/silent_wolf/utils/SWUtils.gd") + +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)) + +static func log_time(log_text, log_level='INFO'): + var timestamp = SWUtils.get_timestamp() + if log_level == 'ERROR': + error(log_text + ": " + str(timestamp)) + elif log_level == 'INFO': + info(log_text + ": " + str(timestamp)) + else: + debug(log_text + ": " + str(timestamp)) diff --git a/addons/silent_wolf/utils/SWLogger.gd.uid b/addons/silent_wolf/utils/SWLogger.gd.uid new file mode 100644 index 0000000..7fa3bb2 --- /dev/null +++ b/addons/silent_wolf/utils/SWLogger.gd.uid @@ -0,0 +1 @@ +uid://fgn4wk1houa6 diff --git a/addons/silent_wolf/utils/SWUtils.gd b/addons/silent_wolf/utils/SWUtils.gd new file mode 100644 index 0000000..8037446 --- /dev/null +++ b/addons/silent_wolf/utils/SWUtils.gd @@ -0,0 +1,35 @@ +extends Node + +const SWLogger = preload("res://addons/silent_wolf/utils/SWLogger.gd") + +static func get_timestamp() -> int: + var unix_time: float = Time.get_unix_time_from_system() + var unix_time_int: int = unix_time + var timestamp = round((unix_time - unix_time_int) * 1000.0) + return timestamp + + +static func check_http_response(response_code, headers, body): + SWLogger.debug("response code: " + str(response_code)) + SWLogger.debug("response headers: " + str(headers)) + SWLogger.debug("response body: " + str(body.get_string_from_utf8())) + + var check_ok = true + if response_code == 0: + no_connection_error() + check_ok = false + elif response_code == 403: + forbidden_error() + return check_ok + + +static func no_connection_error(): + SWLogger.error("Godot couldn't connect to the SilentWolf backend. There are several reasons why this might happen. See https://silentwolf.com/troubleshooting for more details. If the problem persists you can reach out to us: https://silentwolf.com/contact") + + +static func forbidden_error(): + SWLogger.error("You are not authorized to call the SilentWolf API - check your API key configuration or contact us: https://silentwolf.com/contact") + + +static func obfuscate_string(string: String) -> String: + return string.replace(".", "*") diff --git a/addons/silent_wolf/utils/SWUtils.gd.uid b/addons/silent_wolf/utils/SWUtils.gd.uid new file mode 100644 index 0000000..e036fe3 --- /dev/null +++ b/addons/silent_wolf/utils/SWUtils.gd.uid @@ -0,0 +1 @@ +uid://4c6y0rpjclhh diff --git a/addons/silent_wolf/utils/UUID.gd b/addons/silent_wolf/utils/UUID.gd new file mode 100644 index 0000000..9ab2ac1 --- /dev/null +++ b/addons/silent_wolf/utils/UUID.gd @@ -0,0 +1,46 @@ +static func get_random_int(max_value: int) -> int: + randomize() + return randi() % max_value + +static func random_bytes(n: int) -> Array: + var r = [] + for index in range(0, n): + r.append(get_random_int(256)) + return r + +static func uuid_bin() -> Array: + var b = random_bytes(16) + b[6] = (b[6] & 0x0f) | 0x40 + b[8] = (b[8] & 0x3f) | 0x80 + return b + +static func generate_uuid_v4() -> String: + var b = uuid_bin() + 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] + +static func is_uuid(test_string: String) -> bool: + return test_string.count("-") == 4 and test_string.length() == 36 + + +# 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. + +# Changes made: +# - Refactored variable names and function signatures to follow snake case naming convention +# - Added type hints to all variables and function attributes diff --git a/addons/silent_wolf/utils/UUID.gd.uid b/addons/silent_wolf/utils/UUID.gd.uid new file mode 100644 index 0000000..76c05e8 --- /dev/null +++ b/addons/silent_wolf/utils/UUID.gd.uid @@ -0,0 +1 @@ +uid://chlwc7n1acfrb diff --git a/project.godot b/project.godot index 79e3aba..873ca00 100644 --- a/project.godot +++ b/project.godot @@ -21,6 +21,7 @@ GridManager="*res://Scripts/grid_manager.gd" GameManager="*res://Scripts/game_manager.gd" MusicPlayer="*res://Scenes/music_player.tscn" SoundPlayer="*res://Scripts/sound_player.gd" +SilentWolf="*res://addons/silent_wolf/SilentWolf.gd" [display] @@ -73,4 +74,5 @@ up={ textures/canvas_textures/default_texture_filter=3 renderer/rendering_method="gl_compatibility" renderer/rendering_method.mobile="gl_compatibility" +textures/vram_compression/import_etc2_astc=true 2d/snap/snap_2d_transforms_to_pixel=true