diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index f6a1f46..f330d78 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -88,7 +88,7 @@ tasks.dokkaHtmlPartial { dokkaSourceSets.configureEach { perPackageOption { - matchingRegex.set(".*friends|.*signin") + matchingRegex.set(".*signin") suppress.set(true) } } diff --git a/plugin/demo/project.godot b/plugin/demo/project.godot index 3adc1dd..860c33a 100644 --- a/plugin/demo/project.godot +++ b/plugin/demo/project.godot @@ -21,6 +21,7 @@ GodotPlayGameServices="*res://addons/GodotPlayGameServices/autoloads/godot_play_ SignInClient="*res://addons/GodotPlayGameServices/autoloads/sign_in_client.gd" AchievementsClient="*res://addons/GodotPlayGameServices/autoloads/achievements_client.gd" LeaderboardsClient="*res://addons/GodotPlayGameServices/autoloads/leaderboards_client.gd" +PlayersClient="*res://addons/GodotPlayGameServices/autoloads/players_client.gd" [display] diff --git a/plugin/demo/scenes/MainMenu.gd b/plugin/demo/scenes/MainMenu.gd index 372c5dd..7c1fd13 100644 --- a/plugin/demo/scenes/MainMenu.gd +++ b/plugin/demo/scenes/MainMenu.gd @@ -2,6 +2,7 @@ extends Control @onready var achievements_button: Button = %Achievements @onready var leaderboards_button: Button = %Leaderboards +@onready var players_button: Button = %Players func _ready() -> void: achievements_button.pressed.connect(func(): @@ -10,3 +11,6 @@ func _ready() -> void: leaderboards_button.pressed.connect(func(): get_tree().change_scene_to_file("res://scenes/leaderboards/Leaderboards.tscn") ) + players_button.pressed.connect(func(): + get_tree().change_scene_to_file("res://scenes/players/Players.tscn") + ) diff --git a/plugin/demo/scenes/MainMenu.tscn b/plugin/demo/scenes/MainMenu.tscn index 576a7f6..36299d7 100644 --- a/plugin/demo/scenes/MainMenu.tscn +++ b/plugin/demo/scenes/MainMenu.tscn @@ -50,3 +50,10 @@ custom_minimum_size = Vector2(500, 200) layout_mode = 2 theme = ExtResource("2_aajnr") text = "Leaderboads" + +[node name="Players" type="Button" parent="MarginContainer/VBoxContainer"] +unique_name_in_owner = true +custom_minimum_size = Vector2(500, 200) +layout_mode = 2 +theme = ExtResource("2_aajnr") +text = "Players" diff --git a/plugin/demo/scenes/achievements/AchievementDisplay.gd b/plugin/demo/scenes/achievements/AchievementDisplay.gd index 6fe254c..6cb3b03 100644 --- a/plugin/demo/scenes/achievements/AchievementDisplay.gd +++ b/plugin/demo/scenes/achievements/AchievementDisplay.gd @@ -29,8 +29,8 @@ func _set_up_display() -> void: id_label.text = achievement.achievement_id name_label.text = achievement.achievement_name description_label.text = achievement.description - type_label.text = AchievementsClient.Type.keys()[achievement.type] - state_label.text = AchievementsClient.State.keys()[achievement.state] + type_label.text = AchievementsClient.Type.find_key(achievement.type) + state_label.text = AchievementsClient.State.find_key(achievement.state) xp_value_label.text = str(achievement.xp_value) if achievement.type == AchievementsClient.Type.TYPE_INCREMENTAL: diff --git a/plugin/demo/scenes/players/FriendDisplay.gd b/plugin/demo/scenes/players/FriendDisplay.gd new file mode 100644 index 0000000..dfb412f --- /dev/null +++ b/plugin/demo/scenes/players/FriendDisplay.gd @@ -0,0 +1,27 @@ +extends Control + +@onready var id_label: Label = %IdLabel +@onready var name_label: Label = %NameLabel +@onready var title_label: Label = %TitleLabel +@onready var status_label: Label = %StatusLabel +@onready var level_label: Label = %LevelLabel +@onready var xp_label: Label = %XpLabel + +@onready var compare_button: Button = %CompareButton + +var friend: PlayersClient.Player + +func _ready() -> void: + if friend: + _set_up_display() + compare_button.pressed.connect(func(): + PlayersClient.compare_profile_with(friend.player_id) + ) + +func _set_up_display() -> void: + id_label.text = friend.player_id + name_label.text = friend.display_name + title_label.text = friend.title + status_label.text = PlayersClient.PlayerFriendStatus.find_key(friend.friend_status) + level_label.text = str(friend.level_info.current_level.level_number) + xp_label.text = str(friend.level_info.current_xp_total) diff --git a/plugin/demo/scenes/players/FriendDisplay.tscn b/plugin/demo/scenes/players/FriendDisplay.tscn new file mode 100644 index 0000000..bc7b3bd --- /dev/null +++ b/plugin/demo/scenes/players/FriendDisplay.tscn @@ -0,0 +1,148 @@ +[gd_scene load_steps=3 format=3 uid="uid://cipu5ch2lo1ne"] + +[ext_resource type="Theme" uid="uid://bmm3mvq11y045" path="res://theme.tres" id="1_ylrpt"] +[ext_resource type="Script" path="res://scenes/players/FriendDisplay.gd" id="2_nfjqs"] + +[node name="FriendDisplay" type="PanelContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 1 +theme = ExtResource("1_ylrpt") +script = ExtResource("2_nfjqs") + +[node name="MarginContainer" type="MarginContainer" parent="."] +layout_mode = 2 +theme_override_constants/margin_left = 25 +theme_override_constants/margin_top = 25 +theme_override_constants/margin_right = 25 +theme_override_constants/margin_bottom = 25 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +layout_mode = 2 +theme_override_constants/separation = 25 + +[node name="Id" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Id"] +layout_mode = 2 +text = "ID:" + +[node name="IdLabel" type="Label" parent="MarginContainer/VBoxContainer/Id"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +text_overrun_behavior = 3 + +[node name="Name" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Name"] +layout_mode = 2 +text = "Name:" + +[node name="NameLabel" type="Label" parent="MarginContainer/VBoxContainer/Name"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +text_overrun_behavior = 3 + +[node name="Title" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Title"] +layout_mode = 2 +text = "Title:" + +[node name="TitleLabel" type="Label" parent="MarginContainer/VBoxContainer/Title"] +unique_name_in_owner = true +layout_mode = 2 + +[node name="Status" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Status"] +layout_mode = 2 +text = "Status:" + +[node name="StatusLabel" type="Label" parent="MarginContainer/VBoxContainer/Status"] +unique_name_in_owner = true +layout_mode = 2 + +[node name="Level" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Level"] +layout_mode = 2 +text = "Level:" + +[node name="LevelLabel" type="Label" parent="MarginContainer/VBoxContainer/Level"] +unique_name_in_owner = true +layout_mode = 2 + +[node name="Xp" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Xp"] +layout_mode = 2 +text = "XP:" + +[node name="XpLabel" type="Label" parent="MarginContainer/VBoxContainer/Xp"] +unique_name_in_owner = true +layout_mode = 2 + +[node name="HSeparator" type="HSeparator" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="Compare" type="VBoxContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +theme_override_constants/separation = 25 + +[node name="CompareButton" type="Button" parent="MarginContainer/VBoxContainer/Compare"] +unique_name_in_owner = true +layout_mode = 2 +theme = ExtResource("1_ylrpt") +text = "Compare" +text_overrun_behavior = 3 + +[node name="HSeparator2" type="HSeparator" parent="MarginContainer/VBoxContainer"] +visible = false +layout_mode = 2 + +[node name="Variants" type="VBoxContainer" parent="MarginContainer/VBoxContainer"] +visible = false +layout_mode = 2 +theme_override_constants/separation = 25 + +[node name="TimeSpan" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Variants"] +layout_mode = 2 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Variants/TimeSpan"] +layout_mode = 2 +text = "Time Span" + +[node name="TimeSpan" type="OptionButton" parent="MarginContainer/VBoxContainer/Variants/TimeSpan"] +unique_name_in_owner = true +layout_mode = 2 + +[node name="Collection" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Variants"] +layout_mode = 2 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Variants/Collection"] +layout_mode = 2 +text = "Collection" + +[node name="Collection" type="OptionButton" parent="MarginContainer/VBoxContainer/Variants/Collection"] +unique_name_in_owner = true +layout_mode = 2 + +[node name="ShowVariant" type="Button" parent="MarginContainer/VBoxContainer/Variants"] +unique_name_in_owner = true +layout_mode = 2 +theme = ExtResource("1_ylrpt") +disabled = true +text = "Show Leaderboard Variant" +text_overrun_behavior = 3 diff --git a/plugin/demo/scenes/players/Players.gd b/plugin/demo/scenes/players/Players.gd new file mode 100644 index 0000000..6678302 --- /dev/null +++ b/plugin/demo/scenes/players/Players.gd @@ -0,0 +1,25 @@ +extends Control + +@onready var back_button: Button = %Back + +@onready var friends_display: VBoxContainer = %FriendsDisplay + +var _friends_cache: Array[PlayersClient.Player] = [] +var _friend_display := preload("res://scenes/players/FriendDisplay.tscn") + +func _ready() -> void: + if _friends_cache.is_empty(): + PlayersClient.load_friends(10, true, true) + PlayersClient.friends_loaded.connect( + func cache_and_display(friends: Array[PlayersClient.Player]): + _friends_cache = friends + if not _friends_cache.is_empty() and friends_display.get_child_count() == 0: + for friend: PlayersClient.Player in _friends_cache: + var container := _friend_display.instantiate() as Control + container.friend = friend + friends_display.add_child(container) + ) + + back_button.pressed.connect(func(): + get_tree().change_scene_to_file("res://scenes/MainMenu.tscn") + ) diff --git a/plugin/demo/scenes/players/Players.tscn b/plugin/demo/scenes/players/Players.tscn new file mode 100644 index 0000000..a221c3a --- /dev/null +++ b/plugin/demo/scenes/players/Players.tscn @@ -0,0 +1,70 @@ +[gd_scene load_steps=3 format=3 uid="uid://ewhiun7ljryy"] + +[ext_resource type="Theme" uid="uid://bmm3mvq11y045" path="res://theme.tres" id="1_i7feq"] +[ext_resource type="Script" path="res://scenes/players/Players.gd" id="2_xda1v"] + +[node name="Players" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme = ExtResource("1_i7feq") +script = ExtResource("2_xda1v") + +[node name="MarginContainer" type="MarginContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 50 +theme_override_constants/margin_top = 150 +theme_override_constants/margin_right = 50 +theme_override_constants/margin_bottom = 50 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +layout_mode = 2 +theme_override_constants/separation = 50 + +[node name="NavBar" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="Back" type="Button" parent="MarginContainer/VBoxContainer/NavBar"] +unique_name_in_owner = true +layout_mode = 2 +theme = ExtResource("1_i7feq") +text = "Back" + +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 +horizontal_scroll_mode = 0 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/separation = 25 + +[node name="ShowAchievements" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/VBoxContainer"] +unique_name_in_owner = true +visible = false +custom_minimum_size = Vector2(500, 200) +layout_mode = 2 +theme = ExtResource("1_i7feq") +text = "Show Achievements" + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/ScrollContainer/VBoxContainer"] +layout_mode = 2 +text = "Friends" +horizontal_alignment = 1 + +[node name="FriendsDisplay" type="VBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/separation = 25 diff --git a/plugin/export_scripts_template/autoloads/achievements_client.gd b/plugin/export_scripts_template/autoloads/achievements_client.gd index 51a8d00..80b45aa 100644 --- a/plugin/export_scripts_template/autoloads/achievements_client.gd +++ b/plugin/export_scripts_template/autoloads/achievements_client.gd @@ -1,5 +1,5 @@ extends Node -## Client with achievements functionality. +## Client with achievements functionality. ## ## This autoload exposes methods and signals to control the game achievements for ## the currently signed in player. @@ -50,8 +50,6 @@ func _connect_signals() -> void: for dictionary: Dictionary in safe_array: achievements.append(Achievement.new(dictionary)) - print("Achievements loaded! %s" % str(achievements)) - achievements_loaded.emit(achievements) ) GodotPlayGameServices.android_plugin.achievementRevealed.connect(func(is_revealed: bool, achievement_id: String): @@ -113,7 +111,7 @@ class Achievement: var achievement_id: String ## The achievement id. var achievement_name: String ## The achievement name. var description: String ## The description of the achievement. - #var player: Player ## The player associated to this achievement. + var player: PlayersClient.Player ## The player associated to this achievement. var type: Type ## The achievement type. var state: State ## The achievement state. var xp_value: int ## The XP value of this achievement. @@ -139,34 +137,20 @@ class Achievement: ## Constructor that creates an Achievement from a [Dictionary] containing the properties. func _init(dictionary: Dictionary) -> void: - if dictionary.has("achievementId"): - achievement_id = dictionary.achievementId - if dictionary.has("name"): - achievement_name = dictionary.name - if dictionary.has("description"): - description = dictionary.description - #if dictionary.has("player"): - #player = dictionary.player - if dictionary.has("type"): - type = Type[dictionary.type] - if dictionary.has("state"): - state = State[dictionary.state] - if dictionary.has("xpValue"): - xp_value = dictionary.xpValue - if dictionary.has("revealedImageUri"): - revealed_image_uri = dictionary.revealedImageUri - if dictionary.has("unlockedImageUri"): - unlocked_image_uri = dictionary.unlockedImageUri - if dictionary.has("currentSteps"): - current_steps = dictionary.currentSteps - if dictionary.has("totalSteps"): - total_steps = dictionary.totalSteps - if dictionary.has("formattedCurrentSteps"): - formatted_current_steps = dictionary.formattedCurrentSteps - if dictionary.has("formattedTotalSteps"): - formatted_total_steps = dictionary.formattedTotalSteps - if dictionary.has("lastUpdatedTimestamp"): - last_updated_timestamp = dictionary.lastUpdatedTimestamp + if dictionary.has("achievementId"): achievement_id = dictionary.achievementId + if dictionary.has("name"): achievement_name = dictionary.name + if dictionary.has("description"): description = dictionary.description + if dictionary.has("player"): player = PlayersClient.Player.new(dictionary.player) + if dictionary.has("type"): type = Type[dictionary.type] + if dictionary.has("state"): state = State[dictionary.state] + if dictionary.has("xpValue"): xp_value = dictionary.xpValue + if dictionary.has("revealedImageUri"): revealed_image_uri = dictionary.revealedImageUri + if dictionary.has("unlockedImageUri"): unlocked_image_uri = dictionary.unlockedImageUri + if dictionary.has("currentSteps"): current_steps = dictionary.currentSteps + if dictionary.has("totalSteps"): total_steps = dictionary.totalSteps + if dictionary.has("formattedCurrentSteps"): formatted_current_steps = dictionary.formattedCurrentSteps + if dictionary.has("formattedTotalSteps"): formatted_total_steps = dictionary.formattedTotalSteps + if dictionary.has("lastUpdatedTimestamp"): last_updated_timestamp = dictionary.lastUpdatedTimestamp func _to_string() -> String: var result := PackedStringArray() @@ -174,7 +158,7 @@ class Achievement: result.append("achievement_id: %s" % achievement_id) result.append("achievement_name: %s" % achievement_name) result.append("description: %s" % description) - #result.append("player: %s" % str(player) + result.append("player: {%s}" % str(player)) result.append("type: %s" % Type.find_key(type)) result.append("state: %s" % State.find_key(state)) result.append("xp_value: %s" % xp_value) diff --git a/plugin/export_scripts_template/autoloads/leaderboards_client.gd b/plugin/export_scripts_template/autoloads/leaderboards_client.gd index 906e5f0..ae96633 100644 --- a/plugin/export_scripts_template/autoloads/leaderboards_client.gd +++ b/plugin/export_scripts_template/autoloads/leaderboards_client.gd @@ -166,7 +166,7 @@ class Score: var display_score: String ## Formatted string for the score of the player. var rank: int ## Rank of the player. var raw_score: int ## Raw score of the player. - #var score_holder: String ## The player object who holds the score. + var score_holder: PlayersClient.Player ## The player object who holds the score. var score_holder_display_name: String ## Formatted string for the name of the player. var score_holder_hi_res_image_uri: String ## Hi-res image of the player. var score_holder_icon_image_uri: String ## Icon image of the player. @@ -175,26 +175,16 @@ class Score: ## Constructor that creates a Score froma [Dictionary] containing the properties. func _init(dictionary: Dictionary) -> void: - if dictionary.has("displayRank"): - display_rank = dictionary.displayRank - if dictionary.has("displayScore"): - display_score = dictionary.displayScore - if dictionary.has("rank"): - rank = dictionary.rank - if dictionary.has("rawScore"): - raw_score = dictionary.rawScore - #if dictionary.has("scoreHolder"): - #score_holder = dictionary.scoreHolder - if dictionary.has("scoreHolderDisplayName"): - score_holder_display_name = dictionary.scoreHolderDisplayName - if dictionary.has("scoreHolderHiResImageUri"): - score_holder_hi_res_image_uri = dictionary.scoreHolderHiResImageUri - if dictionary.has("scoreHolderIconImageUri"): - score_holder_icon_image_uri = dictionary.scoreHolderIconImageUri - if dictionary.has("scoreTag"): - score_tag = dictionary.scoreTag - if dictionary.has("timestampMillis"): - timestamp_millis = dictionary.timestampMillis + if dictionary.has("displayRank"): display_rank = dictionary.displayRank + if dictionary.has("displayScore"): display_score = dictionary.displayScore + if dictionary.has("rank"): rank = dictionary.rank + if dictionary.has("rawScore"): raw_score = dictionary.rawScore + if dictionary.has("scoreHolder"): score_holder = PlayersClient.Player.new(dictionary.scoreHolder) + if dictionary.has("scoreHolderDisplayName"): score_holder_display_name = dictionary.scoreHolderDisplayName + if dictionary.has("scoreHolderHiResImageUri"): score_holder_hi_res_image_uri = dictionary.scoreHolderHiResImageUri + if dictionary.has("scoreHolderIconImageUri"): score_holder_icon_image_uri = dictionary.scoreHolderIconImageUri + if dictionary.has("scoreTag"): score_tag = dictionary.scoreTag + if dictionary.has("timestampMillis"): timestamp_millis = dictionary.timestampMillis func _to_string() -> String: var result := PackedStringArray() @@ -203,7 +193,7 @@ class Score: result.append("display_score: %s" % display_score) result.append("rank: %s" % rank) result.append("raw_score: %s" % raw_score) - #result.append("score_holder: %s" % str(score_holder) + result.append("score_holder: {%s}" % str(score_holder)) result.append("score_holder_display_name: %s" % score_holder_display_name) result.append("score_holder_hi_res_image_uri: %s" % score_holder_hi_res_image_uri) result.append("score_holder_icon_image_uri: %s" % score_holder_icon_image_uri) @@ -225,15 +215,10 @@ class Leaderboard: ## Constructor that creates a Leaderboard from a [Dictionary] containing the ## properties. func _init(dictionary: Dictionary) -> void: - if dictionary.has("leaderboardId"): - leaderboard_id = dictionary.leaderboardId - if dictionary.has("displayName"): - display_name = dictionary.displayName - if dictionary.has("iconImageUri"): - icon_image_uri = dictionary.iconImageUri - if dictionary.has("scoreOrder"): - score_order = ScoreOrder.get(dictionary.scoreOrder) - + if dictionary.has("leaderboardId"): leaderboard_id = dictionary.leaderboardId + if dictionary.has("displayName"): display_name = dictionary.displayName + if dictionary.has("iconImageUri"): icon_image_uri = dictionary.iconImageUri + if dictionary.has("scoreOrder"): score_order = ScoreOrder.get(dictionary.scoreOrder) if dictionary.has("variants"): for variant: Dictionary in dictionary.variants: variants.append(LeaderboardVariant.new(variant)) @@ -244,7 +229,7 @@ class Leaderboard: result.append("leaderboard_id: %s" % leaderboard_id) result.append("display_name: %s" % display_name) result.append("icon_image_uri: %s" % icon_image_uri) - result.append("score_order: %s" % score_order) + result.append("score_order: %s" % ScoreOrder.find_key(score_order)) for variant: LeaderboardVariant in variants: result.append("{%s}" % str(variant)) @@ -266,24 +251,15 @@ class LeaderboardVariant: ## Constructor that creates a LeaderboardVariant from a [Dictionary] containting ## the properties. func _init(dictionary: Dictionary) -> void: - if dictionary.has("displayPlayerRank"): - display_player_rank = dictionary.displayPlayerRank - if dictionary.has("displayPlayerScore"): - display_player_score = dictionary.displayPlayerScore - if dictionary.has("numScores"): - num_scores = dictionary.numScores - if dictionary.has("playerRank"): - player_rank = dictionary.playerRank - if dictionary.has("playerScoreTag"): - player_score_tag = dictionary.playerScoreTag - if dictionary.has("rawPlayerScore"): - raw_player_score = dictionary.rawPlayerScore - if dictionary.has("hasPlayerInfo"): - has_player_info = dictionary.hasPlayerInfo - if dictionary.has("collection"): - collection = Collection.get(dictionary.collection) - if dictionary.has("timeSpan"): - time_span = TimeSpan.get(dictionary.timeSpan) + if dictionary.has("displayPlayerRank"): display_player_rank = dictionary.displayPlayerRank + if dictionary.has("displayPlayerScore"): display_player_score = dictionary.displayPlayerScore + if dictionary.has("numScores"): num_scores = dictionary.numScores + if dictionary.has("playerRank"): player_rank = dictionary.playerRank + if dictionary.has("playerScoreTag"): player_score_tag = dictionary.playerScoreTag + if dictionary.has("rawPlayerScore"): raw_player_score = dictionary.rawPlayerScore + if dictionary.has("hasPlayerInfo"): has_player_info = dictionary.hasPlayerInfo + if dictionary.has("collection"): collection = Collection.get(dictionary.collection) + if dictionary.has("timeSpan"): time_span = TimeSpan.get(dictionary.timeSpan) func _to_string() -> String: var result := PackedStringArray() diff --git a/plugin/export_scripts_template/autoloads/players_client.gd b/plugin/export_scripts_template/autoloads/players_client.gd new file mode 100644 index 0000000..44f3256 --- /dev/null +++ b/plugin/export_scripts_template/autoloads/players_client.gd @@ -0,0 +1,186 @@ +extends Node +## Client with player functionality. +## +## This autoload exposes methods and signals to control the player information for +## the currently signed in player. + +## Signal emitted after calling the [method load_friends] method.[br] +## [br] +## [param friends]: An array containing the friends for the current player. +## The array will be empty if there was an error loading the friends list. +signal friends_loaded(friends: Array[Player]) + +## Friends list visibility statuses. +enum FriendsListVisibilityStatus { + FEATURE_UNAVAILABLE = 3, ## The friends list is currently unavailable for the game. + REQUEST_REQUIRED = 2, ## The friends list is not visible to the game, but the game can ask for access. + UNKNOWN = 0, ## Unknown if the friends list is visible to the game, or whether the game can ask for access from the user. + VISIBLE = 1 ## The friends list is currently visible to the game. +} + +## This player's friend status relative to the currently signed in player. +enum PlayerFriendStatus { + FRIEND = 4, ## The currently signed in player and this player are friends. + NO_RELATIONSHIP = 0, ## The currently signed in player is not a friend of this player. + UNKNOWN = -1 ## The currently signed in player's friend status with this player is unknown. +} + +func _ready() -> void: + _connect_signals() + +func _connect_signals() -> void: + if GodotPlayGameServices.android_plugin: + GodotPlayGameServices.android_plugin.friendsLoaded.connect(func(friends_json: String): + var safe_array := GodotPlayGameServices.json_marshaller.safe_parse_array(friends_json) + var friends: Array[Player] = [] + for dictionary: Dictionary in safe_array: + friends.append(Player.new(dictionary)) + + friends_loaded.emit(friends) + ) + +## Use this method and subscribe to the emitted signal to receive the list of friends +## for the current player.[br] +## [br] +## The method emits the [signal friends_loaded] signal.[br] +## [br] +## [param page_size]: The number of entries to request for this initial page.[br] +## [param force_reload]: If true, this call will clear any locally cached +## data and attempt to fetch the latest data from the server. Send it set to [code]true[/code] +## the first time, and [code]false[/code] in subsequent calls, or when you want +## to clear the cache.[br] +## [param ask_for_permission]: If the user has not granted access to their friends +## list, and this is set to true, a new window will open asking the user for permission +## to their friends list. +func load_friends(page_size: int, force_reload: bool, ask_for_permission: bool) -> void: + if GodotPlayGameServices.android_plugin: + GodotPlayGameServices.android_plugin.loadFriends( + page_size, + force_reload, + ask_for_permission + ) + +## Displays a screen where the user can see a comparison of their own profile +## against another player's profile.[br] +## [br] +## [param other_player_id]: The player ID of the player to compare with. +func compare_profile(other_player_id: String) -> void: + if GodotPlayGameServices.android_plugin: + GodotPlayGameServices.android_plugin.compareProfile(other_player_id) + +## Displays a screen where the user can see a comparison of their own profile +## against another player's profile.[br] +## [br] +## Should be used when the game has its own player names separate from the Play +## Games Services gamer tag. These names will be used in the profile display and +## only sent to the server if the player initiates a friend invitation to the +## profile being viewed, so that the sender and recipient have context relevant +## to their game experience.[br] +## [br] +## [param other_player_id]: The player ID of the player to compare with.[br] +## [param other_player_in_game_name]: The game's own display name of the player referred to by otherPlayerId.[br] +## [param current_player_in_game_name]: The game's own display name of the current player. +func compare_profile_with_alternative_name_hints( + other_player_id: String, + other_player_in_game_name: String, + current_player_in_game_name: String +) -> void: + if GodotPlayGameServices.android_plugin: + GodotPlayGameServices.android_plugin.compareProfileWithAlternativeNameHints( + other_player_id, + other_player_in_game_name, + current_player_in_game_name + ) + +## Player information. +class Player: + var banner_image_landscape_uri: String ## Banner image of the player in landscape. + var banner_image_portrait_uri: String ## Banner image of the player in portrait. + var friends_list_visibility_status: FriendsListVisibilityStatus ## Visibility status of this player's friend list. + var display_name: String ## The display name of the player. + var hi_res_image_uri: String ## The hi-res image of the player. + var level_info: PlayerLevelInfo ## Information about the player level. + var player_id: String ## The player id. + var friend_status: PlayerFriendStatus ## The friend status of this player with the signed in player. + var retrieved_timestamp: int ## The timestamp at which this player record was last updated locally. + var title: String ## The title of the player. + var has_hi_res_image: bool ## Whether this player has a hi-res profile image to display. + var has_icon_image: bool ## Whether this player has an icon-size profile image to display. + + func _init(dictionary: Dictionary) -> void: + if dictionary.has("bannerImageLandscapeUri"): banner_image_landscape_uri = dictionary.bannerImageLandscapeUri + if dictionary.has("bannerImagePortraitUri"): banner_image_portrait_uri = dictionary.bannerImagePortraitUri + if dictionary.has("friendsListVisibilityStatus"): friends_list_visibility_status = FriendsListVisibilityStatus.get(dictionary.friendsListVisibilityStatus) + if dictionary.has("displayName"): display_name = dictionary.displayName + if dictionary.has("hiResImageUri"): hi_res_image_uri = dictionary.hiResImageUri + if dictionary.has("levelInfo"): level_info = PlayerLevelInfo.new(dictionary.levelInfo) + if dictionary.has("playerId"): player_id = dictionary.playerId + if dictionary.has("friendStatus"): friend_status = PlayerFriendStatus.get(dictionary.friendStatus) + if dictionary.has("retrievedTimestamp"): retrieved_timestamp = dictionary.retrievedTimestamp + if dictionary.has("title"): title = dictionary.title + if dictionary.has("hasHiResImage"): has_hi_res_image = dictionary.hasHiResImage + if dictionary.has("hasIconImage"): has_icon_image = dictionary.hasIconImage + + func _to_string() -> String: + var result := PackedStringArray() + + result.append("banner_image_landscape_uri: %s" % banner_image_landscape_uri) + result.append("banner_image_portrait_uri: %s" % banner_image_portrait_uri) + result.append("friends_list_visibility_status: %s" % FriendsListVisibilityStatus.find_key(friends_list_visibility_status)) + result.append("display_name: %s" % display_name) + result.append("hi_res_image_uri: %s" % hi_res_image_uri) + result.append("level_info: {%s}" % str(level_info)) + result.append("player_id: %s" % player_id) + result.append("friend_status: %s" % PlayerFriendStatus.find_key(friend_status)) + result.append("retrieved_timestamp: %s" % retrieved_timestamp) + result.append("title: %s" % title) + result.append("has_hi_res_image: %s" % has_hi_res_image) + result.append("has_icon_image: %s" % has_icon_image) + + return ", ".join(result) + +## The current level information of a player. +class PlayerLevelInfo: + var current_level: PlayerLevel ## The player's current level object. + var current_xp_total: int ## The player's current XP value. + var last_level_up_timestamp: int ## The timestamp of the player's last level-up. + var next_level: PlayerLevel ## The player's next level object. + var is_max_level: bool ## True if the player reached the maximum level ([member PlayerLevelInfo.current_level] is the same as [member PlayerLevelInfo.next_level]). + + func _init(dictionary: Dictionary) -> void: + if dictionary.has("currentLevel"): current_level = PlayerLevel.new(dictionary.currentLevel) + if dictionary.has("currentXpTotal"): current_xp_total = dictionary.currentXpTotal + if dictionary.has("lastLevelUpTimestamp"): last_level_up_timestamp = dictionary.lastLevelUpTimestamp + if dictionary.has("nextLevel"): next_level = PlayerLevel.new(dictionary.nextLevel) + if dictionary.has("isMaxLevel"): is_max_level = dictionary.isMaxLevel + + func _to_string() -> String: + var result := PackedStringArray() + + result.append("current_level: {%s}" % str(current_level)) + result.append("current_xp_total: %s" % current_xp_total) + result.append("last_level_up_timestamp: %s" % last_level_up_timestamp) + result.append("next_level: {%s}" % str(next_level)) + result.append("is_max_level: %s" % is_max_level) + + return ", ".join(result) + +## The level of a player. +class PlayerLevel: + var level_number: int ## The number for this level. + var max_xp: int ## The maximum XP value represented by this level, exclusive. + var min_xp: int ## The minimum XP value needed to attain this level, inclusive. + + func _init(dictionary: Dictionary) -> void: + if dictionary.has("levelNumber"): level_number = dictionary.levelNumber + if dictionary.has("maxXp"): max_xp = dictionary.maxXp + if dictionary.has("minXp"): min_xp = dictionary.minXp + + func _to_string() -> String: + var result := PackedStringArray() + + result.append("level_number: %s" % level_number) + result.append("max_xp: %s" % max_xp) + result.append("min_xp: %s" % min_xp) + + return ", ".join(result) diff --git a/plugin/export_scripts_template/export_plugin.gd b/plugin/export_scripts_template/export_plugin.gd index 1fe7d45..ec56eca 100644 --- a/plugin/export_scripts_template/export_plugin.gd +++ b/plugin/export_scripts_template/export_plugin.gd @@ -5,6 +5,7 @@ const PLUGIN_AUTOLOAD := "GodotPlayGameServices" const SIGN_IN_AUTOLOAD := "SignInClient" const ACHIEVEMENTS_AUTOLOAD := "AchievementsClient" const LEADERBOARDS_AUTOLOAD := "LeaderboardsClient" +const PLAYERS_AUTOLOAD := "PlayersClient" var _export_plugin : AndroidExportPlugin var _dock : Node @@ -41,12 +42,14 @@ func _add_autoloads() -> void: add_autoload_singleton(SIGN_IN_AUTOLOAD, "res://addons/GodotPlayGameServices/autoloads/sign_in_client.gd") add_autoload_singleton(ACHIEVEMENTS_AUTOLOAD, "res://addons/GodotPlayGameServices/autoloads/achievements_client.gd") add_autoload_singleton(LEADERBOARDS_AUTOLOAD, "res://addons/GodotPlayGameServices/autoloads/leaderboards_client.gd") + add_autoload_singleton(PLAYERS_AUTOLOAD, "res://addons/GodotPlayGameServices/autoloads/players_client.gd") func _remove_autoloads() -> void: remove_autoload_singleton(PLUGIN_AUTOLOAD) remove_autoload_singleton(SIGN_IN_AUTOLOAD) remove_autoload_singleton(ACHIEVEMENTS_AUTOLOAD) remove_autoload_singleton(LEADERBOARDS_AUTOLOAD) + remove_autoload_singleton(PLAYERS_AUTOLOAD) class AndroidExportPlugin extends EditorExportPlugin: var _plugin_name = "GodotPlayGameServices" diff --git a/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/GodotAndroidPlugin.kt b/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/GodotAndroidPlugin.kt index 10dc733..9ea429e 100644 --- a/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/GodotAndroidPlugin.kt +++ b/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/GodotAndroidPlugin.kt @@ -3,7 +3,7 @@ package com.jacobibanez.plugin.android.godotplaygameservices import android.util.Log import com.google.android.gms.games.PlayGamesSdk import com.jacobibanez.plugin.android.godotplaygameservices.achievements.AchievementsProxy -import com.jacobibanez.plugin.android.godotplaygameservices.friends.FriendsProxy +import com.jacobibanez.plugin.android.godotplaygameservices.players.PlayersProxy import com.jacobibanez.plugin.android.godotplaygameservices.leaderboards.LeaderboardsProxy import com.jacobibanez.plugin.android.godotplaygameservices.signals.getSignals import com.jacobibanez.plugin.android.godotplaygameservices.signin.SignInProxy @@ -24,7 +24,7 @@ class GodotAndroidPlugin(godot: Godot) : GodotPlugin(godot) { private val signInProxy = SignInProxy(godot) private val achievementsProxy = AchievementsProxy(godot) private val leaderboardsProxy = LeaderboardsProxy(godot) - private val friendsProxy = FriendsProxy(godot) + private val playersProxy = PlayersProxy(godot) /** @suppress */ override fun getPluginSignals(): MutableSet { @@ -221,19 +221,49 @@ class GodotAndroidPlugin(godot: Godot) : GodotPlugin(godot) { fun loadLeaderboard(leaderboardId: String, forceReload: Boolean) = leaderboardsProxy.loadLeaderboard(leaderboardId, forceReload) + /** + * Call this method and subscribe to the emitted signal to receive the list of friends for the + * currently signed in player in JSON format. The JSON received from the [com.jacobibanez.plugin.android.godotplaygameservices.signals.PlayerSignals.friendsLoaded] + * signal, contains a list of elements representing the [com.google.android.gms.games.Player](https://developers.google.com/android/reference/com/google/android/gms/games/Player) class. + * + * @param pageSize The number of entries to request for this initial page. + * @param forceReload If true, this call will clear any locally cached data and attempt to fetch + * the latest data from the server. + * @param askForPermission If the user has not granted access to their friends list, and this + * is set to true, a new window will open asking the user for permission to their friends list. + */ @UsedByGodot - fun loadFriends(pageSize: Int, forceReload: Boolean) = - friendsProxy.loadFriends(pageSize, forceReload) + fun loadFriends(pageSize: Int, forceReload: Boolean, askForPermission: Boolean) = + playersProxy.loadFriends(pageSize, forceReload, askForPermission) + /** + * Displays a screen where the user can see a comparison of their own profile against another + * player's profile. + * + * @param otherPlayerId The player ID of the player to compare with. + */ @UsedByGodot - fun compareProfile(otherPlayerId: String) = friendsProxy.compareProfile(otherPlayerId) + fun compareProfile(otherPlayerId: String) = playersProxy.compareProfile(otherPlayerId) + /** + * Displays a screen where the user can see a comparison of their own profile against another + * player's profile. + * + * Should be used when the game has its own player names separate from the Play Games Services + * gamer tag. These names will be used in the profile display and only sent to the server if the + * player initiates a friend invitation to the profile being viewed, so that the sender and + * recipient have context relevant to their game experience. + * + * @param otherPlayerId The player ID of the player to compare with. + * @param otherPlayerInGameName The game's own display name of the player referred to by otherPlayerId. + * @param currentPlayerInGameName The game's own display name of the current player. + */ @UsedByGodot fun compareProfileWithAlternativeNameHints( otherPlayerId: String, otherPlayerInGameName: String, currentPlayerInGameName: String - ) = friendsProxy.compareProfileWithAlternativeNameHints( + ) = playersProxy.compareProfileWithAlternativeNameHints( otherPlayerId, otherPlayerInGameName, currentPlayerInGameName diff --git a/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/achievements/AchievementMapper.kt b/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/achievements/AchievementMapper.kt index a8b1498..0fe5b48 100644 --- a/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/achievements/AchievementMapper.kt +++ b/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/achievements/AchievementMapper.kt @@ -2,7 +2,7 @@ package com.jacobibanez.plugin.android.godotplaygameservices.achievements import com.google.android.gms.games.achievement.Achievement import com.google.android.gms.games.achievement.Achievement.TYPE_INCREMENTAL -import com.jacobibanez.plugin.android.godotplaygameservices.friends.fromPlayer +import com.jacobibanez.plugin.android.godotplaygameservices.players.fromPlayer import org.godotengine.godot.Dictionary /** @suppress */ diff --git a/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/achievements/AchievementsProxy.kt b/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/achievements/AchievementsProxy.kt index 15f2d46..addb5cf 100644 --- a/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/achievements/AchievementsProxy.kt +++ b/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/achievements/AchievementsProxy.kt @@ -62,7 +62,7 @@ class AchievementsProxy( if (task.isSuccessful) { Log.d( tag, - "Achievements loaded successfully. Achievements are stale? ${task.result.isStale}" + "Achievements loaded successfully. Data is stale? ${task.result.isStale}" ) val safeBuffer: AchievementBuffer = task.result.get()!! val achievementsCount = safeBuffer.count diff --git a/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/friends/PlayerMapper.kt b/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/friends/PlayerMapper.kt deleted file mode 100644 index ac0d7cf..0000000 --- a/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/friends/PlayerMapper.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.jacobibanez.plugin.android.godotplaygameservices.friends - -import com.google.android.gms.games.Player -import com.google.android.gms.games.Player.FriendsListVisibilityStatus.FEATURE_UNAVAILABLE -import com.google.android.gms.games.Player.FriendsListVisibilityStatus.REQUEST_REQUIRED -import com.google.android.gms.games.Player.FriendsListVisibilityStatus.UNKNOWN -import com.google.android.gms.games.Player.FriendsListVisibilityStatus.VISIBLE -import com.google.android.gms.games.Player.PlayerFriendStatus.FRIEND -import com.google.android.gms.games.Player.PlayerFriendStatus.NO_RELATIONSHIP -import com.google.android.gms.games.Player.PlayerFriendStatus.UNKNOWN as FRIEND_STATUS_UNKNOWN -import com.google.android.gms.games.PlayerLevel -import com.google.android.gms.games.PlayerLevelInfo -import org.godotengine.godot.Dictionary - -fun fromPlayer(player: Player?): Dictionary = if (player != null) { - Dictionary().apply { - put("bannerImageLandscapeUri", player.bannerImageLandscapeUri?.toString()) - put("bannerImagePortraitUri", player.bannerImagePortraitUri?.toString()) - put( - "friendsListVisibilityStatus", if (player.currentPlayerInfo != null) { - getVisibilityStatus(player.currentPlayerInfo!!.friendsListVisibilityStatus) - } else { - "" - } - ) - put("displayName", player.displayName) - put("hiResImageUri", player.hiResImageUri?.toString()) - put("levelInfo", fromPlayerLevelInfo(player.levelInfo)) - put("playerId", player.playerId) - put( - "friendStatus", if (player.relationshipInfo != null) { - getFriendStatus(player.relationshipInfo!!.friendStatus) - } else { - "" - } - ) - put("retrievedTimestamp", player.retrievedTimestamp) - put("title", player.title) - put("hasHiResImage", player.hasHiResImage()) - put("hasIconImage", player.hasIconImage()) - } -} else { - Dictionary() -} - -fun fromPlayerLevelInfo(levelInfo: PlayerLevelInfo?): Dictionary = if (levelInfo != null) { - Dictionary().apply { - put("currentLevel", fromPlayerLevel(levelInfo.currentLevel)) - put("currentXpTotal", levelInfo.currentXpTotal) - put("lastLevelUpTimestamp", levelInfo.lastLevelUpTimestamp) - put("nextLevel", fromPlayerLevel(levelInfo.nextLevel)) - put("isMaxLevel", levelInfo.isMaxLevel) - } -} else { - Dictionary() -} - -fun fromPlayerLevel(playerLevel: PlayerLevel?): Dictionary = if (playerLevel != null) { - Dictionary().apply { - put("levelNumber", playerLevel.levelNumber) - put("maxXp", playerLevel.maxXp) - put("minXp", playerLevel.minXp) - } -} else { - Dictionary() -} - -private fun getVisibilityStatus(state: Int): String = when (state) { - FEATURE_UNAVAILABLE -> "FEATURE_UNAVAILABLE" - REQUEST_REQUIRED -> "REQUEST_REQUIRED" - UNKNOWN -> "UNKNOWN" - VISIBLE -> "VISIBLE" - else -> "" -} - -private fun getFriendStatus(state: Int): String = when (state) { - FRIEND -> "FRIEND" - NO_RELATIONSHIP -> "NO_RELATIONSHIP" - FRIEND_STATUS_UNKNOWN -> "UNKNOWN" - else -> "" -} \ No newline at end of file diff --git a/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/leaderboards/LeaderboardMapper.kt b/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/leaderboards/LeaderboardMapper.kt index f06ddbe..a2d8909 100644 --- a/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/leaderboards/LeaderboardMapper.kt +++ b/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/leaderboards/LeaderboardMapper.kt @@ -3,7 +3,7 @@ package com.jacobibanez.plugin.android.godotplaygameservices.leaderboards import com.google.android.gms.games.leaderboard.Leaderboard import com.google.android.gms.games.leaderboard.LeaderboardScore import com.google.android.gms.games.leaderboard.LeaderboardVariant -import com.jacobibanez.plugin.android.godotplaygameservices.friends.fromPlayer +import com.jacobibanez.plugin.android.godotplaygameservices.players.fromPlayer import org.godotengine.godot.Dictionary /** @suppress */ @@ -12,7 +12,11 @@ fun fromLeaderboardScore(score: LeaderboardScore): Dictionary = Dictionary().app put("displayScore", score.displayScore) put("rank", score.rank) put("rawScore", score.rawScore) - put("scoreHolder", fromPlayer(score.scoreHolder)) + + score.scoreHolder?.let { + put("scoreHolder", fromPlayer(it)) + } + put("scoreHolderDisplayName", score.scoreHolderDisplayName) put("scoreHolderHiResImageUri", score.scoreHolderHiResImageUri?.toString()) put("scoreHolderIconImageUri", score.scoreHolderIconImageUri?.toString()) diff --git a/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/players/Enums.kt b/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/players/Enums.kt new file mode 100644 index 0000000..d931551 --- /dev/null +++ b/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/players/Enums.kt @@ -0,0 +1,83 @@ +package com.jacobibanez.plugin.android.godotplaygameservices.players + +import com.google.android.gms.games.Player + +/** + * Friends list visibility statuses. + */ +enum class FriendsListVisibilityStatus( + /** + * Integer representation of the statuses, taken from [com.google.android.gms.games.Player.FriendsListVisibilityStatus](https://developers.google.com/android/reference/com/google/android/gms/games/Player.FriendsListVisibilityStatus) + */ + val status: Int +) { + /** + * The friends list is currently unavailable for the game. + */ + FEATURE_UNAVAILABLE(Player.FriendsListVisibilityStatus.FEATURE_UNAVAILABLE), + + /** + * The friends list is not visible to the game, but the game can ask for access. + */ + REQUEST_REQUIRED(Player.FriendsListVisibilityStatus.REQUEST_REQUIRED), + + /** + * Unknown if the friends list is visible to the game, or whether the game can ask for access from the user. + */ + UNKNOWN(Player.FriendsListVisibilityStatus.UNKNOWN), + + /** + * The friends list is currently visible to the game. + */ + VISIBLE(Player.FriendsListVisibilityStatus.VISIBLE); + + companion object { + /** + * Returns a [FriendsListVisibilityStatus] given its integer representation, or null if the type is invalid. + */ + fun fromStatus(type: Int): FriendsListVisibilityStatus? = when (type) { + Player.FriendsListVisibilityStatus.FEATURE_UNAVAILABLE -> FEATURE_UNAVAILABLE + Player.FriendsListVisibilityStatus.REQUEST_REQUIRED -> REQUEST_REQUIRED + Player.FriendsListVisibilityStatus.UNKNOWN -> UNKNOWN + Player.FriendsListVisibilityStatus.VISIBLE -> VISIBLE + else -> null + } + } +} + +/** + * This player's friend status relative to the currently signed in player. + */ +enum class PlayerFriendStatus( + /** + * Integer representation of the status, taken from [com.google.android.gms.games.Player.PlayerFriendStatus](https://developers.google.com/android/reference/com/google/android/gms/games/PlayerRelationshipInfo#getFriendStatus()) + */ + val status: Int +) { + /** + * The currently signed in player and this player are friends. + */ + FRIEND(Player.PlayerFriendStatus.FRIEND), + + /** + * The currently signed in player is not a friend of this player. + */ + NO_RELATIONSHIP(Player.PlayerFriendStatus.NO_RELATIONSHIP), + + /** + * The currently signed in player's friend status with this player is unknown. + */ + UNKNOWN(Player.PlayerFriendStatus.UNKNOWN); + + companion object { + /** + * Returns a [PlayerFriendStatus] given its integer representation, or null if the type is invalid. + */ + fun fromStatus(type: Int): PlayerFriendStatus? = when (type) { + Player.PlayerFriendStatus.FRIEND -> FRIEND + Player.PlayerFriendStatus.NO_RELATIONSHIP -> NO_RELATIONSHIP + Player.PlayerFriendStatus.UNKNOWN -> UNKNOWN + else -> null + } + } +} \ No newline at end of file diff --git a/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/players/PlayerMapper.kt b/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/players/PlayerMapper.kt new file mode 100644 index 0000000..7e8b535 --- /dev/null +++ b/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/players/PlayerMapper.kt @@ -0,0 +1,49 @@ +package com.jacobibanez.plugin.android.godotplaygameservices.players + +import com.google.android.gms.games.Player +import com.google.android.gms.games.PlayerLevel +import com.google.android.gms.games.PlayerLevelInfo +import org.godotengine.godot.Dictionary + +/** @suppress */ +fun fromPlayer(player: Player): Dictionary = Dictionary().apply { + put("bannerImageLandscapeUri", player.bannerImageLandscapeUri?.toString()) + put("bannerImagePortraitUri", player.bannerImagePortraitUri?.toString()) + put("displayName", player.displayName) + put("hiResImageUri", player.hiResImageUri?.toString()) + put("playerId", player.playerId) + put("retrievedTimestamp", player.retrievedTimestamp) + put("title", player.title) + put("hasHiResImage", player.hasHiResImage()) + put("hasIconImage", player.hasIconImage()) + player.levelInfo?.let { + put("levelInfo", fromPlayerLevelInfo(it)) + } + player.currentPlayerInfo?.let { currentPlayerInfo -> + FriendsListVisibilityStatus.fromStatus(currentPlayerInfo.friendsListVisibilityStatus) + ?.let { + put("friendsListVisibilityStatus", it.name) + } + } + player.relationshipInfo?.let { relationshipInfo -> + PlayerFriendStatus.fromStatus(relationshipInfo.friendStatus)?.let { + put("friendStatus", it.name) + } + } +} + +/** @suppress */ +fun fromPlayerLevelInfo(levelInfo: PlayerLevelInfo): Dictionary = Dictionary().apply { + put("currentLevel", fromPlayerLevel(levelInfo.currentLevel)) + put("currentXpTotal", levelInfo.currentXpTotal) + put("lastLevelUpTimestamp", levelInfo.lastLevelUpTimestamp) + put("nextLevel", fromPlayerLevel(levelInfo.nextLevel)) + put("isMaxLevel", levelInfo.isMaxLevel) +} + +/** @suppress */ +fun fromPlayerLevel(playerLevel: PlayerLevel): Dictionary = Dictionary().apply { + put("levelNumber", playerLevel.levelNumber) + put("maxXp", playerLevel.maxXp) + put("minXp", playerLevel.minXp) +} \ No newline at end of file diff --git a/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/friends/FriendsProxy.kt b/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/players/PlayersProxy.kt similarity index 65% rename from plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/friends/FriendsProxy.kt rename to plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/players/PlayersProxy.kt index 35cf0d5..a235ab1 100644 --- a/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/friends/FriendsProxy.kt +++ b/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/players/PlayersProxy.kt @@ -1,45 +1,65 @@ -package com.jacobibanez.plugin.android.godotplaygameservices.friends +package com.jacobibanez.plugin.android.godotplaygameservices.players import android.util.Log import androidx.core.app.ActivityCompat -import com.google.android.gms.games.AnnotatedData import com.google.android.gms.games.FriendsResolutionRequiredException import com.google.android.gms.games.PlayGames -import com.google.android.gms.games.PlayerBuffer import com.google.android.gms.games.PlayersClient -import com.google.android.gms.tasks.Task import com.google.gson.Gson import com.jacobibanez.plugin.android.godotplaygameservices.BuildConfig -import com.jacobibanez.plugin.android.godotplaygameservices.signals.FriendSignals.loadFriendsFailure -import com.jacobibanez.plugin.android.godotplaygameservices.signals.FriendSignals.loadFriendsSuccess +import com.jacobibanez.plugin.android.godotplaygameservices.signals.PlayerSignals.friendsLoaded import org.godotengine.godot.Dictionary import org.godotengine.godot.Godot import org.godotengine.godot.plugin.GodotPlugin.emitSignal -class FriendsProxy( +/** @suppress */ +class PlayersProxy( private val godot: Godot, private val playersClient: PlayersClient = PlayGames.getPlayersClient(godot.getActivity()!!) ) { - private val tag: String = FriendsProxy::class.java.simpleName + private val tag: String = PlayersProxy::class.java.simpleName private val showSharingFriendsConsentRequestCode = 9006 private val compareProfileRequestCode = 9007 private val compareProfileWithAlternativeNameHintsRequestCode = 9008 - fun loadFriends(pageSize: Int, forceReload: Boolean) { + fun loadFriends(pageSize: Int, forceReload: Boolean, askForPermission: Boolean) { Log.d(tag, "Loading friends with page size of $pageSize and forceReload = $forceReload") playersClient.loadFriends(pageSize, forceReload).addOnCompleteListener { task -> if (task.isSuccessful) { - emitListOfFriends(task) + Log.d( + tag, + "Friends loaded. Data is stale? ${task.result.isStale}" + ) + val safeBuffer = task.result.get()!! + val friendsCount = safeBuffer.count + val friends: List = + if (friendsCount > 0) { + safeBuffer.map { fromPlayer(it) }.toList() + } else { + emptyList() + } + + emitSignal( + godot, + BuildConfig.GODOT_PLUGIN_NAME, + friendsLoaded, + Gson().toJson(friends) + ) } else { - if (task.exception is FriendsResolutionRequiredException) { - handleAskForFriendsListPermission(task) + if (task.exception is FriendsResolutionRequiredException && askForPermission) { + askForFriendsListPermission(task.exception as FriendsResolutionRequiredException) } else { Log.e( tag, "Unable to load friends. Cause: ${task.exception}", task.exception ) - emitSignal(godot, BuildConfig.GODOT_PLUGIN_NAME, loadFriendsFailure) + emitSignal( + godot, + BuildConfig.GODOT_PLUGIN_NAME, + friendsLoaded, + Gson().toJson(emptyList()) + ) } } } @@ -81,25 +101,8 @@ class FriendsProxy( } } - private fun emitListOfFriends(task: Task>) { - Log.d( - tag, - "Friends loaded. Friends are stale? ${task.result.isStale}" - ) - val friendsCount = task.result.get()!!.count - val friends: List = - if (task.result.get() != null && friendsCount > 0) { - task.result.get()!!.map { fromPlayer(it) }.toList() - } else { - emptyList() - } - - emitSignal(godot, BuildConfig.GODOT_PLUGIN_NAME, loadFriendsSuccess, Gson().toJson(friends)) - } - - private fun handleAskForFriendsListPermission(task: Task>) { - val pendingIntent = - (task.exception as FriendsResolutionRequiredException).resolution + private fun askForFriendsListPermission(exception: FriendsResolutionRequiredException) { + val pendingIntent = exception.resolution godot.getActivity()!!.startIntentSenderForResult( pendingIntent.intentSender, showSharingFriendsConsentRequestCode, diff --git a/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/signals/Signals.kt b/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/signals/Signals.kt index 5085986..d3904cb 100644 --- a/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/signals/Signals.kt +++ b/plugin/src/main/java/com/jacobibanez/plugin/android/godotplaygameservices/signals/Signals.kt @@ -16,12 +16,11 @@ fun getSignals(): MutableSet = mutableSetOf( LeaderboardSignals.allLeaderboardsLoaded, LeaderboardSignals.leaderboardLoaded, - FriendSignals.loadFriendsSuccess, - FriendSignals.loadFriendsFailure + PlayerSignals.friendsLoaded, ) /** - * Signals emitted by Sign In methods + * Signals emitted by Sign In methods. */ object SignInSignals { /** @@ -41,7 +40,7 @@ object SignInSignals { } /** - * Signals emitted by Achievements methods + * Signals emitted by Achievements methods. */ object AchievementsSignals { /** @@ -70,7 +69,7 @@ object AchievementsSignals { } /** - * Signals emitted by Leaderboards methods + * Signals emitted by Leaderboards methods. */ object LeaderboardSignals { /** @@ -103,9 +102,13 @@ object LeaderboardSignals { } /** - * Signals emitted by Friends methods + * Signals emitted by Players methods. */ -object FriendSignals { - val loadFriendsSuccess = SignalInfo("loadFriendsSuccess", String::class.java) - val loadFriendsFailure = SignalInfo("loadFriendsFailure") +object PlayerSignals { + /** + * This signal is emitted when calling the [com.jacobibanez.plugin.android.godotplaygameservices.GodotAndroidPlugin.loadFriends] method. + * + * @return A JSON with a list of [com.google.android.gms.games.Player](https://developers.google.com/android/reference/com/google/android/gms/games/Player). + */ + val friendsLoaded = SignalInfo("friendsLoaded", String::class.java) }