Skip to content

RetroPlayer: Add RetroAchievements Game API callbacks - #150

Closed
sunlollyking wants to merge 1 commit into
garbear:masterfrom
sunlollyking:retroplayer-achievements-api
Closed

RetroPlayer: Add RetroAchievements Game API callbacks#150
sunlollyking wants to merge 1 commit into
garbear:masterfrom
sunlollyking:retroplayer-achievements-api

Conversation

@sunlollyking

@sunlollyking sunlollyking commented Jul 12, 2026

Copy link
Copy Markdown

Description

Extends the Game add-on API with add-on-to-Kodi callbacks for RetroAchievements events, keeping rcheevos as a binary add-on dependency (in game.libretro) rather than compiled into Kodi. This is the Kodi-side counterpart to the game.libretro rc-client-integration PR.

Per garbear's architectural guidance, rcheevos stays in game.libretro. Kodi receives events and handles display only — it performs no RetroAchievements network I/O and holds no knowledge of the achievement runtime. No rcheevos source is compiled into Kodi.

Flow:
game.libretro (owns rc_client) → Game API callbacks → CGameClientCheevos → CAchievementRuntime → OSD dialog

Game API additions (kodi-dev-kit), 6.0.0 → 6.1.0:

  • New C structs: game_rc_achievement, game_rc_game_loaded, game_rc_achievement_triggered, game_rc_login_result, plus GAME_RC_UNLOCK_STATE
  • New callbacks in AddonToKodiFuncTable_Game: RCOnGameLoaded, RCOnAchievementTriggered, RCOnGameCompleted, RCOnRichPresenceUpdated, RCOnLoginResult
  • C++ wrappers in Game.h for add-on-side use
  • The change is purely additive: members are appended to the end of AddonToKodiFuncTable_Game so the offsets used by add-ons built against 6.0.0 stay valid, and KodiToAddonFuncTable_Game is untouched. ADDON_INSTANCE_VERSION_GAME_MIN therefore stays at 6.0.0 and existing add-ons keep working.

Kodi-side implementation:

  • GameClient.cpp: static callback functions wired in Initialize(), forwarding to CGameClientCheevos
  • GameClientCheevos.cpp: event receivers that validate every pointer and count from the add-on, cap what they copy, publish to the achievement runtime and post localized notifications. Badge, icon and avatar URLs are handed to Kodi's texture cache rather than downloaded, so no background threads are involved.
  • AchievementRuntime.h/cpp: extends garbear's existing runtime with targeted accessors for the two fields the info label reads, since GetState() copies the whole achievement list and skins query info labels once per frame per control
  • GamesGUIInfo.cpp: handler for RetroPlayer.AchievementsProgress
  • GameSettings.cpp: SetAchievementsLoggedIn() so a rejected token can't leave the UI claiming the player is signed in

OSD dialog:

  • DialogGameAchievements: achievement list with badges, points, rarity and unlock dates, refreshed live when one is earned. Reuses DialogGameControllers.xml as its window file, as that file documents, so skins without the new layout still work.

Skin (Estuary):

  • GameOSD.xml: an achievements button, shown unconditionally so the dialog can explain whether the player needs to sign in or the game has no achievement set. The menu grows by one row to fit it.
  • Includes_Games.xml: the dialog layout, with the focused row scrolling its title and criteria since achievement criteria are frequently longer than the column

Bugs fixed along the way:

  • CDateTime::GetAsLocalizedDate() was called with a string literal, which binds to the GetAsLocalizedDate(bool) overload as true — the format was silently discarded and the long date used
  • The in-game menu's list extended 80px past its dialog background, letting the bottom row render outside the shaded area
  • GAME_ERROR_NOT_IMPLEMENTED was logged at ERROR. Declining an optional part of the API is not a failure, so it now logs at DEBUG and ERROR is reserved for genuine ones
  • The two hardcoded English strings in CGameSettings::LoginToRA() are now localized, closing a review comment deferred from RetroPlayer: Add RetroAchievements integration xbmc/xbmc#28496

Motivation and context

RetroAchievements integration for Kodi RetroPlayer. Previously rcheevos was vendored directly into the Kodi source tree, which garbear identified as architecturally wrong — third-party libraries used by binary add-ons should stay in the add-on, not in Kodi core. This PR restructures the integration so Kodi only receives and displays events, while game.libretro owns all rcheevos logic.

Addresses garbear's review feedback on #149, and builds on the achievement runtime introduced by #28496.

How has this been tested?

Built and tested on Ubuntu 26.04 x86_64, against the companion game.libretro branch.

