From e324d58f15768f02ad508a2989a62f2216815185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20Ib=C3=A1=C3=B1ez=20S=C3=A1nchez?= Date: Mon, 18 Dec 2023 09:19:24 +0100 Subject: [PATCH] Refactor of demo app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Organise directories Add leaderboards screen Signed-off-by: Jacob Ibáñez Sánchez --- plugin/demo/MainMenu.gd | 8 - plugin/demo/icon.svg | 194 ++++++++++- plugin/demo/launcher_icon.png | Bin 0 -> 7934 bytes plugin/demo/launcher_icon.svg | 171 ++++++++++ plugin/demo/project.godot | 9 +- plugin/demo/scenes/MainMenu.gd | 12 + plugin/demo/{ => scenes}/MainMenu.tscn | 22 +- .../{ => scenes/achievements}/Achievements.gd | 17 +- .../achievements}/Achievements.tscn | 2 +- .../scenes/leaderboards/LeaderboardDisplay.gd | 112 +++++++ .../leaderboards/LeaderboardDisplay.tscn | 140 ++++++++ .../demo/scenes/leaderboards/Leaderboards.gd | 29 ++ .../scenes/leaderboards/Leaderboards.tscn | 57 ++++ plugin/demo/theme.tres | 17 +- .../autoloads/achievements_client.gd | 163 ++++++---- .../autoloads/godot_play_game_services.gd | 7 +- .../autoloads/leaderboards_client.gd | 301 ++++++++++++++++++ .../autoloads/sign_in_client.gd | 20 +- .../export_scripts_template/export_plugin.gd | 3 + .../marshalling/json_marshaller.gd | 43 +++ 20 files changed, 1224 insertions(+), 103 deletions(-) delete mode 100644 plugin/demo/MainMenu.gd create mode 100644 plugin/demo/launcher_icon.png create mode 100644 plugin/demo/launcher_icon.svg create mode 100644 plugin/demo/scenes/MainMenu.gd rename plugin/demo/{ => scenes}/MainMenu.tscn (59%) rename plugin/demo/{ => scenes/achievements}/Achievements.gd (53%) rename plugin/demo/{ => scenes/achievements}/Achievements.tscn (94%) create mode 100644 plugin/demo/scenes/leaderboards/LeaderboardDisplay.gd create mode 100644 plugin/demo/scenes/leaderboards/LeaderboardDisplay.tscn create mode 100644 plugin/demo/scenes/leaderboards/Leaderboards.gd create mode 100644 plugin/demo/scenes/leaderboards/Leaderboards.tscn create mode 100644 plugin/export_scripts_template/autoloads/leaderboards_client.gd create mode 100644 plugin/export_scripts_template/marshalling/json_marshaller.gd 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 0000000000000000000000000000000000000000..bcf367207d050caf5575b0d5dd49bd4443a474af GIT binary patch literal 7934 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 literal 0 HcmV?d00001 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 {}