achivments #1

Closed
Ghost wants to merge 13 commits from achivments into main
69 changed files with 2170 additions and 0 deletions

8
Achievments.meta Normal file
View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ff9bb206aea50d14997771b9a0cc2b04
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

190
Achievments/Achievment.cs Normal file
View File

@ -0,0 +1,190 @@
using NEG.Utils.Achievments.AchievmentTypes;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace NEG.Utils.Achievments
{
/// <summary>
/// Static utility for achievment managment
/// </summary>
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;
/// <summary>
/// You shouldn't have any reason to change this
/// Used for tests.
/// </summary>
public static string ConfigLabel
{
private get => configLabel;
set => configLabel = value;
}
/// <summary>
/// You shouldn't have any reason to change this
/// Used for tests.
/// </summary>
private static string configLabel = "Achivments";
private static AchievmentManager instance;
#region Achievment Manipulation (Sets, Gets)
/// <summary>
/// Returns if an achivment at a given id is completed
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
/// <seealso cref="AchievmentManager.IsCompleted(string)"/>
/// <remarks>throws an <see cref="AchievmentException"/> if there is no achievment under id</remarks>
public static bool IsCompleted(string id)
{
return Instance.IsCompleted(id);
}
#region Toggle
/// <summary>
/// Sets a <see cref="ToggleAchievmentData"/> as completed. <br/>
/// </summary>
/// <seealso cref="ToggleAchievmentDefinition"/>
/// <seealso cref="AchievmentManager.SetToggleAchivment(string)"/>
/// <remarks>throws an <see cref="AchievmentException"/> if there is no achievment under id or an <see cref="AchievmentTypeException"/> if achievment under id is of a different type</remarks>
public static bool SetToggleAchivment(string id)
{
return Instance.SetToggleAchivment(id);
}
/// <summary>
/// Gets a completion state from a <see cref="ToggleAchievmentData"/>.<br/>
/// </summary>
/// <seealso cref="ToggleAchievmentDefinition"/>
/// <seealso cref="AchievmentManager.GetToggleState(string)"/>
/// <remarks>throws an <see cref="AchievmentException"/> if there is no achievment under id or an <see cref="AchievmentTypeException"/> if achievment under id is of a different type</remarks>
public static bool GetToggleState(string id)
{
return Instance.GetToggleState(id);
}
#endregion
#region Int
/// <summary>
/// Sets progress of a given <see cref="IntAchievmentData"/> to <paramref name="progress"/> <br/>
/// </summary>
/// <seealso cref="IntAchievmentDefinition"/>
/// <seealso cref="AchievmentManager.SetIntProgress(string, int)"/>
/// <remarks>throws an <see cref="AchievmentException"/> if there is no achievment under id or an <see cref="AchievmentTypeException"/> if achievment under id is of a different type</remarks>
public static bool SetIntProgress(string id, int progress)
{
return Instance.SetIntProgress(id, progress);
}
/// <summary>
/// Changes progress of a given <see cref="IntAchievmentData"/> by <paramref name="delta"/><br/>
/// </summary>
/// <seealso cref="IntAchievmentDefinition"/>\
/// <seealso cref="AchievmentManager.ChangeIntProgress(string, int)"/>
/// <remarks>throws an <see cref="AchievmentException"/> if there is no achievment under id or an <see cref="AchievmentTypeException"/> if achievment under id is of a different type</remarks>
public static bool ChangeIntProgress(string id, int delta)
{
return Instance.ChangeIntProgress(id, delta);
}
/// <summary>
/// Gets current progress from a <see cref="IntAchievmentData"/>.<br/>
/// </summary>
/// <seealso cref="ToggleAchievmentDefinition"/>
/// <seealso cref="AchievmentManager.GetIntProgress(string)"/>
/// <remarks>throws an <see cref="AchievmentException"/> if there is no achievment under id or an <see cref="AchievmentTypeException"/> if achievment under id is of a different type</remarks>
public static int GetIntProgress(string id)
{
return Instance.GetIntProgress(id);
}
#endregion
#region Float
/// <summary>
/// Sets progress of a given <see cref="FloatAchievmentData"/> to <paramref name="progress"/><br/>
/// </summary>
/// <seealso cref="FloatAchievmentDefinition"/>
/// <seealso cref="AchievmentManager.SetFloatProgress(string, float)"/>
/// <remarks>throws an <see cref="AchievmentException"/> if there is no achievment under id or an <see cref="AchievmentTypeException"/> if achievment under id is of a different type</remarks>
public static bool SetFloatProgress(string id, float progress)
{
return Instance.SetFloatProgress(id, progress);
}
/// <summary>
/// Changes progress of a given <see cref="FloatAchievmentData"/> by <paramref name="delta"/><br/>
/// </summary>
/// <seealso cref="FloatAchievmentDefinition"/>
/// <seealso cref="AchievmentManager.ChangeFloatProgress(string, float)"/>
/// <remarks>throws an <see cref="AchievmentException"/> if there is no achievment under id or an <see cref="AchievmentTypeException"/> if achievment under id is of a different type</remarks>
public static bool ChangeFloatProgress(string id, float delta)
{
return Instance.ChangeFloatProgress(id, delta);
}
/// <summary>
/// Gets current progress from a <see cref="FloatAchievmentData"/>.<br/>
/// </summary>
/// <seealso cref="FloatAchievmentDefinition"/>
/// <seealso cref="AchievmentManager.GetFloatProgress(string)"/>
/// <remarks>throws an <see cref="AchievmentException"/> if there is no achievment under id or an <see cref="AchievmentTypeException"/> if achievment under id is of a different type</remarks>
public static float GetFloatProgress(string id)
{
return Instance.GetFloatProgress(id);
}
#endregion
#endregion
#region Test Api
/// <summary>
/// You shouldn't have any reason to use this <br/>
/// Use at your own risk, may cause unexpected behaviour <br/>
/// Used for tests
/// </summary>
public static void NullifyInstance()
{
instance = null;
}
#endregion
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 696bafd4c06b0a8458f008103441ea7f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,44 @@
using JetBrains.Annotations;
using System;
namespace NEG.Utils.Achievments
{
public class AchievmentException : ApplicationException
{
/// <summary>
/// Id of related achievment
/// </summary>
/// <seealso cref="AchievmentTypes.AchievmentData"/>
/// <seealso cref="AchievmentTypes.AchievmentDefinition"/>
public string Id { get; private set; }
public AchievmentException(string message, string achievmentId) : base(message)
{
Id = achievmentId;
}
}
public class AchievmentTypeException : AchievmentException
{
/// <summary>
/// Expected achievment type under <see cref="AchievmentException.Id"/>
/// </summary>
/// <seealso cref="AchievmentTypes.AchievmentData"/>
/// <seealso cref="AchievmentTypes.AchievmentDefinition"/>
public Type Expected { get; private set; }
/// <summary>
/// Actual achievment type under <see cref="AchievmentException.Id"/>
/// </summary>
/// <seealso cref="AchievmentTypes.AchievmentData"/>
/// <seealso cref="AchievmentTypes.AchievmentDefinition"/>
public Type Actual { get; private set; }
public AchievmentTypeException(string message, string achievmentId, Type expectedType, Type actualType) : base(message, achievmentId)
{
Expected = expectedType;
Actual = actualType;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 241344322b9771049b6962e48ac98085
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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<IAchivmentManagerConfig>((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);
Review

Why need this, when all are empty

Why need this, when all are empty
Review

was needed in earlier version, i will remove it

was needed in earlier version, i will remove it
}
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)
Review

no check for empty label

no check for empty label
Review

empty meaning null or empty meaning label with nothing assigned?

empty meaning null or empty meaning label with nothing assigned?
Review

you check for nothing, why even bother questioning

you check for nothing, why even bother questioning
{
var backendConfigHandle = Addressables.LoadAssetAsync<IAchievmentBackendConfig>(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<string, AchievmentDefinition> definitionCache;
private Dictionary<AchievmentDefinition, AchievmentData> dataCache;
private IAchievmentBackend activeBackend;
private AchievmentManager()
{
definitionCache = new Dictionary<string, AchievmentDefinition>();
dataCache = new Dictionary<AchievmentDefinition, AchievmentData>();
}
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}");
}
}
/// <summary>
/// Initializes a backend syncing achievments data with it and redistering it as a callback reciever
/// </summary>
/// <remarks>Resets all achievments data</remarks>
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<IAchievmentCallbackReciever> 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)
/// <summary>
/// Returns if an achivment at a given id is completed
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
/// <remarks>throws an <see cref="AchievmentException"/> if there is no achievment under id</remarks>
public bool IsCompleted(string id)
{
return GetAchievmentForId(id).IsCompleted;
}
#region Toggle
/// <summary>
/// Sets a <see cref="ToggleAchievmentData"/> as completed, after which sends <see cref="AchievmentStateChanged"/>, also if the achievment is completed sends a <see cref="AchievmentCompleted"/>. <br/>
/// </summary>
/// <seealso cref="ToggleAchievmentDefinition"/>
/// <remarks>throws an <see cref="AchievmentException"/> if there is no achievment under id or an <see cref="AchievmentTypeException"/> if achievment under id is of a different type</remarks>
public bool SetToggleAchivment(string id)
{
return ManipulateAchievment<ToggleAchievmentData>(id, (achievment) => achievment.CompletionState = true);
}
/// <summary>
/// Gets a completion state from a <see cref="ToggleAchievmentData"/>.<br/>
/// </summary>
/// <seealso cref="ToggleAchievmentDefinition"/>
/// <remarks>throws an <see cref="AchievmentException"/> if there is no achievment under id or an <see cref="AchievmentTypeException"/> if achievment under id is of a different type</remarks>
public bool GetToggleState(string id)
{
return GetAchievmentForId<ToggleAchievmentData>(id).CompletionState;
}
#endregion
#region Int
/// <summary>
/// Sets progress of a given <see cref="IntAchievmentData"/> to <paramref name="progress"/>, after which sends <see cref="AchievmentStateChanged"/>, also if the achievment is completed sends a <see cref="AchievmentCompleted"/>. <br/>
/// </summary>
/// <seealso cref="IntAchievmentDefinition"/>
/// <remarks>throws an <see cref="AchievmentException"/> if there is no achievment under id or an <see cref="AchievmentTypeException"/> if achievment under id is of a different type</remarks>
public bool SetIntProgress(string id, int progress)
{
return ManipulateAchievment<IntAchievmentData>(id, (achievment) => achievment.CurrentProgress = progress);
}
/// <summary>
/// Changes progress of a given <see cref="IntAchievmentData"/> by <paramref name="delta"/>, after which sends <see cref="AchievmentStateChanged"/>, also if the achievment is completed sends a <see cref="AchievmentCompleted"/>. <br/>
/// </summary>
/// <seealso cref="IntAchievmentDefinition"/>
/// <remarks>throws an <see cref="AchievmentException"/> if there is no achievment under id or an <see cref="AchievmentTypeException"/> if achievment under id is of a different type</remarks>
public bool ChangeIntProgress(string id, int delta)
{
return ManipulateAchievment<IntAchievmentData>(id, (achievment) => achievment.CurrentProgress += delta);
}
/// <summary>
/// Gets current progress from a <see cref="IntAchievmentData"/>.<br/>
/// </summary>
/// <seealso cref="ToggleAchievmentDefinition"/>
/// <remarks>throws an <see cref="AchievmentException"/> if there is no achievment under id or an <see cref="AchievmentTypeException"/> if achievment under id is of a different type</remarks>
public int GetIntProgress(string id)
{
return GetAchievmentForId<IntAchievmentData>(id).CurrentProgress;
}
#endregion
#region Float
/// <summary>
/// Sets progress of a given <see cref="FloatAchievmentData"/> to <paramref name="progress"/>, after which sends <see cref="AchievmentStateChanged"/>, also if the achievment is completed sends a <see cref="AchievmentCompleted"/>. <br/>
/// </summary>
/// <seealso cref="FloatAchievmentDefinition"/>
/// <remarks>throws an <see cref="AchievmentException"/> if there is no achievment under id or an <see cref="AchievmentTypeException"/> if achievment under id is of a different type</remarks>
public bool SetFloatProgress(string id, float progress)
{
return ManipulateAchievment<FloatAchievmentData>(id, (achievment) => achievment.CurrentProgress = progress);
}
/// <summary>
/// Changes progress of a given <see cref="FloatAchievmentData"/> by <paramref name="delta"/>, after which sends <see cref="AchievmentStateChanged"/>, also if the achievment is completed sends a <see cref="AchievmentCompleted"/>. <br/>
/// </summary>
/// <seealso cref="FloatAchievmentDefinition"/>
/// <remarks>throws an <see cref="AchievmentException"/> if there is no achievment under id or an <see cref="AchievmentTypeException"/> if achievment under id is of a different type</remarks>
public bool ChangeFloatProgress(string id, float delta)
{
return ManipulateAchievment<FloatAchievmentData>(id, (achievment) => achievment.CurrentProgress += delta);
}
/// <summary>
/// Gets current progress from a <see cref="FloatAchievmentData"/>.<br/>
/// </summary>
/// <seealso cref="FloatAchievmentDefinition"/>
/// <remarks>throws an <see cref="AchievmentException"/> if there is no achievment under id or an <see cref="AchievmentTypeException"/> if achievment under id is of a different type</remarks>
public float GetFloatProgress(string id)
{
return GetAchievmentForId<FloatAchievmentData>(id).CurrentProgress;
}
#endregion
#endregion
/// <summary>
/// Returns an achievment of type <typeparamref name="T"/> under <paramref name="id"/>
/// </summary>
/// <typeparam name="T">Type of the achievment</typeparam>
/// <param name="id">Id of requested achievment</param>
/// <remarks>throws an <see cref="AchievmentException"/> if there is no achievment under id or an <see cref="AchievmentTypeException"/> if achievment under id is of a different type</remarks>
public T GetAchievmentForId<T>(string id) where T : AchievmentData
{
return ValidateAchievmentType<T>(GetAchievmentForId(id));
}
/// <summary>
/// Returns an achievment under <paramref name="id"/>
/// </summary>
/// <param name="id">Id of requested achievment</param>
/// <remarks>throws an <see cref="ApplicationException"/> if there is no achievment under id</remarks>
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<T>(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;
}
/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="id"></param>
/// <param name="manipulation">Action to perform on the achievment</param>
private bool ManipulateAchievment<T>(string id, Action<T> manipulation) where T : AchievmentData
{
var data = GetAchievmentForId<T>(id);
if (CheckNotCompleted(data))
{
return true;
}
manipulation(data);
SendUpdateCallbacks(data);
return data.IsCompleted;
}
/// <summary>
/// Helper method to print a warning if an achievment is already completed
/// </summary>
/// <param name="data"></param>
/// <returns>Completion state</returns>
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);
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7339f725e382e4b4bab7db6d7cc14b30
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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<AchievmentDefinition> Achivments { get; private set; } = new List<AchievmentDefinition>();
public void Apply(AchievmentManager.Builder builder)
{
builder.WithDefinitionsFrom(this);
}
public void ApplyLast(AchievmentManager.Builder builder)
{
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 88120b6e616164f489387a6a32a25dee
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 79e626bf8b94e5f4d813912f9b1d304e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1f39500a9deabad43b87bc76122646fc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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();
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4aef60a6b4e41e243845a476862049e1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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;
/// <summary>
/// Use to GET current progress
/// Do not SET the value directly use <see cref="AchievmentManager"/> or <see cref="Achievment"/> Instead <br/>
/// Unless you are in <see cref="IAchievmentBackend.GetStoredAchivment(AchievmentDefinition)">
/// </summary>
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;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 32572809c0644434d8e64878a3c22f0e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2d7270d5452c9b04ca07ef43a491a18d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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;
/// <summary>
/// Use to GET current progress
/// Do not SET the value directly use <see cref="AchievmentManager"/> or <see cref="Achievment"/> Instead <br/>
/// Unless you are in <see cref="IAchievmentBackend.GetStoredAchivment(AchievmentDefinition)">
/// </summary>
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;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0a121b6e6fa8ecc45ab4506c15e5f46e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5318fea685aa56646a3310c38a9a9bac
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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;
/// <summary>
/// Use to GET current progress
/// Do not SET the value directly use <see cref="AchievmentManager"/> or <see cref="Achievment"/> Instead <br/>
/// Unless you are in <see cref="IAchievmentBackend.GetStoredAchivment(AchievmentDefinition)">
/// </summary>
public bool CompletionState { get; set; } = false;
public ToggleAchievmentData(ToggleAchievmentDefinition def) : base(def)
{
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1e0806da00902994f9aeb26d295956f0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 608c7e921b8b16b42919fc6f55b67fcb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,15 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace NEG.Utils.Achievments
{
public class AchievmentsUpdater : MonoBehaviour
Review

Should be created automaticly

Should be created automaticly
Review

How?

How?
Review

Create game object and add component?

Create game object and add component?
Review

When?

When?
Review

Duno it's your system

Duno it's your system
{
void Update()
{
Achievment.Instance.UpdateBackend();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b5a8c2721326a014bb32737116e4d74b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

8
Achievments/Backend.meta Normal file
View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3541d03056defb3468d957da0b69a7b9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e0226747b74e86c4b8e89d01e9eeeb3f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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);
}
}
/// <summary>
/// This backend is not optimised at all, do not use in public builds
/// </summary>
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));
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6a1257a87feec064697193df412554d4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: db06184b29a56ba44aa446107ada7f66
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 243913f72edbe1c4294164fe2ed9dc0c
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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();
Review

No reaction for callback

No reaction for callback
Review

I asked about it, you said to ignore it, but I can change it

I asked about it, you said to ignore it, but I can change it
}
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);
Review

No reaction for bool

No reaction for bool
Review

will fix

will fix
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)
Review

Should be check when last time, min time of refresh sholub be greater than minute

Should be check when last time, min time of refresh sholub be greater than minute
Review

I dont understand

I dont understand
Review

look to documentatnion, you should fire storage callback withing minutes not seconds

look to documentatnion, you should fire storage callback withing minutes not seconds
{
//Reiterate on failure?
isDirty = !SteamUserStats.StoreStats();
Review

No reaction for callback

No reaction for callback
Review

will fix

will fix
}
}
}
#endif
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ad7436a24c5bdb84fa2e60e027b7a734
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,31 @@
using NEG.Utils.Achievments.AchievmentTypes;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace NEG.Utils.Achievments
{
/// <summary>
/// Used to construct <see cref="IAchievmentBackend"/> instance
/// </summary>
public interface IAchievmentBackendConfig
{
/// <returns>Constructed backend</returns>
public IAchievmentBackend ConstructBackend();
}
public interface IAchievmentBackend : IAchievmentCallbackReciever
{
/// <summary>
/// Constructs an AchievmentData for given <paramref name="definition"/>
/// </summary>
/// <remarks>May return null if there is no stored data for this achievment</remarks>
AchievmentData GetStoredAchivment(AchievmentDefinition definition);
/// <summary>
/// Used for e.g. syncing with upstream
/// </summary>
void Update();
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cfab45a1ce7cc0a4899f35a61b6a60f6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,23 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace NEG.Utils.Achievments
{
using AchievmentTypes;
public interface IAchievmentCallbackReciever
{
/// <summary>
/// Called when an achivment is completed
/// </summary>
/// <param name="achievment"></param>
void AchievmentCompleted(AchievmentData achievment);
/// <summary>
/// Called when achivment progress changes
/// </summary>
/// <param name="achievment"></param>
void AchievmentStateChanged(AchievmentData achievment);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7218fed73c5be2c4ba31eca9fe44d37a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,21 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace NEG.Utils.Achievments
{
public interface IAchivmentManagerConfig
{
/// <summary>
/// Used to Apply config data
/// </summary>
/// <param name="builder"></param>
void Apply(AchievmentManager.Builder builder);
/// <summary>
/// Called after <see cref="Apply(AchievmentManager.Builder)"/> was called on every other config
/// </summary>
/// <param name="builder"></param>
void ApplyLast(AchievmentManager.Builder builder);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bd4b0d54603883447b6458263d6b3605
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 380ad496eab7ace4b98ceede94941223
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d7c140577a904c8419a760a8ac6133c7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c09ce9536c2c5f541bb7d07d5eca1d69
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 38e8b1e483202e14182d34baaea3958e
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: fad16eb700fc70c408c359dca9a76fc9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 47c9689c811dc9842a5a5e9ca19c6e3c
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

5
Achievments/TODO.txt Normal file
View File

@ -0,0 +1,5 @@
Static Achievments class (done)
Review

This file shouldn't exist

This file shouldn't exist
Review

will fix

will fix
Implement Storage again API (done)
Fix typos
Merge AchievmentCollection with AchievmentManagerConfig (done)
Static backend constructors (done)

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 94e4aa3c6dc078c4db6a47949655f8a5
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

8
Achievments/Tests.meta Normal file
View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: bab659ddb2d136440a51c5c9b76fefcb
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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<IntAchievmentData, ToggleAchievmentData>(AchievmentIdToggle, "Toggle");
manager.TestInvalidType<FloatAchievmentData, IntAchievmentData>(AchievmentIdInt, "Int");
manager.TestInvalidType<ToggleAchievmentData, FloatAchievmentData>(AchievmentIdFloat, "Float");
}
[Test]
public void AchivmentCorrectIdAndType()
{
AchievmentManager manager = AchievmentManager.Builder.FromLabeledConfig(AchievmentsLabel).Build();
manager.TestValidIdAndType<ToggleAchievmentData>(AchievmentIdToggle, "Toggle");
manager.TestValidIdAndType<IntAchievmentData>(AchievmentIdInt, "Int");
manager.TestValidIdAndType<FloatAchievmentData>(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<ToggleAchievmentData>(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<IntAchievmentData>();
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<IntAchievmentData>();
Assert.AreEqual(CompletedProgress, data.CurrentProgress);
callbackTester.TestCompleted<IntAchievmentData>(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<IntAchievmentData>();
//Testing against completed progress, should be clamped down
Assert.AreEqual(CompletedProgress, data.CurrentProgress);
callbackTester.TestCompleted<IntAchievmentData>(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<IntAchievmentData>();
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<IntAchievmentData>();
Assert.AreEqual(CompletedProgress, changed1.CurrentProgress);
callbackTester.TestCompleted<IntAchievmentData>(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<FloatAchievmentData>();
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<FloatAchievmentData>();
Assert.AreEqual((float)CompletedProgress, data.CurrentProgress);
callbackTester.TestCompleted<FloatAchievmentData>(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<FloatAchievmentData>();
//Testing against completed progress, should be clamped down
Assert.AreEqual((float)CompletedProgress, data.CurrentProgress);
callbackTester.TestCompleted<FloatAchievmentData>(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<FloatAchievmentData>();
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<FloatAchievmentData>();
Assert.AreEqual((float)CompletedProgress, changed1.CurrentProgress);
callbackTester.TestCompleted<FloatAchievmentData>(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<T>() 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<Type>(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<Type>(this AchievmentManager manager, string id, string testName) where Type : AchievmentData
{
TestValidIdAndType(id, (id) => manager.GetAchievmentForId<Type>(id), testName);
}
public static void TestValidIdAndType(string id, Action<string> 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<string> manipulation, string testName)
{
var exception = Assert.Throws<AchievmentException>(() => manipulation(id), $"Expected to fail with {typeof(AchievmentTypeException)}");
Assert.AreEqual(exception.Id, id, $"{testName}: Achievment id does not match");
}
public static void TestInvalidType<Expected, Actual>(this AchievmentManager manager, string id, string testName) where Expected : AchievmentData where Actual : AchievmentData
{
TestInvalidType<Expected, Actual>(id, (id) => manager.GetAchievmentForId<Expected>(id), testName);
}
public static void TestInvalidType<Expected, Actual>(string id, Action<string> manipulation, string testName) where Expected : AchievmentData where Actual : AchievmentData
{
var exception = Assert.Throws<AchievmentTypeException>(() => 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");
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c47a38eaf8bffa849b130320427701cc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 18b8be0ae04b6ad45ba52b2ddeb8198d
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3f7445ed9dd5a4548b89d56c196cbec7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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:
<Achivments>k__BackingField:
- {fileID: 11400000, guid: 7734df2e5d4033346aac56f0a2b2a836, type: 2}
- {fileID: 11400000, guid: c704b1ea2247ad540842a9caff628211, type: 2}
- {fileID: 11400000, guid: c71840de74e747e45afc82ecf8922dcd, type: 2}

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 15513fd07fae44548bac5d923171a2a3
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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:
<Id>k__BackingField: FLOAT
<ProgressRequired>k__BackingField: 100
<InitialProgress>k__BackingField: 0
<Clamped>k__BackingField: 0

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7734df2e5d4033346aac56f0a2b2a836
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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:
<Id>k__BackingField: INT
<ProgressRequired>k__BackingField: 100
<InitialProgress>k__BackingField: 0
<Clamped>k__BackingField: 0

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c704b1ea2247ad540842a9caff628211
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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:
<Id>k__BackingField: TOGGLE

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c71840de74e747e45afc82ecf8922dcd
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant: