diff --git a/GodotSample/AWSGameSDK/AWSGameSDK.gd b/GodotSample/AWSGameSDK/AWSGameSDK.gd deleted file mode 100644 index 9ca22cc..0000000 --- a/GodotSample/AWSGameSDK/AWSGameSDK.gd +++ /dev/null @@ -1,439 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: MIT-0 - -extends Node - -# Class to manage user info -class UserInfo: - var user_id = ""; - var guest_secret = ""; - var auth_token = ""; - var apple_id = ""; - var steam_id = ""; - var google_play_id = ""; - var facebook_id = ""; - var refresh_token = ""; - var auth_token_expires_in = ""; - var refresh_token_expires_in = ""; - - func _to_string(): - return("user_id: " + user_id + "\nguest_secret: " + guest_secret + "\nauth_token: " + auth_token + - "\napple_id: " + apple_id + "\nsteam_id: " + steam_id + "\ngoogle_play_id: " + google_play_id - + "\nfacebook_id: " + facebook_id - + "\nrefresh_token: " + refresh_token + "\nauth_token_expires_in: " + str(auth_token_expires_in) - + "\nrefresh_token_expires_in: " + str(refresh_token_expires_in)) - -var login_endpoint = null # Endpoint for custom identity component passed in Init -var login_error_callback = null # Callback passed in Init for all login errors -var login_callback = null # Callback for the latest login request - -var user_info = null # User info for the logged in user - -var unix_time_for_auth_token_expiration = null # Set when login successful to do refreshes automatically - -# Called when the node enters the scene tree for the first time. -func _ready(): - pass - -# Called every frame. 'delta' is the elapsed time since the previous frame. -func _process(delta): - - # If we have a expiration time for auth token, check if we need to refresh - if(self.unix_time_for_auth_token_expiration != null): - var seconds_difference = self.unix_time_for_auth_token_expiration - Time.get_unix_time_from_system() - - # if it's less than 5 seconds to to expiration, renew - if(seconds_difference < 5): - self.unix_time_for_auth_token_expiration = null - print("Refresh the access token") - self.login_with_refresh_token(self.user_info.refresh_token) - -func init(login_endpoint, login_error_callback): - - self.login_endpoint = login_endpoint - self.login_error_callback = login_error_callback - - print("AWS Game SDK initialized") - -# Logs in as a new guest user -func login_as_new_guest_user(login_callback): - # Set the login callback - self.login_callback = login_callback - - # Create an HTTP request node and connect its completion signal. - var http_request = HTTPRequest.new() - add_child(http_request) - http_request.request_completed.connect(self.sdk_login_callback) - - # Perform a GET request to login as a new guest - var error = http_request.request(login_endpoint+"/login-as-guest") - - # In case of error, trigger the error callback - if error != OK: - self.login_error_callback.call("Error making request to login endpoint") - -# Logs in with existing user -func login_as_guest(user_id, guest_secret, login_callback): - - # Set the login callback - self.login_callback = login_callback - - # Create an HTTP request node and connect its completion signal. - var http_request = HTTPRequest.new() - add_child(http_request) - http_request.request_completed.connect(self.sdk_login_callback) - - # Add the query parameters to the request - var params = "?" - params += "user_id" + "=" + user_id.uri_encode() - params += "&guest_secret" + "=" + guest_secret.uri_encode() - - # Perform a GET request to login as a new guest - var error = http_request.request(login_endpoint+"/login-as-guest"+params) - - # In case of error, trigger the error callback - if error != OK: - self.login_error_callback.call("Error making request to login endpoint") - -# Refresh the access token with a refresh token -func login_with_refresh_token(refresh_token, login_callback = null): - - # Set the login callback - if(login_callback != null): - self.login_callback = login_callback - - # Create an HTTP request node and connect its completion signal. - var http_request = HTTPRequest.new() - add_child(http_request) - http_request.request_completed.connect(self.sdk_login_callback) - - # Add the query parameters to the request - var params = "?" - params += "refresh_token" + "=" + refresh_token.uri_encode() - - # Perform a GET request to login as a new guest - var error = http_request.request(login_endpoint+"/refresh-access-token"+params) - - # In case of error, trigger the error callback - if error != OK: - self.login_error_callback.call("Error making request to login endpoint") - - -# Called to link an existing authenticated user to a Steam ID -func link_steam_id_to_current_user(steam_token, login_callback_steam): - - # Set the login callback - if(login_callback_steam != null): - self.login_callback = login_callback_steam - - if(self.user_info == null): - self.login_error_callback.call("No user info, can't link existing user to Steam ID") - return - - self.login_with_steam(steam_token, self.user_info.auth_token, true) - -# Called to create a new user with steam ID, or to login with existing user linked to Steam ID -func login_with_steam_token(steam_token, login_callback): - - # Set the login callback - if(login_callback != null): - self.login_callback = login_callback - - self.login_with_steam(steam_token, null, false) - -# Logs in with steam either linking existing user or as a steam only / new user -# Called internally by the different Steam login functions -func login_with_steam(steam_token, auth_token, link_to_existing_user): - - # Create an HTTP request node and connect its completion signal. - var http_request = HTTPRequest.new() - add_child(http_request) - http_request.request_completed.connect(self.sdk_login_callback) - - # Add the steam token to request - var params = "?" - params += "steam_auth_token" + "=" + steam_token.uri_encode() - - # If we're linking to existing user, add the relevant parameters - if(auth_token != null and link_to_existing_user == true): - - print("Linking Steam ID to existing user") - params += "&auth_token" + "=" + auth_token.uri_encode() - params += "&link_to_existing_user=Yes" - - print(login_endpoint+"/login-with-steam"+params) - - # Perform a GET request to login as a new guest - var error = http_request.request(login_endpoint+"/login-with-steam"+params) - - # In case of error, trigger the error callback - if error != OK: - self.login_error_callback.call("Error making request to login endpoint for login-with-steam") - -# Called to link an existing authenticated user to a Apple ID -func link_apple_id_to_current_user(apple_auth_token, login_callback_apple): - - # Set the login callback - if(login_callback_apple != null): - self.login_callback = login_callback_apple - - if(self.user_info == null): - self.login_error_callback.call("No user info, can't link existing user to Apple ID") - return - - self.login_with_apple_id(apple_auth_token, self.user_info.auth_token, true) - -# Called to create a new user with Apple ID, or to login with existing user linked to AppleID -func login_with_apple_id_token(apple_auth_token, login_callback): - - # Set the login callback - if(login_callback != null): - self.login_callback = login_callback - - self.login_with_apple_id(apple_auth_token, null, false) - -# Logs in with Apple ID either linking existing user or as a Apple ID only / new user -# Called internally by the different Apple ID login functions -func login_with_apple_id(apple_auth_token, auth_token, link_to_existing_user): - - # Create an HTTP request node and connect its completion signal. - var http_request = HTTPRequest.new() - add_child(http_request) - http_request.request_completed.connect(self.sdk_login_callback) - - # Add the apple auth token to request - var params = "?" - params += "apple_auth_token" + "=" + apple_auth_token.uri_encode() - - # If we're linking to existing user, add the relevant parameters - if(auth_token != null and link_to_existing_user == true): - - print("Linking Apple ID to existing user") - params += "&auth_token" + "=" + auth_token.uri_encode() - params += "&link_to_existing_user=Yes" - - print(login_endpoint+"/login-with-apple-id"+params) - - # Perform a GET request to login as a new guest - var error = http_request.request(login_endpoint+"/login-with-apple-id"+params) - - # In case of error, trigger the error callback - if error != OK: - self.login_error_callback.call("Error making request to login endpoint for login-with-apple-id") - -# Called to link an existing authenticated user to a Google Play ID -func link_google_play_id_to_current_user(google_play_auth_token, login_callback_google): - - # Set the login callback - if(login_callback_google != null): - self.login_callback = login_callback_google - - if(self.user_info == null): - self.login_error_callback.call("No user info, can't link existing user to Google Play ID") - return - - self.login_with_google_play(google_play_auth_token, self.user_info.auth_token, true) - -# Called to create a new user with Google Play ID, or to login with existing user linked to Google Play -func login_with_google_play_token(google_play_auth_token, login_callback): - - # Set the login callback - if(login_callback != null): - self.login_callback = login_callback - - self.login_with_google_play(google_play_auth_token, null, false) - -# Logs in with Google Play ID either linking existing user or as a Google Play ID only / new user -# Called internally by the different Google Play ID login functions -func login_with_google_play(google_play_auth_token, auth_token, link_to_existing_user): - - # Create an HTTP request node and connect its completion signal. - var http_request = HTTPRequest.new() - add_child(http_request) - http_request.request_completed.connect(self.sdk_login_callback) - - # Add the google play auth token to request - var params = "?" - params += "google_play_auth_token" + "=" + google_play_auth_token.uri_encode() - - # If we're linking to existing user, add the relevant parameters - if(auth_token != null and link_to_existing_user == true): - - print("Linking Google Play ID to existing user") - params += "&auth_token" + "=" + auth_token.uri_encode() - params += "&link_to_existing_user=Yes" - - print(login_endpoint+"/login-with-google-play"+params) - - # Perform a GET request to login as a new guest - var error = http_request.request(login_endpoint+"/login-with-google-play"+params) - - # In case of error, trigger the error callback - if error != OK: - self.login_error_callback.call("Error making request to login endpoint for login-with-google-play") - -# Called to link an existing authenticated user to a Facebook ID -func link_facebook_id_to_current_user(facebook_access_token, facebook_user_id, login_callback_facebook): - - # Set the login callback - if(login_callback_facebook != null): - self.login_callback = login_callback_facebook - - if(self.user_info == null): - self.login_error_callback.call("No user info, can't link existing user to Facebook ID") - return - - self.login_with_facebook(facebook_access_token, facebook_user_id, self.user_info.auth_token, true) - -# Called to create a new user with Facebook ID, or to login with existing user linked to Facebook -func login_with_facebook_access_token(facebook_access_token, facebook_user_id, login_callback): - - # Set the login callback - if(login_callback != null): - self.login_callback = login_callback - - self.login_with_facebook(facebook_access_token, facebook_user_id, null, false) - -# Logs in with Facebook ID either linking existing user or as a Facebook ID only / new user -# Called internally by the different Facebook ID login functions -func login_with_facebook(facebook_access_token, facebook_user_id, auth_token, link_to_existing_user): - - # Create an HTTP request node and connect its completion signal. - var http_request = HTTPRequest.new() - add_child(http_request) - http_request.request_completed.connect(self.sdk_login_callback) - - # Add the Facebook auth token and user ID to request - var params = "?" - params += "facebook_access_token" + "=" + facebook_access_token.uri_encode() - params += "&facebook_user_id" + "=" + facebook_user_id.uri_encode() - - # If we're linking to existing user, add the relevant parameters - if(auth_token != null and link_to_existing_user == true): - - print("Linking Facebook ID to existing user") - params += "&auth_token" + "=" + auth_token.uri_encode() - params += "&link_to_existing_user=Yes" - - print(login_endpoint+"/login-with-facebook"+params) - - # Perform a GET request to login as a new guest - var error = http_request.request(login_endpoint+"/login-with-facebook"+params) - - # In case of error, trigger the error callback - if error != OK: - self.login_error_callback.call("Error making request to login endpoint for login-with-facebook") - - - -# callback for login or refresh requests -func sdk_login_callback(result, response_code, headers, body): - var json_string = body.get_string_from_utf8() # Retrieve data - var json = JSON.new() - var error = json.parse(json_string) - - # trigger error if we didn't get a proper response code - if(response_code >= 400): - self.login_error_callback.call(json_string) - return - - # Check we got no error - if error == OK: - var data_received = json.data - # Check that we got a user_id (valid response) - if(!data_received.has("user_id")): - self.login_error_callback.call(json_string) - return - - # We got valid response, let's parse values to UserInfo object - #print(data_received) - if(self.user_info == null): - self.user_info = UserInfo.new() - self.user_info.user_id = data_received["user_id"] - if(data_received.has("guest_secret")): - self.user_info.guest_secret = data_received["guest_secret"] - if(data_received.has("auth_token")): - self.user_info.auth_token = data_received["auth_token"] - if(data_received.has("refresh_token")): - self.user_info.refresh_token = data_received["refresh_token"] - if(data_received.has("auth_token_expires_in")): - self.user_info.auth_token_expires_in = data_received["auth_token_expires_in"] - if(data_received.has("refresh_token_expires_in")): - self.user_info.refresh_token_expires_in = data_received["refresh_token_expires_in"] - if(data_received.has("steam_id")): - self.user_info.steam_id = data_received["steam_id"] - if(data_received.has("apple_id")): - self.user_info.apple_id = data_received["apple_id"] - if(data_received.has("google_play_id")): - self.user_info.google_play_id = data_received["google_play_id"] - if(data_received.has("facebook_id")): - self.user_info.facebook_id = data_received["facebook_id"] - - # Get the current UNIX time, and add the seconds for auth_token expiration - self.unix_time_for_auth_token_expiration = Time.get_unix_time_from_system() + self.user_info.auth_token_expires_in - #print(self.user_info) - - # Send the login info back to original requester - if(self.login_callback != null): - self.login_callback.call(self.user_info) - - else: - print("JSON Parse Error: ", json.get_error_message(), " in ", json_string, " at line ", json.get_error_line()) - # Trigger callback from client side - self.login_error_callback.call(json.get_error_message()) - -# Function to make an authenticated GET request to a backend API -# Called by your custom code to access backend functionality -func backend_get_request(url, resource, query_parameters, callback): - - if(self.user_info == null): - callback.call("Error: no user info set yet, login first") - return - - if(self.user_info.auth_token == ""): - callback.call("No auth token set yet, login first") - return - - # Add the query parameters to the request - if(query_parameters != null): - resource += "?" - for key in query_parameters: - resource += "&" + key + "=" + query_parameters[key].uri_encode() - - # Create an HTTP request node and connect its completion signal. - var http_request = HTTPRequest.new() - add_child(http_request) - http_request.request_completed.connect(callback) - - print(url+resource) - # Perform a GET request to login as a new guest - var error = http_request.request(url+resource, ["Authorization: " + self.user_info.auth_token], HTTPClient.METHOD_GET) - - if error != OK: - callback.call("Error with HTTP request") - - - -# Function to make an authenticated POST request to a backend API -# Called by your custom code to access backend functionality -func backend_post_request(url, resource, request_body, callback): - - if(self.user_info == null): - callback.call("Error: no user info set yet, login first") - return - - if(self.user_info.auth_token == ""): - callback.call("No auth token set yet, login first") - return - - # Create an HTTP request node and connect its completion signal. - var http_request = HTTPRequest.new() - add_child(http_request) - http_request.request_completed.connect(callback) - - print(url+resource) - # Perform a GET request to login as a new guest - var error = http_request.request(url+resource, ["Authorization: " + self.user_info.auth_token, "Content-Type: application/json"], HTTPClient.METHOD_POST, request_body) - - if error != OK: - callback.call("Error with HTTP request") diff --git a/GodotSample/BackendFeatures/AmazonGameLiftIntegration/AmazonGameLiftIntegration.gd b/GodotSample/BackendFeatures/AmazonGameLiftIntegration/AmazonGameLiftIntegration.gd index ced6e03..7a12b14 100644 --- a/GodotSample/BackendFeatures/AmazonGameLiftIntegration/AmazonGameLiftIntegration.gd +++ b/GodotSample/BackendFeatures/AmazonGameLiftIntegration/AmazonGameLiftIntegration.gd @@ -3,33 +3,21 @@ extends Node -# TODO: Add the login endpoint here -const login_endpoint = "https://YOUR_ENDPOINT/prod/" -# TODO: Add your Amazon GameLift backend component endpoint here -const gamelift_integration_backend_endpoint = "https://YOUR_ENDPOINT/prod" +# TODO: set your gamelift_integration_backend_endpoint on the AWSGameSDKBackend property page -var aws_game_sdk +@onready var aws_games_sdk_auth = get_node("AWSGameSDKAuth") +@onready var aws_games_sdk_backend = get_node("AWSGameSDKBackend") var ticket_id var total_tries = 0 # The amount of tries to get match status var latency_data # The JSON latency data for requesting matchmaking - -func save_login_data(user_id, guest_secret): - var file = FileAccess.open("user://save_game.dat", FileAccess.WRITE) - file.store_pascal_string(user_id) - file.store_pascal_string(guest_secret) - file = null - -func load_login_data(): - var file = FileAccess.open("user://save_game2.dat", FileAccess.READ) - if(file == null or file.get_length() == 0): - return null; - - var user_id = file.get_pascal_string() - var guest_secret = file.get_pascal_string() - return [user_id, guest_secret] - + +var logged_in: bool = false +var actions: Array +var current_action: String + + # Measures TCP latency to an endpoint with 3 requests (1 for establishing HTTPS, 2 for average TCP) func measure_tcp_latency(endpoint): @@ -124,7 +112,7 @@ func measure_latencies(): var latencydata = { "latencyInMs": { "us-east-1": region1latency, "us-west-2": region2latency, "eu-west-1": region3latency}} return JSON.stringify(latencydata) -#func _http_request_completed(result, response_code, headers, body): + # Called when the node enters the scene tree for the first time. func _ready(): @@ -135,46 +123,47 @@ func _ready(): print("Got latency data: " + self.latency_data) # Get the SDK and Init - self.aws_game_sdk = get_node("/root/AwsGameSdk") - self.aws_game_sdk.init(self.login_endpoint, self.on_login_error) + aws_games_sdk_auth.init() + aws_games_sdk_auth.aws_login_success.connect(_on_login_success) + aws_games_sdk_auth.aws_login_error.connect(_on_login_error) + aws_games_sdk_auth.aws_sdk_error.connect(_on_aws_sdk_error) + aws_games_sdk_backend.aws_backend_request_successful.connect(_on_gamelift_backend_post_response) + aws_games_sdk_backend.aws_sdk_error.connect(_on_aws_sdk_error) + print("calling login") + aws_games_sdk_auth.login() - # Try to load existing user info - var stored_user_info = self.load_login_data() - - # If we have stored user info, login with existing user - if(stored_user_info != null): - print("Logging in with existing user: " + stored_user_info[0]) - self.aws_game_sdk.login_as_guest(stored_user_info[0], stored_user_info[1], self.login_callback) - # Else we login as new user - else: - print("Logging in as new user") - self.aws_game_sdk.login_as_new_guest_user(self.login_callback) # Called on any login or token refresh failures -func on_login_error(message): +func _on_login_error(message): print("Login error: " + message) + +func _on_aws_sdk_error(error_text): + print("Error received from AWS SDK: ", error_text) + # Receives a UserInfo object after successful login -func login_callback(user_info): - print("Received login info.") - print(user_info) - - # Store the login info for future logins - self.save_login_data(user_info.user_id, user_info.guest_secret) - +func _on_login_success(): + print("Received login success") + logged_in = true + actions = ['create_ticket', 'get_ticket_data'] + current_action = actions.pop_front() + #you can inspect the user_info with this line + #print(aws_games_sdk_auth.user_info.to_string()) # Start matchmaking - self.aws_game_sdk.backend_post_request(self.gamelift_integration_backend_endpoint, "/request-matchmaking", - self.latency_data, self.matchmaking_request_callback) + self.aws_games_sdk_backend.gamelift_backend_post_request(aws_games_sdk_auth.get_auth_token(), + self.latency_data) + +func _on_gamelift_backend_post_response(body): + if current_action == 'create_ticket': + matchmaking_request_callback(body) + current_action = actions.pop_front() + elif current_action == 'get_ticket_data': + get_match_status_callback(body) # We need to use the exact format of the callback required for HTTPRequest -func matchmaking_request_callback(result, response_code, headers, body): - +func matchmaking_request_callback(body): var string_response = body.get_string_from_utf8() - if(response_code >= 400): - print("Error code " + str(response_code) + " Message: " + string_response) - return - print("Matchmaking request response: " + string_response) # Extract the ticket ID from the response @@ -187,10 +176,11 @@ func matchmaking_request_callback(result, response_code, headers, body): self.ticket_id = dict_response.data["TicketId"] print("Ticket id: " + self.ticket_id) # Call the get match status - self.aws_game_sdk.backend_get_request(self.gamelift_integration_backend_endpoint, "/get-match-status", { "ticketId" : self.ticket_id}, self.get_match_status_callback) + self.aws_game_sdk.gamelift_backend_get_request(aws_games_sdk_auth.get_auth_token(), + { "ticketId" : self.ticket_id}) # We need to use the exact format of the callback required for HTTPRequest -func get_match_status_callback(result, response_code, headers, body): +func get_match_status_callback(body): var string_response = body.get_string_from_utf8() @@ -213,14 +203,15 @@ func get_match_status_callback(result, response_code, headers, body): print("Couldn't get a valid response from matchmaking") else: await get_tree().create_timer(1.5).timeout - self.aws_game_sdk.backend_get_request(self.gamelift_integration_backend_endpoint, "/get-match-status", { "ticketId" : self.ticket_id}, self.get_match_status_callback) + #TODO: change this call + self.aws_game_sdk.gamelift_backend_get_request(aws_games_sdk_auth.get_auth_token(), + { "ticketId" : self.ticket_id}) elif ticket_status == "MatchmakingSucceeded": print("Matchmaking done, connect to server...") # TODO: Connect self.connect_to_server(dict_response.data["IpAddress"], dict_response.data["Port"], dict_response.data["PlayerSessionId"]) else: print("Matchmaking failed or timed out.") - self.total_tries += 1 func connect_to_server(host, port, player_session_id): diff --git a/GodotSample/BackendFeatures/AmazonGameLiftIntegration/AmazonGameLiftIntegration.gd.uid b/GodotSample/BackendFeatures/AmazonGameLiftIntegration/AmazonGameLiftIntegration.gd.uid new file mode 100644 index 0000000..cc208a4 --- /dev/null +++ b/GodotSample/BackendFeatures/AmazonGameLiftIntegration/AmazonGameLiftIntegration.gd.uid @@ -0,0 +1 @@ +uid://bj1uupijq8a0p diff --git a/GodotSample/BackendFeatures/AmazonGameLiftIntegration/AmazonGameLiftIntegration.tscn b/GodotSample/BackendFeatures/AmazonGameLiftIntegration/AmazonGameLiftIntegration.tscn index b9c3ac8..0cc9824 100644 --- a/GodotSample/BackendFeatures/AmazonGameLiftIntegration/AmazonGameLiftIntegration.tscn +++ b/GodotSample/BackendFeatures/AmazonGameLiftIntegration/AmazonGameLiftIntegration.tscn @@ -1,8 +1,16 @@ -[gd_scene load_steps=2 format=3 uid="uid://d2rykx55ylfax"] +[gd_scene load_steps=4 format=3 uid="uid://d2rykx55ylfax"] -[ext_resource type="Script" path="res://BackendFeatures/AmazonGameLiftIntegration/AmazonGameLiftIntegration.gd" id="1_f5d5y"] +[ext_resource type="Script" uid="uid://bj1uupijq8a0p" path="res://BackendFeatures/AmazonGameLiftIntegration/AmazonGameLiftIntegration.gd" id="1_f5d5y"] +[ext_resource type="Script" uid="uid://dxp7lgvaxrjry" path="res://addons/AWSGameSDK/scripts/AWSAuthorization.gd" id="2_6och4"] +[ext_resource type="Script" uid="uid://c1iyo651ktydf" path="res://addons/AWSGameSDK/scripts/AWSBackend.gd" id="3_tqorc"] [node name="Node2D" type="Node2D"] - -[node name="Test" type="Node" parent="."] script = ExtResource("1_f5d5y") + +[node name="AWSGameSDKAuth" type="Node" parent="."] +script = ExtResource("2_6och4") +metadata/_custom_type_script = "uid://dxp7lgvaxrjry" + +[node name="AWSGameSDKBackend" type="Node" parent="."] +script = ExtResource("3_tqorc") +metadata/_custom_type_script = "uid://c1iyo651ktydf" diff --git a/GodotSample/README.md b/GodotSample/README.md index add1cc8..95ccaae 100644 --- a/GodotSample/README.md +++ b/GodotSample/README.md @@ -2,48 +2,215 @@ The AWS Game Backend Framework Godot 4 SDK provides integrations to the custom identity component, managed refreshing of access tokens, helper methods for calling custom backend features, and samples for integrating with different game platforms. -# SDK Overview +# SDK Overview - Now a Godot Plugin -## Initializing the SDK +The AWS Game SDK has been updated to now be installed and used as a plugin in Godot 4.x. -The AWS Game SDK has to be configured in your Godot 4 project in _Project Settings -> Autoload_ with the name _AwsGameSdk_. +To begin using the plugin, copy the ./addons/AWSGameSDK directory to your projects ./addons directory. It should appear as it does in the picture below. -The Initializing and accessing the AWS Game SDK within Godot 4 (see the Godot 4 Integration Samples for sample code): +![Godot4 Folder Structure with AWS Games SDK Plugin](images_readme/folder_structure.png) +Next, go to Project -> Project Settings... from your menu and enable the AWS Games SDK plugin by checking the checkbox. + +![AWS for Games SDK Plugin enabled for a Godot project](images_readme/project_settings.png) + +## Adding nodes for the AWS for Games SDK to your scenes + +The AWS Game SDK contains two components that can be added to your projects. These are `AWSGameSDKAuth` and `AWSGameSDKBackend`. The AWSGameSDKAuth component allows you to login as guest, refresh your access token, and link accounts to Facebook, Apple, Google Play, and Steam via an API endpoint. The AWSGameSDKBackend component allows you to make calls to a backend endpoint to save and retrieve player data. You can add the necessary nodes to an appropriate scene for your project. The AWSGameSDKBackend requires the AWSGameSDKAuth component, as access tokens are required to save and retrieve data. These components can be added by adding child nodes to your scene. + +![Adding AWSGameSDK nodes to your Godot scene](images_readme/add_aws_nodes_to_your_scene.png) + +Once added, your scene tree should look similar to this. + +![A Godot scene with nodes added for AWSGameSDKAuth and AWSGameSDKBackend](images_readme/scene_tree_with_plugins.png) + +The plugin also uses signals. This removes the need to register callbacks and you can setup appropriate listeners to features enabled via the plugin. + +## Initializing the SDK and Login + +Initialization of the SDKs has been moved to property sheets to make it easier for developers to make calls. + +To complete setup of the AWSGameSDKAuth component, highlight the component in your scene tree and view the properties in the Inspector window. Update the `Login Endpoint` value with your API Gateway Login Endpoint, as shown below. + +![Setting your authentication endpoint property for the AWSGameSDKAuth component](images_readme/setting_auth_endpoint.png) + +To complete setup of the AWSGameSDKBackend component, highlight the component in your scene tree and view the properties in the Inspector window. Update the `Backend Endpoint` value with your API Gateway Backend Endpoint, as shown below. You can leave the URIs as default. + +![Setting your backend endpoint property for the AWSGameSDKAuth component](images_readme/setting_backend_endpoint.png) + +In your scene's code, you will need to set a variable for each of the SDK components you enable + +```python +@onready var aws_games_sdk_auth = get_node("AWSGameSDKAuth") +@onready var aws_games_sdk_backend = get_node("AWSGameSDKBackend") +``` + +Within the `_ready` function, connect your local functions to the signals from the SDK: + +```python +func _ready(): + # Get the SDK and Init + aws_games_sdk_auth.init() #initialize the Auth SDK + aws_games_sdk_auth.aws_login_success.connect(_on_login_success) #handle successful logins + aws_games_sdk_auth.aws_login_error.connect(_on_login_error) #handle login errors + aws_games_sdk_auth.aws_sdk_error.connect(_on_aws_sdk_error) #handle general SDK errors + aws_games_sdk_backend.aws_backend_request_successful.connect(_on_backend_request_success) #handle successful backend requests + aws_games_sdk_backend.aws_sdk_error.connect(_on_aws_sdk_error) #handle errors from backend requests +``` + +To begin the login process, call login on the AWSGameSDKAuth component, as such: + ```python -# Get the SDK and Init -self.aws_game_sdk = get_node("/root/AwsGameSdk") -self.aws_game_sdk.init(self.login_endpoint, self.on_login_error) + aws_games_sdk_auth.login() +``` + +Errors for login can be managed with similar functions to those below: + +``` +func _on_login_error(message): + print("Login error: " + message) + + +# Receives a UserInfo object after successful login +func _on_login_success(): + print("Received login success") +``` + +## Set and Get Data from your custom backend + +Setting and getting data to and from your backend uses the AWSGameSDKBackend with the `backend_set_request` and `backend_get_request` functions. Both of these functions use HTTP GET requests. To set data, use the following syntax: + +```python + aws_games_sdk_backend.backend_set_request(aws_games_sdk_auth.get_auth_token(), {"player_name" : "John Doe"}) +``` + +In this case, the auth_token to make the call is retrieved from the aws_games_sdk_auth component and the `player_name` is set to `John Doe`. Multiple values can be set in a single dictionary request. + +To retrieve data, use the following syntax: + +```python + aws_games_sdk_backend.backend_get_request(aws_games_sdk_auth.get_auth_token()) +``` + +This call only requires the auth_token from the aws_games_sdk_auth component. + +Success and errors of both calls are handled through signal connections that were added to the `_ready` function above. A sample success function is as follows: + +```python +func _on_backend_request_success(): + print("Backend request successful") + print("Data returned from action: ", aws_games_sdk_backend.get_response_data()) ``` ## SDK Public API -The public API for the SDK includes the following methods. Most of them will require you to provide a callback for results (see the Godot 4 Integration Samples for sample code): +The public API for the AWSGameSDKAuth component includes the following methods. Most of them will require you to provide a callback for results (see the Godot 4 Integration Samples for sample code): ```text -func init(login_endpoint, login_error_callback) -func login_as_new_guest_user(login_callback) -func login_as_guest(user_id, guest_secret, login_callback) -func login_with_refresh_token(refresh_token, login_callback = null) -func link_steam_id_to_current_user(steam_token, login_callback_steam) -func login_with_steam_token(steam_token, login_callback) -func link_apple_id_to_current_user(apple_auth_token, login_callback_apple) -func login_with_apple_id_token(apple_auth_token, login_callback) -func link_google_play_id_to_current_user(google_play_auth_token, login_callback_google) -func login_with_google_play_token(google_play_auth_token, login_callback) -func link_facebook_id_to_current_user(facebook_access_token, facebook_user_id, login_callback_facebook) -func login_with_facebook_access_token(facebook_access_token, facebook_user_id, login_callback) -func backend_get_request(url, resource, query_parameters, callback) -func backend_post_request(url, resource, request_body, callback): +func init() +func login() +func login_with_refresh_token() +func get_auth_token() String +func link_steam_id_to_current_user(steam_token) +func login_with_steam_token(steam_token) +func link_apple_id_to_current_user(apple_auth_token) +func login_with_apple_id_token(apple_auth_token) +func link_google_play_id_to_current_user(google_play_auth_token) +func login_with_google_play_token(google_play_auth_token) +func link_facebook_id_to_current_user(facebook_access_token, facebook_user_id) +func login_with_facebook_access_token(facebook_access_token, facebook_user_id) +``` + +Supported signals are: + +```text +aws_login_success: Emitted when login is successful. +aws_login_error(message: String): Emitted when login fails. Provides an error message.aws_sdk_error(message: String): Emitted when a general SDK error occurs. Provides an error message. +steam_link: Emitted when Steam account linking is successful. +steam_login: Emitted when Steam login is successful. +fb_link: Emitted when Facebook account linking is successful. +fb_login: Emitted when Facebook login is successful. +apple_link: Emitted when Apple account linking is successful. +apple_login: Emitted when Apple login is successful. +goog_link: Emitted when Google Play account linking is successful. +goog_login: Emitted when Google Play login is successful. ``` -## Adding the SDK to an existing project +Parameters for the plugin are: -To add the SDK to an existing project: +```text +login_endpoint # Endpoint for custom identity component +``` -1. Drag and drop the folder `AWSGameSDK` to your Godot 4 project -2. Open _Project Settings -> Autoload_, select the script `AWSGameSDK.gd` with the directory search and select _Open_. Make sure the name is _AwsGameSdk_ and select _Add_. -3. Integrate with the SDK from your custom code (see Godot 4 Integration Samples for example integrations) +The public API for the AWSGameSDKBackend component includes the following methods. + +```python +func backend_get_request(auth_token) +func backend_set_request(auth_token, query_parameters) +func backend_post_request(auth_token, request_body) +func get_response_data() String +func gamelift_backend_post_request(auth_token, request_body) +func gamelift_backend_get_request(auth_token, request_body) +``` + +Supported signals are: + +```text +aws_backend_request_successful: Emitted when a backend request completes successfully. +aws_sdk_error(message: String): Emitted when a backend request fails. Provides an error message. +``` + +Parameters for the plugin are: + +```text +backend_endpoint: Endpoint for backend operations +gamelift_backend_endpoint: Endpoint for Amazon GameLift Backend +get_player_data_uri: Backend URI to retrieve player data +set_player_data_uri: Backend URI to set player data +post_player_data_uri: Backend URI to POST data to - this is not yet used +gamelift_request_match_uri: Amazon GameLift URI to request matchmaking +gamelift_match_status_uri: Amazon GameLift URI to get matchmaking status +``` + +# Migrating from prior version + +This section describes the actions you will need to take if you had integrated the prior version of this sample with your Godot project. You should take these steps prior to integrating the new plugin. Before you make any changes, it is important to backup your project in case any errors are made. + +## Migrating from Callback-based to Signal-based Approach + +When migrating from the previous version's callback-based approach to the new signal-based approach, you'll need to replace callback function registrations with signal connections: + +### Old (Callback-based): +```python +self.aws_game_sdk.login_as_new_guest_user(self.login_callback) +``` + +### New (Signal-based) +```python +@onready var aws_games_sdk_auth = get_node("AWSGameSDKAuth") + +func _ready(): + aws_games_sdk_auth.aws_login_success.connect(_on_login_success) + aws_games_sdk_auth.aws_login_error.connect(_on_login_error) + aws_games_sdk_auth.login() + +func _on_login_success(): + print("Login successful!") + +func _on_login_error(message): + print("Login failed: " + message) +``` + + +## Unregister the AWSGameSDK Autoload +1. Open _Project Settings -> Autoload_ and select the "Globals" tab. +2. Under the "Autoload" tab, uncheck the _AWSGameSDK_ and click "Close." + +## Remove the AWSGameSDK folder from your project +This step assumes you have not added any folders, functionality, or code to the proir AWSGameSDK or the folder thereof. Highlight the folder in the File System viewer in your Godot project, right click, and choose "Delete." + +## Remove the code used to integrate the AWSGameSDK from your code +This code should be highlighted in your project via the Godot IDE. The new plug-in code will operate very similar to the prior version, so it will be good to denote these areas with a comment, such as `#TODO: Add AWSGameSDK Plugin Code Here` while removing the code. # Godot 4 Integration Samples @@ -96,6 +263,3 @@ func link_google_play_id_to_current_user(google_play_auth_token, login_callback_ func login_with_google_play_token(google_play_auth_token, login_callback) ``` - - - diff --git a/GodotSample/Samples/FacebookLogin/FacebookLogin.gd b/GodotSample/Samples/FacebookLogin/FacebookLogin.gd index a107bb4..1380a62 100644 --- a/GodotSample/Samples/FacebookLogin/FacebookLogin.gd +++ b/GodotSample/Samples/FacebookLogin/FacebookLogin.gd @@ -3,46 +3,56 @@ extends Node -# TODO: Add the login endpoint here -const login_endpoint = "https://YOURENDPOINT.execute-api.us-east-1.amazonaws.com/prod/" -var aws_game_sdk +@onready var aws_games_sdk_auth = get_node("AWSGameSDKAuth") + # Called when the node enters the scene tree for the first time. func _ready(): - # Get the SDK and Init - self.aws_game_sdk = get_node("/root/AwsGameSdk") - self.aws_game_sdk.init(self.login_endpoint, self.on_login_error) - - # Log in as new guest user first - self.aws_game_sdk.login_as_new_guest_user(self.login_as_guest_callback) + #init sdk and setup signal listeners + aws_games_sdk_auth.init() + aws_games_sdk_auth.aws_login_success.connect(_on_login_success) + aws_games_sdk_auth.aws_login_error.connect(_on_login_error) + aws_games_sdk_auth.fb_link.connect(on_link_facebook_id_response) + aws_games_sdk_auth.fb_login.connect(on_login_with_facebook_id_response) + aws_games_sdk_auth.aws_sdk_error.connect(_on_aws_sdk_error) + #login + aws_games_sdk_auth.login() + # Called on any login or token refresh failures -func on_login_error(message): +func _on_login_error(message): print("Login error: " + message) + +func _on_aws_sdk_error(message): + print("AWS SDK error: ", message) + + # Receives a UserInfo object after successful guest login -func login_as_guest_callback(user_info): +func _on_login_success(): print("Received guest login info.") - print(user_info) + print(aws_games_sdk_auth.user_info) # Try linking Facebook ID to existing user # NOTE: You'll need to use a community Facebook integration such as https://github.com/DrMoriarty/godot-facebook # Once you've logged in with Facebook, send the access_token and user_id here - self.aws_game_sdk.link_facebook_id_to_current_user("AcceessTokenHere", "UserIdHere", self.on_link_facebook_id_response) + aws_games_sdk_auth.link_facebook_id_to_current_user("AcceessTokenHere", "UserIdHere") + -func on_link_facebook_id_response(user_info): +func on_link_facebook_id_response(): print("Received Facebook ID linking info") - print(user_info) + print(aws_games_sdk_auth.user_info.to_string()) # Let's now try to login with Facebook acccess token directly to access the same user - self.aws_game_sdk.login_with_facebook_access_token("AccessTokenHere", "UserIdHere", self.on_login_with_facebook_response) + aws_games_sdk_auth.login_with_facebook_access_token("AccessTokenHere", "UserIdHere") -func on_login_with_facebook_response(user_info): +func on_login_with_facebook_id_response(): print("Received Facebook ID login info") - print(user_info) + print(aws_games_sdk_auth.user_info.to_string()) + # Called every frame. 'delta' is the elapsed time since the previous frame. func _process(delta): diff --git a/GodotSample/Samples/FacebookLogin/FacebookLogin.gd.uid b/GodotSample/Samples/FacebookLogin/FacebookLogin.gd.uid new file mode 100644 index 0000000..8b506aa --- /dev/null +++ b/GodotSample/Samples/FacebookLogin/FacebookLogin.gd.uid @@ -0,0 +1 @@ +uid://cno7al5g886tr diff --git a/GodotSample/Samples/FacebookLogin/FacebookLogin.tscn b/GodotSample/Samples/FacebookLogin/FacebookLogin.tscn index 47b694b..bd4e2a8 100644 --- a/GodotSample/Samples/FacebookLogin/FacebookLogin.tscn +++ b/GodotSample/Samples/FacebookLogin/FacebookLogin.tscn @@ -1,8 +1,11 @@ -[gd_scene load_steps=2 format=3 uid="uid://qknp5wwmrjlu"] +[gd_scene load_steps=3 format=3 uid="uid://qknp5wwmrjlu"] -[ext_resource type="Script" path="res://Samples/FacebookLogin/FacebookLogin.gd" id="1_gobne"] +[ext_resource type="Script" uid="uid://cno7al5g886tr" path="res://Samples/FacebookLogin/FacebookLogin.gd" id="1_gobne"] +[ext_resource type="Script" uid="uid://dxp7lgvaxrjry" path="res://addons/AWSGameSDK/scripts/AWSAuthorization.gd" id="2_jnwcp"] [node name="Node2D" type="Node2D"] - -[node name="Test" type="Node" parent="."] script = ExtResource("1_gobne") + +[node name="AWSGameSDKAuth" type="Node" parent="."] +script = ExtResource("2_jnwcp") +metadata/_custom_type_script = "uid://dxp7lgvaxrjry" diff --git a/GodotSample/Samples/GuestIdentityAndRestApiBackend/GuestIdentityAndRestApiBackend.gd b/GodotSample/Samples/GuestIdentityAndRestApiBackend/GuestIdentityAndRestApiBackend.gd index c2c6090..f04be83 100644 --- a/GodotSample/Samples/GuestIdentityAndRestApiBackend/GuestIdentityAndRestApiBackend.gd +++ b/GodotSample/Samples/GuestIdentityAndRestApiBackend/GuestIdentityAndRestApiBackend.gd @@ -1,87 +1,60 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 -extends Node +extends Node2D -# TODO: Add the login endpoint here -const login_endpoint = "https://YOURENDPOINTHERE/prod/" -# TODO: Add your backend component endpoint here -const backend_endpoint = "https://YOURENDPOINTHERE/prod" +@onready var aws_games_sdk_auth = get_node("AWSGameSDKAuth") +@onready var aws_games_sdk_backend = get_node("AWSGameSDKBackend") -var aws_game_sdk - -func save_login_data(user_id, guest_secret): - var file = FileAccess.open("user://save_game.dat", FileAccess.WRITE) - file.store_pascal_string(user_id) - file.store_pascal_string(guest_secret) - file = null - -func load_login_data(): - var file = FileAccess.open("user://save_game.dat", FileAccess.READ) - if(file == null or file.get_length() == 0): - return null; - - var user_id = file.get_pascal_string() - var guest_secret = file.get_pascal_string() - return [user_id, guest_secret] +var logged_in: bool = false +var actions: Array +var current_action: String # Called when the node enters the scene tree for the first time. func _ready(): - # Get the SDK and Init - self.aws_game_sdk = get_node("/root/AwsGameSdk") - self.aws_game_sdk.init(self.login_endpoint, self.on_login_error) - - # Try to load existing user info - var stored_user_info = self.load_login_data() - - # If we have stored user info, login with existing user - if(stored_user_info != null): - print("Logging in with existing user: " + stored_user_info[0]) - self.aws_game_sdk.login_as_guest(stored_user_info[0], stored_user_info[1], self.login_callback) - # Else we login as new user - else: - self.aws_game_sdk.login_as_new_guest_user(self.login_callback) + aws_games_sdk_auth.init() + aws_games_sdk_auth.aws_login_success.connect(_on_login_success) + aws_games_sdk_auth.aws_login_error.connect(_on_login_error) + aws_games_sdk_auth.aws_sdk_error.connect(_on_aws_sdk_error) + aws_games_sdk_backend.aws_backend_request_successful.connect(_on_backend_request_success) + aws_games_sdk_backend.aws_sdk_error.connect(_on_aws_sdk_error) + print("calling login") + aws_games_sdk_auth.login() # Called on any login or token refresh failures -func on_login_error(message): +func _on_login_error(message): print("Login error: " + message) + # Receives a UserInfo object after successful login -func login_callback(user_info): - print("Received login info.") - print(user_info) - - # Store the login info for future logins - self.save_login_data(user_info.user_id, user_info.guest_secret) - - # Try setting player data - self.aws_game_sdk.backend_get_request(self.backend_endpoint, "/set-player-data", {"player_name" : "John Doe"}, self.set_player_data_callback) - -# We need to use the exact format of the callback required for HTTPRequest -func set_player_data_callback(result, response_code, headers, body): - - var string_response = body.get_string_from_utf8() - - if(response_code >= 400): - print("Error code " + str(response_code) + " Message: " + string_response) - return - - print("Successfully set player data: " + string_response) - - # Test getting the same date next - self.aws_game_sdk.backend_get_request(self.backend_endpoint, "/get-player-data", null, self.get_player_data_callback) +func _on_login_success(): + print("Received login success") + logged_in = true + actions = ['setdata', 'getdata'] + current_action = actions.pop_front() + #you can inspect the user_info with this line + #print(aws_games_sdk_auth.user_info.to_string()) + + +func _on_backend_request_success(): + print("Backend request successful") + print("Data returned from action: ", aws_games_sdk_backend.get_response_data()) + if len(actions) > 0: + current_action = actions.pop_front() -# We need to use the exact format of the callback required for HTTPRequest -func get_player_data_callback(result, response_code, headers, body): - - var string_response = body.get_string_from_utf8() - - if(response_code >= 400): - print("Error code " + str(response_code) + " Message: " + string_response) - return - - print("Success received player data: " + string_response) # Called every frame. 'delta' is the elapsed time since the previous frame. func _process(delta): - pass + if logged_in and current_action != "": + if current_action == "setdata": + current_action = "" + print("setting player data") + aws_games_sdk_backend.backend_set_request(aws_games_sdk_auth.get_auth_token(), {"player_name" : "John Doe"}) + if current_action == "getdata": + current_action = "" + print("getting player data") + aws_games_sdk_backend.backend_get_request(aws_games_sdk_auth.get_auth_token()) + return + +func _on_aws_sdk_error(error_text): + print("Error received from AWS SDK: ", error_text) diff --git a/GodotSample/Samples/GuestIdentityAndRestApiBackend/GuestIdentityAndRestApiBackend.gd.uid b/GodotSample/Samples/GuestIdentityAndRestApiBackend/GuestIdentityAndRestApiBackend.gd.uid new file mode 100644 index 0000000..1b147f8 --- /dev/null +++ b/GodotSample/Samples/GuestIdentityAndRestApiBackend/GuestIdentityAndRestApiBackend.gd.uid @@ -0,0 +1 @@ +uid://bf2280tko4u2w diff --git a/GodotSample/Samples/GuestIdentityAndRestApiBackend/GuestIdentityAndRestApiBackend.tscn b/GodotSample/Samples/GuestIdentityAndRestApiBackend/GuestIdentityAndRestApiBackend.tscn index 55d8fb9..6d3d42b 100644 --- a/GodotSample/Samples/GuestIdentityAndRestApiBackend/GuestIdentityAndRestApiBackend.tscn +++ b/GodotSample/Samples/GuestIdentityAndRestApiBackend/GuestIdentityAndRestApiBackend.tscn @@ -1,8 +1,18 @@ -[gd_scene load_steps=2 format=3 uid="uid://dpdmqpyoy6pg"] +[gd_scene load_steps=4 format=3 uid="uid://dpdmqpyoy6pg"] -[ext_resource type="Script" path="res://Samples/GuestIdentityAndRestApiBackend/GuestIdentityAndRestApiBackend.gd" id="1_xv65c"] +[ext_resource type="Script" uid="uid://bf2280tko4u2w" path="res://Samples/GuestIdentityAndRestApiBackend/GuestIdentityAndRestApiBackend.gd" id="1_rr0aj"] +[ext_resource type="Script" uid="uid://dxp7lgvaxrjry" path="res://addons/AWSGameSDK/scripts/AWSAuthorization.gd" id="2_fx0df"] +[ext_resource type="Script" uid="uid://c1iyo651ktydf" path="res://addons/AWSGameSDK/scripts/AWSBackend.gd" id="3_rr0aj"] [node name="Node2D" type="Node2D"] +script = ExtResource("1_rr0aj") -[node name="Test" type="Node" parent="."] -script = ExtResource("1_xv65c") +[node name="AWSGameSDKAuth" type="Node" parent="."] +script = ExtResource("2_fx0df") +login_endpoint = "https://xxxxxxxxx.execute-api.us-west-2.amazonaws.com/prod" +metadata/_custom_type_script = "uid://dxp7lgvaxrjry" + +[node name="AWSGameSDKBackend" type="Node" parent="."] +script = ExtResource("3_rr0aj") +backend_endpoint = "https://xxxxxxxx.execute-api.us-west-2.amazonaws.com/prod" +metadata/_custom_type_script = "uid://c1iyo651ktydf" diff --git a/GodotSample/Samples/SteamIdLogin/SteamIdLogin.gd b/GodotSample/Samples/SteamIdLogin/SteamIdLogin.gd index b0006cd..874fd58 100644 --- a/GodotSample/Samples/SteamIdLogin/SteamIdLogin.gd +++ b/GodotSample/Samples/SteamIdLogin/SteamIdLogin.gd @@ -3,47 +3,57 @@ extends Node -# TODO: Add the login endpoint here -const login_endpoint = "https://YOURENDPOINTHERE/prod/" +@onready var aws_games_sdk_auth = get_node("AWSGameSDKAuth") -var aws_game_sdk # Called when the node enters the scene tree for the first time. func _ready(): - - # Get the SDK and Init - self.aws_game_sdk = get_node("/root/AwsGameSdk") - self.aws_game_sdk.init(self.login_endpoint, self.on_login_error) - - # Log in as new guest user first - self.aws_game_sdk.login_as_new_guest_user(self.login_as_guest_callback) + + #init sdk and setup signal listeners + aws_games_sdk_auth.init() + aws_games_sdk_auth.aws_login_success.connect(_on_login_success) + aws_games_sdk_auth.aws_login_error.connect(_on_login_error) + aws_games_sdk_auth.steam_link.connect(on_link_steam_id_response) + aws_games_sdk_auth.steam_login.connect(on_login_with_steam_id_response) + aws_games_sdk_auth.aws_sdk_error.connect(_on_aws_sdk_error) + #login + aws_games_sdk_auth.login() + # Called on any login or token refresh failures -func on_login_error(message): - print("Login error: " + message) +func _on_login_error(message): + print("Login error: ", message) + + +func _on_aws_sdk_error(message): + print("AWS SDK error: ", message) + # Receives a UserInfo object after successful guest login -func login_as_guest_callback(user_info): +func _on_login_success(): print("Received guest login info.") - print(user_info) + print(aws_games_sdk_auth.user_info.to_string()) # Try linking Steam ID to existing user # NOTE: You need to use the Godot Steamworks SDK (https://godotsteam.com/) to integrate with Steam # Use the GetAuthTicketForWebAPI to get the steam auth token (https://godotsteam.com/functions/users/#getauthticketforwebapi) - self.aws_game_sdk.link_steam_id_to_current_user("YourTokenHere", self.on_link_steam_id_response) + aws_games_sdk_auth.link_steam_id_to_current_user("YourTokenHere") + -func on_link_steam_id_response(user_info): +func on_link_steam_id_response(): print("Received steam ID linking info") - print(user_info) + print(aws_games_sdk_auth.user_info.to_string()) # Let's now try to login with Steam token directly to access the same user # NOTE: You need to use the Godot Steamworks SDK (https://godotsteam.com/) to integrate with Steam # Use the GetAuthTicketForWebAPI to get the steam auth token (https://godotsteam.com/functions/users/#getauthticketforwebapi) - self.aws_game_sdk.login_with_steam_token("YourTokenHere", self.on_login_with_steam_response) + aws_games_sdk_auth.login_with_steam_token("YourTokenHere") + -func on_login_with_steam_response(user_info): +func on_login_with_steam_id_response(): print("Received steam ID login info") - print(user_info) + print(aws_games_sdk_auth.user_info.to_string()) + # Called every frame. 'delta' is the elapsed time since the previous frame. func _process(delta): diff --git a/GodotSample/Samples/SteamIdLogin/SteamIdLogin.gd.uid b/GodotSample/Samples/SteamIdLogin/SteamIdLogin.gd.uid new file mode 100644 index 0000000..28cdc86 --- /dev/null +++ b/GodotSample/Samples/SteamIdLogin/SteamIdLogin.gd.uid @@ -0,0 +1 @@ +uid://ba368vjxpctom diff --git a/GodotSample/Samples/SteamIdLogin/SteamIdLogin.tscn b/GodotSample/Samples/SteamIdLogin/SteamIdLogin.tscn index 90e57ee..b4221db 100644 --- a/GodotSample/Samples/SteamIdLogin/SteamIdLogin.tscn +++ b/GodotSample/Samples/SteamIdLogin/SteamIdLogin.tscn @@ -1,8 +1,12 @@ -[gd_scene load_steps=2 format=3 uid="uid://b8blf3o26rhc3"] +[gd_scene load_steps=3 format=3 uid="uid://b8blf3o26rhc3"] -[ext_resource type="Script" path="res://Samples/SteamIdLogin/SteamIdLogin.gd" id="1_kitbl"] +[ext_resource type="Script" uid="uid://ba368vjxpctom" path="res://Samples/SteamIdLogin/SteamIdLogin.gd" id="1_kitbl"] +[ext_resource type="Script" uid="uid://dxp7lgvaxrjry" path="res://addons/AWSGameSDK/scripts/AWSAuthorization.gd" id="2_nmo6t"] [node name="Node2D" type="Node2D"] - -[node name="Test" type="Node" parent="."] +visible = false script = ExtResource("1_kitbl") + +[node name="AWSGameSDKAuth" type="Node" parent="."] +script = ExtResource("2_nmo6t") +metadata/_custom_type_script = "uid://dxp7lgvaxrjry" diff --git a/GodotSample/addons/AWSGameSDK/AWSGameSDK.gd b/GodotSample/addons/AWSGameSDK/AWSGameSDK.gd new file mode 100644 index 0000000..c74defd --- /dev/null +++ b/GodotSample/addons/AWSGameSDK/AWSGameSDK.gd @@ -0,0 +1,22 @@ +@tool +extends EditorPlugin + +const AUTOLOAD_NAME_SDK = "AWSGameSDK" +const AUTOLOAD_NAME_AUTH = "AWSGameSDKAuth" + +static var AWSGameSDKAuth = preload("./scripts/AWSAuthorization.gd") +static var AWSGameSDKBackend = preload("./scripts/AWSBackend.gd") + + +func _enable_plugin(): + add_autoload_singleton(AUTOLOAD_NAME_AUTH, "res://addons/AWSGameSDK/AWSAuthorization.gd") + add_autoload_singleton(AUTOLOAD_NAME_SDK, "res://addons/AWSGameSDK/AWSBackend.gd") + + +func _disable_plugin(): + remove_autoload_singleton(AUTOLOAD_NAME_SDK) + remove_autoload_singleton(AUTOLOAD_NAME_AUTH) + + +func _init() -> void: + pass diff --git a/GodotSample/addons/AWSGameSDK/AWSGameSDK.gd.uid b/GodotSample/addons/AWSGameSDK/AWSGameSDK.gd.uid new file mode 100644 index 0000000..f7270c3 --- /dev/null +++ b/GodotSample/addons/AWSGameSDK/AWSGameSDK.gd.uid @@ -0,0 +1 @@ +uid://g4ac3hmyqh5o diff --git a/GodotSample/addons/AWSGameSDK/plugin.cfg b/GodotSample/addons/AWSGameSDK/plugin.cfg new file mode 100644 index 0000000..46d4e9a --- /dev/null +++ b/GodotSample/addons/AWSGameSDK/plugin.cfg @@ -0,0 +1,20 @@ +[plugin] +name="AWSGameSDK" +description="AWS Game SDK for Godot v.0.1.0. This plugin allows developers to offer players the + ability to login to an AWS API Gateway endpoint as guests and link their Facebook, Apple, Steam, + and/or Google Play accounts to the guest identity. + + Also, using the data management backend, developers can read and write data to the cloud, developing + backend features. + + This plugin requires the deployment of the custom identity components and backend components from + https://github.com/aws-solutions-library-samples/guidance-for-custom-game-backend-hosting-on-aws. + + Full documentation of this plugin and related services are at: + https://github.com/aws-solutions-library-samples/guidance-for-custom-game-backend-hosting-on-aws + + This plugin and code are copyrighted by Amazon Web Services and released under the + MIT-0 license." +author="Amazon Web Services" +version="1.0.0" +script="AWSGameSDK.gd" diff --git a/GodotSample/addons/AWSGameSDK/scripts/AWSAuthorization.gd b/GodotSample/addons/AWSGameSDK/scripts/AWSAuthorization.gd new file mode 100644 index 0000000..0384365 --- /dev/null +++ b/GodotSample/addons/AWSGameSDK/scripts/AWSAuthorization.gd @@ -0,0 +1,315 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +extends Node +class_name AWSGameSDKAuth + +signal aws_login_success +signal aws_login_error +signal aws_sdk_error +signal steam_link +signal steam_login +signal fb_link +signal fb_login +signal apple_link +signal apple_login +signal goog_link +signal goog_login + +#only used for internal signalling for the SDK +signal _new_login_succesful + +@export var login_endpoint: String = "" # Endpoint for custom identity component + +const UserInfo = preload("user_info.gd") + +var user_info: UserInfo = null # User info for the logged in user +var refresh_timer: Timer #used to check on refresh for access token +var http_request: HTTPRequest +var error_string: String = "" +var new_user_login: bool = false + +var linking: Dictionary = { "steam_link": false, "steam_login": false, "fb_link": false, + "fb_login": false, "apple_link": false, "apple_login": false, + "goog_link": false, "goog_login": false} + +# Called when the node enters the scene tree for the first time. +func _ready(): + refresh_timer = Timer.new() + refresh_timer.one_shot = true + add_child(refresh_timer) + refresh_timer.timeout.connect(_on_timer_timeout) + pass + + +func _save_login_data(): + var file = FileAccess.open("user://save_game.dat", FileAccess.WRITE) + file.store_pascal_string(user_info.user_id) + file.store_pascal_string(user_info.guest_secret) + file.close() + file = null + + +func _load_login_data(): + if user_info == null: + aws_sdk_error.emit("Please call init instead of _load_login_data()") + var file = FileAccess.open("user://save_game.dat", FileAccess.READ) + if file == null: + return null + if file.get_length() == 0: + file.close() + return null + user_info.user_id = file.get_pascal_string() + user_info.guest_secret = file.get_pascal_string() + file.close() + +func get_auth_token() -> String: + if user_info != null: + return user_info.auth_token + else: + return "" + + +func login(): + if user_info == null: + aws_login_error.emit("user_info not initialized. Please call init() before login.") + return + if user_info.user_id == "" or user_info.guest_secret == "": + _login_as_new_guest() + else: + _login_as_guest() + + +func _on_timer_timeout(): + login_with_refresh_token(user_info.refresh_token) + + +func init(): + _new_login_succesful.connect(_save_login_data) + user_info = UserInfo.new() + _load_login_data() + print("AWS Game SDK initialized") + + +func _make_auth_http_request(url: String, method: HTTPClient.Method = HTTPClient.METHOD_GET, form_data: Dictionary = {}) -> void: + # Create an HTTP request node and connect its completion signal. + http_request = HTTPRequest.new() + add_child(http_request) + http_request.request_completed.connect(_auth_request_completed) + + var error = http_request.request(url) + #send signal on failure + if error != OK: + print(error) + aws_login_error.emit("Error making request to login endpoint") + + +func _auth_request_completed(result, response_code, headers, body): + http_request.queue_free() + var json_string = body.get_string_from_utf8() # Retrieve data + var json = JSON.new() + var error = json.parse(json_string) + + # trigger error if we didn't get a proper response code + if(response_code >= 400): + error_string = json_string + aws_login_error.emit("HTTP Error: " + str(response_code) + " - " + error_string) + return + + # Check we got no error + if error == OK: + var data_received = json.data + # Check that we got a user_id (valid response) + if(!data_received.has("user_id")): + error_string = json_string + return + + # We got valid response, let's parse values to UserInfo object + if(user_info == null): + user_info = UserInfo.new() + user_info.user_id = data_received["user_id"] + if(data_received.has("guest_secret")): + user_info.guest_secret = data_received["guest_secret"] + if(data_received.has("auth_token")): + user_info.auth_token = data_received["auth_token"] + if(data_received.has("refresh_token")): + user_info.refresh_token = data_received["refresh_token"] + if(data_received.has("auth_token_expires_in")): + user_info.auth_token_expires_in = data_received["auth_token_expires_in"] + if(data_received.has("refresh_token_expires_in")): + user_info.refresh_token_expires_in = data_received["refresh_token_expires_in"] + if(data_received.has("steam_id")): + user_info.steam_id = data_received["steam_id"] + if(data_received.has("apple_id")): + user_info.apple_id = data_received["apple_id"] + if(data_received.has("google_play_id")): + user_info.google_play_id = data_received["google_play_id"] + if(data_received.has("facebook_id")): + user_info.facebook_id = data_received["facebook_id"] + if new_user_login: + _new_login_succesful.emit() + new_user_login = false + # Send the appropriate signal back for login succes + var signal_emitted = false + for key in linking: + if linking[key]: + linking[key] = false + signal_emitted = true + emit_signal(key) + if !signal_emitted: + refresh_timer.wait_time = user_info.auth_token_expires_in - 15 + # Set the token refresh timer for 15 seconds before token expiration + refresh_timer.start() + aws_login_success.emit() + + + + else: + print("JSON Parse Error: ", json.get_error_message(), " in ", json_string, " at line ", json.get_error_line()) + # Trigger callback from client side + aws_login_error.emit(json.get_error_message()) + error_string = json.get_error_message() + + +# Logs in as a new guest user +func _login_as_new_guest(): + new_user_login = true + # Perform a GET request to login as a new guest + _make_auth_http_request(login_endpoint+"/login-as-guest") + + +# Logs in with existing user +func _login_as_guest(): + # Add the query parameters to the request + var params = login_endpoint + "/login-as-guest?" + \ + "user_id" + "=" + user_info.user_id.uri_encode() + \ + "&guest_secret" + "=" + user_info.guest_secret.uri_encode() + # Perform a GET request to login as a new guest + _make_auth_http_request(params) + + +# Refresh the access token with a refresh token +func login_with_refresh_token(refresh_token): + print('refreshing token') + var params = login_endpoint + "/refresh-access-token?" + \ + "refresh_token" + "=" + refresh_token.uri_encode() + _make_auth_http_request(params) + + +# Called to link an existing authenticated user to a Steam ID +func link_steam_id_to_current_user(steam_token): + if user_info == null: + aws_sdk_error.emit("No user info, can't link existing user to Steam ID") + return + linking["steam_link"] = true + _login_with_steam(steam_token, user_info.auth_token, true) + + +# Called to create a new user with steam ID, or to login with existing user linked to Steam ID +func login_with_steam_token(steam_token): + # Set the login callback + linking["steam_login"] = true + _login_with_steam(steam_token, null, false) + + +# Logs in with steam either linking existing user or as a steam only / new user +# Called internally by the different Steam login functions +func _login_with_steam(steam_token, auth_token, link_to_existing_user): + # Add the steam token to request + var params = login_endpoint+"/login-with-steam?" + \ + "steam_auth_token=" + steam_token.uri_encode() + # If we're linking to existing user, add the relevant parameters + if auth_token != null and link_to_existing_user == true: + print("Linking Steam ID to existing user") + params += "&auth_token" + "=" + auth_token.uri_encode() + \ + "&link_to_existing_user=Yes" + _make_auth_http_request(params) + + +# Called to link an existing authenticated user to a Apple ID +func link_apple_id_to_current_user(apple_auth_token): + if user_info == null: + aws_sdk_error.emit("No user info, can't link existing user to Apple ID") + return + linking["apple_link"] = true + _login_with_apple_id(apple_auth_token, user_info.auth_token, true) + + +# Called to create a new user with Apple ID, or to login with existing user linked to AppleID +func login_with_apple_id_token(apple_auth_token): + linking["apple_login"] = true + _login_with_apple_id(apple_auth_token, null, false) + + +# Logs in with Apple ID either linking existing user or as a Apple ID only / new user +# Called internally by the different Apple ID login functions +func _login_with_apple_id(apple_auth_token, auth_token, link_to_existing_user): + # Add the apple auth token to request + var params = login_endpoint + "/login-with-apple-id?" + \ + "apple_auth_token=" + apple_auth_token.uri_encode() + # If we're linking to existing user, add the relevant parameters + if auth_token != null and link_to_existing_user == true: + print("Linking Apple ID to existing user") + params += "&auth_token" + "=" + auth_token.uri_encode() + \ + "&link_to_existing_user=Yes" + # Perform a GET request to login as a new guest + _make_auth_http_request(params) + + +# Called to link an existing authenticated user to a Google Play ID +func link_google_play_id_to_current_user(google_play_auth_token): + if user_info == null: + aws_sdk_error.emit("No user info, can't link existing user to Google Play ID") + return + linking["goog_link"] = true + _login_with_google_play(google_play_auth_token, user_info.auth_token, true) + + +# Called to create a new user with Google Play ID, or to login with existing user linked to Google Play +func login_with_google_play_token(google_play_auth_token): + linking["goog_login"] = true + _login_with_google_play(google_play_auth_token, null, false) + + +# Logs in with Google Play ID either linking existing user or as a Google Play ID only / new user +# Called internally by the different Google Play ID login functions +func _login_with_google_play(google_play_auth_token, auth_token, link_to_existing_user): + # Add the google play auth token to request + var params = login_endpoint+"/login-with-google-play?" + \ + "google_play_auth_token" + "=" + google_play_auth_token.uri_encode() + # If we're linking to existing user, add the relevant parameters + if auth_token != null and link_to_existing_user == true: + print("Linking Google Play ID to existing user") + params += "&auth_token" + "=" + auth_token.uri_encode() + \ + "&link_to_existing_user=Yes" + _make_auth_http_request(params) + + +# Called to link an existing authenticated user to a Facebook ID +func link_facebook_id_to_current_user(facebook_access_token, facebook_user_id): + if(user_info == null): + aws_sdk_error.emit("No user info, can't link existing user to Facebook ID") + return + linking["fb_link"] = true + _login_with_facebook(facebook_access_token, facebook_user_id, user_info.auth_token, true) + + +# Called to create a new user with Facebook ID, or to login with existing user linked to Facebook +func login_with_facebook_access_token(facebook_access_token, facebook_user_id): + linking["fb_login"] = true + _login_with_facebook(facebook_access_token, facebook_user_id, null, false) + + +# Logs in with Facebook ID either linking existing user or as a Facebook ID only / new user +# Called internally by the different Facebook ID login functions +func _login_with_facebook(facebook_access_token, facebook_user_id, auth_token, link_to_existing_user): + # Add the Facebook auth token and user ID to request + var params = login_endpoint+"/login-with-facebook?" + \ + "facebook_access_token" + "=" + facebook_access_token.uri_encode() + \ + "&facebook_user_id" + "=" + facebook_user_id.uri_encode() + # If we're linking to existing user, add the relevant parameters + if auth_token != null and link_to_existing_user == true: + print("Linking Facebook ID to existing user") + params += "&auth_token" + "=" + auth_token.uri_encode() + \ + "&link_to_existing_user=Yes" + # Perform a GET request to login as a new guest + _make_auth_http_request(params) diff --git a/GodotSample/addons/AWSGameSDK/scripts/AWSAuthorization.gd.uid b/GodotSample/addons/AWSGameSDK/scripts/AWSAuthorization.gd.uid new file mode 100644 index 0000000..504c691 --- /dev/null +++ b/GodotSample/addons/AWSGameSDK/scripts/AWSAuthorization.gd.uid @@ -0,0 +1 @@ +uid://dxp7lgvaxrjry diff --git a/GodotSample/addons/AWSGameSDK/scripts/AWSBackend.gd b/GodotSample/addons/AWSGameSDK/scripts/AWSBackend.gd new file mode 100644 index 0000000..921e86b --- /dev/null +++ b/GodotSample/addons/AWSGameSDK/scripts/AWSBackend.gd @@ -0,0 +1,126 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +extends Node +class_name AWSGameSDKBackend + +signal aws_backend_request_successful +signal aws_sdk_error + +@export var backend_endpoint: String = "" #Endpoint for backend operations +@export var gamelift_backend_endpoint: String = "" #Endpoint for Amazon GameLift Backend +@export var get_player_data_uri: String = "/get-player-data" #Backend URI to retrieve player data +@export var set_player_data_uri: String = "/set-player-data" #Backend URI to set player data +@export var post_player_data_uri: String = "/post-to-uri" #Backend URI to POST data to - this is not yet used +@export var gamelift_request_match_uri: String = "/request-matchmaking" #Amazon GameLift URI to request matchmaking +@export var gamelift_match_status_uri: String = "/get-match-status" #Amazon GameLift URI to get matchmaking status + +var http_request: HTTPRequest +var error_string: String +#data cannot be a forced type at this time, as some data is returned as text and some as json +#you will need to evaluate the data returned for typing +var data_received + + +func get_response_data(): + return data_received + + +# Functions to make an authenticated GET request to a backend API +# Called by your custom code to access backend functionality +func backend_set_request(auth_token: String, query_parameters: Dictionary = {}): + _backend_get_request(set_player_data_uri, auth_token, query_parameters) + return + + +func backend_get_request(auth_token:String, query_parameters: Dictionary = {}): + _backend_get_request(get_player_data_uri, auth_token, query_parameters) + return + + +func backend_gamelift_request(auth_token: String, query_parameters: Dictionary = {}): + pass + + + +func _backend_get_request(resource: String, auth_token: String, query_parameters: Dictionary = {}): + var params: String = "" + if(auth_token == ""): + aws_sdk_error.emit("No auth token set yet, login first") + return + # Add the query parameters to the request + params += backend_endpoint + resource + if query_parameters != null and query_parameters != {}: + params += "?" + for key in query_parameters: + params += key + "=" + query_parameters[key].uri_encode() + "&" + # Perform a GET request to login as a new guest + _make_backend_http_request(params, HTTPClient.Method.METHOD_GET, auth_token) + + +func gamelift_backend_post_request(auth_token, request_body: Dictionary = {}): + if auth_token == "": + aws_sdk_error.emit("No auth token set yet, login first") + return + # Create an HTTP request node and connect its completion signal. + var params = gamelift_backend_endpoint + gamelift_request_match_uri + # Perform a GET request to login as a new guest + _make_backend_http_request(params, HTTPClient.Method.METHOD_POST, auth_token, request_body) + + +func gamelift_backend_get_request(auth_token, request_body: Dictionary = {}): + if auth_token == "": + aws_sdk_error.emit("No auth token set yet, login first") + return + # Create an HTTP request node and connect its completion signal. + var params = gamelift_backend_endpoint + gamelift_request_match_uri + # Perform a GET request to login as a new guest + _make_backend_http_request(params, HTTPClient.Method.METHOD_GET, auth_token, request_body) + +# Function to make an authenticated POST request to a backend API +# Called by your custom code to access backend functionality +func backend_post_request(auth_token, request_body: Dictionary = {}): + if auth_token == "": + aws_sdk_error.emit("No auth token set yet, login first") + return + # Create an HTTP request node and connect its completion signal. + var params = backend_endpoint + post_player_data_uri + # Perform a GET request to login as a new guest + _make_backend_http_request(params, HTTPClient.Method.METHOD_POST, auth_token, request_body) + + +func _make_backend_http_request(url, method: HTTPClient.Method, auth_token: String, request_body: Dictionary = {}): + var error + #clear out prior data + data_received = {} + http_request = HTTPRequest.new() + http_request.request_completed.connect(_backend_request_completed) + add_child(http_request) + var headers = ["Authorization: " + auth_token] + if method == HTTPClient.Method.METHOD_POST: + error = http_request.request(url, headers, method, str(request_body)) + elif method == HTTPClient.Method.METHOD_GET: + error = http_request.request(url, headers, method) + else: + #unsupported method at this time + aws_sdk_error.emit("Unsupported HTTP verb in request") + if error != OK: + print(error) + aws_sdk_error.emit("Error making backend request") + + +func _backend_request_completed(result, response_code, headers, body): + http_request.queue_free() + var json_string = body.get_string_from_utf8() # Retrieve data + var json = JSON.new() + var error = json.parse(json_string) + + # trigger error if we didn't get a proper response code + if(response_code >= 400): + error_string = json_string + aws_sdk_error.emit("HTTP Error: " + str(response_code) + " - " + error_string) + return + # Check we got no error + if error == OK: + data_received = json.data + aws_backend_request_successful.emit() + pass diff --git a/GodotSample/addons/AWSGameSDK/scripts/AWSBackend.gd.uid b/GodotSample/addons/AWSGameSDK/scripts/AWSBackend.gd.uid new file mode 100644 index 0000000..8c31f2b --- /dev/null +++ b/GodotSample/addons/AWSGameSDK/scripts/AWSBackend.gd.uid @@ -0,0 +1 @@ +uid://c1iyo651ktydf diff --git a/GodotSample/addons/AWSGameSDK/scripts/user_info.gd b/GodotSample/addons/AWSGameSDK/scripts/user_info.gd new file mode 100644 index 0000000..e1d54ec --- /dev/null +++ b/GodotSample/addons/AWSGameSDK/scripts/user_info.gd @@ -0,0 +1,20 @@ +class_name UserInfo + +# Class to manage user info +var user_id = ""; +var guest_secret = ""; +var auth_token = ""; +var apple_id = ""; +var steam_id = ""; +var google_play_id = ""; +var facebook_id = ""; +var refresh_token = ""; +var auth_token_expires_in = 0; +var refresh_token_expires_in = 0; + +func to_string(): + return("user_id: " + user_id + "\nguest_secret: " + guest_secret + "\nauth_token: " + auth_token + + "\napple_id: " + apple_id + "\nsteam_id: " + steam_id + "\ngoogle_play_id: " + google_play_id + + "\nfacebook_id: " + facebook_id + + "\nrefresh_token: " + refresh_token + "\nauth_token_expires_in: " + str(auth_token_expires_in) + + "\nrefresh_token_expires_in: " + str(refresh_token_expires_in)) diff --git a/GodotSample/addons/AWSGameSDK/scripts/user_info.gd.uid b/GodotSample/addons/AWSGameSDK/scripts/user_info.gd.uid new file mode 100644 index 0000000..1ca1702 --- /dev/null +++ b/GodotSample/addons/AWSGameSDK/scripts/user_info.gd.uid @@ -0,0 +1 @@ +uid://cb0us4elk7if5 diff --git a/GodotSample/addons/gd_data_binding/plugin.cfg b/GodotSample/addons/gd_data_binding/plugin.cfg new file mode 100644 index 0000000..a3e957d --- /dev/null +++ b/GodotSample/addons/gd_data_binding/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="GDDataBinding" +description="A plugin to provide data binding features." +author="Hotari" +version="1.0.0" +script="scripts/plugin.gd" diff --git a/GodotSample/addons/gd_data_binding/scripts/base_binding_source.gd b/GodotSample/addons/gd_data_binding/scripts/base_binding_source.gd new file mode 100644 index 0000000..d99d1a7 --- /dev/null +++ b/GodotSample/addons/gd_data_binding/scripts/base_binding_source.gd @@ -0,0 +1,187 @@ +class_name BaseBindingSource + +var _source_object +var _property_list: Array[Dictionary] + +var _target_dict = {} + + +func _init(source_object = self, source_value_change_notify_signal = null): + assert(source_object != null, "The source object must not be null.") + assert( + source_object is not Object or source_object != self or BaseBindingSource != get_script(), + "The source object must be passed unless the class inherits BindingSource." + ) + assert( + source_object is not Object or source_object != self, + "Currently, initialize by self is not supported." + ) + + _source_object = source_object + + if source_object is Object: + _property_list = source_object.get_property_list() + + elif source_object is Dictionary: + _property_list = _get_dict_property_list(source_object) + + else: + push_error("The source object must be an object or a dict.") + + var signal_instance = _get_signal(source_object, source_value_change_notify_signal) + if signal_instance is Signal: + signal_instance.connect(_on_source_value_change_notified) + + +func _get_property_list(): + return _property_list + + +func _get(property): + if property in _source_object: + return _source_object[property] + + return null + + +func _set(property, value): + _source_object[property] = value + _update_target(property, value) + return true + + +func bind_to( + source_property: StringName, + target_object, + target_property: StringName, + converter_pipeline: BindingConverterPipeline = null, + target_value_change_signal = null +): + var binding_dict = _target_dict.get_or_add(source_property, {}) as Dictionary + var binding_key = _get_binding_key(target_object, target_property) + + assert( + not binding_dict.has(binding_key), + ( + "The source property %s has already been bound to the target property %s." + % [source_property, target_property] + ) + ) + + var binding = BindingWithTargetSignal.new( + self, + source_property, + target_object, + target_property, + converter_pipeline, + _get_signal(target_object, target_value_change_signal) + ) + binding_dict[binding_key] = binding + + +func unbind_from(source_property: StringName, target_object, target_property: StringName): + assert( + _target_dict.has(source_property), + "The source property %s has not been bound to any target properties." % source_property + ) + + var binding_dict = _target_dict.get(source_property) as Dictionary + var binding_key = _get_binding_key(target_object, target_property) + + assert( + binding_dict.has(binding_key), + ( + "The source property %s has not been bound to the target property %s." + % [source_property, target_property] + ) + ) + + binding_dict.erase(binding_key) + + +func _on_source_value_change_notified(source_property: StringName): + var source_value = _source_object[source_property] + _update_target(source_property, source_value) + + +func _update_target(source_property: StringName, source_value: Variant): + var binding_dict_or_null = _target_dict.get(source_property) + if binding_dict_or_null == null: + return + + var binding_dict = binding_dict_or_null as Dictionary + + for binding_key in binding_dict.keys(): + var binding = binding_dict[binding_key] as Binding + if binding.is_valid: + binding.pass_source_value(source_value) + else: + binding_dict.erase(binding_key) + + +static func _get_dict_property_list(dict: Dictionary): + var property_list: Array[Dictionary] = [] + + for key in dict: + var value = dict[key] + var type = typeof(value) + + var property = { + "name": key, + "type": type, + "class_name": value.get_class() if type == TYPE_OBJECT else "", + } + property_list.append(property) + + return property_list + + +static func _get_signal(object, signal_ref): + if signal_ref == null: + return null + + if signal_ref is String or signal_ref is StringName: + assert( + object.has_signal(signal_ref), + "The signal name must refer to an existing signal of the specified object." + ) + return Signal(object, signal_ref) + + if signal_ref is Signal: + return signal_ref + + push_error("The arg signal_ref must be null, String, StringName, or Signal.") + + +static func _get_binding_key(target_object, target_property: StringName): + if target_object is Object: + return "%s.%s" % [target_object.get_instance_id(), target_property] + + if target_object is Dictionary: + var id = target_object.get_or_add("__BINDING_ID__", UUID.v7()) + return "%s.%s" % [id, target_property] + + push_error("The target object must be an object or a dict.") + + +class BindingWithTargetSignal: + extends Binding + + func _init( + source_object, + source_property: StringName, + target_object, + target_property: StringName, + converter_pipeline: BindingConverterPipeline, + target_value_change_signal + ): + super(source_object, source_property, target_object, target_property, converter_pipeline) + + var source_value = source_object[source_property] + pass_source_value(source_value) + + if target_value_change_signal is Signal: + target_value_change_signal.connect(_on_target_value_changed) + + func _on_target_value_changed(target_value: Variant): + pass_target_value(target_value) diff --git a/GodotSample/addons/gd_data_binding/scripts/base_binding_source.gd.uid b/GodotSample/addons/gd_data_binding/scripts/base_binding_source.gd.uid new file mode 100644 index 0000000..228132c --- /dev/null +++ b/GodotSample/addons/gd_data_binding/scripts/base_binding_source.gd.uid @@ -0,0 +1 @@ +uid://dskn3ld5efsoy diff --git a/GodotSample/addons/gd_data_binding/scripts/binding.gd b/GodotSample/addons/gd_data_binding/scripts/binding.gd new file mode 100644 index 0000000..e1370d8 --- /dev/null +++ b/GodotSample/addons/gd_data_binding/scripts/binding.gd @@ -0,0 +1,85 @@ +class_name Binding + +var is_valid: bool: + get: + return _source_validator.call(_source_object) and _target_validator.call(_target_object) + +var _source_object +var _source_property: StringName +var _source_validator: Callable + +var _target_object +var _target_property: StringName +var _target_validator: Callable + +var _converter_pipeline: BindingConverterPipeline + + +func _init( + source_object, + source_property: StringName, + target_object, + target_property: StringName, + converter_pipeline: BindingConverterPipeline = null +): + assert( + source_object is Object or source_object is Dictionary, + "The source object must be an object or a dict." + ) + assert(source_object != null, "The source object must not be null.") + assert( + source_property in source_object, + "The source property %s was not in the source object." % source_property + ) + + assert( + target_object is Object or target_object is Dictionary, + "The target object must be an object or a dict." + ) + assert(target_object != null, "The target object must not be null.") + assert( + target_property in target_object, + "The target property %s was not in the target object." % target_property + ) + + _source_object = source_object + _source_property = source_property + _source_validator = _get_validator(source_object) + + _target_object = target_object + _target_property = target_property + _target_validator = _get_validator(target_object) + + if converter_pipeline == null: + _converter_pipeline = BindingConverterPipeline.new() + else: + _converter_pipeline = converter_pipeline + + +func pass_source_value(source_value: Variant): + var prev_target_value = _target_object[_target_property] + var next_target_value = _converter_pipeline.source_to_target(source_value) + if prev_target_value == next_target_value: + return + + _target_object[_target_property] = next_target_value + + +func pass_target_value(target_value: Variant): + var prev_source_value = _source_object[_source_property] + var next_source_value = _converter_pipeline.target_to_source(target_value) + if prev_source_value == next_source_value: + return + + _source_object[_source_property] = next_source_value + + +static func _get_validator(object) -> Callable: + if object is Object: + return is_instance_valid + + return _none_object_validator + + +static func _none_object_validator(_p) -> bool: + return true diff --git a/GodotSample/addons/gd_data_binding/scripts/binding.gd.uid b/GodotSample/addons/gd_data_binding/scripts/binding.gd.uid new file mode 100644 index 0000000..32c174b --- /dev/null +++ b/GodotSample/addons/gd_data_binding/scripts/binding.gd.uid @@ -0,0 +1 @@ +uid://dde4vq75mralw diff --git a/GodotSample/addons/gd_data_binding/scripts/binding_converter.gd b/GodotSample/addons/gd_data_binding/scripts/binding_converter.gd new file mode 100644 index 0000000..dfa9727 --- /dev/null +++ b/GodotSample/addons/gd_data_binding/scripts/binding_converter.gd @@ -0,0 +1,9 @@ +class_name BindingConverter + + +func source_to_target(source_value: Variant) -> Variant: + return source_value + + +func target_to_source(target_value: Variant) -> Variant: + return target_value diff --git a/GodotSample/addons/gd_data_binding/scripts/binding_converter.gd.uid b/GodotSample/addons/gd_data_binding/scripts/binding_converter.gd.uid new file mode 100644 index 0000000..58d5cce --- /dev/null +++ b/GodotSample/addons/gd_data_binding/scripts/binding_converter.gd.uid @@ -0,0 +1 @@ +uid://lo1iyi0isxo diff --git a/GodotSample/addons/gd_data_binding/scripts/binding_converter_pipeline.gd b/GodotSample/addons/gd_data_binding/scripts/binding_converter_pipeline.gd new file mode 100644 index 0000000..188d27a --- /dev/null +++ b/GodotSample/addons/gd_data_binding/scripts/binding_converter_pipeline.gd @@ -0,0 +1,58 @@ +class_name BindingConverterPipeline + +var _source_to_target_funcs: Array[Callable] = [] +var _target_to_source_funcs: Array[Callable] = [] + +var _converters: Array[BindingConverter] = [] + + +func _init( + source_to_target_funcs: Array[Callable] = [], target_to_source_funcs: Array[Callable] = [] +): + _source_to_target_funcs = source_to_target_funcs + _target_to_source_funcs = target_to_source_funcs + + +func copy(): + var converter_pipeline = BindingConverterPipeline.new( + _source_to_target_funcs.duplicate(), _target_to_source_funcs.duplicate() + ) + converter_pipeline._converters = _converters.duplicate() + return converter_pipeline + + +func append(converter): + if converter is BindingConverter: + _source_to_target_funcs.push_back(converter.source_to_target) + _target_to_source_funcs.push_front(converter.target_to_source) + + _converters.append(converter) + + elif converter is Callable: + _source_to_target_funcs.push_back(converter) + + elif converter is Array: + _source_to_target_funcs.push_back(converter[0]) + _target_to_source_funcs.push_front(converter[1]) + + else: + push_error("The arg converter must be BindingConverter, Callable, or [Callable, Callable].") + breakpoint + + +func source_to_target(source_value: Variant) -> Variant: + var converted_value = source_value + + for source_to_target_func in _source_to_target_funcs: + converted_value = source_to_target_func.call(converted_value) + + return converted_value + + +func target_to_source(target_value: Variant) -> Variant: + var converted_value = target_value + + for target_to_source_func in _target_to_source_funcs: + converted_value = target_to_source_func.call(converted_value) + + return converted_value diff --git a/GodotSample/addons/gd_data_binding/scripts/binding_converter_pipeline.gd.uid b/GodotSample/addons/gd_data_binding/scripts/binding_converter_pipeline.gd.uid new file mode 100644 index 0000000..3d4c866 --- /dev/null +++ b/GodotSample/addons/gd_data_binding/scripts/binding_converter_pipeline.gd.uid @@ -0,0 +1 @@ +uid://dsagymlfrfb72 diff --git a/GodotSample/addons/gd_data_binding/scripts/binding_source.gd b/GodotSample/addons/gd_data_binding/scripts/binding_source.gd new file mode 100644 index 0000000..a455289 --- /dev/null +++ b/GodotSample/addons/gd_data_binding/scripts/binding_source.gd @@ -0,0 +1,307 @@ +class_name BindingSource +extends BaseBindingSource + +enum LineEditTrigger { ON_SUBMITTED, ON_FOCUS_EXITED, ON_CHANGED } +enum TextEditTrigger { ON_FOCUS_EXITED, ON_CHANGED } + +var _wrapper_dict = {} + + +func bind(source_property: StringName) -> Binder: + return Binder.new(self, source_property) + + +func unbind(source_property: StringName) -> Unbinder: + return Unbinder.new(self, source_property) + + +func _add_wrapper(source_property: StringName, wrapper: Wrapper): + var wrappers = _wrapper_dict.get_or_add(source_property, []) as Array[Wrapper] + wrappers.append(wrapper) + + +func _remove_wrapper(source_property: StringName, wrapped_object: Object): + var wrappers = _wrapper_dict.get(source_property, []) as Array[Wrapper] + _wrapper_dict[source_property] = wrappers.filter( + func(wrapper): return not wrapper.wraps(wrapped_object) + ) + + +# gdlint:ignore = max-public-methods +class Binder: + var _source: BindingSource + var _source_property: StringName + var _converter_pipeline: BindingConverterPipeline + + func _init( + source: BindingSource, + source_property: StringName, + converter_pipeline: BindingConverterPipeline = null + ): + _source = source + _source_property = source_property + + if converter_pipeline == null: + _converter_pipeline = BindingConverterPipeline.new() + else: + _converter_pipeline = converter_pipeline + + func using(converter) -> Binder: + var converter_pipeline = _converter_pipeline.copy() + converter_pipeline.append(converter) + return Binder.new(_source, _source_property, converter_pipeline) + + func to(target_object, target_property: StringName, target_value_change_signal = null): + _source.bind_to( + _source_property, + target_object, + target_property, + _converter_pipeline, + target_value_change_signal + ) + + func to_toggle_button(toggle_button: BaseButton): + assert(_convert_to(TYPE_BOOL), "A value bound to Button must be a bool.") + assert(toggle_button.toggle_mode, "The button must be toggle mode.") + to(toggle_button, &"button_pressed", toggle_button.toggled) + + func to_check_box(check_box: CheckBox): + assert(_convert_to(TYPE_BOOL), "A value bound to CheckBox must be a bool.") + to_toggle_button(check_box) + + func to_check_button(check_button: CheckButton): + assert(_convert_to(TYPE_BOOL), "A value bound to CheckButton must be a bool.") + to_toggle_button(check_button) + + func to_color_picker_button(color_picker_button: ColorPickerButton): + assert(_convert_to(TYPE_COLOR), "A value bound to ColorPickerButton must be a color.") + to(color_picker_button, &"color", color_picker_button.color_changed) + + func to_option_button(option_button: OptionButton): + assert(_convert_to(TYPE_INT), "A value bound to OptionButton must be an int.") + to(option_button, &"selected", option_button.item_selected) + + func to_texture_button(texture_button: TextureButton): + assert(_convert_to(TYPE_BOOL), "A value bound to TextureButton must be a bool.") + to_toggle_button(texture_button) + + func to_color_rect(color_rect: ColorRect): + assert(_convert_to(TYPE_COLOR), "A value bound to ColorRect must be a color.") + to(color_rect, &"color") + + func to_color_picker(color_picker: ColorPicker): + assert(_convert_to(TYPE_COLOR), "A value bound to ColorPicker must be a color.") + to(color_picker, &"color", color_picker.color_changed) + + func to_split_container(split_container: SplitContainer): + assert(_convert_to(TYPE_INT), "A value bound to SplitContainer must be an int.") + to(split_container, &"split_offset", split_container.dragged) + + func to_tab_container(tab_container: TabContainer): + assert(_convert_to(TYPE_INT), "A value bound to TabContainer must be an int.") + to(tab_container, &"current_tab", tab_container.tab_selected) + + func to_label(label: Label): + assert(_convert_to(TYPE_STRING), "A value bound to Label must be a string.") + to(label, &"text") + + func to_line_edit( + line_edit: LineEdit, trigger: LineEditTrigger = LineEditTrigger.ON_FOCUS_EXITED + ): + assert(_convert_to(TYPE_STRING), "A value bound to LineEdit must be a string.") + var target_value_change_signal: Signal + + match trigger: + LineEditTrigger.ON_SUBMITTED: + target_value_change_signal = line_edit.text_submitted + LineEditTrigger.ON_FOCUS_EXITED: + var edit_wrapper = EditWrapper.new(line_edit) + target_value_change_signal = edit_wrapper.focus_exited_without_ui_cancel + # gdlint:ignore = private-method-call + _source._add_wrapper(_source_property, edit_wrapper) + LineEditTrigger.ON_CHANGED: + target_value_change_signal = line_edit.text_changed + + to(line_edit, &"text", target_value_change_signal) + + func to_range(range: Range, target_value_change_signal = range.value_changed): + assert(_convert_to(TYPE_FLOAT), "A value bound to Range must be a float.") + to(range, &"value", target_value_change_signal) + + func to_progress_bar(progress_bar: ProgressBar): + assert(_convert_to(TYPE_FLOAT), "A value bound to ProgressBar must be a float.") + to_range(progress_bar, null) + + func to_slider(slider: Slider): + assert(_convert_to(TYPE_FLOAT), "A value bound to Slider must be a float.") + to_range(slider) + + func to_spin_box(spin_box: SpinBox): + assert(_convert_to(TYPE_FLOAT), "A value bound to SpinBox must be a float.") + to_range(spin_box) + + func to_texture_progress_bar(texture_progress_bar: TextureProgressBar): + assert(_convert_to(TYPE_FLOAT), "A value bound to TextureProgressBar must be a float.") + to_range(texture_progress_bar, null) + + func to_tab_bar(tab_bar: TabBar): + assert(_convert_to(TYPE_INT), "A value bound to TabBar must be an int.") + to(tab_bar, &"current_tab", tab_bar.tab_selected) + + func to_text_edit( + text_edit: TextEdit, trigger: TextEditTrigger = TextEditTrigger.ON_FOCUS_EXITED + ): + assert(_convert_to(TYPE_STRING), "A value bound to TextEdit must be a string.") + + var target_value_change_signal: Signal + + match trigger: + TextEditTrigger.ON_FOCUS_EXITED: + var edit_wrapper = EditWrapper.new(text_edit) + target_value_change_signal = edit_wrapper.focus_exited_without_ui_cancel + # gdlint:ignore = private-method-call + _source._add_wrapper(_source_property, edit_wrapper) + TextEditTrigger.ON_CHANGED: + var text_edit_wrapper = TextEditWrapper.new(text_edit) + target_value_change_signal = text_edit_wrapper.text_changed + # gdlint:ignore = private-method-call + _source._add_wrapper(_source_property, text_edit_wrapper) + + to(text_edit, &"text", target_value_change_signal) + + func to_code_edit( + code_edit: CodeEdit, trigger: TextEditTrigger = TextEditTrigger.ON_FOCUS_EXITED + ): + assert(_convert_to(TYPE_STRING), "A value bound to CodeEdit must be a string.") + to_text_edit(code_edit, trigger) + + func _convert_to(target_type: Variant.Type): + assert( + _source_property in _source, + "The source property %s was not in the source object." % _source_property + ) + var source_value = _source[_source_property] + var target_value = _converter_pipeline.source_to_target(source_value) + return typeof(target_value) == target_type + + +# gdlint:ignore = max-public-methods +class Unbinder: + var _source: BindingSource + var _source_property: StringName + + func _init(source: BindingSource, source_property: StringName): + _source = source + _source_property = source_property + + func from(target_object, target_property: StringName): + _source.unbind_from(_source_property, target_object, target_property) + + func from_toggle_button(button: BaseButton): + from(button, &"button_pressed") + + func from_check_box(check_box: CheckBox): + from_toggle_button(check_box) + + func from_check_button(check_button: CheckButton): + from_toggle_button(check_button) + + func from_color_picker_button(color_picker_button: ColorPickerButton): + from(color_picker_button, &"color") + + func from_option_button(option_button: OptionButton): + from(option_button, &"selected") + + func from_texture_button(texture_button: TextureButton): + from_toggle_button(texture_button) + + func from_color_rect(color_rect: ColorRect): + from(color_rect, &"color") + + func from_color_picker(color_picker: ColorPicker): + from(color_picker, &"color") + + func from_split_container(split_container: SplitContainer): + from(split_container, &"split_offset") + + func from_tab_container(tab_container: TabContainer): + from(tab_container, &"current_tab") + + func from_label(label: Label): + from(label, &"text") + + func from_line_edit(line_edit: LineEdit): + # gdlint:ignore = private-method-call + _source._remove_wrapper(_source_property, line_edit) + from(line_edit, &"text") + + func from_range(range: Range): + from(range, &"value") + + func from_progress_bar(progress_bar: ProgressBar): + from_range(progress_bar) + + func from_slider(slider: Slider): + from_range(slider) + + func from_spin_box(spin_box: SpinBox): + from_range(spin_box) + + func from_texture_progress_bar(texture_progress_bar: TextureProgressBar): + from_range(texture_progress_bar) + + func from_tab_bar(tab_bar: TabBar): + from(tab_bar, &"current_tab") + + func from_text_edit(text_edit: TextEdit): + # gdlint:ignore = private-method-call + _source._remove_wrapper(_source_property, text_edit) + from(text_edit, &"text") + + func from_code_edit(code_edit: CodeEdit): + from_text_edit(code_edit) + + +class Wrapper: + var _object: Object + + func _init(object: Object): + _object = object + + func wraps(object: Object): + return _object == object + + +class EditWrapper: + extends Wrapper + + signal focus_exited_without_ui_cancel(new_text: String) + + var _is_canceled: bool = false + + func _init(edit): + super(edit) + + assert(edit is LineEdit or edit is TextEdit) + + edit.focus_exited.connect(_on_focus_exited) + + func _on_focus_exited(): + if Input.is_action_pressed("ui_cancel"): + return + + focus_exited_without_ui_cancel.emit(_object.text) + + +class TextEditWrapper: + extends Wrapper + + signal text_changed(new_text: String) + + func _init(text_edit: TextEdit): + super(text_edit) + + text_edit.text_changed.connect(_on_text_changed) + + func _on_text_changed(): + text_changed.emit(_object.text) diff --git a/GodotSample/addons/gd_data_binding/scripts/binding_source.gd.uid b/GodotSample/addons/gd_data_binding/scripts/binding_source.gd.uid new file mode 100644 index 0000000..de33166 --- /dev/null +++ b/GodotSample/addons/gd_data_binding/scripts/binding_source.gd.uid @@ -0,0 +1 @@ +uid://bbalwvok7gyfu diff --git a/GodotSample/addons/gd_data_binding/scripts/binding_utils.gd b/GodotSample/addons/gd_data_binding/scripts/binding_utils.gd new file mode 100644 index 0000000..629b3d4 --- /dev/null +++ b/GodotSample/addons/gd_data_binding/scripts/binding_utils.gd @@ -0,0 +1,9 @@ +class_name BindingUtils + + +static func to_int(value: Variant): + return int(value) + + +static func to_float(value: Variant): + return float(value) diff --git a/GodotSample/addons/gd_data_binding/scripts/binding_utils.gd.uid b/GodotSample/addons/gd_data_binding/scripts/binding_utils.gd.uid new file mode 100644 index 0000000..0f9a899 --- /dev/null +++ b/GodotSample/addons/gd_data_binding/scripts/binding_utils.gd.uid @@ -0,0 +1 @@ +uid://ciw5e0wh8oyy3 diff --git a/GodotSample/addons/gd_data_binding/scripts/converters/case_binding_converter.gd b/GodotSample/addons/gd_data_binding/scripts/converters/case_binding_converter.gd new file mode 100644 index 0000000..022af75 --- /dev/null +++ b/GodotSample/addons/gd_data_binding/scripts/converters/case_binding_converter.gd @@ -0,0 +1,18 @@ +class_name CaseBindingConverter +extends BindingConverter + +var _case_value: Variant +var _last_source_value: Variant + + +func _init(case_value: Variant): + _case_value = case_value + + +func source_to_target(source_value: Variant) -> Variant: + _last_source_value = source_value + return source_value == _case_value + + +func target_to_source(target_value: Variant) -> Variant: + return _case_value if target_value else _last_source_value diff --git a/GodotSample/addons/gd_data_binding/scripts/converters/case_binding_converter.gd.uid b/GodotSample/addons/gd_data_binding/scripts/converters/case_binding_converter.gd.uid new file mode 100644 index 0000000..2077e5d --- /dev/null +++ b/GodotSample/addons/gd_data_binding/scripts/converters/case_binding_converter.gd.uid @@ -0,0 +1 @@ +uid://buw5ejg0g6l72 diff --git a/GodotSample/addons/gd_data_binding/scripts/converters/invert_bool_binding_converter.gd b/GodotSample/addons/gd_data_binding/scripts/converters/invert_bool_binding_converter.gd new file mode 100644 index 0000000..2a150b3 --- /dev/null +++ b/GodotSample/addons/gd_data_binding/scripts/converters/invert_bool_binding_converter.gd @@ -0,0 +1,10 @@ +class_name InvertBoolBindingConverter +extends BindingConverter + + +func source_to_target(source_value: Variant) -> Variant: + return not source_value + + +func target_to_source(target_value: Variant) -> Variant: + return not target_value diff --git a/GodotSample/addons/gd_data_binding/scripts/converters/invert_bool_binding_converter.gd.uid b/GodotSample/addons/gd_data_binding/scripts/converters/invert_bool_binding_converter.gd.uid new file mode 100644 index 0000000..4f9f376 --- /dev/null +++ b/GodotSample/addons/gd_data_binding/scripts/converters/invert_bool_binding_converter.gd.uid @@ -0,0 +1 @@ +uid://cmnfyf20qvj4c diff --git a/GodotSample/addons/gd_data_binding/scripts/converters/plus_one_converter.gd b/GodotSample/addons/gd_data_binding/scripts/converters/plus_one_converter.gd new file mode 100644 index 0000000..85f0a39 --- /dev/null +++ b/GodotSample/addons/gd_data_binding/scripts/converters/plus_one_converter.gd @@ -0,0 +1,10 @@ +class_name PlusOneConverter +extends BindingConverter + + +func source_to_target(source_value: Variant) -> Variant: + return source_value + 1 + + +func target_to_source(target_value: Variant) -> Variant: + return target_value - 1 diff --git a/GodotSample/addons/gd_data_binding/scripts/converters/plus_one_converter.gd.uid b/GodotSample/addons/gd_data_binding/scripts/converters/plus_one_converter.gd.uid new file mode 100644 index 0000000..f2829bf --- /dev/null +++ b/GodotSample/addons/gd_data_binding/scripts/converters/plus_one_converter.gd.uid @@ -0,0 +1 @@ +uid://db8juyrm270qk diff --git a/GodotSample/addons/gd_data_binding/scripts/plugin.gd b/GodotSample/addons/gd_data_binding/scripts/plugin.gd new file mode 100644 index 0000000..45a417e --- /dev/null +++ b/GodotSample/addons/gd_data_binding/scripts/plugin.gd @@ -0,0 +1,12 @@ +@tool +extends EditorPlugin + + +func _enter_tree(): + # Initialization of the plugin goes here. + pass + + +func _exit_tree(): + # Clean-up of the plugin goes here. + pass diff --git a/GodotSample/addons/gd_data_binding/scripts/plugin.gd.uid b/GodotSample/addons/gd_data_binding/scripts/plugin.gd.uid new file mode 100644 index 0000000..6cfed9e --- /dev/null +++ b/GodotSample/addons/gd_data_binding/scripts/plugin.gd.uid @@ -0,0 +1 @@ +uid://vntg37hvqli1 diff --git a/GodotSample/addons/gd_uuid/base_random.gd b/GodotSample/addons/gd_uuid/base_random.gd new file mode 100644 index 0000000..44e7e63 --- /dev/null +++ b/GodotSample/addons/gd_uuid/base_random.gd @@ -0,0 +1,7 @@ +class_name BaseRandom + + +func bytes(size: int) -> PackedByteArray: + var result = PackedByteArray() + result.resize(size) + return result diff --git a/GodotSample/addons/gd_uuid/base_random.gd.uid b/GodotSample/addons/gd_uuid/base_random.gd.uid new file mode 100644 index 0000000..7847ec0 --- /dev/null +++ b/GodotSample/addons/gd_uuid/base_random.gd.uid @@ -0,0 +1 @@ +uid://cc4ryiy6ophec diff --git a/GodotSample/addons/gd_uuid/build_in_random.gd b/GodotSample/addons/gd_uuid/build_in_random.gd new file mode 100644 index 0000000..55a5396 --- /dev/null +++ b/GodotSample/addons/gd_uuid/build_in_random.gd @@ -0,0 +1,26 @@ +class_name BuildInRandom +extends CryptoRandom + +var _random = RandomNumberGenerator.new() + + +func _init(): + var seed = 0 + var seed_bytes = _crypto.generate_random_bytes(4) + seed |= seed_bytes[0] << 24 + seed |= seed_bytes[1] << 16 + seed |= seed_bytes[2] << 8 + seed |= seed_bytes[3] + _random.seed = seed + + +func bytes(size: int) -> PackedByteArray: + var len_ints = (size + 3) / 4 + + var ints = PackedInt32Array() + ints.resize(len_ints) + + for index in range(len_ints): + ints[index] = _random.randi() + + return ints.to_byte_array() diff --git a/GodotSample/addons/gd_uuid/build_in_random.gd.uid b/GodotSample/addons/gd_uuid/build_in_random.gd.uid new file mode 100644 index 0000000..4fadc42 --- /dev/null +++ b/GodotSample/addons/gd_uuid/build_in_random.gd.uid @@ -0,0 +1 @@ +uid://cfj61fgxpvhb diff --git a/GodotSample/addons/gd_uuid/crypto_random.gd b/GodotSample/addons/gd_uuid/crypto_random.gd new file mode 100644 index 0000000..ba7365b --- /dev/null +++ b/GodotSample/addons/gd_uuid/crypto_random.gd @@ -0,0 +1,8 @@ +class_name CryptoRandom +extends BaseRandom + +static var _crypto = Crypto.new() + + +func bytes(size: int) -> PackedByteArray: + return _crypto.generate_random_bytes(size) diff --git a/GodotSample/addons/gd_uuid/crypto_random.gd.uid b/GodotSample/addons/gd_uuid/crypto_random.gd.uid new file mode 100644 index 0000000..45ee0fc --- /dev/null +++ b/GodotSample/addons/gd_uuid/crypto_random.gd.uid @@ -0,0 +1 @@ +uid://dhds538hp8nj4 diff --git a/GodotSample/addons/gd_uuid/uuid.gd b/GodotSample/addons/gd_uuid/uuid.gd new file mode 100644 index 0000000..ad840ca --- /dev/null +++ b/GodotSample/addons/gd_uuid/uuid.gd @@ -0,0 +1,41 @@ +class_name UUID + +static var fallback_random = BuildInRandom.new() + + +static func v7(random: BaseRandom = fallback_random) -> String: + var bytes = PackedByteArray() + bytes.resize(16) + + var unix_time_ms = get_unix_time_ms() + var rands = random.bytes(10) + + bytes[0] = (unix_time_ms >> 40) & 0xff + bytes[1] = (unix_time_ms >> 32) & 0xff + bytes[2] = (unix_time_ms >> 24) & 0xff + bytes[3] = (unix_time_ms >> 16) & 0xff + + bytes[4] = (unix_time_ms >> 8) & 0xff + bytes[5] = unix_time_ms & 0xff + bytes[6] = 0x70 | (rands[0] & 0x0f) + bytes[7] = rands[1] + + bytes[8] = 0x80 | rands[2] & 0x3f + bytes[9] = rands[3] + bytes[10] = rands[4] + bytes[11] = rands[5] + + bytes[12] = rands[6] + bytes[13] = rands[7] + bytes[14] = rands[8] + bytes[15] = rands[9] + + return format(bytes) + + +static func get_unix_time_ms() -> int: + return int(Time.get_unix_time_from_system() * 1000) + + +static func format(bytes: PackedByteArray) -> String: + return "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x" % Array(bytes) diff --git a/GodotSample/addons/gd_uuid/uuid.gd.uid b/GodotSample/addons/gd_uuid/uuid.gd.uid new file mode 100644 index 0000000..46d6ea4 --- /dev/null +++ b/GodotSample/addons/gd_uuid/uuid.gd.uid @@ -0,0 +1 @@ +uid://djjeq75105vu3 diff --git a/GodotSample/icon.svg.import b/GodotSample/icon.svg.import index 8f49f5a..4936167 100644 --- a/GodotSample/icon.svg.import +++ b/GodotSample/icon.svg.import @@ -2,7 +2,7 @@ importer="texture" type="CompressedTexture2D" -uid="uid://bf67bersrmkrg" +uid="uid://cjlov1q5qfmjc" path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" metadata={ "vram_texture": false diff --git a/GodotSample/images_readme/add_aws_nodes_to_your_scene.png b/GodotSample/images_readme/add_aws_nodes_to_your_scene.png new file mode 100644 index 0000000..4790539 Binary files /dev/null and b/GodotSample/images_readme/add_aws_nodes_to_your_scene.png differ diff --git a/GodotSample/images_readme/add_aws_nodes_to_your_scene.png.import b/GodotSample/images_readme/add_aws_nodes_to_your_scene.png.import new file mode 100644 index 0000000..a373815 --- /dev/null +++ b/GodotSample/images_readme/add_aws_nodes_to_your_scene.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cdesyoonww2fg" +path="res://.godot/imported/add_aws_nodes_to_your_scene.png-69f4916ec05e646f1c112d4aeaad76a7.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://images_readme/add_aws_nodes_to_your_scene.png" +dest_files=["res://.godot/imported/add_aws_nodes_to_your_scene.png-69f4916ec05e646f1c112d4aeaad76a7.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/GodotSample/images_readme/folder_structure.png b/GodotSample/images_readme/folder_structure.png new file mode 100644 index 0000000..2abb602 Binary files /dev/null and b/GodotSample/images_readme/folder_structure.png differ diff --git a/GodotSample/images_readme/folder_structure.png.import b/GodotSample/images_readme/folder_structure.png.import new file mode 100644 index 0000000..2a5f609 --- /dev/null +++ b/GodotSample/images_readme/folder_structure.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://yywpevov2l7" +path="res://.godot/imported/folder_structure.png-4d8859052716d9c9036ac809cfad7b1b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://images_readme/folder_structure.png" +dest_files=["res://.godot/imported/folder_structure.png-4d8859052716d9c9036ac809cfad7b1b.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/GodotSample/images_readme/project_settings.png b/GodotSample/images_readme/project_settings.png new file mode 100644 index 0000000..c38a74e Binary files /dev/null and b/GodotSample/images_readme/project_settings.png differ diff --git a/GodotSample/images_readme/project_settings.png.import b/GodotSample/images_readme/project_settings.png.import new file mode 100644 index 0000000..37d7809 --- /dev/null +++ b/GodotSample/images_readme/project_settings.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dy8yiiqs3rxxw" +path="res://.godot/imported/project_settings.png-70a1e49961f1c56bf70d7ca4949ed256.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://images_readme/project_settings.png" +dest_files=["res://.godot/imported/project_settings.png-70a1e49961f1c56bf70d7ca4949ed256.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/GodotSample/images_readme/scene_tree_with_plugins.png b/GodotSample/images_readme/scene_tree_with_plugins.png new file mode 100644 index 0000000..181b65f Binary files /dev/null and b/GodotSample/images_readme/scene_tree_with_plugins.png differ diff --git a/GodotSample/images_readme/scene_tree_with_plugins.png.import b/GodotSample/images_readme/scene_tree_with_plugins.png.import new file mode 100644 index 0000000..2289f61 --- /dev/null +++ b/GodotSample/images_readme/scene_tree_with_plugins.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://www8uydfo0tk" +path="res://.godot/imported/scene_tree_with_plugins.png-87ca49001a3091fe89f97cfde03455a1.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://images_readme/scene_tree_with_plugins.png" +dest_files=["res://.godot/imported/scene_tree_with_plugins.png-87ca49001a3091fe89f97cfde03455a1.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/GodotSample/images_readme/setting_auth_endpoint.png b/GodotSample/images_readme/setting_auth_endpoint.png new file mode 100644 index 0000000..55284c2 Binary files /dev/null and b/GodotSample/images_readme/setting_auth_endpoint.png differ diff --git a/GodotSample/images_readme/setting_auth_endpoint.png.import b/GodotSample/images_readme/setting_auth_endpoint.png.import new file mode 100644 index 0000000..458de38 --- /dev/null +++ b/GodotSample/images_readme/setting_auth_endpoint.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://djjvfvvp4miuy" +path="res://.godot/imported/setting_auth_endpoint.png-3d3fe78510d22003c31d4dd721c085e4.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://images_readme/setting_auth_endpoint.png" +dest_files=["res://.godot/imported/setting_auth_endpoint.png-3d3fe78510d22003c31d4dd721c085e4.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/GodotSample/images_readme/setting_backend_endpoint.png b/GodotSample/images_readme/setting_backend_endpoint.png new file mode 100644 index 0000000..2054333 Binary files /dev/null and b/GodotSample/images_readme/setting_backend_endpoint.png differ diff --git a/GodotSample/images_readme/setting_backend_endpoint.png.import b/GodotSample/images_readme/setting_backend_endpoint.png.import new file mode 100644 index 0000000..7fefed3 --- /dev/null +++ b/GodotSample/images_readme/setting_backend_endpoint.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cbwevf4h5ahxk" +path="res://.godot/imported/setting_backend_endpoint.png-bb468a0ba13c72779b3cf2d2fb13c160.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://images_readme/setting_backend_endpoint.png" +dest_files=["res://.godot/imported/setting_backend_endpoint.png-bb468a0ba13c72779b3cf2d2fb13c160.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/GodotSample/project.godot b/GodotSample/project.godot index b2cdb66..2de9e6a 100644 --- a/GodotSample/project.godot +++ b/GodotSample/project.godot @@ -11,14 +11,10 @@ config_version=5 [application] config/name="GodotSample" -run/main_scene="res://Samples/GuestIdentityAndRestApiBackend/GuestIdentityAndRestApiBackend.tscn" -config/features=PackedStringArray("4.0", "Mobile") +run/main_scene="uid://dpdmqpyoy6pg" +config/features=PackedStringArray("4.4", "Mobile") config/icon="res://icon.svg" -[autoload] +[editor_plugins] -AwsGameSdk="*res://AWSGameSDK/AWSGameSDK.gd" - -[rendering] - -renderer/rendering_method="mobile" +enabled=PackedStringArray("res://addons/AWSGameSDK/plugin.cfg", "res://addons/gd_data_binding/plugin.cfg")