diff --git a/Achievments.meta b/Achievments.meta
new file mode 100644
index 0000000..b169e48
--- /dev/null
+++ b/Achievments.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: ff9bb206aea50d14997771b9a0cc2b04
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/Achievment.cs b/Achievments/Achievment.cs
new file mode 100644
index 0000000..448fd58
--- /dev/null
+++ b/Achievments/Achievment.cs
@@ -0,0 +1,190 @@
+using NEG.Utils.Achievments.AchievmentTypes;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace NEG.Utils.Achievments
+{
+ ///
+ /// Static utility for achievment managment
+ ///
+ public static class Achievment
+ {
+ public static AchievmentManager Instance
+ {
+ get
+ {
+ if (instance == null)
+ {
+ instance = AchievmentManager.Builder.FromLabeledConfig(ConfigLabel)
+ .WithLabeledBackend(BackendLabel)
+ .Build();
+ }
+ return instance;
+ }
+ }
+
+ public static string BackendLabel
+ {
+ get => backendLabel;
+ set
+ {
+ if(instance != null)
+ {
+ //Log + Quit helps debug builds
+ Debug.LogError("Achievments - Cannot set backend label, Managed already created");
+ Application.Quit(1);
+ }
+ if (backendLabel != null)
+ {
+ //Log + Quit helps debug builds
+ Debug.LogError("Multiple AchievmentBackends enabled, this is not allowed");
+ Application.Quit(1);
+ }
+ backendLabel = value;
+ }
+ }
+ private static string backendLabel;
+
+ ///
+ /// You shouldn't have any reason to change this
+ /// Used for tests.
+ ///
+ public static string ConfigLabel
+ {
+ private get => configLabel;
+ set => configLabel = value;
+ }
+
+ ///
+ /// You shouldn't have any reason to change this
+ /// Used for tests.
+ ///
+ private static string configLabel = "Achivments";
+
+ private static AchievmentManager instance;
+
+ #region Achievment Manipulation (Sets, Gets)
+
+ ///
+ /// Returns if an achivment at a given id is completed
+ ///
+ ///
+ ///
+ ///
+ /// throws an if there is no achievment under id
+ public static bool IsCompleted(string id)
+ {
+ return Instance.IsCompleted(id);
+ }
+
+ #region Toggle
+ ///
+ /// Sets a as completed.
+ ///
+ ///
+ ///
+ /// throws an if there is no achievment under id or an if achievment under id is of a different type
+ public static bool SetToggleAchivment(string id)
+ {
+ return Instance.SetToggleAchivment(id);
+ }
+
+ ///
+ /// Gets a completion state from a .
+ ///
+ ///
+ ///
+ /// throws an if there is no achievment under id or an if achievment under id is of a different type
+ public static bool GetToggleState(string id)
+ {
+ return Instance.GetToggleState(id);
+ }
+ #endregion
+
+ #region Int
+ ///
+ /// Sets progress of a given to
+ ///
+ ///
+ ///
+ /// throws an if there is no achievment under id or an if achievment under id is of a different type
+ public static bool SetIntProgress(string id, int progress)
+ {
+ return Instance.SetIntProgress(id, progress);
+ }
+
+ ///
+ /// Changes progress of a given by
+ ///
+ /// \
+ ///
+ /// throws an if there is no achievment under id or an if achievment under id is of a different type
+ public static bool ChangeIntProgress(string id, int delta)
+ {
+ return Instance.ChangeIntProgress(id, delta);
+ }
+
+ ///
+ /// Gets current progress from a .
+ ///
+ ///
+ ///
+ /// throws an if there is no achievment under id or an if achievment under id is of a different type
+ public static int GetIntProgress(string id)
+ {
+ return Instance.GetIntProgress(id);
+ }
+ #endregion
+
+ #region Float
+ ///
+ /// Sets progress of a given to
+ ///
+ ///
+ ///
+ /// throws an if there is no achievment under id or an if achievment under id is of a different type
+ public static bool SetFloatProgress(string id, float progress)
+ {
+ return Instance.SetFloatProgress(id, progress);
+ }
+
+ ///
+ /// Changes progress of a given by
+ ///
+ ///
+ ///
+ /// throws an if there is no achievment under id or an if achievment under id is of a different type
+ public static bool ChangeFloatProgress(string id, float delta)
+ {
+ return Instance.ChangeFloatProgress(id, delta);
+ }
+
+ ///
+ /// Gets current progress from a .
+ ///
+ ///
+ ///
+ /// throws an if there is no achievment under id or an if achievment under id is of a different type
+ public static float GetFloatProgress(string id)
+ {
+ return Instance.GetFloatProgress(id);
+ }
+ #endregion
+ #endregion
+
+ #region Test Api
+ ///
+ /// You shouldn't have any reason to use this
+ /// Use at your own risk, may cause unexpected behaviour
+ /// Used for tests
+ ///
+ public static void NullifyInstance()
+ {
+ instance = null;
+ }
+ #endregion
+
+ }
+}
\ No newline at end of file
diff --git a/Achievments/Achievment.cs.meta b/Achievments/Achievment.cs.meta
new file mode 100644
index 0000000..d293cde
--- /dev/null
+++ b/Achievments/Achievment.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 696bafd4c06b0a8458f008103441ea7f
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/AchievmentException.cs b/Achievments/AchievmentException.cs
new file mode 100644
index 0000000..041c7b9
--- /dev/null
+++ b/Achievments/AchievmentException.cs
@@ -0,0 +1,44 @@
+using JetBrains.Annotations;
+using System;
+
+namespace NEG.Utils.Achievments
+{
+ public class AchievmentException : ApplicationException
+ {
+ ///
+ /// Id of related achievment
+ ///
+ ///
+ ///
+ public string Id { get; private set; }
+
+ public AchievmentException(string message, string achievmentId) : base(message)
+ {
+ Id = achievmentId;
+ }
+ }
+
+ public class AchievmentTypeException : AchievmentException
+ {
+ ///
+ /// Expected achievment type under
+ ///
+ ///
+ ///
+ public Type Expected { get; private set; }
+
+ ///
+ /// Actual achievment type under
+ ///
+ ///
+ ///
+ public Type Actual { get; private set; }
+
+ public AchievmentTypeException(string message, string achievmentId, Type expectedType, Type actualType) : base(message, achievmentId)
+ {
+ Expected = expectedType;
+ Actual = actualType;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/Achievments/AchievmentException.cs.meta b/Achievments/AchievmentException.cs.meta
new file mode 100644
index 0000000..b09eea7
--- /dev/null
+++ b/Achievments/AchievmentException.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 241344322b9771049b6962e48ac98085
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/AchievmentManager.cs b/Achievments/AchievmentManager.cs
new file mode 100644
index 0000000..6aaa90f
--- /dev/null
+++ b/Achievments/AchievmentManager.cs
@@ -0,0 +1,366 @@
+using System.Collections;
+using System.Collections.Generic;
+using System;
+using System.Linq;
+using UnityEngine;
+using UnityEngine.AddressableAssets;
+using System.Runtime.CompilerServices;
+using UnityEditor;
+
+namespace NEG.Utils.Achievments
+{
+ using AchievmentTypes;
+
+ public class AchievmentManager
+ {
+ public class Builder
+ {
+ public const string DefaultAchivmentsConfigLabel = "Achivments";
+
+ private AchievmentManager manager = new AchievmentManager();
+ private IAchievmentBackend backend;
+
+ public static Builder FromDefaultConfig()
+ {
+ return FromLabeledConfig(DefaultAchivmentsConfigLabel);
+ }
+
+ public static Builder FromLabeledConfig(string label)
+ {
+ var builder = new Builder();
+
+ var handle = Addressables.LoadAssetsAsync((IEnumerable)new string[] { label }, delegate { }, Addressables.MergeMode.Union, false);
+
+ var configs = handle.WaitForCompletion();
+
+ foreach (var config in configs)
+ {
+ config.Apply(builder);
+ }
+
+ foreach (var config in configs)
+ {
+ config.ApplyLast(builder);
+ }
+
+ Addressables.Release(handle);
+
+ return builder;
+ }
+
+ public Builder WithDefinitionsFrom(AchievmentManagerConfig collection)
+ {
+ foreach (var def in collection.Achivments)
+ {
+ manager.RegisterAchivment(def);
+ }
+
+ return this;
+ }
+
+ public Builder WithLabeledBackend(string label)
+ {
+ var backendConfigHandle = Addressables.LoadAssetAsync(label);
+
+ var backendConfig = backendConfigHandle.WaitForCompletion();
+
+ WithBackend(backendConfig.ConstructBackend());
+
+ Addressables.Release(backendConfigHandle);
+ return this;
+ }
+
+ public Builder WithBackend(IAchievmentBackend backendIn)
+ {
+ if (backend != null)
+ {
+ throw new ApplicationException("There can only be one Achievment Backend at a time");
+ }
+
+ this.backend = backendIn;
+
+ return this;
+ }
+
+ public Builder WithCallbackReciever(IAchievmentCallbackReciever callbackReciever)
+ {
+ manager.AddCallbackReciever(callbackReciever);
+ return this;
+ }
+
+ public AchievmentManager Build()
+ {
+ if (backend != null)
+ {
+ manager.InitBackend(backend);
+ }else
+ {
+ Debug.LogWarning("No AchievmentBackend selected. Is this intended?");
+ }
+ return manager;
+ }
+ }
+
+ public delegate void AchievmentCompletedCallback(AchievmentData achivment);
+ public delegate void AchievmentStateChangedCallback(AchievmentData achivment);
+
+ public event AchievmentCompletedCallback AchievmentCompleted;
+ public event AchievmentStateChangedCallback AchievmentStateChanged;
+
+ private Dictionary definitionCache;
+ private Dictionary dataCache;
+
+ private IAchievmentBackend activeBackend;
+
+ private AchievmentManager()
+ {
+ definitionCache = new Dictionary();
+ dataCache = new Dictionary();
+ }
+
+ private void RegisterAchivment(AchievmentDefinition definition)
+ {
+ if (!definitionCache.ContainsKey(definition.Id))
+ {
+ definitionCache.Add(definition.Id, definition);
+ dataCache.Add(definition, definition.Construct());
+ }
+ else
+ {
+ Debug.LogWarning($"Duplicate Achivment with ID: {definition.Id}");
+ }
+ }
+
+ ///
+ /// Initializes a backend syncing achievments data with it and redistering it as a callback reciever
+ ///
+ /// Resets all achievments data
+ private void InitBackend(IAchievmentBackend achievmentBackend)
+ {
+ activeBackend = achievmentBackend;
+ foreach (var definition in definitionCache.Values)
+ {
+ var storedProgress = achievmentBackend.GetStoredAchivment(definition);
+
+ if (storedProgress != null)
+ {
+ dataCache[definition] = storedProgress;
+ }
+ else
+ {
+ dataCache[definition] = definition.Construct();
+ }
+ }
+ AddCallbackReciever(achievmentBackend);
+ }
+
+ public void AddCallbackRecievers(IEnumerable initialCallbacks)
+ {
+ foreach (var callback in initialCallbacks)
+ {
+ AddCallbackReciever(callback);
+ }
+ }
+
+ public void AddCallbackReciever(IAchievmentCallbackReciever callback)
+ {
+ AchievmentCompleted += callback.AchievmentCompleted;
+ AchievmentStateChanged += callback.AchievmentStateChanged;
+ }
+
+ public void RemoveCallbackReciever(IAchievmentCallbackReciever callback)
+ {
+ AchievmentCompleted -= callback.AchievmentCompleted;
+ AchievmentStateChanged -= callback.AchievmentStateChanged;
+ }
+
+ #region Achievment Manipulation (Sets, Gets)
+
+ ///
+ /// Returns if an achivment at a given id is completed
+ ///
+ ///
+ ///
+ /// throws an if there is no achievment under id
+ public bool IsCompleted(string id)
+ {
+ return GetAchievmentForId(id).IsCompleted;
+ }
+
+ #region Toggle
+ ///
+ /// Sets a as completed, after which sends , also if the achievment is completed sends a .
+ ///
+ ///
+ /// throws an if there is no achievment under id or an if achievment under id is of a different type
+ public bool SetToggleAchivment(string id)
+ {
+ return ManipulateAchievment(id, (achievment) => achievment.CompletionState = true);
+ }
+
+ ///
+ /// Gets a completion state from a .
+ ///
+ ///
+ /// throws an if there is no achievment under id or an if achievment under id is of a different type
+ public bool GetToggleState(string id)
+ {
+ return GetAchievmentForId(id).CompletionState;
+ }
+ #endregion
+
+ #region Int
+ ///
+ /// Sets progress of a given to , after which sends , also if the achievment is completed sends a .
+ ///
+ ///
+ /// throws an if there is no achievment under id or an if achievment under id is of a different type
+ public bool SetIntProgress(string id, int progress)
+ {
+ return ManipulateAchievment(id, (achievment) => achievment.CurrentProgress = progress);
+ }
+
+ ///
+ /// Changes progress of a given by , after which sends , also if the achievment is completed sends a .
+ ///
+ ///
+ /// throws an if there is no achievment under id or an if achievment under id is of a different type
+ public bool ChangeIntProgress(string id, int delta)
+ {
+ return ManipulateAchievment(id, (achievment) => achievment.CurrentProgress += delta);
+ }
+
+ ///
+ /// Gets current progress from a .
+ ///
+ ///
+ /// throws an if there is no achievment under id or an if achievment under id is of a different type
+ public int GetIntProgress(string id)
+ {
+ return GetAchievmentForId(id).CurrentProgress;
+ }
+ #endregion
+
+ #region Float
+ ///
+ /// Sets progress of a given to , after which sends , also if the achievment is completed sends a .
+ ///
+ ///
+ /// throws an if there is no achievment under id or an if achievment under id is of a different type
+ public bool SetFloatProgress(string id, float progress)
+ {
+ return ManipulateAchievment(id, (achievment) => achievment.CurrentProgress = progress);
+ }
+
+ ///
+ /// Changes progress of a given by , after which sends , also if the achievment is completed sends a .
+ ///
+ ///
+ /// throws an if there is no achievment under id or an if achievment under id is of a different type
+ public bool ChangeFloatProgress(string id, float delta)
+ {
+ return ManipulateAchievment(id, (achievment) => achievment.CurrentProgress += delta);
+ }
+
+ ///
+ /// Gets current progress from a .
+ ///
+ ///
+ /// throws an if there is no achievment under id or an if achievment under id is of a different type
+ public float GetFloatProgress(string id)
+ {
+ return GetAchievmentForId(id).CurrentProgress;
+ }
+ #endregion
+ #endregion
+
+ ///
+ /// Returns an achievment of type under
+ ///
+ /// Type of the achievment
+ /// Id of requested achievment
+ /// throws an if there is no achievment under id or an if achievment under id is of a different type
+ public T GetAchievmentForId(string id) where T : AchievmentData
+ {
+ return ValidateAchievmentType(GetAchievmentForId(id));
+ }
+
+ ///
+ /// Returns an achievment under
+ ///
+ /// Id of requested achievment
+ /// throws an if there is no achievment under id
+ public AchievmentData GetAchievmentForId(string id)
+ {
+ var def = definitionCache.GetValueOrDefault(id);
+ if (def != null)
+ {
+ return dataCache[def];
+ }
+ else
+ {
+ throw new AchievmentException($"Invalid achivment id {id}", id);
+ }
+ }
+
+ internal void UpdateBackend()
+ {
+ activeBackend?.Update();
+ }
+
+ private T ValidateAchievmentType(AchievmentData data) where T : AchievmentData
+ {
+ if (data is not T convetred)
+ {
+ throw new AchievmentTypeException($"Attempting to perform an operation on an invalid achievment type. Expected {typeof(T)} got {data.GetType()}", data.Achievment.Id, typeof(T), data.GetType());
+ }
+ return convetred;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// Action to perform on the achievment
+ private bool ManipulateAchievment(string id, Action manipulation) where T : AchievmentData
+ {
+ var data = GetAchievmentForId(id);
+
+ if (CheckNotCompleted(data))
+ {
+ return true;
+ }
+
+ manipulation(data);
+
+ SendUpdateCallbacks(data);
+
+ return data.IsCompleted;
+ }
+
+ ///
+ /// Helper method to print a warning if an achievment is already completed
+ ///
+ ///
+ /// Completion state
+ private bool CheckNotCompleted(AchievmentData data)
+ {
+ if (data.IsCompleted)
+ {
+ Debug.LogWarning($"Achievment already completed: {data.Achievment.Id}");
+ }
+ return data.IsCompleted;
+ }
+
+ private void SendUpdateCallbacks(AchievmentData data)
+ {
+ AchievmentStateChanged?.Invoke(data);
+
+ if (data.IsCompleted)
+ {
+ AchievmentCompleted?.Invoke(data);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Achievments/AchievmentManager.cs.meta b/Achievments/AchievmentManager.cs.meta
new file mode 100644
index 0000000..64e574c
--- /dev/null
+++ b/Achievments/AchievmentManager.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7339f725e382e4b4bab7db6d7cc14b30
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/AchievmentManagerConfig.cs b/Achievments/AchievmentManagerConfig.cs
new file mode 100644
index 0000000..371a722
--- /dev/null
+++ b/Achievments/AchievmentManagerConfig.cs
@@ -0,0 +1,24 @@
+using NEG.Utils.Achievments.AchievmentTypes;
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace NEG.Utils.Achievments
+{
+ [CreateAssetMenu(menuName = "Achievments/Config/BaseConfig")]
+ public class AchievmentManagerConfig : ScriptableObject, IAchivmentManagerConfig
+ {
+ [field: SerializeField]
+ public List Achivments { get; private set; } = new List();
+
+ public void Apply(AchievmentManager.Builder builder)
+ {
+ builder.WithDefinitionsFrom(this);
+ }
+
+ public void ApplyLast(AchievmentManager.Builder builder)
+ {
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/Achievments/AchievmentManagerConfig.cs.meta b/Achievments/AchievmentManagerConfig.cs.meta
new file mode 100644
index 0000000..3e43f82
--- /dev/null
+++ b/Achievments/AchievmentManagerConfig.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 88120b6e616164f489387a6a32a25dee
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/AchievmentTypes.meta b/Achievments/AchievmentTypes.meta
new file mode 100644
index 0000000..bb52672
--- /dev/null
+++ b/Achievments/AchievmentTypes.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 79e626bf8b94e5f4d813912f9b1d304e
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/AchievmentTypes/AchievmentData.cs b/Achievments/AchievmentTypes/AchievmentData.cs
new file mode 100644
index 0000000..9407979
--- /dev/null
+++ b/Achievments/AchievmentTypes/AchievmentData.cs
@@ -0,0 +1,21 @@
+using Newtonsoft.Json.Linq;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using UnityEngine;
+
+namespace NEG.Utils.Achievments.AchievmentTypes
+{
+ public abstract class AchievmentData
+ {
+ public AchievmentDefinition Achievment { get; private set; }
+
+ public abstract bool IsCompleted { get; }
+
+ public AchievmentData(AchievmentDefinition achivment)
+ {
+ Achievment = achivment;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Achievments/AchievmentTypes/AchievmentData.cs.meta b/Achievments/AchievmentTypes/AchievmentData.cs.meta
new file mode 100644
index 0000000..5fb4884
--- /dev/null
+++ b/Achievments/AchievmentTypes/AchievmentData.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1f39500a9deabad43b87bc76122646fc
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/AchievmentTypes/AchievmentDefinition.cs b/Achievments/AchievmentTypes/AchievmentDefinition.cs
new file mode 100644
index 0000000..b43c4d3
--- /dev/null
+++ b/Achievments/AchievmentTypes/AchievmentDefinition.cs
@@ -0,0 +1,15 @@
+using Newtonsoft.Json.Linq;
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace NEG.Utils.Achievments.AchievmentTypes
+{
+ public abstract class AchievmentDefinition : ScriptableObject
+ {
+ [field: SerializeField]
+ public string Id { get; private set; }
+
+ public abstract AchievmentData Construct();
+ }
+}
\ No newline at end of file
diff --git a/Achievments/AchievmentTypes/AchievmentDefinition.cs.meta b/Achievments/AchievmentTypes/AchievmentDefinition.cs.meta
new file mode 100644
index 0000000..1d52a8a
--- /dev/null
+++ b/Achievments/AchievmentTypes/AchievmentDefinition.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 4aef60a6b4e41e243845a476862049e1
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/AchievmentTypes/FloatAchievmentData.cs b/Achievments/AchievmentTypes/FloatAchievmentData.cs
new file mode 100644
index 0000000..b9ef389
--- /dev/null
+++ b/Achievments/AchievmentTypes/FloatAchievmentData.cs
@@ -0,0 +1,40 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace NEG.Utils.Achievments.AchievmentTypes
+{
+ public class FloatAchievmentData : AchievmentData
+ {
+ public override bool IsCompleted => CurrentProgress >= Def.ProgressRequired;
+
+ ///
+ /// Use to GET current progress
+ /// Do not SET the value directly use or Instead
+ /// Unless you are in
+ ///
+ public float CurrentProgress
+ {
+ get => currentProgress;
+ set
+ {
+ if (Def.Clamped)
+ {
+ value = Mathf.Max(value, Def.LowerBound);
+ }
+ currentProgress = Mathf.Min(value, Def.ProgressRequired);
+ }
+ }
+
+ public float ProgressLeft => Def.ProgressRequired - CurrentProgress;
+
+ private FloatAchievmentDefinition Def => (FloatAchievmentDefinition)Achievment;
+
+ private float currentProgress;
+
+ public FloatAchievmentData(FloatAchievmentDefinition def) : base(def)
+ {
+ currentProgress = def.InitialProgress;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Achievments/AchievmentTypes/FloatAchievmentData.cs.meta b/Achievments/AchievmentTypes/FloatAchievmentData.cs.meta
new file mode 100644
index 0000000..8d1afc2
--- /dev/null
+++ b/Achievments/AchievmentTypes/FloatAchievmentData.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 32572809c0644434d8e64878a3c22f0e
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/AchievmentTypes/FloatAchievmentDefinition.cs b/Achievments/AchievmentTypes/FloatAchievmentDefinition.cs
new file mode 100644
index 0000000..19dac84
--- /dev/null
+++ b/Achievments/AchievmentTypes/FloatAchievmentDefinition.cs
@@ -0,0 +1,29 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace NEG.Utils.Achievments.AchievmentTypes
+{
+ [CreateAssetMenu(menuName = "Achievments/Float Achievment")]
+ public class FloatAchievmentDefinition : AchievmentDefinition
+ {
+ [field: Tooltip("Amount of progress required for completion, required to be at leas 1, otherwise would be considered completed from the beginning")]
+ [field: Min(0)]
+ [field: SerializeField]
+ public float ProgressRequired { get; private set; } = 1;
+
+ [field: SerializeField]
+ public float InitialProgress { get; private set; } = 0;
+
+ [field: SerializeField]
+ public bool Clamped { get; private set; } = false;
+
+ public float LowerBound { get; private set; } = 0;
+
+
+ public override AchievmentData Construct()
+ {
+ return new FloatAchievmentData(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Achievments/AchievmentTypes/FloatAchievmentDefinition.cs.meta b/Achievments/AchievmentTypes/FloatAchievmentDefinition.cs.meta
new file mode 100644
index 0000000..4a7def5
--- /dev/null
+++ b/Achievments/AchievmentTypes/FloatAchievmentDefinition.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 2d7270d5452c9b04ca07ef43a491a18d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/AchievmentTypes/IntAchievmentData.cs b/Achievments/AchievmentTypes/IntAchievmentData.cs
new file mode 100644
index 0000000..3460aea
--- /dev/null
+++ b/Achievments/AchievmentTypes/IntAchievmentData.cs
@@ -0,0 +1,41 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Runtime.Serialization.Formatters;
+using UnityEngine;
+
+namespace NEG.Utils.Achievments.AchievmentTypes
+{
+ public class IntAchievmentData : AchievmentData
+ {
+ public override bool IsCompleted => CurrentProgress >= Def.ProgressRequired;
+
+ ///
+ /// Use to GET current progress
+ /// Do not SET the value directly use or Instead
+ /// Unless you are in
+ ///
+ public int CurrentProgress
+ {
+ get => currentProgress;
+ set
+ {
+ if (Def.Clamped)
+ {
+ value = Mathf.Max(value, Def.LowerBound);
+ }
+ currentProgress = Mathf.Min(value, Def.ProgressRequired);
+ }
+ }
+
+ public int ProgressLeft => Def.ProgressRequired - CurrentProgress;
+
+ private IntAchievmentDefinition Def => (IntAchievmentDefinition)Achievment;
+
+ private int currentProgress;
+
+ public IntAchievmentData(IntAchievmentDefinition def) : base(def)
+ {
+ currentProgress = def.InitialProgress;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Achievments/AchievmentTypes/IntAchievmentData.cs.meta b/Achievments/AchievmentTypes/IntAchievmentData.cs.meta
new file mode 100644
index 0000000..52c3e80
--- /dev/null
+++ b/Achievments/AchievmentTypes/IntAchievmentData.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 0a121b6e6fa8ecc45ab4506c15e5f46e
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/AchievmentTypes/IntAchievmentDefinition.cs b/Achievments/AchievmentTypes/IntAchievmentDefinition.cs
new file mode 100644
index 0000000..aa87274
--- /dev/null
+++ b/Achievments/AchievmentTypes/IntAchievmentDefinition.cs
@@ -0,0 +1,27 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace NEG.Utils.Achievments.AchievmentTypes
+{
+ [CreateAssetMenu(menuName = "Achievments/Int Achievment")]
+ public class IntAchievmentDefinition : AchievmentDefinition
+ {
+ [field: Tooltip("Amount of progress required for completion, required to be at leas 1, otherwise would be considered completed from the beginning")]
+ [field: SerializeField]
+ public int ProgressRequired { get; private set; } = 1;
+
+ [field: SerializeField]
+ public int InitialProgress { get; private set; } = 0;
+
+ [field: SerializeField]
+ public bool Clamped { get; private set; } = false;
+
+ public int LowerBound { get; private set; } = 0;
+
+ public override AchievmentData Construct()
+ {
+ return new IntAchievmentData(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Achievments/AchievmentTypes/IntAchievmentDefinition.cs.meta b/Achievments/AchievmentTypes/IntAchievmentDefinition.cs.meta
new file mode 100644
index 0000000..16c6c30
--- /dev/null
+++ b/Achievments/AchievmentTypes/IntAchievmentDefinition.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5318fea685aa56646a3310c38a9a9bac
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/AchievmentTypes/ToggleAchievmentData.cs b/Achievments/AchievmentTypes/ToggleAchievmentData.cs
new file mode 100644
index 0000000..0075d49
--- /dev/null
+++ b/Achievments/AchievmentTypes/ToggleAchievmentData.cs
@@ -0,0 +1,23 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace NEG.Utils.Achievments.AchievmentTypes
+{
+ public class ToggleAchievmentData : AchievmentData
+ {
+ public override bool IsCompleted => CompletionState;
+
+ ///
+ /// Use to GET current progress
+ /// Do not SET the value directly use or Instead
+ /// Unless you are in
+ ///
+ public bool CompletionState { get; set; } = false;
+
+ public ToggleAchievmentData(ToggleAchievmentDefinition def) : base(def)
+ {
+
+ }
+ }
+}
diff --git a/Achievments/AchievmentTypes/ToggleAchievmentData.cs.meta b/Achievments/AchievmentTypes/ToggleAchievmentData.cs.meta
new file mode 100644
index 0000000..84bacbf
--- /dev/null
+++ b/Achievments/AchievmentTypes/ToggleAchievmentData.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1e0806da00902994f9aeb26d295956f0
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/AchievmentTypes/ToggleAchievmentDefinition.cs b/Achievments/AchievmentTypes/ToggleAchievmentDefinition.cs
new file mode 100644
index 0000000..25fd6f0
--- /dev/null
+++ b/Achievments/AchievmentTypes/ToggleAchievmentDefinition.cs
@@ -0,0 +1,15 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace NEG.Utils.Achievments.AchievmentTypes
+{
+ [CreateAssetMenu(menuName = "Achievments/Toggle Achievment")]
+ public class ToggleAchievmentDefinition : AchievmentDefinition
+ {
+ public override AchievmentData Construct()
+ {
+ return new ToggleAchievmentData(this);
+ }
+ }
+}
diff --git a/Achievments/AchievmentTypes/ToggleAchievmentDefinition.cs.meta b/Achievments/AchievmentTypes/ToggleAchievmentDefinition.cs.meta
new file mode 100644
index 0000000..c10231a
--- /dev/null
+++ b/Achievments/AchievmentTypes/ToggleAchievmentDefinition.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 608c7e921b8b16b42919fc6f55b67fcb
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/AchievmentsUpdater.cs b/Achievments/AchievmentsUpdater.cs
new file mode 100644
index 0000000..3b6980a
--- /dev/null
+++ b/Achievments/AchievmentsUpdater.cs
@@ -0,0 +1,15 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+
+namespace NEG.Utils.Achievments
+{
+ public class AchievmentsUpdater : MonoBehaviour
+ {
+ void Update()
+ {
+ Achievment.Instance.UpdateBackend();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Achievments/AchievmentsUpdater.cs.meta b/Achievments/AchievmentsUpdater.cs.meta
new file mode 100644
index 0000000..13c7c08
--- /dev/null
+++ b/Achievments/AchievmentsUpdater.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b5a8c2721326a014bb32737116e4d74b
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/Backend.meta b/Achievments/Backend.meta
new file mode 100644
index 0000000..a5e7f74
--- /dev/null
+++ b/Achievments/Backend.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 3541d03056defb3468d957da0b69a7b9
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/Backend/LoacalBaackend.meta b/Achievments/Backend/LoacalBaackend.meta
new file mode 100644
index 0000000..46efe33
--- /dev/null
+++ b/Achievments/Backend/LoacalBaackend.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: e0226747b74e86c4b8e89d01e9eeeb3f
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/Backend/LoacalBaackend/LocalBackendConfig.cs b/Achievments/Backend/LoacalBaackend/LocalBackendConfig.cs
new file mode 100644
index 0000000..7afdda4
--- /dev/null
+++ b/Achievments/Backend/LoacalBaackend/LocalBackendConfig.cs
@@ -0,0 +1,145 @@
+using NEG.Utils.Achievments.AchievmentTypes;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using UnityEngine;
+
+namespace NEG.Utils.Achievments
+{
+ [CreateAssetMenu(menuName = "Achievments/Config/Backend/Local")]
+ public class LocalBackendConfig : ScriptableObject, IAchievmentBackendConfig
+ {
+ [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
+ private static void Init()
+ {
+#if LOCAL_ACHIEVMENT_BACKEND
+ Achievment.BackendLabel = "LocalAchievments";
+#endif
+ }
+
+ [SerializeField]
+ private string saveLocation;
+
+ public IAchievmentBackend ConstructBackend()
+ {
+ return new LocalBackend(saveLocation);
+ }
+ }
+
+ ///
+ /// This backend is not optimised at all, do not use in public builds
+ ///
+ public class LocalBackend : IAchievmentBackend
+ {
+ private string saveLocation;
+
+ public LocalBackend(string saveLocation)
+ {
+ this.saveLocation = saveLocation;
+ }
+
+ public void AchievmentCompleted(AchievmentData achievment)
+ {
+ string id = achievment.Achievment.Id;
+ JObject jobj = LoadJson();
+
+ JToken token = jobj[id];
+
+ if (token is not JObject achievmentObj)
+ {
+ achievmentObj = new JObject();
+ }
+
+ achievmentObj["completed"] = true;
+
+ jobj[id] = achievmentObj;
+
+ SaveJson(jobj);
+ }
+
+ public void AchievmentStateChanged(AchievmentData achievment)
+ {
+ string id = achievment.Achievment.Id;
+ JObject jobj = LoadJson();
+
+ JToken token = jobj[id];
+
+ if (token is not JObject achievmentObj)
+ {
+ achievmentObj = new JObject();
+ }
+
+ switch (achievment)
+ {
+ case IntAchievmentData intAchievment:
+ achievmentObj["data"] = intAchievment.CurrentProgress;
+ break;
+ case FloatAchievmentData floatAchievment:
+ achievmentObj["data"] = floatAchievment.CurrentProgress;
+ break;
+ case ToggleAchievmentData toggleAchievment:
+ achievmentObj["data"] = toggleAchievment.IsCompleted;
+ break;
+ }
+
+ jobj[id] = achievmentObj;
+
+ SaveJson(jobj);
+ }
+
+ public AchievmentData GetStoredAchivment(AchievmentDefinition definition)
+ {
+ JObject jobj = LoadJson();
+
+ JToken token = jobj[definition.Id];
+
+ if (token is not JObject)
+ {
+ return null;
+ }
+
+ AchievmentData achievment = definition.Construct();
+
+ switch (achievment)
+ {
+ case IntAchievmentData intAchievment:
+ intAchievment.CurrentProgress = (int)token["data"];
+ break;
+ case FloatAchievmentData floatAchievment:
+ floatAchievment.CurrentProgress = (float)token["data"];
+ break;
+ case ToggleAchievmentData toggleAchievment:
+ toggleAchievment.CompletionState = (bool)token["data"];
+ break;
+ }
+
+ return achievment;
+ }
+
+ public void Update()
+ {
+ //Nothing here
+ }
+
+ private JObject LoadJson()
+ {
+ if (Directory.Exists(Path.GetDirectoryName(saveLocation)) && File.Exists(saveLocation))
+ {
+ return JObject.Parse(File.ReadAllText(saveLocation));
+ }
+ return new JObject();
+ }
+
+ private void SaveJson(JObject obj)
+ {
+ if (!Directory.Exists(Path.GetDirectoryName(saveLocation)))
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(saveLocation));
+ }
+
+ File.WriteAllText(saveLocation, obj.ToString(Formatting.Indented));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Achievments/Backend/LoacalBaackend/LocalBackendConfig.cs.meta b/Achievments/Backend/LoacalBaackend/LocalBackendConfig.cs.meta
new file mode 100644
index 0000000..39bcb87
--- /dev/null
+++ b/Achievments/Backend/LoacalBaackend/LocalBackendConfig.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 6a1257a87feec064697193df412554d4
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/Backend/SteamBackend.meta b/Achievments/Backend/SteamBackend.meta
new file mode 100644
index 0000000..f611099
--- /dev/null
+++ b/Achievments/Backend/SteamBackend.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: db06184b29a56ba44aa446107ada7f66
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/Backend/SteamBackend/NEG.Utils.Achivmnets.Backend.SteamBackend.asmdef b/Achievments/Backend/SteamBackend/NEG.Utils.Achivmnets.Backend.SteamBackend.asmdef
new file mode 100644
index 0000000..750a792
--- /dev/null
+++ b/Achievments/Backend/SteamBackend/NEG.Utils.Achivmnets.Backend.SteamBackend.asmdef
@@ -0,0 +1,19 @@
+{
+ "name": "NEG.Utils.Achivmnets.Backend.SteamBackend",
+ "rootNamespace": "",
+ "references": [
+ "GUID:380ad496eab7ace4b98ceede94941223",
+ "GUID:68bd7fdb68ef2684e982e8a9825b18a5"
+ ],
+ "includePlatforms": [],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": true,
+ "defineConstraints": [
+ "STEAM_ACHIEVMENT_BACKEND"
+ ],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
\ No newline at end of file
diff --git a/Achievments/Backend/SteamBackend/NEG.Utils.Achivmnets.Backend.SteamBackend.asmdef.meta b/Achievments/Backend/SteamBackend/NEG.Utils.Achivmnets.Backend.SteamBackend.asmdef.meta
new file mode 100644
index 0000000..8828da9
--- /dev/null
+++ b/Achievments/Backend/SteamBackend/NEG.Utils.Achivmnets.Backend.SteamBackend.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 243913f72edbe1c4294164fe2ed9dc0c
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/Backend/SteamBackend/SteamBackendConfig.cs b/Achievments/Backend/SteamBackend/SteamBackendConfig.cs
new file mode 100644
index 0000000..d651182
--- /dev/null
+++ b/Achievments/Backend/SteamBackend/SteamBackendConfig.cs
@@ -0,0 +1,115 @@
+using NEG.Utils.Achievments.AchievmentTypes;
+#if STEAM_ACHIEVMENT_BACKEND
+using Steamworks;
+#endif
+using UnityEngine;
+
+namespace NEG.Utils.Achievments
+{
+ [CreateAssetMenu(menuName = "Achievments/Config/Backend/Local")]
+ public class SteamBackendConfig : ScriptableObject, IAchievmentBackendConfig
+ {
+ [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
+ private static void Init()
+ {
+#if STEAM_ACHIEVMENT_BACKEND
+ Achievment.BackendLabel = "SteamAchievments";
+#endif
+ }
+
+ public IAchievmentBackend ConstructBackend()
+ {
+ return new SteamBackend();
+ }
+ }
+
+#if STEAM_ACHIEVMENT_BACKEND
+ public class SteamBackend : IAchievmentBackend
+ {
+ private bool isDirty = false;
+
+ public SteamBackend()
+ {
+ //
+ SteamUserStats.RequestCurrentStats();
+ }
+
+ public void AchievmentCompleted(AchievmentData achievment)
+ {
+ SteamUserStats.SetAchievement(achievment.Achievment.Id);
+ isDirty = true;
+ }
+
+ public void AchievmentStateChanged(AchievmentData achievment)
+ {
+ string id = achievment.Achievment.Id;
+
+ switch (achievment)
+ {
+ case IntAchievmentData intAchievment:
+ SteamUserStats.SetStat(id, intAchievment.CurrentProgress);
+ break;
+ case FloatAchievmentData floatAchievment:
+ SteamUserStats.SetStat(id, floatAchievment.CurrentProgress);
+ break;
+ case ToggleAchievmentData toggleAchievment:
+ //Do nothing, Achievment completed will also be called
+ break;
+ }
+ isDirty = true;
+ }
+
+ public AchievmentData GetStoredAchivment(AchievmentDefinition definition)
+ {
+ string id = definition.Id;
+
+ AchievmentData achievment = definition.Construct();
+
+ switch (achievment)
+ {
+ case IntAchievmentData intAchievment:
+ if (SteamUserStats.GetStat(id, out int statI))
+ {
+ intAchievment.CurrentProgress = statI;
+ }
+ else
+ {
+ Debug.Log("Cannot get user stat, is steam initialised correctly?");
+ }
+ break;
+ case FloatAchievmentData floatAchievment:
+ if (SteamUserStats.GetStat(id, out float statF))
+ {
+ floatAchievment.CurrentProgress = statF;
+ }
+ else
+ {
+ Debug.Log("Cannot get user stat, is steam initialised correctly?");
+ }
+ break;
+ case ToggleAchievmentData toggleAchievment:
+ if (SteamUserStats.GetAchievement(id, out bool ach))
+ {
+ toggleAchievment.CompletionState = ach;
+ }
+ else
+ {
+ Debug.Log("Cannot get user stat, is steam initialised correctly?");
+ }
+ break;
+ }
+
+ return achievment;
+ }
+
+ public void Update()
+ {
+ if (isDirty)
+ {
+ //Reiterate on failure?
+ isDirty = !SteamUserStats.StoreStats();
+ }
+ }
+ }
+#endif
+}
\ No newline at end of file
diff --git a/Achievments/Backend/SteamBackend/SteamBackendConfig.cs.meta b/Achievments/Backend/SteamBackend/SteamBackendConfig.cs.meta
new file mode 100644
index 0000000..f519037
--- /dev/null
+++ b/Achievments/Backend/SteamBackend/SteamBackendConfig.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ad7436a24c5bdb84fa2e60e027b7a734
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/IAchievmentBackend.cs b/Achievments/IAchievmentBackend.cs
new file mode 100644
index 0000000..f868b6c
--- /dev/null
+++ b/Achievments/IAchievmentBackend.cs
@@ -0,0 +1,31 @@
+using NEG.Utils.Achievments.AchievmentTypes;
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace NEG.Utils.Achievments
+{
+ ///
+ /// Used to construct instance
+ ///
+ public interface IAchievmentBackendConfig
+ {
+ /// Constructed backend
+ public IAchievmentBackend ConstructBackend();
+ }
+
+
+ public interface IAchievmentBackend : IAchievmentCallbackReciever
+ {
+ ///
+ /// Constructs an AchievmentData for given
+ ///
+ /// May return null if there is no stored data for this achievment
+ AchievmentData GetStoredAchivment(AchievmentDefinition definition);
+
+ ///
+ /// Used for e.g. syncing with upstream
+ ///
+ void Update();
+ }
+}
\ No newline at end of file
diff --git a/Achievments/IAchievmentBackend.cs.meta b/Achievments/IAchievmentBackend.cs.meta
new file mode 100644
index 0000000..3204777
--- /dev/null
+++ b/Achievments/IAchievmentBackend.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: cfab45a1ce7cc0a4899f35a61b6a60f6
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/IAchievmentCallbackReciever.cs b/Achievments/IAchievmentCallbackReciever.cs
new file mode 100644
index 0000000..133a4e5
--- /dev/null
+++ b/Achievments/IAchievmentCallbackReciever.cs
@@ -0,0 +1,23 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace NEG.Utils.Achievments
+{
+ using AchievmentTypes;
+
+ public interface IAchievmentCallbackReciever
+ {
+ ///
+ /// Called when an achivment is completed
+ ///
+ ///
+ void AchievmentCompleted(AchievmentData achievment);
+
+ ///
+ /// Called when achivment progress changes
+ ///
+ ///
+ void AchievmentStateChanged(AchievmentData achievment);
+ }
+}
\ No newline at end of file
diff --git a/Achievments/IAchievmentCallbackReciever.cs.meta b/Achievments/IAchievmentCallbackReciever.cs.meta
new file mode 100644
index 0000000..beb55a4
--- /dev/null
+++ b/Achievments/IAchievmentCallbackReciever.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7218fed73c5be2c4ba31eca9fe44d37a
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/IAchivmentManagerConfig.cs b/Achievments/IAchivmentManagerConfig.cs
new file mode 100644
index 0000000..15628c9
--- /dev/null
+++ b/Achievments/IAchivmentManagerConfig.cs
@@ -0,0 +1,21 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace NEG.Utils.Achievments
+{
+ public interface IAchivmentManagerConfig
+ {
+ ///
+ /// Used to Apply config data
+ ///
+ ///
+ void Apply(AchievmentManager.Builder builder);
+
+ ///
+ /// Called after was called on every other config
+ ///
+ ///
+ void ApplyLast(AchievmentManager.Builder builder);
+ }
+}
\ No newline at end of file
diff --git a/Achievments/IAchivmentManagerConfig.cs.meta b/Achievments/IAchivmentManagerConfig.cs.meta
new file mode 100644
index 0000000..2fa22ce
--- /dev/null
+++ b/Achievments/IAchivmentManagerConfig.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: bd4b0d54603883447b6458263d6b3605
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/NEG.Utils.Achievments.asmdef b/Achievments/NEG.Utils.Achievments.asmdef
new file mode 100644
index 0000000..5d6fce1
--- /dev/null
+++ b/Achievments/NEG.Utils.Achievments.asmdef
@@ -0,0 +1,17 @@
+{
+ "name": "NEG.Utils.Achievments",
+ "rootNamespace": "",
+ "references": [
+ "GUID:9e24947de15b9834991c9d8411ea37cf",
+ "GUID:84651a3751eca9349aac36a66bba901b"
+ ],
+ "includePlatforms": [],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": true,
+ "defineConstraints": [],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
\ No newline at end of file
diff --git a/Achievments/NEG.Utils.Achievments.asmdef.meta b/Achievments/NEG.Utils.Achievments.asmdef.meta
new file mode 100644
index 0000000..14bc2c4
--- /dev/null
+++ b/Achievments/NEG.Utils.Achievments.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 380ad496eab7ace4b98ceede94941223
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/PlaymodeTests.meta b/Achievments/PlaymodeTests.meta
new file mode 100644
index 0000000..852d17f
--- /dev/null
+++ b/Achievments/PlaymodeTests.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: d7c140577a904c8419a760a8ac6133c7
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/PlaymodeTests/BackendTests.cs b/Achievments/PlaymodeTests/BackendTests.cs
new file mode 100644
index 0000000..e3f6bd8
--- /dev/null
+++ b/Achievments/PlaymodeTests/BackendTests.cs
@@ -0,0 +1,65 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using NUnit.Framework;
+using UnityEngine;
+
+namespace NEG.Utils.Achievments.Tests
+{
+ public class BackendTests
+ {
+ //If stests start to fail first make sure these are correct in relation to test config asset
+ public const string configLabel = "TestAchievments";
+ public const string backendLabel = "AchievmentsLocalTests";
+ public const string saveLocation = "./LocalAchievments/Tests.json";
+
+ public const string AchievmentIdToggle = "TOGGLE";
+ public const string AchievmentIdInt = "INT";
+ public const string AchievmentIdFloat = "FLOAT";
+
+ [OneTimeSetUp]
+ public void OneTtimeSetup()
+ {
+ Achievment.BackendLabel = backendLabel;
+ Achievment.ConfigLabel = configLabel;
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ Achievment.NullifyInstance();
+ if (File.Exists(saveLocation))
+ {
+ File.Delete(saveLocation);
+ }
+ }
+
+ [Test]
+ public void ReadWrite()
+ {
+#if ACHIEVMENT_BACKEND_TESTS
+ //We assume that the achievments are set correctly because otherwise other tests would fail
+ Achievment.SetToggleAchivment(AchievmentIdToggle);
+ Achievment.SetIntProgress(AchievmentIdInt, 20);
+ Achievment.SetFloatProgress(AchievmentIdFloat, 20);
+
+ //We need to assume NullifyInstance works correctly because we dont have access to an AchievmentManager which has not syncked yet
+ Achievment.NullifyInstance();
+
+ Assert.IsTrue(Achievment.IsCompleted(AchievmentIdToggle));
+ Assert.AreEqual(Achievment.GetIntProgress(AchievmentIdInt), 20);
+ Assert.AreEqual(Achievment.GetFloatProgress(AchievmentIdFloat), 20, 0f);
+
+ Achievment.SetIntProgress(AchievmentIdInt, 30);
+ Achievment.SetFloatProgress(AchievmentIdFloat, 30);
+
+ Achievment.NullifyInstance();
+
+ Assert.AreEqual(Achievment.GetIntProgress(AchievmentIdInt), 30);
+ Assert.AreEqual(Achievment.GetFloatProgress(AchievmentIdFloat), 30, 0f);
+#else
+ throw new System.Exception("Backend tests are not enabled. To enable Backend tests add define ACHIEVMENT_BACKEND_TESTS");
+#endif
+ }
+ }
+}
\ No newline at end of file
diff --git a/Achievments/PlaymodeTests/BackendTests.cs.meta b/Achievments/PlaymodeTests/BackendTests.cs.meta
new file mode 100644
index 0000000..a543b95
--- /dev/null
+++ b/Achievments/PlaymodeTests/BackendTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c09ce9536c2c5f541bb7d07d5eca1d69
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/PlaymodeTests/NEG.Utils.Achievments.Tests.Playmode.asmdef b/Achievments/PlaymodeTests/NEG.Utils.Achievments.Tests.Playmode.asmdef
new file mode 100644
index 0000000..e1754a3
--- /dev/null
+++ b/Achievments/PlaymodeTests/NEG.Utils.Achievments.Tests.Playmode.asmdef
@@ -0,0 +1,22 @@
+{
+ "name": "NEG.Utils.Achievments.Tests.Playmode",
+ "rootNamespace": "",
+ "references": [
+ "UnityEngine.TestRunner",
+ "UnityEditor.TestRunner",
+ "NEG.Utils.Achievments"
+ ],
+ "includePlatforms": [],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": true,
+ "precompiledReferences": [
+ "nunit.framework.dll"
+ ],
+ "autoReferenced": false,
+ "defineConstraints": [
+ "UNITY_INCLUDE_TESTS"
+ ],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
\ No newline at end of file
diff --git a/Achievments/PlaymodeTests/NEG.Utils.Achievments.Tests.Playmode.asmdef.meta b/Achievments/PlaymodeTests/NEG.Utils.Achievments.Tests.Playmode.asmdef.meta
new file mode 100644
index 0000000..1bb12eb
--- /dev/null
+++ b/Achievments/PlaymodeTests/NEG.Utils.Achievments.Tests.Playmode.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 38e8b1e483202e14182d34baaea3958e
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/PlaymodeTests/TestAssets.meta b/Achievments/PlaymodeTests/TestAssets.meta
new file mode 100644
index 0000000..ebead8c
--- /dev/null
+++ b/Achievments/PlaymodeTests/TestAssets.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: fad16eb700fc70c408c359dca9a76fc9
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/PlaymodeTests/TestAssets/TestLocalBackend.asset b/Achievments/PlaymodeTests/TestAssets/TestLocalBackend.asset
new file mode 100644
index 0000000..5cb8195
--- /dev/null
+++ b/Achievments/PlaymodeTests/TestAssets/TestLocalBackend.asset
@@ -0,0 +1,15 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 6a1257a87feec064697193df412554d4, type: 3}
+ m_Name: TestLocalBackend
+ m_EditorClassIdentifier:
+ saveLocation: ./LocalAchievments/Tests.json
diff --git a/Achievments/PlaymodeTests/TestAssets/TestLocalBackend.asset.meta b/Achievments/PlaymodeTests/TestAssets/TestLocalBackend.asset.meta
new file mode 100644
index 0000000..dd21e44
--- /dev/null
+++ b/Achievments/PlaymodeTests/TestAssets/TestLocalBackend.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 47c9689c811dc9842a5a5e9ca19c6e3c
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/TODO.txt b/Achievments/TODO.txt
new file mode 100644
index 0000000..ca82cc6
--- /dev/null
+++ b/Achievments/TODO.txt
@@ -0,0 +1,5 @@
+Static Achievments class (done)
+Implement Storage again API (done)
+Fix typos
+Merge AchievmentCollection with AchievmentManagerConfig (done)
+Static backend constructors (done)
\ No newline at end of file
diff --git a/Achievments/TODO.txt.meta b/Achievments/TODO.txt.meta
new file mode 100644
index 0000000..df317ce
--- /dev/null
+++ b/Achievments/TODO.txt.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 94e4aa3c6dc078c4db6a47949655f8a5
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/Tests.meta b/Achievments/Tests.meta
new file mode 100644
index 0000000..ddd7f84
--- /dev/null
+++ b/Achievments/Tests.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: bab659ddb2d136440a51c5c9b76fefcb
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/Tests/ConfigTests.cs b/Achievments/Tests/ConfigTests.cs
new file mode 100644
index 0000000..50bca12
--- /dev/null
+++ b/Achievments/Tests/ConfigTests.cs
@@ -0,0 +1,380 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using NUnit.Framework;
+using UnityEngine;
+using UnityEngine.TestTools;
+
+namespace NEG.Utils.Achievments.Tests
+{
+ using AchievmentTypes;
+ using static Internal.Extensions;
+
+ public class ConfigTests
+ {
+ public const string AchievmentsLabel = "TestAchievments";
+
+ public const string AchievmentIdToggle = "TOGGLE";
+ public const string AchievmentIdInt = "INT";
+ public const string AchievmentIdFloat = "FLOAT";
+ public const string AchievmentIdInvalid = "huptyrz";
+ public const int RandomProgress = 15;
+ public const int ProgressChangeDelta = 15;
+ public const int CompletedProgress = 100;
+ public const int OvershootProgress = 150;
+
+ #region Id And Types
+ [Test]
+ public void AchivmentInvalidId()
+ {
+ AchievmentManager manager = AchievmentManager.Builder.FromLabeledConfig(AchievmentsLabel).Build();
+
+ TestInvalidId(AchievmentIdInvalid, (id) => manager.GetAchievmentForId(id), "Get");
+ }
+
+ [Test]
+ public void AchivmentInvalidType()
+ {
+ AchievmentManager manager = AchievmentManager.Builder.FromLabeledConfig(AchievmentsLabel).Build();
+
+ manager.TestInvalidType(AchievmentIdToggle, "Toggle");
+ manager.TestInvalidType(AchievmentIdInt, "Int");
+ manager.TestInvalidType(AchievmentIdFloat, "Float");
+ }
+
+ [Test]
+ public void AchivmentCorrectIdAndType()
+ {
+ AchievmentManager manager = AchievmentManager.Builder.FromLabeledConfig(AchievmentsLabel).Build();
+
+ manager.TestValidIdAndType(AchievmentIdToggle, "Toggle");
+ manager.TestValidIdAndType(AchievmentIdInt, "Int");
+ manager.TestValidIdAndType(AchievmentIdFloat, "Float");
+ }
+ #endregion
+
+ #region Toggle
+ [Test]
+ public void AchivmentToggleSet()
+ {
+ var callbackTester = new TestCallbackRereiver();
+
+ AchievmentManager manager = AchievmentManager.Builder.FromLabeledConfig(AchievmentsLabel)
+ .WithCallbackReciever(callbackTester)
+ .Build();
+
+ manager.SetToggleAchivment(AchievmentIdToggle);
+ callbackTester.TestCompleted(AchievmentIdToggle);
+ callbackTester.Reset();
+
+ manager.SetToggleAchivment(AchievmentIdToggle);
+ callbackTester.TestNoChanges();
+ }
+
+ #endregion
+
+ #region Int
+ [Test]
+ public void AchivmentIntSetLess()
+ {
+ var callbackTester = new TestCallbackRereiver();
+
+ AchievmentManager manager = AchievmentManager.Builder.FromLabeledConfig(AchievmentsLabel)
+ .WithCallbackReciever(callbackTester)
+ .Build();
+
+ //Set progress to some value progress
+ manager.SetIntProgress(AchievmentIdInt, RandomProgress);
+ var data = callbackTester.GetChanged();
+
+ Assert.AreEqual(RandomProgress, data.CurrentProgress);
+ Assert.IsFalse(data.IsCompleted);
+ callbackTester.Reset();
+ }
+
+ [Test]
+ public void AchivmentIntSetEqual()
+ {
+ var callbackTester = new TestCallbackRereiver();
+
+ AchievmentManager manager = AchievmentManager.Builder.FromLabeledConfig(AchievmentsLabel)
+ .WithCallbackReciever(callbackTester)
+ .Build();
+
+ //Set progress to some value equal to required value
+ manager.SetIntProgress(AchievmentIdInt, CompletedProgress);
+ var data = callbackTester.GetChanged();
+ Assert.AreEqual(CompletedProgress, data.CurrentProgress);
+ callbackTester.TestCompleted(AchievmentIdInt);
+ callbackTester.Reset();
+
+ //Do that again, this time nothing sould change
+ manager.SetIntProgress(AchievmentIdInt, CompletedProgress);
+ callbackTester.TestNoChanges();
+ callbackTester.Reset();
+
+ }
+
+ [Test]
+ public void AchivmentIntSetGreater()
+ {
+ var callbackTester = new TestCallbackRereiver();
+
+ AchievmentManager manager = AchievmentManager.Builder.FromLabeledConfig(AchievmentsLabel)
+ .WithCallbackReciever(callbackTester)
+ .Build();
+
+ //Set progress to some value greater than required
+ manager.SetIntProgress(AchievmentIdInt, OvershootProgress);
+ var data = callbackTester.GetChanged();
+ //Testing against completed progress, should be clamped down
+ Assert.AreEqual(CompletedProgress, data.CurrentProgress);
+ callbackTester.TestCompleted(AchievmentIdInt);
+ callbackTester.Reset();
+
+ //Do that again, this time nothing sould change
+ manager.SetIntProgress(AchievmentIdInt, OvershootProgress);
+ callbackTester.TestNoChanges();
+ callbackTester.Reset();
+ }
+
+ [Test]
+ public void AchivmentIntChangeCompletion()
+ {
+ var callbackTester = new TestCallbackRereiver();
+
+ AchievmentManager manager = AchievmentManager.Builder.FromLabeledConfig(AchievmentsLabel)
+ .WithCallbackReciever(callbackTester)
+ .Build();
+
+ //Add progresss one interval below completion
+ for (int i = 0; i < CompletedProgress / ProgressChangeDelta; i++)
+ {
+ manager.ChangeIntProgress(AchievmentIdInt, ProgressChangeDelta);
+ callbackTester.TestNotCompleted();
+ var changed = callbackTester.GetChanged();
+
+ Assert.AreEqual((i + 1) * ProgressChangeDelta, changed.CurrentProgress);
+ callbackTester.Reset();
+ }
+
+ //Add progress one more time, should now be completed
+ manager.ChangeIntProgress(AchievmentIdInt, ProgressChangeDelta);
+ var changed1 = callbackTester.GetChanged();
+ Assert.AreEqual(CompletedProgress, changed1.CurrentProgress);
+ callbackTester.TestCompleted(AchievmentIdInt);
+ callbackTester.Reset();
+
+ //Do that again, this time nothing should change
+ manager.ChangeIntProgress(AchievmentIdInt, ProgressChangeDelta);
+ callbackTester.TestNoChanges();
+ callbackTester.Reset();
+
+ //Do that again, but down this time also nothing should change
+ manager.ChangeIntProgress(AchievmentIdInt, -ProgressChangeDelta);
+ callbackTester.TestNoChanges();
+ callbackTester.Reset();
+ }
+ #endregion
+
+ #region Float
+ [Test]
+ public void AchivmentFloatSetLess()
+ {
+ var callbackTester = new TestCallbackRereiver();
+
+ AchievmentManager manager = AchievmentManager.Builder.FromLabeledConfig(AchievmentsLabel)
+ .WithCallbackReciever(callbackTester)
+ .Build();
+
+ //Set progress to some value progress
+ manager.SetFloatProgress(AchievmentIdFloat, RandomProgress);
+ var data = callbackTester.GetChanged();
+
+ Assert.AreEqual((float)RandomProgress, data.CurrentProgress);
+ Assert.IsFalse(data.IsCompleted);
+ callbackTester.Reset();
+ }
+
+ [Test]
+ public void AchivmentFloatSetEqual()
+ {
+ var callbackTester = new TestCallbackRereiver();
+
+ AchievmentManager manager = AchievmentManager.Builder.FromLabeledConfig(AchievmentsLabel)
+ .WithCallbackReciever(callbackTester)
+ .Build();
+
+ //Set progress to some value equal to required value
+ manager.SetFloatProgress(AchievmentIdFloat, CompletedProgress);
+ var data = callbackTester.GetChanged();
+ Assert.AreEqual((float)CompletedProgress, data.CurrentProgress);
+ callbackTester.TestCompleted(AchievmentIdFloat);
+ callbackTester.Reset();
+
+ //Do that again, this time nothing sould change
+ manager.SetFloatProgress(AchievmentIdFloat, CompletedProgress);
+ callbackTester.TestNoChanges();
+ callbackTester.Reset();
+
+ }
+
+ [Test]
+ public void AchivmentFloatSetGreater()
+ {
+ var callbackTester = new TestCallbackRereiver();
+
+ AchievmentManager manager = AchievmentManager.Builder.FromLabeledConfig(AchievmentsLabel)
+ .WithCallbackReciever(callbackTester)
+ .Build();
+
+ //Set progress to some value greater than required
+ manager.SetFloatProgress(AchievmentIdFloat, OvershootProgress);
+ var data = callbackTester.GetChanged();
+ //Testing against completed progress, should be clamped down
+ Assert.AreEqual((float)CompletedProgress, data.CurrentProgress);
+ callbackTester.TestCompleted(AchievmentIdFloat);
+ callbackTester.Reset();
+
+ //Do that again, this time nothing sould change
+ manager.SetFloatProgress(AchievmentIdFloat, OvershootProgress);
+ callbackTester.TestNoChanges();
+ callbackTester.Reset();
+ }
+
+ [Test]
+ public void AchivmentFloatChangeCompletion()
+ {
+ var callbackTester = new TestCallbackRereiver();
+
+ AchievmentManager manager = AchievmentManager.Builder.FromLabeledConfig(AchievmentsLabel)
+ .WithCallbackReciever(callbackTester)
+ .Build();
+
+ //Add progresss one interval below completion
+ for (int i = 0; i < CompletedProgress / ProgressChangeDelta; i++)
+ {
+ manager.ChangeFloatProgress(AchievmentIdFloat, ProgressChangeDelta);
+ callbackTester.TestNotCompleted();
+ var changed = callbackTester.GetChanged();
+
+ Assert.AreEqual((i + 1) * ProgressChangeDelta, changed.CurrentProgress, 0.0f);
+ callbackTester.Reset();
+ }
+
+ //Add progress one more time, should now be completed
+ manager.ChangeFloatProgress(AchievmentIdFloat, ProgressChangeDelta);
+ var changed1 = callbackTester.GetChanged();
+ Assert.AreEqual((float)CompletedProgress, changed1.CurrentProgress);
+ callbackTester.TestCompleted(AchievmentIdFloat);
+ callbackTester.Reset();
+
+ //Do that again, this time nothing should change
+ manager.ChangeFloatProgress(AchievmentIdFloat, ProgressChangeDelta);
+ callbackTester.TestNoChanges();
+ callbackTester.Reset();
+
+ //Do that again, but down this time also nothing should change
+ manager.ChangeFloatProgress(AchievmentIdFloat, -ProgressChangeDelta);
+ callbackTester.TestNoChanges();
+ callbackTester.Reset();
+ }
+ #endregion
+
+ private class TestCallbackRereiver : IAchievmentCallbackReciever
+ {
+ public AchievmentData LastDataUpdated { get; private set; } = null;
+ public Type LastTypeSet { get; private set; } = null;
+ public string LastIdSet { get; private set; } = null;
+
+ public void Reset()
+ {
+ LastDataUpdated = null;
+ LastTypeSet = null;
+ LastIdSet = null;
+ }
+
+ public T GetChanged() where T : AchievmentData
+ {
+ Assert.IsInstanceOf(typeof(T), LastDataUpdated);
+
+ return (T)LastDataUpdated;
+ }
+
+ public void TestNoChanges()
+ {
+ Assert.IsNull(LastDataUpdated);
+ Assert.IsNull(LastTypeSet);
+ Assert.IsNull(LastIdSet);
+ }
+
+ public void TestNotCompleted()
+ {
+ //No need to also check LastTypeSet, they are both set or bot null
+ Assert.IsNull(LastIdSet);
+ }
+
+ public void TestCompleted(string id)
+ {
+ Assert.AreEqual(typeof(Type), LastTypeSet);
+ Assert.AreEqual(id, LastIdSet);
+ //Shold not be null: if we axpect achievment to be completed it must have also been updated
+ Assert.IsTrue(LastDataUpdated.IsCompleted);
+ }
+
+ public void AchievmentCompleted(AchievmentData achievment)
+ {
+ LastTypeSet = achievment.GetType();
+ LastIdSet = achievment.Achievment.Id;
+ }
+
+ public void AchievmentStateChanged(AchievmentData achievment)
+ {
+ LastDataUpdated = achievment;
+ }
+ }
+ }
+
+ namespace Internal
+ {
+ public static class Extensions
+ {
+ public static void TestValidIdAndType(this AchievmentManager manager, string id, string testName) where Type : AchievmentData
+ {
+ TestValidIdAndType(id, (id) => manager.GetAchievmentForId(id), testName);
+ }
+
+ public static void TestValidIdAndType(string id, Action manipulation, string testName)
+ {
+ Assert.DoesNotThrow(() => manipulation(id), $"{testName}: Invalid type or id");
+ }
+
+ public static void TestInvalidId(this AchievmentManager manager, string id, string testName)
+ {
+ TestInvalidId(id, (id) => manager.GetAchievmentForId(id), testName);
+ }
+
+ public static void TestInvalidId(string id, Action manipulation, string testName)
+ {
+ var exception = Assert.Throws(() => manipulation(id), $"Expected to fail with {typeof(AchievmentTypeException)}");
+
+ Assert.AreEqual(exception.Id, id, $"{testName}: Achievment id does not match");
+ }
+
+ public static void TestInvalidType(this AchievmentManager manager, string id, string testName) where Expected : AchievmentData where Actual : AchievmentData
+ {
+ TestInvalidType(id, (id) => manager.GetAchievmentForId(id), testName);
+ }
+
+ public static void TestInvalidType(string id, Action manipulation, string testName) where Expected : AchievmentData where Actual : AchievmentData
+ {
+ var exception = Assert.Throws(() => manipulation(id), $"Expected to fail with {typeof(AchievmentTypeException)}");
+
+ Assert.AreEqual(id, exception.Id, $"{testName}: Achievment id does not match");
+ Assert.AreSame(typeof(Expected), exception.Expected, $"{testName}: Target achievment type does not match");
+ Assert.AreSame(typeof(Actual), exception.Actual, $"{testName}: Actual achievment type does not match");
+ }
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/Achievments/Tests/ConfigTests.cs.meta b/Achievments/Tests/ConfigTests.cs.meta
new file mode 100644
index 0000000..fb7923b
--- /dev/null
+++ b/Achievments/Tests/ConfigTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c47a38eaf8bffa849b130320427701cc
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/Tests/NEG.Utils.Achivments.Tests.asmdef b/Achievments/Tests/NEG.Utils.Achivments.Tests.asmdef
new file mode 100644
index 0000000..32915c0
--- /dev/null
+++ b/Achievments/Tests/NEG.Utils.Achivments.Tests.asmdef
@@ -0,0 +1,26 @@
+{
+ "name": "NEG.Utils.Achivments.Tests",
+ "rootNamespace": "",
+ "references": [
+ "UnityEngine.TestRunner",
+ "UnityEditor.TestRunner",
+ "Unity.Addressables",
+ "Unity.ResourceManager",
+ "NEG.Utils.Achievments"
+ ],
+ "includePlatforms": [
+ "Editor"
+ ],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": true,
+ "precompiledReferences": [
+ "nunit.framework.dll"
+ ],
+ "autoReferenced": false,
+ "defineConstraints": [
+ "UNITY_INCLUDE_TESTS"
+ ],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
\ No newline at end of file
diff --git a/Achievments/Tests/NEG.Utils.Achivments.Tests.asmdef.meta b/Achievments/Tests/NEG.Utils.Achivments.Tests.asmdef.meta
new file mode 100644
index 0000000..8d89c87
--- /dev/null
+++ b/Achievments/Tests/NEG.Utils.Achivments.Tests.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 18b8be0ae04b6ad45ba52b2ddeb8198d
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/Tests/TestAssets.meta b/Achievments/Tests/TestAssets.meta
new file mode 100644
index 0000000..98c83f5
--- /dev/null
+++ b/Achievments/Tests/TestAssets.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 3f7445ed9dd5a4548b89d56c196cbec7
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/Tests/TestAssets/BaseConfig.asset b/Achievments/Tests/TestAssets/BaseConfig.asset
new file mode 100644
index 0000000..1511dcb
--- /dev/null
+++ b/Achievments/Tests/TestAssets/BaseConfig.asset
@@ -0,0 +1,18 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 88120b6e616164f489387a6a32a25dee, type: 3}
+ m_Name: BaseConfig
+ m_EditorClassIdentifier:
+ k__BackingField:
+ - {fileID: 11400000, guid: 7734df2e5d4033346aac56f0a2b2a836, type: 2}
+ - {fileID: 11400000, guid: c704b1ea2247ad540842a9caff628211, type: 2}
+ - {fileID: 11400000, guid: c71840de74e747e45afc82ecf8922dcd, type: 2}
diff --git a/Achievments/Tests/TestAssets/BaseConfig.asset.meta b/Achievments/Tests/TestAssets/BaseConfig.asset.meta
new file mode 100644
index 0000000..5f01ffd
--- /dev/null
+++ b/Achievments/Tests/TestAssets/BaseConfig.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 15513fd07fae44548bac5d923171a2a3
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/Tests/TestAssets/Float.asset b/Achievments/Tests/TestAssets/Float.asset
new file mode 100644
index 0000000..8d39d71
--- /dev/null
+++ b/Achievments/Tests/TestAssets/Float.asset
@@ -0,0 +1,18 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 2d7270d5452c9b04ca07ef43a491a18d, type: 3}
+ m_Name: Float
+ m_EditorClassIdentifier:
+ k__BackingField: FLOAT
+ k__BackingField: 100
+ k__BackingField: 0
+ k__BackingField: 0
diff --git a/Achievments/Tests/TestAssets/Float.asset.meta b/Achievments/Tests/TestAssets/Float.asset.meta
new file mode 100644
index 0000000..326ee38
--- /dev/null
+++ b/Achievments/Tests/TestAssets/Float.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 7734df2e5d4033346aac56f0a2b2a836
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/Tests/TestAssets/Int.asset b/Achievments/Tests/TestAssets/Int.asset
new file mode 100644
index 0000000..5e61751
--- /dev/null
+++ b/Achievments/Tests/TestAssets/Int.asset
@@ -0,0 +1,18 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 5318fea685aa56646a3310c38a9a9bac, type: 3}
+ m_Name: Int
+ m_EditorClassIdentifier:
+ k__BackingField: INT
+ k__BackingField: 100
+ k__BackingField: 0
+ k__BackingField: 0
diff --git a/Achievments/Tests/TestAssets/Int.asset.meta b/Achievments/Tests/TestAssets/Int.asset.meta
new file mode 100644
index 0000000..b89ba06
--- /dev/null
+++ b/Achievments/Tests/TestAssets/Int.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: c704b1ea2247ad540842a9caff628211
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Achievments/Tests/TestAssets/Toggle.asset b/Achievments/Tests/TestAssets/Toggle.asset
new file mode 100644
index 0000000..afd65bb
--- /dev/null
+++ b/Achievments/Tests/TestAssets/Toggle.asset
@@ -0,0 +1,15 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 608c7e921b8b16b42919fc6f55b67fcb, type: 3}
+ m_Name: Toggle
+ m_EditorClassIdentifier:
+ k__BackingField: TOGGLE
diff --git a/Achievments/Tests/TestAssets/Toggle.asset.meta b/Achievments/Tests/TestAssets/Toggle.asset.meta
new file mode 100644
index 0000000..ca5f86a
--- /dev/null
+++ b/Achievments/Tests/TestAssets/Toggle.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: c71840de74e747e45afc82ecf8922dcd
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant: