Skip to content

Commit a1ed32a

Browse files
authored
Merge pull request #172 from TaloDev/develop
Release 0.39.0
2 parents 41c155c + fbf918b commit a1ed32a

15 files changed

+318
-55
lines changed

.github/workflows/claude-code-review.yml

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ jobs:
2121
runs-on: ubuntu-latest
2222
permissions:
2323
contents: read
24-
pull-requests: read
25-
issues: read
24+
pull-requests: write
2625
id-token: write
2726

2827
steps:
@@ -37,23 +36,67 @@ jobs:
3736
with:
3837
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
3938
prompt: |
40-
Please review this pull request and provide feedback on:
41-
- Code quality and best practices
42-
- Potential bugs or issues
43-
- Performance considerations
44-
- Security concerns
45-
- Backwards compatibility
46-
- Documentation: public methods (not prefixed with _) should generally have docstrings (##) and classes should contain the @tutorial tag
47-
48-
For each of the points above, do not point out what works well, only what could be improved (if anything).
49-
Be constructive and helpful in your feedback but do not repeat yourself - only summarise potential issues.
50-
Test coverage is currently not a priority.
51-
Prefix section headers with emojis and use dividers for better readability.
52-
53-
Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
54-
Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
55-
56-
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
57-
# or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options
58-
claude_args: '--model sonnet --allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'
59-
use_sticky_comment: true
39+
You are reviewing PR #${{ github.event.pull_request.number }} in the repository ${{ github.repository }}.
40+
41+
Review this pull request and provide feedback focused only on improvements needed (not what works well):
42+
43+
**Categories to check:**
44+
1. Code quality and best practices
45+
2. Potential bugs or issues
46+
3. Performance considerations
47+
4. Security concerns
48+
5. Backwards compatibility
49+
50+
**Process:**
51+
- The PR number is: ${{ github.event.pull_request.number }}
52+
- View the PR using: `gh pr view ${{ github.event.pull_request.number }} --repo ${{ github.repository }}`
53+
- Read CLAUDE.md to understand best practices
54+
- View the PR diff using: `gh pr diff ${{ github.event.pull_request.number }} --repo ${{ github.repository }}`
55+
- If an issue spans multiple categories, list it only once in the most relevant section
56+
- Prioritize by severity: 🔴 Critical → 🟡 Major → 🔵 Minor
57+
- Focus only on changes introduced in this PR, not pre-existing code issues
58+
- Test coverage is currently not a priority
59+
60+
**Review Workflow (Follow these steps):**
61+
1. **Analysis Phase**: Review the PR diff and identify potential issues
62+
2. **Validation Phase**: For each issue you find, verify it by:
63+
- Re-reading the relevant code carefully
64+
- Checking if your suggested fix is actually different from the current code
65+
- Confirming the issue violates documented standards (check CLAUDE.md)
66+
- Ensuring your criticism is actionable and specific
67+
3. **Draft Phase**: Write your review only after validating all issues
68+
4. **Quality Check**: Before posting, remove any issues where:
69+
- Your "before" and "after" code snippets are identical
70+
- You're uncertain or use phrases like "appears", "might", "should verify"
71+
- The issue is theoretical without clear impact
72+
5. **Post Phase**: Only post the review if you have concrete, validated feedback
73+
74+
**Edge Case Policy:**
75+
Only flag edge cases that meet ALL of these criteria:
76+
1. Realistic: Could happen in normal usage or common error scenarios
77+
2. Impactful: Would cause bugs, security issues, or data problems (not just "it's not perfect")
78+
3. Actionable: Can be fixed with reasonable effort in this PR's scope
79+
80+
Ignore theoretical issues that require multiple unlikely conditions or malicious input patterns.
81+
Use the "would this bother a pragmatic senior developer?" test.
82+
83+
Maximum chain of assumptions: 2 levels deep. Skip exotic input combinations that violate documented assumptions.
84+
85+
**Feedback style:**
86+
- Provide specific code examples or line references showing the issue
87+
- Suggest concrete fixes with code snippets where helpful
88+
- Keep total feedback under 500 words
89+
- Use section headers with emojis and horizontal dividers (---)
90+
- If no improvements needed in a category, simply state "No issues found"
91+
- Use neutral language; focus on the code, not the author
92+
- If the PR looks good overall, say so clearly rather than forcing criticism
93+
94+
**Comment Management (IMPORTANT):**
95+
Post your review using this command, which will edit your last comment if one exists, or create a new one:
96+
```bash
97+
gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --edit-last --create-if-none --body "<review>"
98+
```
99+
100+
Ensure proper escaping of quotes and special characters in the comment body. Use single quotes around the body and escape any single quotes inside with '\''
101+
claude_args: |
102+
--allowedTools "Read,Bash(gh pr:*),Grep,Glob"

CLAUDE.md

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
This is the **Talo Godot Plugin** - a self-hostable game development backend plugin for the Godot Engine (v4.4+). Talo provides leaderboards, player authentication, event tracking, game saves, stats, channels, live config, and more. The plugin is distributed via the Godot Asset Library and GitHub releases.
8+
9+
## Development Commands
10+
11+
### Building & Testing
12+
```bash
13+
# Export for all platforms (runs on push via CI)
14+
godot --headless --export-release 'Windows Desktop'
15+
godot --headless --export-release 'macOS'
16+
godot --headless --export-release 'Linux'
17+
godot --headless --export-release 'Web'
18+
19+
# The CI checks for SCRIPT ERROR in build output and fails if found
20+
```
21+
22+
### Running Samples
23+
The project includes sample scenes demonstrating plugin features. The main scene is configured as `res://addons/talo/samples/playground/playground.tscn`. To test samples, change the main scene in [project.godot](project.godot) or run scenes directly from the editor.
24+
25+
Available samples:
26+
- **Playground**: Text-based playground for testing identify, events, stats, leaderboards
27+
- **Authentication**: Registration/login/account management flow
28+
- **Leaderboards**: Basic leaderboard UI
29+
- **Multi-scene saves**: Persist save data across multiple scenes
30+
- **Persistent buttons**: Simple save/load demo
31+
- **Chat**: Real-time messaging using channels
32+
- **Channel storage**: Shared player data storage
33+
34+
## Architecture
35+
36+
### Core Structure
37+
38+
The plugin is an **autoload singleton** called `Talo` (defined in [talo_manager.gd](addons/talo/talo_manager.gd:20)) that initializes on `_ready()` and provides access to all APIs, settings, and utilities.
39+
40+
**Key architectural components:**
41+
42+
1. **TaloManager** ([talo_manager.gd](addons/talo/talo_manager.gd)) - Main autoload singleton
43+
- Initializes all API instances, crypto manager, continuity manager, and socket
44+
- Manages current player/alias state
45+
- Emits `init_completed`, `connection_lost`, and `connection_restored` signals
46+
- Handles app quit and focus events to flush pending data
47+
48+
2. **TaloClient** ([talo_client.gd](addons/talo/talo_client.gd)) - HTTP client wrapper
49+
- Used by all API classes (via [apis/api.gd](addons/talo/apis/api.gd))
50+
- Builds authenticated requests with proper headers (access key, player/alias/session tokens)
51+
- Logs requests/responses if enabled in settings
52+
- Triggers continuity system on failed requests
53+
- Version: Auto-updated by pre-commit hook
54+
55+
3. **TaloSettings** ([talo_settings.gd](addons/talo/talo_settings.gd)) - Configuration management
56+
- Reads/writes [settings.cfg](addons/talo/settings.cfg)
57+
- Key settings: `access_key`, `api_url`, `socket_url`, `auto_connect_socket`, `continuity_enabled`, `debounce_timer_seconds`
58+
- Auto-creates settings.cfg with defaults if missing
59+
- Feature tags: `talo_dev` (force debug), `talo_live` (force release)
60+
61+
4. **Continuity System** ([utils/continuity_manager.gd](addons/talo/utils/continuity_manager.gd)) - Offline resilience
62+
- Automatically retries failed POST/PUT/PATCH/DELETE requests
63+
- Excludes: health checks, auth, identify, socket tickets
64+
- Stores encrypted requests in `user://tc.bin`
65+
- Replays up to 10 requests every 10 seconds when online
66+
- Ignores time scale for reliability
67+
68+
5. **TaloSocket** ([talo_socket.gd](addons/talo/talo_socket.gd)) - WebSocket communication
69+
- Connects to `wss://api.trytalo.com` by default
70+
- Requires ticket creation via [socket_tickets_api.gd](addons/talo/apis/socket_tickets_api.gd)
71+
- Handles player identification with socket token
72+
- Polls in `_process()` loop
73+
- Used by channels, player presence, and custom real-time features
74+
75+
### API Layer
76+
77+
All API classes extend `TaloAPI` ([apis/api.gd](addons/talo/apis/api.gd)), which provides a `TaloClient` instance. There are 14 API classes:
78+
79+
- [players_api.gd](addons/talo/apis/players_api.gd) - Player identification and management
80+
- [player_auth_api.gd](addons/talo/apis/player_auth_api.gd) - Authentication and sessions
81+
- [events_api.gd](addons/talo/apis/events_api.gd) - Event tracking
82+
- [stats_api.gd](addons/talo/apis/stats_api.gd) - Player and global stats
83+
- [leaderboards_api.gd](addons/talo/apis/leaderboards_api.gd) - Leaderboard management
84+
- [saves_api.gd](addons/talo/apis/saves_api.gd) - Game save operations
85+
- [feedback_api.gd](addons/talo/apis/feedback_api.gd) - Player feedback collection
86+
- [game_config_api.gd](addons/talo/apis/game_config_api.gd) - Live config
87+
- [health_check_api.gd](addons/talo/apis/health_check_api.gd) - Connectivity checks (debounced)
88+
- [player_groups_api.gd](addons/talo/apis/player_groups_api.gd) - Player grouping
89+
- [channels_api.gd](addons/talo/apis/channels_api.gd) - Real-time messaging
90+
- [socket_tickets_api.gd](addons/talo/apis/socket_tickets_api.gd) - Socket authentication
91+
- [player_presence_api.gd](addons/talo/apis/player_presence_api.gd) - Online status
92+
93+
### Entity System
94+
95+
18 entity classes in [addons/talo/entities/](addons/talo/entities/) represent API data models. Key entities:
96+
- `TaloPlayer` - Player data with props
97+
- `TaloPlayerAlias` - Player identity/device association
98+
- `TaloLeaderboardEntry` - Leaderboard scores
99+
- `TaloGameSave` - Saved game state
100+
- `TaloLiveConfig` - Dynamic configuration
101+
- `TaloChannel` - Real-time messaging channels
102+
103+
Most entities extend `TaloLoadable` ([entities/loadable.gd](addons/talo/entities/loadable.gd)) which provides JSON serialization and `from_api()` factory methods.
104+
105+
### Utilities
106+
107+
Key utilities in [addons/talo/utils/](addons/talo/utils/):
108+
- **SavesManager** - Handles save CRUD, caching, offline support
109+
- **LeaderboardEntriesManager** - Manages leaderboard entry state
110+
- **ChannelStorageManager** - Manages channel-based shared storage
111+
- **CryptoManager** - Encryption key generation/storage for offline data
112+
- **SessionManager** - Session token persistence
113+
- **DebounceTimer** - Debounces health checks, player updates, save updates (1s default, configurable via `debounce_timer_seconds`)
114+
115+
## GDScript Standards
116+
117+
This project enforces **strict type safety** - all warnings are set to error level (2) in [project.godot](project.godot:24-58):
118+
- All variables must be explicitly typed (`var foo: String`) unless their type can be easily inferred using the `:=` operator
119+
- No unsafe property/method access
120+
- No unsafe casts or call arguments
121+
- All function parameters and return types must be typed
122+
123+
When writing code:
124+
- Always use explicit type annotations
125+
- Use `class_name` for all classes
126+
- Prefer `await` for async operations (avoid `yield`)
127+
- Follow existing patterns in API/entity/utility files
128+
129+
## Plugin Configuration
130+
131+
The plugin autoload is configured in [project.godot](project.godot:20):
132+
```
133+
Talo="*res://addons/talo/talo_manager.gd"
134+
```
135+
136+
Settings are in [addons/talo/settings.cfg](addons/talo/settings.cfg) - this file is auto-generated and should be filled with the user's access key.
137+
138+
## CI/CD
139+
140+
GitHub Actions workflows in [.github/workflows/](/.github/workflows/):
141+
- **ci.yml** - Runs on every push, exports for all platforms, fails on script errors
142+
- **create-release.yml** - Automates releases
143+
- **tag.yml** - Handles version tagging
144+
- **claude-code-review.yml** - Automated code review
145+
146+
## Important Notes
147+
148+
- **Process mode**: TaloManager uses `PROCESS_MODE_ALWAYS` ([talo_manager.gd](addons/talo/talo_manager.gd:48))
149+
- **Auto accept quit**: Disabled to ensure proper flush on exit ([talo_manager.gd](addons/talo/talo_manager.gd:47))
150+
- **Identity checks**: Most API operations require `Talo.players.identify()` first
151+
- **Offline mode**: Setting `offline_mode = true` simulates no internet for testing
152+
- **Debouncing**: Health checks, player updates, and save updates are debounced (configurable via `debounce_timer_seconds`)
153+
- **Time scale**: Continuity manager and debounce timer ignore time scale for reliability

addons/talo/apis/health_check_api.gd

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,37 @@ enum HealthCheckStatus {
1111
UNKNOWN
1212
}
1313

14-
var _last_health_check_status := HealthCheckStatus.UNKNOWN
14+
var _cached_result := HealthCheckStatus.UNKNOWN
15+
var _can_ping := true
16+
var _timer := TaloDebounceTimer.new(func (): _can_ping = true)
1517

16-
## Ping the Talo Health Check API to check if Talo can be reached.
18+
func _ready() -> void:
19+
add_child(_timer)
20+
21+
## Check if the Talo API can be reached.
1722
func ping() -> bool:
23+
var bust_cache := _can_ping or _cached_result == HealthCheckStatus.UNKNOWN
24+
if not bust_cache:
25+
return _cached_result == HealthCheckStatus.OK
26+
27+
_can_ping = false
28+
_timer.debounce()
29+
1830
var res := await client.make_request(HTTPClient.METHOD_GET, "")
1931
var success := true if res.status == 204 else false
20-
var failed_last_health_check := true if _last_health_check_status == HealthCheckStatus.FAILED else false
32+
var failed_last_health_check := _cached_result == HealthCheckStatus.FAILED
2133

2234
if success:
23-
_last_health_check_status = HealthCheckStatus.OK
35+
_cached_result = HealthCheckStatus.OK
2436
if failed_last_health_check:
2537
Talo.connection_restored.emit()
2638
else:
27-
_last_health_check_status = HealthCheckStatus.FAILED
39+
_cached_result = HealthCheckStatus.FAILED
2840
if not failed_last_health_check:
2941
Talo.connection_lost.emit()
3042

3143
return success
3244

3345
## Get the latest known health check status.
3446
func get_last_status() -> HealthCheckStatus:
35-
return _last_health_check_status
47+
return _cached_result

addons/talo/apis/leaderboards_api.gd

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ func add_entry(internal_name: String, score: float, props: Dictionary[String, Va
8484

8585
match res.status:
8686
200:
87-
var entry = TaloLeaderboardEntry.new(res.body.entry)
88-
_entries_manager.upsert_entry(internal_name, entry)
87+
var entry := TaloLeaderboardEntry.new(res.body.entry)
88+
_entries_manager.upsert_entry(internal_name, entry, true)
8989

9090
return AddEntryResult.new(entry, res.body.updated)
9191
_:

addons/talo/apis/players_api.gd

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,14 @@ signal identification_failed()
1717
## Emitted after calling clear_identity().
1818
signal identity_cleared()
1919

20+
var _update_timer := TaloDebounceTimer.new(_handle_update_timer_timeout)
21+
2022
func _ready() -> void:
2123
Talo.connection_restored.connect(_on_connection_restored)
24+
add_child(_update_timer)
25+
26+
func _handle_update_timer_timeout() -> void:
27+
await Talo.players.update()
2228

2329
func _handle_identify_success(alias: TaloPlayerAlias, socket_token: String = "") -> TaloPlayer:
2430
if not await Talo.is_offline():
@@ -57,6 +63,10 @@ func identify_steam(ticket: String, identity: String = "") -> TaloPlayer:
5763
else:
5864
return await identify("steam", "%s:%s" % [identity, ticket])
5965

66+
## Queue a debounced update to the current player. The timer will reset every time this method is called.
67+
func debounce_update() -> void:
68+
_update_timer.debounce()
69+
6070
## Flush and sync the player's current data with Talo.
6171
func update() -> TaloPlayer:
6272
if Talo.identity_check() != OK:

addons/talo/apis/saves_api.gd

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ var latest: TaloGameSave:
2828
var current: TaloGameSave:
2929
get: return _saves_manager.current_save
3030

31+
var _update_timer := TaloDebounceTimer.new(_handle_update_timer_timeout)
32+
33+
func _ready() -> void:
34+
add_child(_update_timer)
35+
36+
func _handle_update_timer_timeout() -> void:
37+
if _saves_manager.current_save:
38+
await update_save(_saves_manager.current_save)
39+
3140
## Sync an offline save with an online save using the offline save data.
3241
func replace_save_with_offline_save(offline_save: TaloGameSave) -> TaloGameSave:
3342
var res := await client.make_request(HTTPClient.METHOD_PATCH, "/%s" % offline_save.id, {
@@ -112,25 +121,37 @@ func register(loadable: TaloLoadable) -> void:
112121

113122
## Update the currently loaded save using the current state of the game and with the given name.
114123
func update_current_save(new_name: String = "") -> TaloGameSave:
115-
return await update_save(_saves_manager.current_save, new_name)
124+
if not _saves_manager.current_save:
125+
return null
126+
127+
# if the save is being renamed, sync it immediately
128+
if not new_name.is_empty():
129+
return await update_save(_saves_manager.current_save, new_name)
130+
# else, update the save locally and queue it for syncing
131+
else:
132+
_update_timer.debounce()
133+
_saves_manager.current_save.content = _saves_manager.get_save_content()
134+
return _saves_manager.current_save
116135

117136
## Update the given save using the current state of the game and with the given name.
118137
func update_save(save: TaloGameSave, new_name: String = "") -> TaloGameSave:
119-
var content := _saves_manager.get_save_content()
138+
var is_offline := await Talo.is_offline()
139+
var can_update_save := Talo.identity_check() == OK or is_offline
140+
if not can_update_save:
141+
return null
120142

121-
if await Talo.is_offline():
122-
if not new_name.is_empty():
123-
save.name = new_name
143+
if not new_name.is_empty():
144+
save.name = new_name
145+
146+
var content := _saves_manager.get_save_content()
147+
save.content = content
124148

125-
save.content = content
149+
if is_offline:
126150
save.updated_at = TaloTimeUtils.get_current_datetime_string()
127151
else:
128-
if Talo.identity_check() != OK:
129-
return
130-
131152
var res := await client.make_request(HTTPClient.METHOD_PATCH, "/%s" % save.id, {
132-
name=save.name if new_name.is_empty() else new_name,
133-
content=content
153+
name=save.name,
154+
content=save.content
134155
})
135156

136157
match res.status:

0 commit comments

Comments
 (0)