Skip to content

Commit a34f9e5

Browse files
authored
Merge pull request #102 from TaloDev/develop
Release 0.25.0
2 parents e1903df + 16205a7 commit a34f9e5

36 files changed

+472
-92
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ channels](https://trytalo.com/channels): Send real-time messages between players
3232
- 🔒 Authentication: a registration/login/account management flow, showing how to create player accounts and authenticate them.
3333
- 🎮 Playground: a text-based playground allowing you to test identifying, events, stats and leaderboards.
3434
- 🕹️ Leaderboards: a basic leaderboard UI, allowing you to add and update entries.
35-
- 💾 Stateful buttons: showing how to save and load game state.
35+
- 💾 Multi-scene saves: a small game showing how to persist save data across multiple scenes.
36+
- 💾 Stateful buttons: a simple demo showing how to save and load game state.
3637
- 💬 Chat: showing how to send messages between channels in a chat room.
3738

3839
## Documentation

addons/talo/apis/saves_api.gd

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,6 @@ func get_saves() -> Array[TaloGameSave]:
6868
## Set the chosen save and optionally (default true) load it.
6969
func choose_save(save: TaloGameSave, load_save = true) -> void:
7070
_saves_manager.set_chosen_save(save, load_save)
71-
if load_save:
72-
save_chosen.emit(save)
7371

7472
## Unload the current save.
7573
func unload_current_save() -> void:
@@ -78,8 +76,6 @@ func unload_current_save() -> void:
7876
## Create a new save with the given name and content.
7977
func create_save(save_name: String, content: Dictionary = {}) -> TaloGameSave:
8078
var save: TaloGameSave
81-
82-
_saves_manager.register_fields_for_saved_objects()
8379
var save_content := content if not content.is_empty() else _saves_manager.get_save_content()
8480

8581
if await Talo.is_offline():
@@ -108,12 +104,6 @@ func create_save(save_name: String, content: Dictionary = {}) -> TaloGameSave:
108104
func register(loadable: TaloLoadable) -> void:
109105
_saves_manager.register(loadable)
110106

111-
## Mark an object as loaded.
112-
func set_object_loaded(id: String) -> void:
113-
_saves_manager.push_loaded_object(id)
114-
if _saves_manager.is_loading_completed():
115-
save_loading_completed.emit()
116-
117107
## Update the currently loaded save using the current state of the game and with the given name.
118108
func update_current_save(new_name: String = "") -> TaloGameSave:
119109
return await update_save(_saves_manager.current_save, new_name)
@@ -161,3 +151,7 @@ func delete_save(save: TaloGameSave) -> void:
161151

162152
if _saves_manager.current_save and _saves_manager.current_save.id == save.id:
163153
unload_current_save()
154+
155+
## Get the format version for the current save.
156+
func get_format_version() -> String:
157+
return _saves_manager.get_format_version()

addons/talo/entities/loadable.gd

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,43 +8,31 @@ class_name TaloLoadable extends Node
88
## The unique identifier for this loadable.
99
@export var id: String
1010

11-
var _saved_fields: Dictionary
11+
var _saved_fields: Dictionary[String, Variant]
1212

1313
func _ready() -> void:
14-
Talo.saves.save_chosen.connect(_load_data)
1514
Talo.saves.register(self)
1615

17-
func _load_data(save: TaloGameSave) -> void:
18-
if not save:
19-
return
16+
func _convert_serialised_data(item: Dictionary) -> Variant:
17+
match Talo.saves.get_format_version():
18+
"godot.v1": return type_convert(item.value, int(item.type))
19+
_: return str_to_var(item.value)
2020

21+
## Update this loadable with the latest data.
22+
func hydrate(data: Array[Dictionary]) -> void:
2123
var fields := {}
22-
23-
var filtered := save.content.objects.filter(func (obj: Dictionary): return obj.id == id) as Array
24-
if filtered.is_empty():
25-
push_warning("Loadable with id '%s' not found in save '%s'" % [id, save.name])
26-
return
27-
28-
var saved_object = filtered.front()
29-
30-
for item in saved_object.data:
31-
fields[item.key] = type_convert(item.value, int(item.type))
24+
for item in data:
25+
fields[item.key] = _convert_serialised_data(item)
3226

3327
on_loaded(fields)
3428

35-
Talo.saves.set_object_loaded(id)
36-
37-
## Clear all the saved data for this loadable.
38-
func clear_saved_fields() -> void:
39-
_saved_fields.clear()
40-
41-
## Register all the fields that should be saved and loaded. This must be implemented by the child class.
29+
## Register all the fields that should be saved and loaded.
4230
func register_fields() -> void:
43-
assert(false, "register_fields() must be implemented")
31+
pass
4432

4533
## Register the given key with a value. When this object is saved, the value will be saved and loaded.
4634
func register_field(key: String, value: Variant) -> void:
47-
_saved_fields[key] = value
35+
_saved_fields.set(key, value)
4836

4937
## Handle the loaded data. This must be implemented by the child class.
5038
func on_loaded(data: Dictionary) -> void:
@@ -58,10 +46,18 @@ func handle_destroyed(data: Dictionary) -> bool:
5846

5947
return destroyed
6048

61-
## Serialise the saved fields.
62-
func get_saved_object_data() -> Array:
63-
return _saved_fields.keys().map(
64-
func (key: String):
65-
var value = _saved_fields[key]
66-
return {key = key, value = str(value), type = str(typeof(value))}
67-
)
49+
## Ensure the data is up-to-date and return the serialised saved fields.
50+
func get_latest_data() -> Array[Dictionary]:
51+
register_fields()
52+
53+
var data: Array[Dictionary] = []
54+
data.assign(_saved_fields.keys().map(
55+
func (key: String):
56+
var value = _saved_fields[key]
57+
return {
58+
key = key,
59+
value = var_to_str(value),
60+
type = str(typeof(value))
61+
}
62+
))
63+
return data

addons/talo/entities/player_stat_snapshot.gd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ func _init(data: Dictionary):
1010
player_alias = TaloPlayerAlias.new(data.playerAlias)
1111
change = data.change
1212
value = data.value
13-
global_value = data.globalValue
13+
global_value = data.get("globalValue", 0.0)
1414
created_at = data.createdAt
Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,59 @@
11
class_name TaloSavedObject extends RefCounted
22

3+
var loadable: TaloLoadable
34
var id: String
45
var name: String
5-
var loadable: TaloLoadable
6+
var _cached_data: Array[Dictionary] = []
7+
var _root_path: String
8+
9+
func _init(saved_object: Dictionary) -> void:
10+
id = saved_object.id
11+
name = saved_object.get("name", "unknown")
12+
_cached_data.assign(saved_object.data)
613

7-
func _init(loadable: TaloLoadable) -> void:
8-
id = loadable.id
9-
name = loadable.get_path()
14+
# Match a loadable with a saved object to hydrate the loadable with the latest data.
15+
func register_loadable(loadable: TaloLoadable, hydrate = true) -> void:
1016
self.loadable = loadable
17+
_root_path = loadable.get_tree().current_scene.get_path()
18+
if hydrate:
19+
loadable.hydrate(_cached_data)
1120

12-
## Register the fields that should be saved and loaded for this object.
13-
func register_loadable_fields():
14-
if is_instance_valid(loadable):
15-
loadable.clear_saved_fields()
16-
loadable.register_fields()
21+
func _get_latest_data() -> Array[Dictionary]:
22+
_cached_data.assign(loadable.get_latest_data())
23+
return _cached_data
1724

18-
func to_dictionary() -> Dictionary:
19-
register_loadable_fields()
25+
func _is_loadable_valid() -> bool:
26+
return is_instance_valid(loadable) and not loadable.is_queued_for_deletion()
27+
28+
func _current_scene_matches_name() -> bool:
29+
# if the root path hasn't been set, it may not have been registered yet
30+
# in that case, we should only return cached data
31+
return _root_path and name.begins_with(_root_path)
2032

21-
var destroyed_data := [{ key = "meta.destroyed", value = str(true), type = str(TYPE_BOOL) }]
33+
func _serialise_data() -> Array[Dictionary]:
34+
var valid := _is_loadable_valid()
2235

36+
# doesn't exist but isn't part of the scene, don't modify it
37+
if not valid and not _current_scene_matches_name():
38+
return _cached_data
39+
40+
# exists and is part of the scene, serialise it
41+
if valid:
42+
return _get_latest_data()
43+
44+
# doesn't exist and is part of the scene, mark it as destroyed
45+
_cached_data.push_back({
46+
key = "meta.destroyed",
47+
value = str(true),
48+
type = str(TYPE_BOOL)
49+
})
50+
51+
return _cached_data
52+
53+
# Serialise the saved object so that it can be saved.
54+
func to_dictionary() -> Dictionary:
2355
return {
2456
id = id,
2557
name = name,
26-
data = destroyed_data if not is_instance_valid(loadable) else loadable.get_saved_object_data()
58+
data = _serialise_data()
2759
}

addons/talo/plugin.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
name="Talo Game Services"
44
description="Talo (https://trytalo.com) is an open-source game backend with services designed to help you build games faster. You can currently:\n\n- Identify and authenticate players\n- Store persistent data across players\n- Track events (levelling up, finding loot, etc)\n- Display high scores with leaderboards\n- Store and load player saves\n- Load game config options and flags from the cloud\n- Get feedback directly from your players\n- Keep your data in-sync even when players are offline\n- Send channel messages between players\n- See if players are online and set custom statuses"
55
author="trytalo"
6-
version="0.24.0"
6+
version="0.25.0"
77
script="talo_autoload.gd"
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
[gd_scene load_steps=4 format=3 uid="uid://c4doqk5ectldu"]
2+
3+
[ext_resource type="PackedScene" uid="uid://ci52f1pjsppv3" path="res://addons/talo/samples/multiscene_saves/scenes/star.tscn" id="1_h5eft"]
4+
[ext_resource type="PackedScene" uid="uid://b0jdoni6crl0s" path="res://addons/talo/samples/multiscene_saves/scenes/portal.tscn" id="2_yfx2i"]
5+
[ext_resource type="PackedScene" uid="uid://b3b4ek8meeflf" path="res://addons/talo/samples/multiscene_saves/scenes/player.tscn" id="3_v3fgw"]
6+
7+
[node name="BlueZone" type="Node2D"]
8+
9+
[node name="Map" type="Polygon2D" parent="."]
10+
color = Color(0.277352, 0.505327, 0.795826, 1)
11+
polygon = PackedVector2Array(86, -140, 240, -128, 430, -135, 488, 81, 227, 319, 8.73462, 225.248, -107, 285, -378, 217, -315, -2, -359, -186, -175, -303, 2, -263)
12+
13+
[node name="Star1" parent="." instance=ExtResource("1_h5eft")]
14+
position = Vector2(350, -80)
15+
id = "blue_zone_star1"
16+
17+
[node name="Star2" parent="." instance=ExtResource("1_h5eft")]
18+
position = Vector2(-250, -51)
19+
id = "blue_zone_star2"
20+
21+
[node name="Star3" parent="." instance=ExtResource("1_h5eft")]
22+
position = Vector2(-222, 165)
23+
id = "blue_zone_star3"
24+
25+
[node name="Star4" parent="." instance=ExtResource("1_h5eft")]
26+
position = Vector2(216, 245)
27+
id = "blue_zone_star4"
28+
29+
[node name="Star5" parent="." instance=ExtResource("1_h5eft")]
30+
position = Vector2(221, 49)
31+
id = "blue_zone_star5"
32+
33+
[node name="Star6" parent="." instance=ExtResource("1_h5eft")]
34+
position = Vector2(-99, -178)
35+
id = "blue_zone_star6"
36+
37+
[node name="Portal" parent="." instance=ExtResource("2_yfx2i")]
38+
position = Vector2(-233, -237)
39+
rotation = 1.00356
40+
to_level = "green_zone"
41+
spawn_point = Vector2(225, 166)
42+
43+
[node name="Player" parent="." instance=ExtResource("3_v3fgw")]
44+
position = Vector2(-192, -175)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
[gd_scene load_steps=4 format=3 uid="uid://bucpvtjej40ju"]
2+
3+
[ext_resource type="PackedScene" uid="uid://b3b4ek8meeflf" path="res://addons/talo/samples/multiscene_saves/scenes/player.tscn" id="1_mdugq"]
4+
[ext_resource type="PackedScene" uid="uid://ci52f1pjsppv3" path="res://addons/talo/samples/multiscene_saves/scenes/star.tscn" id="2_4im17"]
5+
[ext_resource type="PackedScene" uid="uid://b0jdoni6crl0s" path="res://addons/talo/samples/multiscene_saves/scenes/portal.tscn" id="3_vv7xr"]
6+
7+
[node name="GreenZone" type="Node2D"]
8+
9+
[node name="Map" type="Polygon2D" parent="."]
10+
color = Color(0.160784, 0.588235, 0.360784, 1)
11+
polygon = PackedVector2Array(100, -186, 347, -261, 416, -21, 364, 185, 227, 319, -235, 344, -395, 38, -314, -337, 0, -287)
12+
13+
[node name="Star1" parent="." instance=ExtResource("2_4im17")]
14+
position = Vector2(76, 63)
15+
id = "green_zone_star1"
16+
17+
[node name="Star2" parent="." instance=ExtResource("2_4im17")]
18+
position = Vector2(-147, 227)
19+
id = "green_zone_star2"
20+
21+
[node name="Star3" parent="." instance=ExtResource("2_4im17")]
22+
position = Vector2(-130, -141)
23+
id = "green_zone_star3"
24+
25+
[node name="Star4" parent="." instance=ExtResource("2_4im17")]
26+
position = Vector2(281, 61)
27+
id = "green_zone_star4"
28+
29+
[node name="PortalBlueZone" parent="." instance=ExtResource("3_vv7xr")]
30+
position = Vector2(288, 225)
31+
rotation = 0.794125
32+
to_level = "blue_zone"
33+
spawn_point = Vector2(-192, -175)
34+
35+
[node name="PortalStartingZone" parent="." instance=ExtResource("3_vv7xr")]
36+
position = Vector2(-329, -151)
37+
rotation = 0.214676
38+
to_level = "starting_zone"
39+
spawn_point = Vector2(68, -106)
40+
41+
[node name="Player" parent="." instance=ExtResource("1_mdugq")]
42+
position = Vector2(-262, -136)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
[gd_scene load_steps=5 format=3 uid="uid://b3b4ek8meeflf"]
2+
3+
[ext_resource type="Texture2D" uid="uid://b3naludt1ank2" path="res://icon.png" id="1_rswej"]
4+
[ext_resource type="Script" uid="uid://pjwribovgraj" path="res://addons/talo/samples/multiscene_saves/scripts/loadable_player.gd" id="1_t5o6r"]
5+
[ext_resource type="Script" uid="uid://cbm1re05xae6k" path="res://addons/talo/samples/multiscene_saves/scripts/player_controller.gd" id="2_bg0gd"]
6+
7+
[sub_resource type="CircleShape2D" id="CircleShape2D_mdugq"]
8+
radius = 450.0
9+
10+
[node name="Player" type="CharacterBody2D"]
11+
scale = Vector2(0.05, 0.05)
12+
script = ExtResource("2_bg0gd")
13+
14+
[node name="Loadable" type="Node2D" parent="."]
15+
script = ExtResource("1_t5o6r")
16+
username = "saves_demo_player"
17+
18+
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
19+
shape = SubResource("CircleShape2D_mdugq")
20+
21+
[node name="Sprite2D" type="Sprite2D" parent="."]
22+
texture = ExtResource("1_rswej")
23+
24+
[node name="Camera" type="Camera2D" parent="."]
25+
unique_name_in_owner = true
26+
zoom = Vector2(1.5, 1.5)
27+
28+
[node name="Stars" type="Label" parent="."]
29+
unique_name_in_owner = true
30+
anchors_preset = 7
31+
anchor_left = 0.5
32+
anchor_top = 1.0
33+
anchor_right = 0.5
34+
anchor_bottom = 1.0
35+
offset_left = -1000.0
36+
offset_top = 464.0
37+
offset_right = 1000.0
38+
offset_bottom = 1010.0
39+
grow_horizontal = 2
40+
grow_vertical = 0
41+
theme_override_font_sizes/font_size = 350
42+
text = "0 stars"
43+
horizontal_alignment = 1
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[gd_scene load_steps=4 format=3 uid="uid://b0jdoni6crl0s"]
2+
3+
[ext_resource type="Script" uid="uid://bnkfuweqaugsr" path="res://addons/talo/samples/multiscene_saves/scripts/portal.gd" id="1_dmas5"]
4+
5+
[sub_resource type="CanvasTexture" id="CanvasTexture_mdugq"]
6+
7+
[sub_resource type="RectangleShape2D" id="RectangleShape2D_4im17"]
8+
size = Vector2(45, 45)
9+
10+
[node name="Portal" type="Area2D"]
11+
script = ExtResource("1_dmas5")
12+
13+
[node name="Sprite2D" type="Sprite2D" parent="."]
14+
modulate = Color(0.299775, 0.228743, 0.432167, 1)
15+
scale = Vector2(50, 50)
16+
texture = SubResource("CanvasTexture_mdugq")
17+
18+
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
19+
shape = SubResource("RectangleShape2D_4im17")
20+
21+
[connection signal="body_entered" from="." to="." method="_on_body_entered"]

0 commit comments

Comments
 (0)