diff --git a/plugin/demo/MainMenu.gd b/plugin/demo/MainMenu.gd
deleted file mode 100644
index ebfd999..0000000
--- a/plugin/demo/MainMenu.gd
+++ /dev/null
@@ -1,8 +0,0 @@
-extends Control
-
-@onready var achievements_button: Button = %Achievements
-
-func _ready() -> void:
- achievements_button.pressed.connect(func():
- get_tree().change_scene_to_file("res://Achievements.tscn")
- )
diff --git a/plugin/demo/icon.svg b/plugin/demo/icon.svg
index b370ceb..fc5ea79 100644
--- a/plugin/demo/icon.svg
+++ b/plugin/demo/icon.svg
@@ -1 +1,193 @@
-
+
+
+
+
diff --git a/plugin/demo/launcher_icon.png b/plugin/demo/launcher_icon.png
new file mode 100644
index 0000000..bcf3672
Binary files /dev/null and b/plugin/demo/launcher_icon.png differ
diff --git a/plugin/demo/launcher_icon.svg b/plugin/demo/launcher_icon.svg
new file mode 100644
index 0000000..9c72040
--- /dev/null
+++ b/plugin/demo/launcher_icon.svg
@@ -0,0 +1,171 @@
+
+
+
+
diff --git a/plugin/demo/project.godot b/plugin/demo/project.godot
index 139842b..3adc1dd 100644
--- a/plugin/demo/project.godot
+++ b/plugin/demo/project.godot
@@ -11,15 +11,16 @@ config_version=5
[application]
config/name="Google Play Game Services Godot Plugin"
-run/main_scene="res://MainMenu.tscn"
+run/main_scene="res://scenes/MainMenu.tscn"
config/features=PackedStringArray("4.2", "Mobile")
-config/icon="res://icon.png"
+config/icon="res://icon.svg"
[autoload]
GodotPlayGameServices="*res://addons/GodotPlayGameServices/autoloads/godot_play_game_services.gd"
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"
[display]
@@ -31,6 +32,10 @@ window/handheld/orientation=1
enabled=PackedStringArray("res://addons/GodotPlayGameServices/plugin.cfg")
+[input_devices]
+
+pointing/emulate_touch_from_mouse=true
+
[rendering]
renderer/rendering_method="mobile"
diff --git a/plugin/demo/scenes/MainMenu.gd b/plugin/demo/scenes/MainMenu.gd
new file mode 100644
index 0000000..372c5dd
--- /dev/null
+++ b/plugin/demo/scenes/MainMenu.gd
@@ -0,0 +1,12 @@
+extends Control
+
+@onready var achievements_button: Button = %Achievements
+@onready var leaderboards_button: Button = %Leaderboards
+
+func _ready() -> void:
+ achievements_button.pressed.connect(func():
+ get_tree().change_scene_to_file("res://scenes/achievements/Achievements.tscn")
+ )
+ leaderboards_button.pressed.connect(func():
+ get_tree().change_scene_to_file("res://scenes/leaderboards/Leaderboards.tscn")
+ )
diff --git a/plugin/demo/MainMenu.tscn b/plugin/demo/scenes/MainMenu.tscn
similarity index 59%
rename from plugin/demo/MainMenu.tscn
rename to plugin/demo/scenes/MainMenu.tscn
index f10fc15..576a7f6 100644
--- a/plugin/demo/MainMenu.tscn
+++ b/plugin/demo/scenes/MainMenu.tscn
@@ -1,9 +1,9 @@
[gd_scene load_steps=3 format=3 uid="uid://bxnmbeo2w51s3"]
-[ext_resource type="Script" path="res://MainMenu.gd" id="1_smv14"]
+[ext_resource type="Script" path="res://scenes/MainMenu.gd" id="1_smv14"]
[ext_resource type="Theme" uid="uid://bmm3mvq11y045" path="res://theme.tres" id="2_aajnr"]
-[node name="Control" type="Control"]
+[node name="MainMenu" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
@@ -26,11 +26,27 @@ 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="Title" type="Label" parent="MarginContainer/VBoxContainer/NavBar"]
+layout_mode = 2
+size_flags_horizontal = 6
+theme = ExtResource("2_aajnr")
+text = "Main Menu"
[node name="Achievements" type="Button" parent="MarginContainer/VBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(500, 200)
layout_mode = 2
-size_flags_vertical = 2
theme = ExtResource("2_aajnr")
text = "Achievements"
+
+[node name="Leaderboards" type="Button" parent="MarginContainer/VBoxContainer"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(500, 200)
+layout_mode = 2
+theme = ExtResource("2_aajnr")
+text = "Leaderboads"
diff --git a/plugin/demo/Achievements.gd b/plugin/demo/scenes/achievements/Achievements.gd
similarity index 53%
rename from plugin/demo/Achievements.gd
rename to plugin/demo/scenes/achievements/Achievements.gd
index ecb5863..bbda4b3 100644
--- a/plugin/demo/Achievements.gd
+++ b/plugin/demo/scenes/achievements/Achievements.gd
@@ -4,23 +4,24 @@ extends Control
@onready var show_achievements_button: Button = %ShowAchievements
@onready var reveal_achievements_button: Button = %RevealAchievements
-var achievements: Array[AchievementsClient.Achievement] = []
+var _achievements_cache: Array[AchievementsClient.Achievement] = []
func _ready() -> void:
- AchievementsClient.achievements_loaded.connect(func(_achievements: Array[AchievementsClient.Achievement]):
- self.achievements = _achievements
- )
- if achievements.is_empty():
+ if _achievements_cache.is_empty():
AchievementsClient.load_achievements(true)
+ AchievementsClient.achievements_loaded.connect(
+ func cache(achievements: Array[AchievementsClient.Achievement]):
+ _achievements_cache = achievements
+ )
back_button.pressed.connect(func():
- get_tree().change_scene_to_file("res://MainMenu.tscn")
+ get_tree().change_scene_to_file("res://scenes/MainMenu.tscn")
)
show_achievements_button.pressed.connect(func():
AchievementsClient.show_achievements()
)
reveal_achievements_button.pressed.connect(func():
- for achievement: AchievementsClient.Achievement in achievements:
- if achievement.state == AchievementsClient.Achievement.State.STATE_HIDDEN:
+ for achievement: AchievementsClient.Achievement in _achievements_cache:
+ if achievement.state == AchievementsClient.State.STATE_HIDDEN:
AchievementsClient.reveal_achievement(achievement.achievement_id)
)
diff --git a/plugin/demo/Achievements.tscn b/plugin/demo/scenes/achievements/Achievements.tscn
similarity index 94%
rename from plugin/demo/Achievements.tscn
rename to plugin/demo/scenes/achievements/Achievements.tscn
index b0c3905..3a05a07 100644
--- a/plugin/demo/Achievements.tscn
+++ b/plugin/demo/scenes/achievements/Achievements.tscn
@@ -1,7 +1,7 @@
[gd_scene load_steps=3 format=3 uid="uid://d4gluloo7edt5"]
[ext_resource type="Theme" uid="uid://bmm3mvq11y045" path="res://theme.tres" id="1_a4mr7"]
-[ext_resource type="Script" path="res://Achievements.gd" id="1_h3sdc"]
+[ext_resource type="Script" path="res://scenes/achievements/Achievements.gd" id="1_h3sdc"]
[node name="Achievements" type="Control"]
layout_mode = 3
diff --git a/plugin/demo/scenes/leaderboards/LeaderboardDisplay.gd b/plugin/demo/scenes/leaderboards/LeaderboardDisplay.gd
new file mode 100644
index 0000000..dda69af
--- /dev/null
+++ b/plugin/demo/scenes/leaderboards/LeaderboardDisplay.gd
@@ -0,0 +1,112 @@
+extends Control
+
+@onready var leaderboard_id_label: Label = %LeaderboardId
+@onready var leaderboard_name_label: Label = %LeaderboardName
+
+@onready var player_rank_label: Label = %PlayerRank
+@onready var player_score_label: Label = %PlayerScore
+
+@onready var new_score_line_edit: LineEdit = %NewScore
+@onready var submit_score_button: Button = %SubmitScore
+
+@onready var time_span_option: OptionButton = %TimeSpan
+@onready var collection_option: OptionButton = %Collection
+@onready var show_variant_button: Button = %ShowVariant
+
+var leaderboard: LeaderboardsClient.Leaderboard
+
+const _EMPTY_SCORE := -1
+
+var _score: LeaderboardsClient.Score
+var _new_raw_score := _EMPTY_SCORE
+var _selected_time_span: LeaderboardsClient.TimeSpan
+var _selected_collection: LeaderboardsClient.Collection
+
+func _ready() -> void:
+ if leaderboard:
+ leaderboard_id_label.text = leaderboard.leaderboard_id
+ leaderboard_name_label.text = leaderboard.display_name
+ _set_up_display_score()
+ _set_up_submit_score()
+ _set_up_variants()
+
+func _set_up_display_score() -> void:
+ if _score == null:
+ _load_player_score()
+ LeaderboardsClient.score_loaded.connect(func(leaderboard_id: String, score: LeaderboardsClient.Score):
+ if leaderboard_id == leaderboard.leaderboard_id:
+ _score = score
+ _refresh_score_data()
+ )
+
+func _set_up_submit_score() -> void:
+ LeaderboardsClient.score_submitted.connect(
+ func refresh_score(is_submitted: bool, leaderboard_id: String):
+ if is_submitted and leaderboard_id == leaderboard.leaderboard_id:
+ _load_player_score()
+ )
+ new_score_line_edit.text_changed.connect(
+ func validate_and_refresh_button(new_text: String):
+ if new_text.is_valid_int():
+ _new_raw_score = new_text.to_int()
+ else:
+ _new_raw_score = _EMPTY_SCORE
+
+ _refresh_submit_score_button()
+ )
+ submit_score_button.pressed.connect(func():
+ if _new_raw_score:
+ LeaderboardsClient.submit_score(leaderboard.leaderboard_id, _new_raw_score)
+ )
+
+func _set_up_variants() -> void:
+ for timeSpan: String in LeaderboardsClient.TimeSpan.keys():
+ time_span_option.add_item(timeSpan)
+ for collection: String in LeaderboardsClient.Collection.keys():
+ collection_option.add_item(collection)
+
+ _selected_time_span = time_span_option.selected as LeaderboardsClient.TimeSpan
+ _selected_collection = collection_option.selected as LeaderboardsClient.Collection
+
+ time_span_option.item_selected.connect(func(index: int):
+ var selected_option := time_span_option.get_item_text(index)
+ var new_time_span: LeaderboardsClient.TimeSpan = LeaderboardsClient\
+ .TimeSpan[selected_option]
+ _selected_time_span = new_time_span
+ )
+ collection_option.item_selected.connect(func(index: int):
+ var selected_option := collection_option.get_item_text(index)
+ var new_collection: LeaderboardsClient.Collection = LeaderboardsClient\
+ .Collection[selected_option]
+ _selected_collection = new_collection
+ )
+
+ show_variant_button.disabled = false
+ show_variant_button.pressed.connect(func():
+ LeaderboardsClient.show_leaderboard_for_time_span_and_collection(
+ leaderboard.leaderboard_id,
+ _selected_time_span,
+ _selected_collection
+ )
+ )
+
+func _load_player_score() -> void:
+ LeaderboardsClient.load_player_score(
+ leaderboard.leaderboard_id,
+ LeaderboardsClient.TimeSpan.TIME_SPAN_ALL_TIME,
+ LeaderboardsClient.Collection.COLLECTION_PUBLIC
+ )
+
+func _refresh_score_data() -> void:
+ if _score:
+ player_rank_label.text = _score.display_rank
+ player_score_label.text = _score.display_score
+
+func _refresh_submit_score_button() -> void:
+ if _new_raw_score == _EMPTY_SCORE:
+ submit_score_button.text = "Submit to score"
+ submit_score_button.disabled = true
+ else:
+ submit_score_button.text = "Submit %s to score" % _new_raw_score
+ submit_score_button.disabled = false
+
diff --git a/plugin/demo/scenes/leaderboards/LeaderboardDisplay.tscn b/plugin/demo/scenes/leaderboards/LeaderboardDisplay.tscn
new file mode 100644
index 0000000..eb76c2a
--- /dev/null
+++ b/plugin/demo/scenes/leaderboards/LeaderboardDisplay.tscn
@@ -0,0 +1,140 @@
+[gd_scene load_steps=3 format=3 uid="uid://sf02uyky2w1b"]
+
+[ext_resource type="Theme" uid="uid://bmm3mvq11y045" path="res://theme.tres" id="1_oo2te"]
+[ext_resource type="Script" path="res://scenes/leaderboards/LeaderboardDisplay.gd" id="2_13i4k"]
+
+[node name="LeaderboardDisplay" 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_oo2te")
+script = ExtResource("2_13i4k")
+
+[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="LeaderboardId" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/LeaderboardId"]
+layout_mode = 2
+text = "Leaderboard ID:"
+
+[node name="LeaderboardId" type="Label" parent="MarginContainer/VBoxContainer/LeaderboardId"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+text_overrun_behavior = 3
+
+[node name="LeaderboardName" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/LeaderboardName"]
+layout_mode = 2
+text = "Leaderboard Name:"
+
+[node name="LeaderboardName" type="Label" parent="MarginContainer/VBoxContainer/LeaderboardName"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+text_overrun_behavior = 3
+
+[node name="PlayerRank" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/PlayerRank"]
+layout_mode = 2
+text = "Player Rank:"
+
+[node name="PlayerRank" type="Label" parent="MarginContainer/VBoxContainer/PlayerRank"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="PlayerScore" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/PlayerScore"]
+layout_mode = 2
+text = "Player Score:"
+
+[node name="PlayerScore" type="Label" parent="MarginContainer/VBoxContainer/PlayerScore"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="HSeparator" type="HSeparator" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="SubmitScore" type="VBoxContainer" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+theme_override_constants/separation = 25
+
+[node name="NewScore" type="VBoxContainer" parent="MarginContainer/VBoxContainer/SubmitScore"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/SubmitScore/NewScore"]
+layout_mode = 2
+text = "New score:"
+horizontal_alignment = 1
+
+[node name="NewScore" type="LineEdit" parent="MarginContainer/VBoxContainer/SubmitScore/NewScore"]
+unique_name_in_owner = true
+layout_mode = 2
+alignment = 1
+virtual_keyboard_type = 2
+clear_button_enabled = true
+
+[node name="SubmitScore" type="Button" parent="MarginContainer/VBoxContainer/SubmitScore"]
+unique_name_in_owner = true
+layout_mode = 2
+theme = ExtResource("1_oo2te")
+disabled = true
+text = "Submit to score"
+text_overrun_behavior = 3
+
+[node name="HSeparator2" type="HSeparator" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="Variants" type="VBoxContainer" parent="MarginContainer/VBoxContainer"]
+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_oo2te")
+disabled = true
+text = "Show Leaderboard Variant"
+text_overrun_behavior = 3
diff --git a/plugin/demo/scenes/leaderboards/Leaderboards.gd b/plugin/demo/scenes/leaderboards/Leaderboards.gd
new file mode 100644
index 0000000..f7f5f6d
--- /dev/null
+++ b/plugin/demo/scenes/leaderboards/Leaderboards.gd
@@ -0,0 +1,29 @@
+extends Control
+
+@onready var back_button: Button = %Back
+@onready var show_leaderboards_button: Button = %ShowLeaderboards
+@onready var leaderboard_displays: VBoxContainer = %LeaderboardDisplays
+
+var _leaderboards_cache: Array[LeaderboardsClient.Leaderboard] = []
+var _leaderboard_display := preload("res://scenes/leaderboards/LeaderboardDisplay.tscn")
+
+func _ready() -> void:
+ if _leaderboards_cache.is_empty():
+ LeaderboardsClient.load_all_leaderboards(true)
+ LeaderboardsClient.all_leaderboards_loaded.connect(
+ func cache_and_display(leaderboards: Array[LeaderboardsClient.Leaderboard]):
+ _leaderboards_cache = leaderboards
+ if not _leaderboards_cache.is_empty():
+ for leaderboard: LeaderboardsClient.Leaderboard in _leaderboards_cache:
+ var container := _leaderboard_display.instantiate() as Control
+ container.leaderboard = leaderboard
+ leaderboard_displays.add_child(container)
+ )
+
+ back_button.pressed.connect(func():
+ get_tree().change_scene_to_file("res://scenes/MainMenu.tscn")
+ )
+ show_leaderboards_button.pressed.connect(func():
+ LeaderboardsClient.show_all_leaderboards()
+ )
+
diff --git a/plugin/demo/scenes/leaderboards/Leaderboards.tscn b/plugin/demo/scenes/leaderboards/Leaderboards.tscn
new file mode 100644
index 0000000..0d56fd6
--- /dev/null
+++ b/plugin/demo/scenes/leaderboards/Leaderboards.tscn
@@ -0,0 +1,57 @@
+[gd_scene load_steps=3 format=3 uid="uid://bq8grv0nlkd6i"]
+
+[ext_resource type="Script" path="res://scenes/leaderboards/Leaderboards.gd" id="1_gf0nl"]
+[ext_resource type="Theme" uid="uid://bmm3mvq11y045" path="res://theme.tres" id="1_r8adt"]
+
+[node name="Leaderboards" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_gf0nl")
+
+[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_r8adt")
+text = "Back"
+
+[node name="ShowLeaderboards" type="Button" parent="MarginContainer/VBoxContainer"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(500, 200)
+layout_mode = 2
+theme = ExtResource("1_r8adt")
+text = "Show Leaderboards"
+
+[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+horizontal_scroll_mode = 0
+
+[node name="LeaderboardDisplays" type="VBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer"]
+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/demo/theme.tres b/plugin/demo/theme.tres
index 6dd31bc..0fb4f32 100644
--- a/plugin/demo/theme.tres
+++ b/plugin/demo/theme.tres
@@ -1,4 +1,17 @@
-[gd_resource type="Theme" format=3 uid="uid://bmm3mvq11y045"]
+[gd_resource type="Theme" load_steps=2 format=3 uid="uid://bmm3mvq11y045"]
+
+[sub_resource type="StyleBoxLine" id="StyleBoxLine_4da3r"]
+content_margin_left = 4.0
+content_margin_top = 0.0
+content_margin_right = 4.0
+content_margin_bottom = 0.0
+color = Color(0.5, 0.5, 0.5, 1)
+thickness = 5
[resource]
-Button/font_sizes/font_size = 36
+Button/font_sizes/font_size = 56
+HSeparator/constants/separation = 12
+HSeparator/styles/separator = SubResource("StyleBoxLine_4da3r")
+Label/font_sizes/font_size = 56
+LineEdit/font_sizes/font_size = 56
+PopupMenu/font_sizes/font_size = 56
diff --git a/plugin/export_scripts_template/autoloads/achievements_client.gd b/plugin/export_scripts_template/autoloads/achievements_client.gd
index d071274..51a8d00 100644
--- a/plugin/export_scripts_template/autoloads/achievements_client.gd
+++ b/plugin/export_scripts_template/autoloads/achievements_client.gd
@@ -4,22 +4,38 @@ extends Node
## This autoload exposes methods and signals to control the game achievements for
## the currently signed in player.
-## Signal emitted after calling the [code]increment_achievement()[/code]
-## and [code]unlock_achievement()[/code] methods.[br]
+## Signal emitted after calling the [method increment_achievement]
+## or [method unlock_achievement] methods.[br]
## [br]
## [param is_unlocked]: Indicates if the achievement is unlocked or not.[br]
## [param achievement_id]: The achievement id.
signal achievement_unlocked(is_unlocked: bool, achievement_id: String)
-## Signal emitted after calling the [code]load_achievements()[/code] method.[br]
+
+## Signal emitted after calling the [method load_achievements] method.[br]
## [br]
## [param achievements]: An array containing all the achievements for the game.
+## The array will be empty if there was an error loading the achievements.
signal achievements_loaded(achievements: Array[Achievement])
-## Signal emitted after calling the [code]reveal_achievement()[/code] method.[br]
+
+## Signal emitted after calling the [method reveal_achievement] method.[br]
## [br]
## [param is_revealed]: Indicates if the achievement is revealed or not.[br]
## [param achievement_id]: The achievement id.
signal achievement_revealed(is_revealed: bool, achievement_id: String)
+## Achievement type.
+enum Type {
+ TYPE_STANDARD = 0, ## A standard achievement.
+ TYPE_INCREMENTAL = 1 ## An incremental achievement.
+}
+
+## Achievement state.
+enum State {
+ STATE_UNLOCKED = 0, ## An unlocked achievement.
+ STATE_REVEALED = 1, ## A revealed achievement.
+ STATE_HIDDEN = 2 ## A hidden achievement.
+}
+
func _ready() -> void:
_connect_signals()
@@ -29,31 +45,26 @@ func _connect_signals() -> void:
achievement_unlocked.emit(is_unlocked, achievement_id)
)
GodotPlayGameServices.android_plugin.achievementsLoaded.connect(func(achievements_json: String):
- var json = JSON.new()
- var error = json.parse(achievements_json)
- if error == OK:
- var data_received = json.data
- if typeof(data_received) == TYPE_ARRAY:
- var mapped_array: Array[Achievement] = []
- for achievement: Dictionary in data_received:
- mapped_array.append(Achievement.new(achievement))
- achievements_loaded.emit(mapped_array)
- else:
- printerr("Unexpected data")
- else:
- printerr("JSON Parse Error: ", json.get_error_message(), " in ", achievements_json, " at line ", json.get_error_line())
+ var safe_array := GodotPlayGameServices.json_marshaller.safe_parse_array(achievements_json)
+ var achievements: Array[Achievement] = []
+ 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):
achievement_revealed.emit(is_revealed, achievement_id)
)
## Use this method to increment a given achievement in the given amount. For normal
-## achievements, use the [code]unlockAchievement[/code] method instead.[br]
-##
-## [br]The method emits the [code]achievement_unlocked[/code] signal.[br]
-##
-## [br][param achievement_id]: The achievement id.
-## [br][param amount]: The number of steps to increment by. Must be greater than 0.
+## achievements, use the [method unlock_achievement] method instead.[br]
+## [br]
+## The method emits the [signal achievement_unlocked] signal.[br]
+## [br]
+## [param achievement_id]: The achievement id.[br]
+## [param amount]: The number of steps to increment by. Must be greater than 0.
func increment_achievement(achievement_id: String, amount: int) -> void:
if GodotPlayGameServices.android_plugin:
GodotPlayGameServices.android_plugin.incrementAchievement(achievement_id, amount)
@@ -61,26 +72,28 @@ func increment_achievement(achievement_id: String, amount: int) -> void:
## Use this method and subscribe to the emitted signal to receive the list of the game
## achievements.[br]
## [br]
-## The method emits the [code]achievements_loaded[/code] signal.
+## The method emits the [signal achievements_loaded] signal.[br]
## [br]
-## [br][param force_reload]: If true, this call will clear any locally cached
-## data and attempt to fetch the latest data from the server.
+## [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.
func load_achievements(force_reload: bool) -> void:
if GodotPlayGameServices.android_plugin:
GodotPlayGameServices.android_plugin.loadAchievements(force_reload)
-## Use this method to reveal a hidden achievement to the current signed player.
+## Use this method to reveal a hidden achievement to the current signed in player.
## If the achievement is already unlocked, this method will have no effect.[br]
## [br]
-## The method emits the [code]achievement_revealed[/code] signal.
+## The method emits the [signal achievement_revealed] signal.[br]
## [br]
-## [br][param achievement_id]: The achievement id.
+## [param achievement_id]: The achievement id.
func reveal_achievement(achievement_id: String) -> void:
if GodotPlayGameServices.android_plugin:
GodotPlayGameServices.android_plugin.revealAchievement(achievement_id)
## Use this method to open a new window with the achievements of the game, and
-## the progress of the player to unlock those achievements.
+## the progress of the player made so far to unlock those achievements.
func show_achievements() -> void:
if GodotPlayGameServices.android_plugin:
GodotPlayGameServices.android_plugin.showAchievements()
@@ -88,55 +101,72 @@ func show_achievements() -> void:
## Immediately unlocks the given achievement for the signed in player. If the
## achievement is secret, it will be revealed to the player.[br]
## [br]
-## The method emits the [code]achievement_unlocked[/code] signal.
+## The method emits the [signal achievement_unlocked] signal.[br]
## [br]
-## [br][param achievement_id]: The achievement id.
+## [param achievement_id]: The achievement id.
func unlock_achievement(achievement_id: String) -> void:
if GodotPlayGameServices.android_plugin:
GodotPlayGameServices.android_plugin.unlockAchievement(achievement_id)
-## A class representing an achievement
+## A class representing an achievement.
class Achievement:
-
- enum Type {
- TYPE_STANDARD,
- TYPE_INCREMENTAL
- }
-
- enum State {
- STATE_UNLOCKED,
- STATE_REVEALED,
- STATE_HIDDEN
- }
-
- var achievement_id: String
- var achievement_name: String
- var description: String
- var type: Type
- var state: State
- var xp_value: int
- var revealed_image_uri: String
- var unlocked_image_uri: String
+ 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 type: Type ## The achievement type.
+ var state: State ## The achievement state.
+ var xp_value: int ## The XP value of this achievement.
+ var revealed_image_uri: String ## A URI that can be used to load the achievement's revealed image icon.
+ var unlocked_image_uri: String ## A URI that can be used to load the achievement's unlocked image icon.
+ ## The number of steps this user has gone toward unlocking this achievement;
+ ## only applicable for [code]TYPE_INCREMENTAL[/code] achievement types.
var current_steps: int
+ ## Retrieves the total number of steps necessary to unlock this achievement;
+ ## only applicable for [code]TYPE_INCREMENTAL[/code] achievement types.
var total_steps: int
+ ## Retrieves the number of steps this user has gone toward unlocking this
+ ## achievement, formatted for the user's locale; only applicable for
+ ## [code]TYPE_INCREMENTAL[/code] achievement types.
var formatted_current_steps: String
+ ## Loads the total number of steps necessary to unlock this achievement,
+ ## formatted for the user's local; only applicable for [code]TYPE_INCREMENTAL[/code]
+ ## achievement types.
var formatted_total_steps: String
+ ## Retrieves the timestamp (in millseconds since epoch) at which this achievement
+ ## was last updated.
var last_updated_timestamp: int
+ ## Constructor that creates an Achievement from a [Dictionary] containing the properties.
func _init(dictionary: Dictionary) -> void:
- achievement_id = dictionary.achievementId
- achievement_name = dictionary.name
- description = dictionary.description
- type = Type[dictionary.type]
- state = State[dictionary.state]
- xp_value = dictionary.xpValue
- revealed_image_uri = dictionary.revealedImageUri
- unlocked_image_uri = dictionary.unlockedImageUri
- current_steps = dictionary.currentSteps
- total_steps = dictionary.totalSteps
- formatted_current_steps = dictionary.formattedCurrentSteps
- formatted_total_steps = dictionary.formattedTotalSteps
- 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 = 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()
@@ -144,6 +174,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("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/godot_play_game_services.gd b/plugin/export_scripts_template/autoloads/godot_play_game_services.gd
index f2430c7..36ba09b 100644
--- a/plugin/export_scripts_template/autoloads/godot_play_game_services.gd
+++ b/plugin/export_scripts_template/autoloads/godot_play_game_services.gd
@@ -8,10 +8,13 @@ extends Node
## This Autoload also calls the [code]initialize()[/code] method of the plugin,
## checking if the user is authenticated.
-## This is the main entry point to the android plugin. With this object,
-## you can call the kotlin methods directly.
+## Main entry point to the android plugin. With this object, you can call the
+## kotlin methods directly.
var android_plugin: Object
+## A helper JSON marshaller to safely access JSON data from the plugin.
+var json_marshaller := JsonMarshaller.new()
+
func _ready() -> void:
var plugin_name := "GodotPlayGameServices"
diff --git a/plugin/export_scripts_template/autoloads/leaderboards_client.gd b/plugin/export_scripts_template/autoloads/leaderboards_client.gd
new file mode 100644
index 0000000..906e5f0
--- /dev/null
+++ b/plugin/export_scripts_template/autoloads/leaderboards_client.gd
@@ -0,0 +1,301 @@
+extends Node
+## Client with leaderboards functionality.
+##
+## This autoload exposes methods and signals to show the game leaderboards, as well
+## as submitting and retrieving the player's score.
+
+## Signal emitted after calling the [method submit_score] method.[br]
+## [br]
+## [param is_submitted]: Indicates if the score was submitted or not.[br]
+## [param leaderboard_id]: The leaderboard id.
+signal score_submitted(is_submitted: bool, leaderboard_id: String)
+
+## Signal emitted after calling the [method load_player_score] method.[br]
+## [br]
+## [param leaderboard_id]: The leaderboard id.[br]
+## [param score]: The score of the player. It can be null if there is an error
+## retrieving it.
+signal score_loaded(leaderboard_id: String, score: Score)
+
+## Signal emitted after calling the [method load_all_leaderboards] method.[br]
+## [br]
+## [param leaderboards]: An array containing all the leaderboards for the game.
+## The array will be empty if there was an error loading the leaderboards.
+signal all_leaderboards_loaded(leaderboards: Array[Leaderboard])
+
+## Signal emitted after calling the [method load_leaderboard] method.[br]
+## [br]
+## [param leaderboard]: The leaderboard. It can be null if there is an error
+## retrieving it.
+signal leaderboard_loaded(leaderboard: Leaderboard)
+
+## Time span for leaderboards.
+enum TimeSpan {
+ TIME_SPAN_DAILY = 0, ## A leaderboard that resets everyday.
+ TIME_SPAN_WEEKLY = 1, ## A leaderboard that resets every week.
+ TIME_SPAN_ALL_TIME = 2 ## A leaderboard that never resets.
+}
+
+## Collection type for leaderboards.
+enum Collection {
+ COLLECTION_PUBLIC = 0, ## A public leaderboard.
+ COLLECTION_FRIENDS = 3 ## A leaderboard only with friends.
+}
+
+## Score order for leadeboards.
+enum ScoreOrder {
+ SCORE_ORDER_LARGER_IS_BETTER = 1, ## Scores are sorted in descending order.
+ SCORE_ORDER_SMALLER_IS_BETTER = 0 ## Scores are sorted in ascending order.
+}
+
+func _ready() -> void:
+ _connect_signals()
+
+func _connect_signals() -> void:
+ if GodotPlayGameServices.android_plugin:
+ GodotPlayGameServices.android_plugin.scoreSubmitted.connect(func(is_submitted: bool, leaderboard_id: String):
+ score_submitted.emit(is_submitted, leaderboard_id)
+ )
+ GodotPlayGameServices.android_plugin.scoreLoaded.connect(func(leaderboard_id: String, json_data: String):
+ var safe_dictionary := GodotPlayGameServices.json_marshaller.safe_parse_dictionary(json_data)
+ score_loaded.emit(leaderboard_id, Score.new(safe_dictionary))
+ )
+ GodotPlayGameServices.android_plugin.allLeaderboardsLoaded.connect(func(leaderboards_json: String):
+ var safe_array := GodotPlayGameServices.json_marshaller.safe_parse_array(leaderboards_json)
+ var leaderboards: Array[Leaderboard] = []
+ for dictionary: Dictionary in safe_array:
+ leaderboards.append(Leaderboard.new(dictionary))
+ all_leaderboards_loaded.emit(leaderboards)
+ )
+ GodotPlayGameServices.android_plugin.leaderboardLoaded.connect(func(json_data: String):
+ var safe_dictionary := GodotPlayGameServices.json_marshaller.safe_parse_dictionary(json_data)
+ leaderboard_loaded.emit(Leaderboard.new(safe_dictionary))
+ )
+
+## Use this method to show all leaderbords for this game in a new screen.
+func show_all_leaderboards() -> void:
+ if GodotPlayGameServices.android_plugin:
+ GodotPlayGameServices.android_plugin.showAllLeaderboards()
+
+## Use this method to show a specific leaderboard in a new screen.[br]
+## [br]
+## [param leaderboard_id]: The leaderboard id.
+func show_leaderboard(leaderboard_id: String) -> void:
+ if GodotPlayGameServices.android_plugin:
+ GodotPlayGameServices.android_plugin.showAllLeaderboards(leaderboard_id)
+
+## Use this method to show a specific leaderboard for a given time span in a new screen.[br]
+## [br]
+## [param leaderboard_id]: The leaderboard id.[br]
+## [param time_span]: The time span for the leaderboard. See the [enum TimeSpan] enum.
+func show_leaderboard_for_time_span(leaderboard_id: String, time_span: TimeSpan) -> void:
+ if GodotPlayGameServices.android_plugin:
+ GodotPlayGameServices.android_plugin.showLeaderboardForTimeSpan(leaderboard_id, time_span)
+
+## Use this method to show a specific leaderboard for a given time span and
+## collection type in a new screen.[br]
+## [br]
+## [param leaderboard_id]: The leaderboard id.[br]
+## [param time_span]: The time span for the leaderboard. See the [enum TimeSpan] enum.[br]
+## [param collection]: The collection type for the leaderboard. See the [enum Collection] enum.
+func show_leaderboard_for_time_span_and_collection(
+ leaderboard_id: String,
+ time_span: TimeSpan,
+ collection: Collection
+) -> void:
+ if GodotPlayGameServices.android_plugin:
+ GodotPlayGameServices.android_plugin.showLeaderboardForTimeSpanAndCollection(leaderboard_id, time_span, collection)
+
+## Submits the score to the leaderboard for the currently signed in player. The score
+## is ignored if it is worse (as defined by the leaderboard configuration) than a previously
+## submitted score for the same player.[br]
+## [br]
+## The method emits the [signal score_submitted] signal.[br]
+## [br]
+## [param leaderboard_id]: The leaderboard id.[br]
+## [param score]: The raw score value.
+func submit_score(leaderboard_id: String, score: int) -> void:
+ if GodotPlayGameServices.android_plugin:
+ GodotPlayGameServices.android_plugin.submitScore(leaderboard_id, score)
+
+## Use this method and subscribe to the emitted signal to receive the score of the
+## currently signed in player for the specified leaderboard, time span, and collection.[br]
+## [br]
+## The method emits the [signal score_loaded] signal.[br]
+## [br]
+## [param leaderboard_id]: The leaderboard id.[br]
+## [param time_span]: The time span for the leaderboard. See the [enum TimeSpan] enum.[br]
+## [param collection]: The collection type for the leaderboard. See the [enum Collection] enum.
+func load_player_score(
+ leaderboard_id: String,
+ time_span: TimeSpan,
+ collection: Collection
+) -> void:
+ if GodotPlayGameServices.android_plugin:
+ GodotPlayGameServices.android_plugin.loadPlayerScore(leaderboard_id, time_span, collection)
+
+## Use this method and subscribe to the emitted signal to receive the list of the game
+## leaderboards.[br]
+## [br]
+## The method emits the [signal all_leaderboards_loaded] signal.[br]
+## [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.
+func load_all_leaderboards(force_reload: bool) -> void:
+ if GodotPlayGameServices.android_plugin:
+ GodotPlayGameServices.android_plugin.loadAllLeaderboards(force_reload)
+
+## Use this method and subscribe to the emitted signal to receive a leaderboard.[br]
+## [br]
+## The method emits the [signal leaderboard_loaded] signal.[br]
+## [br]
+## [param leaderboard_id]: The leaderboard id.[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.
+func load_leaderboard(leaderboard_id: String, force_reload: bool) -> void:
+ if GodotPlayGameServices.android_plugin:
+ GodotPlayGameServices.android_plugin.loadLeaderboard(leaderboard_id, force_reload)
+
+## The score of a player for a specific leaderboard.
+class Score:
+ var display_rank: String ## Formatted string for the rank of the player.
+ 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_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.
+ var score_tag: String ## Optional score tag associated with this score.
+ var timestamp_millis: int ## Timestamp (in milliseconds from epoch) at which this score was achieved.
+
+ ## 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
+
+ func _to_string() -> String:
+ var result := PackedStringArray()
+
+ result.append("display_rank: %s" % display_rank)
+ 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_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)
+ result.append("score_tag: %s" % score_tag)
+ result.append("timestamp_millis: %s" % timestamp_millis)
+
+ return ", ".join(result)
+
+## A leaderboard.
+class Leaderboard:
+ var leaderboard_id: String ## The leaderboard id.
+ var display_name: String ## The display name of the leaderboard.
+ var icon_image_uri: String ## The URI to the leaderboard icon image.
+ var score_order: ScoreOrder ## The sorting order of the leaderboard, based on the score.
+ ## A list of variants of this leaderboard, based on the combination of the
+ ## leaderboard [enum TimeSpan] and [enum Collection].
+ var variants: Array[LeaderboardVariant] = []
+
+ ## 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("variants"):
+ for variant: Dictionary in dictionary.variants:
+ variants.append(LeaderboardVariant.new(variant))
+
+ func _to_string() -> String:
+ var result := PackedStringArray()
+
+ 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)
+
+ for variant: LeaderboardVariant in variants:
+ result.append("{%s}" % str(variant))
+
+ return ", ".join(result)
+
+## A specific variant of [enum TimeSpan] and [enum Collection] for a leaderboard.
+class LeaderboardVariant:
+ var display_player_rank: String ## The formatted rank of the player for this variant.
+ var display_player_score: String ## The formatted score of the player for this variant.
+ var num_scores: int ## The total number of scores for this variant.
+ var player_rank: int ## The rank of the player for this variant.
+ var player_score_tag: String ## The score tag of the player for this variant.
+ var raw_player_score: int ## The score of the player for this variant.
+ var has_player_info: bool ## Whether or not this variant contains score information for the player.
+ var collection: Collection ## The type of [enum Collection] of this variant.
+ var time_span: TimeSpan ## The type of [enum TimeSpan] of this variant.
+
+ ## 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)
+
+ func _to_string() -> String:
+ var result := PackedStringArray()
+
+ result.append("display_player_rank: %s" % display_player_rank)
+ result.append("display_player_score: %s" % display_player_score)
+ result.append("num_scores: %s" % num_scores)
+ result.append("player_rank: %s" % player_rank)
+ result.append("player_score_tag: %s" % player_score_tag)
+ result.append("raw_player_score: %s" % raw_player_score)
+ result.append("has_player_info: %s" % has_player_info)
+ result.append("collection: %s" % str(collection))
+ result.append("time_span: %s" % str(time_span))
+
+ return ", ".join(result)
diff --git a/plugin/export_scripts_template/autoloads/sign_in_client.gd b/plugin/export_scripts_template/autoloads/sign_in_client.gd
index e2632a5..c83b76e 100644
--- a/plugin/export_scripts_template/autoloads/sign_in_client.gd
+++ b/plugin/export_scripts_template/autoloads/sign_in_client.gd
@@ -7,13 +7,13 @@ extends Node
## a check at startup, so usually you don't have to use these methods. Use them only
## to provide a manual way for the user to sign in.
-## Signal emitted after calling the [code]is_authenticated()[/code] method.[br]
-##
-## [br][param is_authenticated]: Indicates if the user is authenticated or not.
+## Signal emitted after calling the [method is_authenticated] method.[br]
+## [br]
+## [param is_authenticated]: Indicates if the user is authenticated or not.
signal user_authenticated(is_authenticated: bool)
-## Signal emitted after calling the [code]sign_in()[/code] method.[br]
-##
-## [br][param is_signed_in]: Indicates if the user is signed in or not.
+## Signal emitted after calling the [method sign_in] method.[br]
+## [br]
+## [param is_signed_in]: Indicates if the user is signed in or not.
signal user_signed_in(is_signed_in: bool)
func _ready() -> void:
@@ -30,15 +30,15 @@ func _connect_signals() -> void:
## Use this method to check if the user is already authenticated. If the user is authenticated,
## a popup will be shown on screen.[br]
-##
-## [br]The method emits the [code]user_authenticated[/code] signal.
+## [br]
+## The method emits the [signal user_authenticated] signal.
func is_authenticated() -> void:
if GodotPlayGameServices.android_plugin:
GodotPlayGameServices.android_plugin.isAuthenticated()
## Use this method to provide a manual way to the user for signing in.[br]
-##
-## [br]The method emits the [code]user_signed_in[/code] signal.
+## [br]
+## The method emits the [signal user_signed_in] signal.
func sign_in() -> void:
if GodotPlayGameServices.android_plugin:
GodotPlayGameServices.android_plugin.signIn()
diff --git a/plugin/export_scripts_template/export_plugin.gd b/plugin/export_scripts_template/export_plugin.gd
index 51b3b7c..1fe7d45 100644
--- a/plugin/export_scripts_template/export_plugin.gd
+++ b/plugin/export_scripts_template/export_plugin.gd
@@ -4,6 +4,7 @@ extends EditorPlugin
const PLUGIN_AUTOLOAD := "GodotPlayGameServices"
const SIGN_IN_AUTOLOAD := "SignInClient"
const ACHIEVEMENTS_AUTOLOAD := "AchievementsClient"
+const LEADERBOARDS_AUTOLOAD := "LeaderboardsClient"
var _export_plugin : AndroidExportPlugin
var _dock : Node
@@ -39,11 +40,13 @@ func _add_autoloads() -> void:
add_autoload_singleton(PLUGIN_AUTOLOAD, "res://addons/GodotPlayGameServices/autoloads/godot_play_game_services.gd")
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")
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)
class AndroidExportPlugin extends EditorExportPlugin:
var _plugin_name = "GodotPlayGameServices"
diff --git a/plugin/export_scripts_template/marshalling/json_marshaller.gd b/plugin/export_scripts_template/marshalling/json_marshaller.gd
new file mode 100644
index 0000000..e2ff0ff
--- /dev/null
+++ b/plugin/export_scripts_template/marshalling/json_marshaller.gd
@@ -0,0 +1,43 @@
+class_name JsonMarshaller
+extends Object
+## A Class for encapsulating JSON parsing
+##
+## This class exposes methods to parse JSON arrays and dictionaries.
+
+var _json = JSON.new()
+
+## Safely parses a JSON array and returns the Array containing dictionaries.[br]
+## [br]
+## [param json_array]: The JSON array in String format.[br]
+func safe_parse_array(json_array: String) -> Array[Dictionary]:
+ var error := _json.parse(json_array)
+ if error == OK:
+ var data_received = _json.data
+ if typeof(data_received) == TYPE_ARRAY:
+ var safe_array: Array[Dictionary] = []
+ for element: Dictionary in data_received:
+ safe_array.append(element)
+ return safe_array
+ else:
+ printerr("Unexpected data received from JSON Array:\n%s" % json_array)
+ else:
+ printerr("JSON Parse Error: ", _json.get_error_message(), " in ", json_array, " at line ", _json.get_error_line())
+
+ return []
+
+## Safely parses a JSON dictionary and returns it, or an empty dictionary if
+## something went wrong.[br]
+## [br]
+## [param json_array]: The JSON dictionary in String format.[br]
+func safe_parse_dictionary(json_dictionary: String) -> Dictionary:
+ var error := _json.parse(json_dictionary)
+ if error == OK:
+ var data_received = _json.data
+ if typeof(data_received) == TYPE_DICTIONARY:
+ return data_received
+ else:
+ printerr("Unexpected data received from JSON Dictionary:\n%s" % json_dictionary)
+ else:
+ printerr("JSON Parse Error: ", _json.get_error_message(), " in ", json_dictionary, " at line ", _json.get_error_line())
+
+ return {}