366 lines
14 KiB
C#
366 lines
14 KiB
C#
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);
|
|
}
|
|
|
|
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<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);
|
|
}
|
|
}
|
|
}
|
|
} |