Verified end to end: signed in, loaded Sonic the Hedgehog 2 (Mega Drive), earned Hold Your Breath in play, and confirmed via the log that the event reached Kodi, the toast appeared with its badge, the award was submitted to RetroAchievements and the score updated. Also verified on Super Mario Bros. (NES) that the achievement list, counts and progress label populate correctly.

  • Unit tests: 8 in TestGamesGUIInfo, covering the info label, the empty case, and CAchievementRuntime::MarkEarned() not double-counting a re-reported unlock
  • Full suite passes (3513 tests), clang-format clean
  • Verified the guard paths: not signed in, and a game with no achievement set, each show their toast and close without a frame of render
  • Verified the OSD menu fits without scrolling in both the disc and non-disc variants
  • Verified the sign-in notification carries the player's avatar

Platforms tested: Ubuntu 26.04 (development), LibreELEC Generic x86_64 (previous branch iterations)

Requires the companion game.libretro#162 for the events to arrive; without it Kodi simply shows no achievement data.

What is the effect on users?

Adds RetroAchievements support to Kodi RetroPlayer. Users with a RetroAchievements account can:

  • See achievement unlock notifications with badge images while playing retro games
  • Browse a game's achievement list with descriptions, point values, rarity and unlock status
  • See rich presence updates showing current in-game status
  • See a mastery notification when all achievements are unlocked

No impact on users who don't use RetroAchievements. The achievements button is always present in the game OSD, and tells the player they need to sign in rather than silently hiding — the same reasoning as offering port 2 on a one-player game.

Leaderboards are deliberately out of scope. They are redundant until hardcore mode lands and are better reviewed alongside it.

Screenshots (if appropriate):

Types of change

  • New feature (non-breaking change which adds functionality)

Checklist:

  • My code follows the Code Guidelines of this project
  • My change requires a change to the documentation, either Doxygen or wiki
  • I have updated the documentation accordingly
  • I have read the Contributing document
  • I have added tests to cover my change
  • All new and existing tests passed

@sunlollyking

Copy link
Copy Markdown
Author

@garbear I know you're very busy with Beta 2 - I'd be keen when you're doing the next round of Retroplayer builds based on the release to see if we could include this and kodi-game/game.libretro#162 so we're properly able to test unlocking across all platforms. If working then we can start hacking out features into V23 :)

@garbear

garbear commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Indeed, busy with Beta 2, I'll report back here after release (tho realistically prolly before) with a build that includes the open PRs. Just get any PRs open and fully updated and when I can do a build it'll include them all.

I'm still doing my own local testing against retroplayer-xxx builds.

@sunlollyking
sunlollyking force-pushed the retroplayer-achievements-api branch from 17aa36d to bf78933 Compare August 4, 2026 12:46
@sunlollyking
sunlollyking changed the base branch from master to retroplayer-22beta2 August 4, 2026 12:52
@sunlollyking
sunlollyking force-pushed the retroplayer-achievements-api branch 7 times, most recently from 06ee351 to 5c15304 Compare August 5, 2026 15:29
@garbear
garbear force-pushed the retroplayer-22beta2 branch 2 times, most recently from e893bf7 to c5bf624 Compare August 5, 2026 23:23
@garbear garbear self-assigned this Aug 6, 2026
@garbear

garbear commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Failed test cases, needed:

--- a/xbmc/guilib/test/TestGamesGUIInfo.cpp
+++ b/xbmc/guilib/test/TestGamesGUIInfo.cpp
@@ -123,18 +123,20 @@ TEST_F(TestGamesGUIInfo, MarkEarnedOnlyCountsTheFirstUnlock)
   achievementRuntime.SetState(MakeAchievementState());
 
   bool newlyEarned = false;
-  AchievementState state = achievementRuntime.MarkEarned(2, newlyEarned);
+  AchievementState state = achievementRuntime.MarkEarned(2, "2026-08-05 19:20", newlyEarned);
   EXPECT_TRUE(newlyEarned);
   EXPECT_EQ(state.unlockedAchievements, 2U);
+  EXPECT_EQ(state.achievements[1].unlockedDate, "2026-08-05 19:20");
 
   // The achievement runtime re-reports achievements earned in an earlier
-  // session, which must not inflate the count
-  state = achievementRuntime.MarkEarned(2, newlyEarned);
+  // session, which must not inflate the count or replace the unlock date
+  state = achievementRuntime.MarkEarned(2, "2026-08-06 08:15", newlyEarned);
   EXPECT_FALSE(newlyEarned);
   EXPECT_EQ(state.unlockedAchievements, 2U);
+  EXPECT_EQ(state.achievements[1].unlockedDate, "2026-08-05 19:20");
 
   // An unknown ID must not change anything
