using System; using System.Collections.Generic; using System.Linq; using System.Text; using SteamNative; namespace Facepunch.Steamworks { public class Achievements : IDisposable { internal Client client; public Achievement[] All { get; private set; } public event Action OnUpdated; public event Action OnAchievementStateChanged; private List unlockedRecently = new List(); internal Achievements( Client c ) { client = c; All = new Achievement[0]; c.RegisterCallback( UserStatsReceived ); c.RegisterCallback( UserStatsStored ); Refresh(); } public void Refresh() { var old = All; All = Enumerable.Range( 0, (int)client.native.userstats.GetNumAchievements() ) .Select( x => { if ( old != null ) { var name = client.native.userstats.GetAchievementName( (uint)x ); var found = old.FirstOrDefault( y => y.Id == name ); if ( found != null ) { if ( found.Refresh() ) { unlockedRecently.Add( found ); } return found; } } return new Achievement( client, x ); } ) .ToArray(); foreach ( var i in unlockedRecently ) { OnUnlocked( i ); } unlockedRecently.Clear(); } internal void OnUnlocked( Achievement a ) { OnAchievementStateChanged?.Invoke( a ); } public void Dispose() { client = null; } /// /// Find an achievement by name. Will be null if not found, or not ready. /// public Achievement Find( string identifier ) { return All.FirstOrDefault( x => x.Id == identifier ); } /// /// Unlock an achievement by identifier. If apply is true this will happen as expected /// and the achievement overlay will popup etc. If it's false then you'll have to manually /// call Stats.StoreStats() to actually trigger it. /// public bool Trigger( string identifier, bool apply = true ) { var a = Find( identifier ); if ( a == null ) return false; return a.Trigger( apply ); } /// /// Reset an achievement by identifier /// public bool Reset( string identifier ) { return client.native.userstats.ClearAchievement( identifier ); } private void UserStatsReceived( UserStatsReceived_t stats ) { if ( stats.GameID != client.AppId ) return; Refresh(); OnUpdated?.Invoke(); } private void UserStatsStored( UserStatsStored_t stats ) { if ( stats.GameID != client.AppId ) return; Refresh(); OnUpdated?.Invoke(); } } public class Achievement { private Client client; public string Id { get; private set; } public string Name { get; private set; } public string Description { get; private set; } /// /// True if unlocked /// public bool State { get; private set; } /// /// Should hold the unlock time if State is true /// public DateTime UnlockTime { get; private set; } private int iconId { get; set; } = -1; private int refreshCount = 0; /// /// Returns the percentage of users who have unlocked the specified achievement, or -1 if no data available. /// public float GlobalUnlockedPercentage { get { if ( State ) return 1; float pct = 0; if ( !client.native.userstats.GetAchievementAchievedPercent( Id, out pct ) ) return -1.0f; return pct; } } private Image _icon; public Image Icon { get { if ( iconId <= 0 ) return null; if ( _icon == null ) { _icon = new Image(); _icon.Id = iconId; } if ( _icon.IsLoaded ) return _icon; if ( !_icon.TryLoad( client.native.utils ) ) return null; return _icon; } } public Achievement( Client client, int index ) { this.client = client; Id = client.native.userstats.GetAchievementName( (uint) index ); Name = client.native.userstats.GetAchievementDisplayAttribute( Id, "name" ); Description = client.native.userstats.GetAchievementDisplayAttribute( Id, "desc" ); iconId = client.native.userstats.GetAchievementIcon( Id ); Refresh(); } /// /// Make this achievement earned /// public bool Trigger( bool apply = true ) { if ( State ) return false; State = true; UnlockTime = DateTime.Now; var r = client.native.userstats.SetAchievement( Id ); if ( apply ) { client.Stats.StoreStats(); } client.Achievements.OnUnlocked( this ); return r; } /// /// Reset this achievement to not achieved /// public bool Reset() { State = false; UnlockTime = DateTime.Now; return client.native.userstats.ClearAchievement( Id ); } /// /// Refresh the unlock state. You shouldn't need to call this manually /// but it's here if you have to for some reason. Retuns true if state changed (meaning, probably unlocked) /// public bool Refresh() { bool previousState = State; bool state = false; uint unlockTime; State = false; if ( client.native.userstats.GetAchievementAndUnlockTime( Id, ref state, out unlockTime ) ) { State = state; UnlockTime = Utility.Epoch.ToDateTime( unlockTime ); } refreshCount++; if ( previousState != State && refreshCount > 1 ) { return true; } return false; } } }