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 {}