-  state = achievementRuntime.MarkEarned(99, newlyEarned);
+  state = achievementRuntime.MarkEarned(99, "2026-08-07 12:00", newlyEarned);
   EXPECT_FALSE(newlyEarned);
   EXPECT_EQ(state.unlockedAchievements, 2U);
 }

I'm starting the process of making new RP builds. Still working through build/test failures: https://jenkins.kodi.tv/view/Automation/job/BuildMulti-All/3343/

@garbear

garbear commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Probably retarget my master? Then I can control what the retroplayer-22beta2 branch does on top of this.

@sunlollyking
sunlollyking force-pushed the retroplayer-achievements-api branch from 5c15304 to 3fad5fa Compare August 6, 2026 07:12
@sunlollyking
sunlollyking changed the base branch from retroplayer-22beta2 to master August 6, 2026 07:13
@sunlollyking

sunlollyking commented Aug 6, 2026

Copy link
Copy Markdown
Author

Thanks @garbear — both points addressed, and the branch is updated.

Testing: I've had a LibreELEC Generic x86_64 build running with all of this and it works well — achievements unlock and submit across Genesis and NES titles, progress bars track measured achievements, and unlock notifications carry the right badge.

@garbear

garbear commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Fixed the two baby rebase conflicts. I'll include this in my test builds today.

@garbear

garbear commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Test builds are up! https://github.com/garbear/xbmc/releases/tag/retroplayer-22beta2-20260819

Included everything so far, all work, except for HW rendering.

The new dialog works great. Next I'm working on an achievement to test unlocking.

We should get this work against master soon. I'll do a review here first then we can upstream.

@sunlollyking

Copy link
Copy Markdown
Author

Amazing. Yep I've been using it for some time now and really keen on building on it I've just been waiting for the master merge. It feels rock solid though it's more just making sure your using the rom version that aligns with what's in the RA dB. Open to more feature suggestions and tightening

Comment thread xbmc/games/addons/GameClient.cpp Outdated
Comment thread xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/game.h Outdated
sunlollyking pushed a commit to sunlollyking/game.libretro that referenced this pull request Aug 21, 2026
Review on garbear/xbmc#150 asked for the Kodi prefix to come off the
RetroAchievements callbacks, so the C++ wrapper now matches the C table that
already named them RCOnGameLoaded and friends. Follow that here.

Eleven call sites, no behaviour change. This has to land with the Kodi side
rather than before it -- the old names no longer exist.
sunlollyking pushed a commit to sunlollyking/game.libretro that referenced this pull request Aug 21, 2026
Two things, both following from review elsewhere.

garbear/xbmc#150 removes the seven legacy RetroAchievements entry points from
the Game API now that rc_client lives here, so the stubs that answered them
with GAME_ERROR_NOT_IMPLEMENTED override nothing any more. Gone, along with
their declarations.

The login guard was one-way. m_loginStarted is set when a login begins and was
only cleared in Deinitialize(), so a rejected token left it set and corrected
credentials could not start another attempt until the game was unloaded. It
exists to stop a second login racing one already in flight, not to make failure
permanent, so the failure path clears it. SetCredentials() already calls
BeginLogin(), so a corrected username or token now retries on the spot.
Comment thread xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/game.h Outdated
Comment thread xbmc/cores/RetroPlayer/playback/ReversiblePlayback.cpp Outdated
Comment thread xbmc/games/addons/cheevos/GameClientCheevos.cpp Outdated
Comment thread xbmc/games/addons/cheevos/GameClientCheevos.cpp
Comment thread xbmc/games/GameSettings.cpp Outdated
Comment thread xbmc/games/AchievementRuntime.cpp Outdated
Comment thread xbmc/games/AchievementRuntime.h Outdated
Comment thread xbmc/games/AchievementRuntime.h Outdated

@garbear garbear left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commits can be squashed. Everything here LGTM now. Time to upstream? I wanna do B2 this weekend.

@garbear

garbear commented Aug 25, 2026

Copy link
Copy Markdown
Owner

