The "welcome message" that shows up to players when a game starts up and Achievements are active will now either tell the player upon load that there's no support for achievements, or will wait until the first attempt to load the game's badge either succeeds or fails and then display a full message with title and progress and status. This is in part facilitated by a boolean field for when to display a welcome message, set to true upon loading and to false upon a message being displayed.
If achievements were disabled but a player token is in settings, prior to this change the Achievement Manager dialog would show a box with no player name and score zero, which is unnecessary.
Bugfix for hardcore-disabled items being disabled when hardcore was true but achievement integration was false, which should mean hardcore is effectively disabled. Now everything checks the IsHardcoreModeActive method in AchievementManager which processes the setting AND the game state to determine if hardcore mode is actually active.
Spectator Mode is a new mode added by rc_client that allows for achievement and leaderboard functionality, but does not submit this data to the site, partially allowing for offline achievements. It effectively replaces the former settings for disabling achievements, leaderboards, and RP, which are now always active internally as long as the client is active.
The client can take care of itself and handle its own hardcore status when it toggles, so I can tell the settings widget to contact the manager directly to set it.
Also, gradually reorganizing the settings dialog over the next handful of commits.
The client can handle media changes natively so disabling can take place internally. This code uses the same external calls to load data, but will call either BeginLoad or BeginChangeMedia based on whether any media is already loaded.
Due to the client's handling of media changes (it simply disables hardcore if an unknown media is detected) the existing functionality for "disabling" the achievements is no longer necessary and can be deleted.
Two portions of this need updating.
Anything related to points and unlock counts and scoring uses game_summary now instead of the TallyScore method. Unfortunately this comes with the drawback that I cannot easily at this time access the number of points/unlocks from the other hardcore mode, so things like the second progress bar have been deleted.
Rich presence, which no longer needs to be stored, but can be calculated at request. As the AchievementHeader can now update just the Rich Presence, DoFrame can now simply call a header update with .rp=true and the current Rich Presence will be calculated immediately.
As the two items above are the last remaining things to use a number of the components in AchievementManager, this also deletes: Request V1 (V2 is renamed accordingly), ResponseType, PointSpread, TallyScore, UnlockStatus, and the RP generation and ping methods.
UpdateData in AchievementsWindow now only updates the components being requested, massively improving the window's performance. The parameter is UpdatedItems in AchievementManager, which tracks which portions of the system have been updated for every update callback.
Similarly to the Progress widget (though without the separate object for each box, because each Leaderboard unit is just three text fields stacked vertically), AchievementLeaderboardWidget.UpdateData will now accept three options: destroy all and rebuild, update all, or update a set of rows.
As part of this, AchievementManager::GetLeaderboardsInfo has been refactored to GetLeaderboardInfo to return a single leaderboard for the ID passed in.
AchievementProgressWidget maintains in memory a map of AchievementBox pointers so that UpdateData can operate on them individually. UpdateData is overhauled for three options: UpdateData(true) will destroy the entire list and re-create it from scratch as before, to be used if the game or player changes/closes/logs out. UpdateData(false) will loop through the map and call UpdateData on every achievement box, to be used for certain settings changes such as enabling badges or disabling hardcore mode. UpdateData(set<IDs>) will call UpdateData on only the IDs in the set, to be used when achievements are unlocked.
AchievementBox is an extension of QGroupBox that contains the data for a single achievement, initialized with the achievement data and able to reference AchievementManager to update itself.
While state loading is not allowed in the hardcore mode that most players will use, it is allowed in softcore mode; more importantly, if something fails to unlock or unlocks when it shouldn't in either mode the player can create a save that retains the current achievement state.
This is not a 1 to 1 relationship with how the events look primarily because currently achievement
progress messages are in OnScreenDisplay, which currently vanishes messages automatically.
As this covers the last remaining runtime-based event from the old event handler, that handler has been deleted and the new event handler has been renamed to take its place.
Up to four leaderboards are displayed in a window in the bottom right of the screen (vertically above challenge icons, if there are any). As per RetroAchievements standards, the markers only display the current leaderboard values with no further context necessary.
The active leaderboard data (leaderboards currently being attempted, which get displayed on screen) is now tracked. When a leaderboard is started its value is added to a vector (sorted by start frame). There are a separate set of client events specifically to handle leaderboard trackers, that are used to populate and manage this vector. The top portion of this vector (by RetroAchievement standards, the first four items) is exposed to be displayed on screen.
Also deletes the old runtime-based Achievement Triggered event from the old handler, and the methods used by it to publish to the server and reactivate/deactivate achievements in the runtime.
This change was primarily made to refactor the badge fetching to use the client instead of the runtime, but in the process I also refactored the code to cut down on complexity and duplication. Now the FetchBadge method is passed a function that generates the badge name; this is used to ensure that once the badge is loaded that it is still the desired badge to avoid race conditions.
HashGame has become LoadGame, similar structure with the file loaders but using the client instead. LoadGameCallback has been created to handle the results. The old LoadGameSync has been deleted as have
several hash and load methods that it called.
Deletes AchievementManager::Login, renames LoginAsync to Login, and replaces the one synchronous call in the AchievementSettingsWidget with the async call. There is a minor usability regression in that the UI currently does not notify the user when a login has failed; this will be addressed in a later change (possibly in a different PR).
Testing found that spamming toggles for Enable Leaderboards etc risked leaderboards being deleted while the runtime was in the process of recalculating them; this should clear up those conflicts.
Not only was the extra call to the update callback in the AchievementEventHandler method unnecessary, it was getting called on events that don't even need to be tracked here, causing a lot of lag when it turned out one achievement was repeatedly spamming Achievement Reset Events as a shortcut.
Failing to do this was causing challenge icons to carry over into the next game if a game was closed while the icons were active, even if the next game to run was a completely different game entirely with completely different badges.
RetroAchievements plans to use the user_agent in unlock requests to determine which software version was used to play the game, and can filter older software versions out. As such, I have been given the go-ahead to remove the hardcoded line that forces hardcore to always be false.
Two minor updates to improve the Achievement Manager's handling of a player's completion rate.
One, UnlockStatus and the unlock map now track achievement category, such that TallyScore does not count unofficial achievements in counts/points.
Two, the determinations for mastery/completion are now improved to check (1) that the achievement triggering this is CORE (not UNOFFICIAL) and (2) that it has not already been unlocked at this level on the site, which should be sufficient to determine that the unlocking of this particular achievement completes/masters the game.
Most obviously, there is no longer a warning message to the player in the achievement window that achievements are disabled if a game is not currently running.
Adds a setting field under the hood to retain which folder the player last saved/loaded a state to/from, so that the dialog box to select a state to save/load reopens at that folder.
The Host URL setting in the RetroAchievements config will, if set, be used as the host URL for all server requests for achievements. This allows for an easy switch to the RetroAchievements staging server for testing.
Two fixes: verify that there's an API token before attempting to automatically log in, and don't attempt to hash a game and load it unless achievements are enabled and the player is logged in. This prevents multiple API requests that we already know will fail, one of which will display an unnecessary error message to the player.
The active challenges, aka the primed achievements, are displayed on screen as a series of icons in the bottom right corner of the screen via OnScreenUI.
When an achievement is "primed", a challenge is active, for example completing a portion of the game in under a time limit or without taking damage or using certain items. This is stored in a map in the Achievement Manager (and removed when the achievement is unprimed) so a later commit can display it on screen.
The Disabled state sits between Game Closed and completely Shutdown - stronger than the former, as it refuses to let a game be opened again until AchievementManager is restored (which only happens upon a fresh core boot) but it isn't completely shut down and will still allow the player to be logged in and access the achievement settings and their (global) achievement header.
This change splits LoadGameAsync into three methods: two HashGame methods to accept either a string filepath or a volume, and a common LoadGameSync that both HashGames call to actually process the code. In the process, some minor cleanup, and the hash resolution now takes place on the work queue asynchronously.
This adds the actual switch to turn on Hardcore Mode to the settings tab of the Achievements dialog. It is accompanied by a large tooltip warning explaining what it does and when it can be enabled.
The switch is only enabled to be turned on when no game is running, so that games are started in hardcore mode and can only be loaded via the console's memory card, as in the original hardware. Hardcore may be turned off while a game is running, but cannot be turned back on until the game is disabled.
The toggle trigger for hardcore mode also automatically disables the settings that are not allowed during hardcore mode.
Finally, the original flag in AchievementSettingsWidget to set whether things are enabled in hardcore mode (primarily Leaderboards) is replaced with the actual Hardcore Mode setting.
Play Input Recording would potentially unlock achievements without any player input and needs to be disabled. If a recording is already playing, hardcore mode cannot be enabled.
The player getting a better view of their surroundings than the game would normally allow could possibly give the player an advantage over the original hardware, so Freelook is disabled in hardcore mode. To do this, I disable the config flag for Freelook when it is accessed, to make sure that it is disabled whether it was enabled before or after hardcore mode was enabled.
Memory patches would be an easy way to manipulate the memory needed to calculate achievement logic, so they must be disabled. Riivolution patches that do not affect memory are allowed, as they will be hashed with the game file.
Debug Mode gives players direct read and write access to memory, which could be used to completely manipulate RetroAchievements logic and therefore is not allowed in hardcore mode.
Slowing down the emulator would artificially improve reaction times and is therefore disallowed in hardcore mode. Speeds faster than 1x are allowed, but any speed below 1x is scaled back up to 1x.
Frame advancing is easily exploitable for slowing down a game and artificially improving reaction times and is not allowed in RetroAchievements hardcore mode.
While saving states is allowed (especially for the purpose of debugging), RetroAchievements does not allow loading saved states when hardcore mode is on.
This widget will be used in several places to notify the player that a feature has been disabled because hardcore mode is on. It includes a button to open the Achievement Settings so that Hardcore Mode may be turned off. Also included is the framework required to open AchievementsWindow specifically on the Settings tab.
Hardcore Mode is a RetroAchievements feature for enabling as close to original hardware as possible, to keep a fair, challenging, and competitive playing field for achievements (which get tallied differently and emphasized more in hardcore) and leaderboards (where it is mandatory) at the cost of several common emulator features that provide advantages, such as state loading and slower emulation speeds.
This commit just adds the flag to the AchievementSettings, with more to come.
Split the "welcome" messages letting players know achievements are active into a separate method that gets called (currently) after a number of frames to ensure that the emulator has started properly and has somewhere to display the messages.
A new tab is added to the Achievements dialog to chart out the leaderboards in a table. Each row of the table contains the leaderboard information and up to four relevant entries, varying based on how many entries are in the leaderboard, whether or not the player has a submitted score, and where in the leaderboard the player's score is.
FetchBoardInfo is called (via the work queue asynchronously) on a leaderboard every time it is activated or submitted to. It makes two calls to the RetroAchievements API for fetching leaderboard info, one that requests the top four entries in the leaderboard and another that requests the player's entry, the two entries above the player and the two entries below. All of these are inserted into a single map (resolving any overlaps) so the result can be exposed to the UI.
The leaderboard map created here contains information useful to displaying leaderboard stats in the Achievement dialog, including each leaderboard's name and description and a partial list of entries for display. The entire map is exposed to the UI in a single call for simplicity.
Added handling to Achievement Progress Events, which are generated when an achievement with a Measured field updates in value. For example, an achievement for collecting 120 stars will throw this event when the player collects each star, and with this handling, the player will get a message on screen informing them of this progress. This message will only appear if the newly added RA_PROGRESS_ENABLED setting is true.
A deep-copy method CopyReader has been added to BlobReader (virtual) and all of its subclasses (override). This should create a second BlobReader to open the same set of data but with an independent read pointer so that it doesn't interfere with any reads done on the original Reader.
As part of this, IOFile has added code to create a deep copy IOFile pointer onto the same file, with code based on the platform in question to find the file ID from the file pointer and open a new one. There has also been a small piece added to FileInfo to enable a deep copy, but its only subclass at this time already had a copy constructor so this was relatively minor.
The achievement badges will now have a blue or gold border to identify whether they have been unlocked in softcore or hardcore mode. Similarly, the game badge will have a blue border if all achievements have been unlocked in either mode or a gold border if all achievements have been unlocked in hardcore mode.
Provided the badges are turned on in the settings, each achievement will have a badge next to it on the progress tab. There are different badges for locked and unlocked (usually locked is grayscale while unlocked is in color but not necessarily) and the badge chosen depends on the player's current unlock and hardcore status.
Provided badges are turned on, if there's a player logged in their RetroAchievements icon will appear next to their player info in the header of the Achievements dialog. If they're playing a game, so will the icon for the game. Also performed some refactoring and reorganizing to the header as a whole so that it looks consistent whether a game is running or not.
FetchBadges fetches all available badges (player, game, achievements) and stores them in AchievementManager's memory. New fields and accessors have been added as necessary. Badges for individual achievements are stored in the UnlockStatus map. The method is public so that the settings dialog can call it if badges are turned on after a game is started. Badges are deleted at game close and logout.
Image requests from RetroAchievements have a slightly different format from other requests, on account of being GET calls that return strings of image data, so this is a separate function that makes such calls. To handle this, I create a BadgeStatus object that ties each badge to a boolean flag for whether it is loaded and thus usable.
This refactors the Rich Presence generation to store to a member field that can be exposed to the UI to display the Rich Presence in the achievement header. It still updates at its original rate of once per two minutes, but calls an update on the dialog when that ticks.
Moved AchievementManager Init further down in the MainWindow constructor; its original position was because it had an impact on the contents of the menu bar, and this is no longer the case.
Expanded the use of the lock mutex already used for loading the player's existing unlock status to guard against races involving the Achievements dialog window reading from data AchievementManager might be in the process of updating. The lock has been exposed publicly and the AchievementsWindow uses it in its UpdateData method, and anywhere else that might modify data used to render that window has also been wrapped with it.
AchievementManager now has a SetUpdateCallback method for providing a single universal callback for anytime something important changes in the achievement state, such as logging in/out, game load/close, or events such as achievement unlocks. AchievementsWindow sets this callback in its own init to its UpdateData method so that the AchievementsWindow gets updated when one of these changes takes place.
This widget is a tab in the AchievementsWindow that displays the player's current achievement progress: which achievements are locked or unlocked, and the progress of achievements that have progress metrics.
This widget displays a header on the AchievementsWindow dialog above the tabs that shows the currently logged in user (if there is one) and the game they are playing (if there is one).
Added some small methods to AchievementManager to expose useful data for displaying in an achievement UI. Also moved a couple things from private to public for the same purpose.