I needed this to fix clang static analysis (https://jenkins.kodi.tv/job/android-arm64-docker/17484/clang/source.31108e58-e5e7-410a-9672-8bea76e806cf/#38):

--- a/xbmc/games/addons/cheevos/GameClientCheevos.cpp
+++ b/xbmc/games/addons/cheevos/GameClientCheevos.cpp
@@ -34,30 +34,6 @@
 
 namespace
 {
-// C ABI expects a raw callback + context pointer, so we bridge to std::function here
-void __cdecl GetCheevoUrlIdCallback(const void* context,
-                                    const char* achievementUrl,
-                                    unsigned int cheevoId)
-{
-  if (context == nullptr)
-    return;
-
-  const auto* callback = static_cast<
-      const std::function<void(const std::string& achievementUrl, unsigned int cheevoId)>*>(
-      context);
-  if (!(*callback))
-    return;
-
-  try
-  {
-    (*callback)(achievementUrl != nullptr ? std::string{achievementUrl} : std::string{}, cheevoId);
-  }
-  catch (...)
-  {
-    // Never allow exceptions to unwind through the C ABI callback boundary
-  }
-}
-
 // The add-on is not trusted to send sane counts, so cap what we copy out of it.
 // The largest sets published by RetroAchievements are an order of magnitude
 // below these limits.

sunlollyking added a commit to sunlollyking/xbmc that referenced this pull request Aug 25, 2026
Four things from review on garbear#150.

Drop the Kodi prefix from the eight RetroAchievements callbacks in the C++
wrapper. The C table already named them RCOnGameLoaded and so on; only the
wrapper carried the branding, so this makes the two halves agree and follows
what Game API 7.1.1 did for GetPlaybackSpeed. KodiInputEvent is left alone,
being none of this PR's business.

Destroy the achievements dialog. It was added in CreateWindows() and never
torn down, so it outlived DestroyWindows() while every other game dialog did
not.

Move GameAchievementBadgeTintVar out of the generic Variables.xml and put it
directly above GameAchievementItem in Includes_Games.xml, beside the only
thing that uses it.

Drop the comment about appending to the callback table. It restated what the
struct layout already implies.
@sunlollyking
sunlollyking force-pushed the retroplayer-achievements-api branch from 8a283a0 to 3c2ac3f Compare August 25, 2026 07:07
sunlollyking added a commit to sunlollyking/xbmc that referenced this pull request Aug 25, 2026
Four things from review on garbear#150.

Drop the Kodi prefix from the eight RetroAchievements callbacks in the C++
wrapper. The C table already named them RCOnGameLoaded and so on; only the
wrapper carried the branding, so this makes the two halves agree and follows
what Game API 7.1.1 did for GetPlaybackSpeed. KodiInputEvent is left alone,
being none of this PR's business.

Destroy the achievements dialog. It was added in CreateWindows() and never
torn down, so it outlived DestroyWindows() while every other game dialog did
not.

Move GameAchievementBadgeTintVar out of the generic Variables.xml and put it
directly above GameAchievementItem in Includes_Games.xml, beside the only
thing that uses it.

Drop the comment about appending to the callback table. It restated what the
struct layout already implies.
@sunlollyking
sunlollyking force-pushed the retroplayer-achievements-api branch from 3c2ac3f to c7e9eae Compare August 25, 2026 07:08
@sunlollyking

Copy link
Copy Markdown
Author

Done, and rebased onto master. Should be good now

Also dropped the header's #include <functional>that comment was the last mention of std::function in the file.

Comment thread xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/game.h
@garbear

garbear commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Time to PR against master? Clean up history slop, comment slop clearly explains all design decisions so the squashed commit message can be minimal (or at least useful).

@sunlollyking
sunlollyking force-pushed the retroplayer-achievements-api branch 3 times, most recently from ec3a90a to 0771867 Compare August 26, 2026 08:06
Kodi hashed the ROM, talked to the RetroAchievements server and ran the
achievement runtime itself. The add-on is better placed for all three: it
already owns the emulator's memory, and rcheevos ships with it.

Kodi now receives events instead. Eight callbacks report what the runtime
decided - game loaded, achievement triggered, progress, challenge and
leaderboard indicators, rich presence, connection state - and Kodi keeps
the state they describe for skins to show. Achievement state travels in
its own savestate field, sized from what the runtime reports rather than a
fixed reserve, leaving the emulator's blob the exact size the core reports.

The implementation this replaces is removed, along with the seven legacy
entry points it drove. That is what earns Game API 8.0.0.

Pairs with kodi-game/game.libretro#162.
@sunlollyking
sunlollyking force-pushed the retroplayer-achievements-api branch from 0771867 to 6521685 Compare August 26, 2026 08:09
@sunlollyking

sunlollyking commented Aug 26, 2026

Copy link
Copy Markdown
Author

Raised xbmc#29074 !!!! Really excited to get this in to unlock functionality for everyone, see what skinners can produce and also to bring down my diff on my big spaghetti build thats just continued to buidl on this!

@garbear

garbear commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Closing as completed by xbmc#29074. Great work!

@garbear garbear closed this Aug 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants