Compare commits

..

65 Commits

Author SHA1 Message Date
d14e2ca055 Merge pull request 'add serializable guid' (#5) from serializable-guid into main
Reviewed-on: https://internal.negames.live/gitea/negames/Neg_Utils/pulls/5
2025-03-20 16:39:35 +01:00
d0d7134b68 delete unused usings 2025-03-20 16:39:15 +01:00
987c7b1be2 add serializable guid 2025-03-20 16:35:34 +01:00
f7a7427610 clean up 2025-03-03 19:17:05 +01:00
b3c1d2fd14 revert stupid change 2024-03-05 23:13:30 +01:00
5dd6a3b0de null check 2024-03-04 11:46:39 +01:00
e6af704f30 more cleanup 2024-03-03 15:39:03 +01:00
85dee79cc8 Cleanup 2024-03-03 15:38:40 +01:00
20e281f9dd Fix for error on close 2024-03-02 23:58:55 +01:00
f8d8b9fd12 fix build error 2024-03-02 21:45:06 +01:00
a9d35a5447 Read only attribute - build fix 2024-03-02 11:47:21 +01:00
bcb239587b Update path 2024-02-14 18:40:28 +01:00
e819020474 Code cleanup 2024-02-12 21:26:24 +01:00
ae89122e42 Merge pull request 'ui-system-fix' (#4) from ui-system-fix into main
Reviewed-on: http://newexceptiongames.dynamic-dns.net:3000/NewExceptionGames/Neg_Utils/pulls/4
2024-02-12 18:06:53 +00:00
46d192ea27 Merge pull request 'Update tools window' (#3) from ToolsWindowUpdate into main
Reviewed-on: http://newexceptiongames.dynamic-dns.net:3000/NewExceptionGames/Neg_Utils/pulls/3
2024-02-12 18:04:37 +00:00
1818d99caf null fix 2024-02-06 18:07:28 +01:00
8d5b903b1a working on city 2023-12-16 22:22:25 +01:00
d9fc097783 login screen done 2023-11-21 16:34:45 +01:00
da89d7a7fc Category item view button list 2023-11-09 12:04:53 +01:00
cd8c819585 working on dialogs system 2023-10-25 20:52:17 +02:00
566430650f moa fixes 2023-10-10 20:25:34 +02:00
164fd2f794 ui update 2023-10-03 15:55:02 +02:00
0148de0db7 Moa nullchecks 2023-10-02 21:52:09 +02:00
610179c21d Add linux builds 2023-09-19 21:41:39 +02:00
ebfeb14515 Add new auto window open 2023-09-19 12:42:51 +02:00
1abfcc0042 null check 2023-09-15 16:53:41 +02:00
6b01de0095 fix 2023-09-15 14:03:55 +02:00
8835f620e9 null check 2023-09-15 14:01:24 +02:00
ab5a33742a Mono ui input manager 2023-09-14 19:24:38 +02:00
b7dfa7bffa Apply pr changes 2023-09-08 21:57:20 +02:00
b7aa670e70 Merge branch 'VectorSwizzleUpdate' 2023-09-02 14:34:44 +02:00
d06350da06 Merge pull request 'better-ui-manager' (#2) from better-ui-manager into main
Reviewed-on: http://newexceptiongames.dynamic-dns.net:3000/NewExceptionGames/Neg_Utils/pulls/2
2023-09-02 12:34:16 +00:00
15ea7f7328 event rename 2023-09-02 14:34:01 +02:00
aafd5dbc75 Apply pr comments 2023-09-02 01:12:42 +02:00
fb90337a3f final update befor pr 2023-09-01 21:42:37 +02:00
0007e30aca switch to swizzle form Michal 2023-09-01 20:22:35 +02:00
33d0a9078f Fixed for avoiding scripts execution order 2023-08-26 01:03:34 +02:00
63d3d908e8 Naming update 2023-08-20 14:07:45 +02:00
a83c4cfb3b Cleanup 2023-08-17 10:47:21 +02:00
85daf89041 Merge branch 'better-ui-manager' of newexceptiongames.dynamic-dns.net:NewExceptionGames/Neg_Utils into better-ui-manager 2023-08-12 11:15:06 +02:00
9c91269a9e New componnets 2023-08-12 11:14:57 +02:00
f3f386d4c0 Update tools window 2023-08-11 19:44:44 +02:00
9e60fdf32d fix popup show 2023-07-31 11:05:57 +02:00
17f48f7eb2 update 2023-06-30 13:41:43 +02:00
34807988d8 add define 2023-06-12 19:33:57 +02:00
f837aec845 add features 2023-06-09 01:01:46 +02:00
611f417d5e Merge remote-tracking branch 'origin/main' into better-ui-manager 2023-06-05 20:56:41 +02:00
1f2c251d0c popups fix 2023-06-01 00:25:38 +02:00
8fe292818e Add custom editor 2023-05-27 21:39:28 +02:00
c0dd3dfdc5 add define 2023-05-22 20:33:17 +02:00
1e66136bfc Cleanup 2023-05-20 20:03:25 +02:00
e30470d27a update window 2023-05-19 22:52:09 +02:00
4239816cd8 Merge remote-tracking branch 'origin/main' into better-ui-manager 2023-05-19 21:05:52 +02:00
7bf4512c43 Working on selection 2023-05-19 20:58:53 +02:00
7aecbeb240 ui system improved 2023-05-14 01:04:34 +02:00
42dbfc8827 popups + android build utils 2023-05-14 01:00:03 +02:00
359b616be9 Vector int add to swizzle
Upgrade Building utils
2023-04-12 00:40:44 +02:00
ad760f57b0 Vectror swizzle, carousel list update 2023-04-08 11:47:55 +02:00
482e70b21a add asmdef for uinty ui editor 2023-03-20 19:57:08 +01:00
989a21a2c5 Update ui manager 2023-02-23 20:32:21 +01:00
c0923cc884 Merge branch 'main' into MultiSelectChip 2023-02-16 22:44:41 +01:00
055df4e427 fix compile error 2023-01-09 17:08:54 +01:00
32b208d1b6 Merge remote-tracking branch 'origin/main' into MultiSelectChip 2023-01-09 17:04:52 +01:00
9647327168 multi chip update 2022-12-12 19:22:13 +01:00
6c19b566d0 test of assembly definition 2022-11-26 19:24:00 +01:00
208 changed files with 10141 additions and 2924 deletions

View File

@ -1,190 +0,0 @@
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

@ -1,44 +0,0 @@
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

@ -1,366 +0,0 @@
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);
}
}
}
}

View File

@ -1,24 +0,0 @@
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

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

View File

@ -1,21 +0,0 @@
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

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

View File

@ -1,15 +0,0 @@
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

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

View File

@ -1,40 +0,0 @@
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

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

View File

@ -1,29 +0,0 @@
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

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

View File

@ -1,41 +0,0 @@
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

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

View File

@ -1,27 +0,0 @@
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

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

View File

@ -1,23 +0,0 @@
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

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

View File

@ -1,15 +0,0 @@
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

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

View File

@ -1,15 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace NEG.Utils.Achievments
{
public class AchievmentsUpdater : MonoBehaviour
{
void Update()
{
Achievment.Instance.UpdateBackend();
}
}
}

View File

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

View File

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

View File

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

View File

@ -1,145 +0,0 @@
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

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

View File

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

View File

@ -1,19 +0,0 @@
{
"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

@ -1,115 +0,0 @@
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
}

View File

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

View File

@ -1,31 +0,0 @@
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

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

View File

@ -1,23 +0,0 @@
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

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

View File

@ -1,21 +0,0 @@
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

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

View File

@ -1,17 +0,0 @@
{
"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

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

View File

@ -1,65 +0,0 @@
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

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

View File

@ -1,22 +0,0 @@
{
"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

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

View File

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

View File

@ -1,15 +0,0 @@
%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

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

View File

@ -1,5 +0,0 @@
Static Achievments class (done)
Implement Storage again API (done)
Fix typos
Merge AchievmentCollection with AchievmentManagerConfig (done)
Static backend constructors (done)

View File

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

View File

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

View File

@ -1,380 +0,0 @@
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

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

View File

@ -1,26 +0,0 @@
{
"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

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

View File

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

View File

@ -1,18 +0,0 @@
%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

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

View File

@ -1,18 +0,0 @@
%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

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

View File

@ -1,18 +0,0 @@
%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

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

View File

@ -1,15 +0,0 @@
%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

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

View File

@ -1,5 +1,4 @@
using UnityEngine;
using UnityEngine.SceneManagement;
namespace NEG.Utils
{
@ -9,5 +8,4 @@ namespace NEG.Utils
private void Start() => SceneManager.LoadScene(1);
#endif
}
}
}

View File

@ -5,7 +5,8 @@ namespace NEG.Utils.Collections
public static class DictionaryExtensions
{
/// <summary>
/// Adds given value to a dictionary if there was no element at given <paramref name="key"/>, replaces element with <paramref name="value"> otherwise.
/// Adds given value to a dictionary if there was no element at given <paramref name="key" />, replaces element with
/// <paramref name="value"> otherwise.
/// </summary>
/// <returns>true if element was added, false if it was replaced</returns>
public static bool AddOrUpdate<K, V>(this Dictionary<K, V> dict, K key, V value)
@ -15,27 +16,23 @@ namespace NEG.Utils.Collections
dict[key] = value;
return false;
}
else
{
dict.Add(key, value);
return true;
}
dict.Add(key, value);
return true;
}
/// <summary>
/// Gets a value from the dictionary under a specified key or adds it if did not exist and returns <paramref name="defaultValue"/>.
/// Gets a value from the dictionary under a specified key or adds it if did not exist and returns
/// <paramref name="defaultValue" />.
/// </summary>
/// <returns>value under a given <paramref name="key"/> if it exists, <paramref name="defaultValue"/> otherwise</returns>
/// <returns>value under a given <paramref name="key" /> if it exists, <paramref name="defaultValue" /> otherwise</returns>
public static V GetOrSetToDefault<K, V>(this Dictionary<K, V> dict, K key, V defaultValue)
{
if (dict.TryGetValue(key, out V value))
{
return value;
}
if (dict.TryGetValue(key, out var value)) return value;
dict.Add(key, defaultValue);
return defaultValue;
}
}
}
}

View File

@ -6,40 +6,42 @@ namespace NEG.Utils
{
public static class CoroutineUtils
{
private static readonly WaitForEndOfFrame WaitForEndOfFrame = new WaitForEndOfFrame();
private static readonly WaitForEndOfFrame WaitForEndOfFrame = new();
public static IEnumerator WaitForFrames(int count)
{
for (int i = 0; i < count; i++)
{
yield return null;
}
for (int i = 0; i < count; i++) yield return null;
}
public static void ActionAfterFrames(this MonoBehaviour mono, int count, Action action)
{
public static void ActionAfterFrames(this MonoBehaviour mono, int count, Action action) =>
mono.StartCoroutine(ActionAfterFrames(count, action));
}
public static IEnumerator ActionAfterFrames(int count, Action action)
{
yield return WaitForFrames(count);
action?.Invoke();
}
public static void ActionAfterEndOfFrame(this MonoBehaviour mono, Action action) =>
mono.StartCoroutine(ActionAtNextFrame(action));
public static IEnumerator ActionAfterEndOfFrame(Action action)
{
yield return WaitForEndOfFrame;
action?.Invoke();
}
public static void ActionAtNextFrame(this MonoBehaviour mono, Action action) => mono.StartCoroutine(ActionAtNextFrame(action));
public static void ActionAtNextFrame(this MonoBehaviour mono, Action action) =>
mono.StartCoroutine(ActionAtNextFrame(action));
public static IEnumerator ActionAtNextFrame(Action action)
{
yield return null;
action?.Invoke();
}
public static void ActionAfterTime(this MonoBehaviour mono, float time, Action action) => mono.StartCoroutine(ActionAfterTime(time, action));
public static void ActionAfterTime(this MonoBehaviour mono, float time, Action action) =>
mono.StartCoroutine(ActionAfterTime(time, action));
public static IEnumerator ActionAfterTime(float time, Action action)
{

View File

@ -1,13 +1,45 @@
using System.Diagnostics;
using UnityEngine;
using System.Linq;
using UnityEditor;
using UnityEditor.Build;
using UnityEngine;
using Debug = UnityEngine.Debug;
public static class BuildingUtils
{
private const string SteamBuildDefine = "STEAM_BUILD";
[MenuItem("Tools/PrepareForBuild", priority = -10)]
public static void PrepareForBuild()
{
var namedBuildTarget = NamedBuildTarget.FromBuildTargetGroup(
BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget));
string[] args = PlayerSettings.GetAdditionalCompilerArguments(namedBuildTarget);
var argsList = args.ToList();
argsList.Remove(SteamBuildDefine);
PlayerSettings.SetScriptingDefineSymbols(namedBuildTarget, argsList.ToArray());
}
[MenuItem("Tools/PlatformDefines/Steam", priority = -1)]
public static void SetDefinesForSteam()
{
PrepareForBuild();
var namedBuildTarget = NamedBuildTarget.FromBuildTargetGroup(
BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget));
string[] args = PlayerSettings.GetAdditionalCompilerArguments(namedBuildTarget);
var argsList = args.ToList();
argsList.Add(SteamBuildDefine);
PlayerSettings.SetScriptingDefineSymbols(namedBuildTarget, argsList.ToArray());
}
[MenuItem("Tools/Build/Steam/Release")]
public static void SteamRelease()
{
if (!CanBuild())
return;
IncreaseBuildNumber();
BuildRelease();
UploadSteam();
@ -16,38 +48,102 @@ public static class BuildingUtils
[MenuItem("Tools/Build/Steam/Development")]
public static void SteamDevelopment()
{
if (!CanBuild())
return;
IncreaseBuildNumber();
BuildDevelopment();
UploadSteam();
}
[MenuItem("Tools/Build/Steam/Demo")]
public static void SteamDemo()
{
if (!CanBuild())
return;
IncreaseBuildNumber();
BuildDemo();
UploadSteam(true);
}
[MenuItem("Tools/Build/All Release")]
public static void BuildRelease()
{
if (!CanBuild())
return;
BuildWindowsRelease();
BuildLinuxRelease();
}
[MenuItem("Tools/Build/All Development")]
public static void BuildDevelopment()
{
if (!CanBuild())
return;
BuildWindowsDevelopment();
}
[MenuItem("Tools/Build/All Demo")]
public static void BuildDemo()
{
if (!CanBuild())
return;
BuildWindows(true, new[] { "DEMO" });
BuildLinux(true, new[] { "DEMO" });
}
[MenuItem("Tools/Build/Platform/Windows/x64-Development")]
public static void BuildWindowsDevelopment() => BuildWindows(false);
public static void BuildWindowsDevelopment()
{
if (!CanBuild())
return;
BuildWindows(false);
}
[MenuItem("Tools/Build/Platform/Windows/x64-Release")]
public static void BuildWindowsRelease() => BuildWindows(true);
public static void BuildWindowsRelease()
{
if (!CanBuild())
return;
BuildWindows(true);
}
[MenuItem("Tools/Build/Platform/Linux/x64-Release")]
public static void BuildLinuxRelease()
{
if (!CanBuild())
return;
BuildLinux(true);
}
private static void BuildWindows(bool release)
[MenuItem("Tools/Build/Platform/Android/GooglePlay")]
public static void BuildGooglePlay()
{
IncreaseBuildNumber();
PlayerSettings.Android.bundleVersionCode++;
EditorUserBuildSettings.buildAppBundle = true;
var buildPlayerOptions = new BuildPlayerOptions { scenes = new string[EditorBuildSettings.scenes.Length] };
for (int i = 0; i < EditorBuildSettings.scenes.Length; i++)
buildPlayerOptions.scenes[i] = EditorBuildSettings.scenes[i].path;
buildPlayerOptions.target = BuildTarget.Android;
buildPlayerOptions.options = BuildOptions.None;
buildPlayerOptions.locationPathName = Application.dataPath +
$"/../../{Application.productName}-GooglePlay/{Application.productName}.aab";
BuildPipeline.BuildPlayer(buildPlayerOptions);
}
private static void BuildWindows(bool release, string[] additionalDefines = default)
{
var buildPlayerOptions = new BuildPlayerOptions { scenes = new string[EditorBuildSettings.scenes.Length] };
for (int i = 0; i < EditorBuildSettings.scenes.Length; i++)
{
buildPlayerOptions.scenes[i] = EditorBuildSettings.scenes[i].path;
}
buildPlayerOptions.extraScriptingDefines = additionalDefines;
buildPlayerOptions.target = BuildTarget.StandaloneWindows64;
buildPlayerOptions.options = release ? BuildOptions.None : BuildOptions.Development;
@ -56,29 +152,73 @@ public static class BuildingUtils
BuildPipeline.BuildPlayer(buildPlayerOptions);
}
private static void BuildLinux(bool release, string[] additionalDefines = default)
{
var buildPlayerOptions = new BuildPlayerOptions { scenes = new string[EditorBuildSettings.scenes.Length] };
for (int i = 0; i < EditorBuildSettings.scenes.Length; i++)
buildPlayerOptions.scenes[i] = EditorBuildSettings.scenes[i].path;
buildPlayerOptions.extraScriptingDefines = additionalDefines;
buildPlayerOptions.target = BuildTarget.StandaloneLinux64;
buildPlayerOptions.options = release ? BuildOptions.None : BuildOptions.Development;
buildPlayerOptions.locationPathName = Application.dataPath +
$"/../../{Application.productName}-Steam/ContentBuilder/content/linux/{Application.productName}.x86_64";
BuildPipeline.BuildPlayer(buildPlayerOptions);
}
private static void IncreaseBuildNumber()
{
string[] versionParts = PlayerSettings.bundleVersion.Split('.');
if (versionParts.Length != 3 || !int.TryParse(versionParts[2], out int version)) {
string[] versionParts = PlayerSettings.bundleVersion.Split('.');
if (versionParts.Length != 3 || !int.TryParse(versionParts[2], out int version))
{
Debug.LogError("IncreaseBuildNumber failed to update version " + PlayerSettings.bundleVersion);
return;
}
versionParts[2] = (version + 1).ToString();
PlayerSettings.bundleVersion = string.Join(".", versionParts);
}
private static void UploadSteam()
private static void UploadSteam(bool demo = false)
{
string command = $"cd {Application.dataPath}/../../{Application.productName}-Steam/ContentBuilder && run_build.bat";
string command =
$"cd {Application.dataPath}/../../{Application.productName}-Steam/ContentBuilder && push_build.bat";
if (demo)
{
command =
$"cd {Application.dataPath}/../../{Application.productName}-Steam/ContentBuilder && push_demo.bat";
}
var processInfo = new ProcessStartInfo("cmd.exe", $"/c {command}")
{
CreateNoWindow = true,
UseShellExecute = false
};
{
CreateNoWindow = true, UseShellExecute = false
};
var process = Process.Start(processInfo);
process.WaitForExit();
Debug.Log(process.ExitCode);
process.Close();
}
private static bool CanBuild()
{
if (CanBuildUtil())
return true;
Debug.LogError(
"Cannot build with defines set in project, please use PrepareForBuild and wait for scripts recompilation");
return false;
}
private static bool CanBuildUtil()
{
var namedBuildTarget = NamedBuildTarget.FromBuildTargetGroup(
BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget));
string[] args = PlayerSettings.GetAdditionalCompilerArguments(namedBuildTarget);
var argsList = args.ToList();
if (argsList.Contains(SteamBuildDefine))
return false;
return true;
}
}

View File

@ -8,14 +8,19 @@ namespace NEG.Utils.Editor.ComponentsAdditionalItems
{
[MenuItem("CONTEXT/CanvasScaler/Full HD horizontal", false, 2000)]
public static void SetFullHdHorizontal(MenuCommand command) => SetComponent(command, 1920, 1080);
[MenuItem("CONTEXT/CanvasScaler/Full HD vertical", false, 2000)]
public static void SetFullHdVertical(MenuCommand command) => SetComponent(command, 1080, 1920);
[MenuItem("CONTEXT/CanvasScaler/Full 2k horizontal", false, 2000)]
public static void Set2KHorizontal(MenuCommand command) => SetComponent(command, 2560, 1440 );
public static void Set2KHorizontal(MenuCommand command) => SetComponent(command, 2560, 1440);
[MenuItem("CONTEXT/CanvasScaler/Full 2k vertical", false, 2000)]
public static void Set2KVertical(MenuCommand command) => SetComponent(command, 1440, 2560);
[MenuItem("CONTEXT/CanvasScaler/Full 4k horizontal", false, 2000)]
public static void Set4KHorizontal(MenuCommand command) => SetComponent(command, 3840, 2160);
[MenuItem("CONTEXT/CanvasScaler/Full 4k vertical", false, 2000)]
public static void Set4KVertical(MenuCommand command) => SetComponent(command, 2160, 3840);

View File

@ -11,61 +11,57 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using UnityEngine;
using UnityEditor;
using UnityEngine;
using Debug = UnityEngine.Debug;
namespace TheGamedevGuru
{
public class EditorInstanceCreator : EditorWindow
{
string _projectInstanceName;
string _extraSubdirectories;
bool _includeProjectSettings = true;
private string _extraSubdirectories;
private bool _includeProjectSettings = true;
private string _projectInstanceName;
[MenuItem("Window/The Gamedev Guru/Editor Instance Creator")]
static void Init()
{
((EditorInstanceCreator)EditorWindow.GetWindow(typeof(EditorInstanceCreator))).Show();
}
void OnGUI()
private void OnGUI()
{
if (string.IsNullOrEmpty(_projectInstanceName))
{
_projectInstanceName = PlayerSettings.productName + "_Slave_1";
}
EditorGUILayout.Separator();
EditorGUILayout.LabelField("The Gamedev Guru - Project Instance Creator");
EditorGUILayout.Separator();
EditorGUILayout.LabelField("Slave Project Name");
_projectInstanceName = EditorGUILayout.TextField("", _projectInstanceName);
EditorGUILayout.Separator();
EditorGUILayout.LabelField("Include Project Settings? (Recommended)");
_includeProjectSettings = EditorGUILayout.Toggle("", _includeProjectSettings);
EditorGUILayout.Separator();
EditorGUILayout.LabelField("Extra Subdirectories? (Separate by comma)");
_extraSubdirectories = EditorGUILayout.TextField("", _extraSubdirectories);
EditorGUILayout.Separator();
if (GUILayout.Button("Create"))
{
CreateProjectInstance(_projectInstanceName, _includeProjectSettings, _extraSubdirectories);
}
if (GUILayout.Button("Help"))
{
Application.OpenURL("https://thegamedev.guru/multiple-unity-editor-instances-within-a-single-project/");
}
}
static void CreateProjectInstance(string projectInstanceName, bool includeProjectSettings, string extraSubdirectories)
[MenuItem("Window/The Gamedev Guru/Editor Instance Creator")]
private static void Init() => ((EditorInstanceCreator)GetWindow(typeof(EditorInstanceCreator))).Show();
private static void CreateProjectInstance(string projectInstanceName, bool includeProjectSettings,
string extraSubdirectories)
{
var targetDirectory = Path.Combine(Directory.GetCurrentDirectory(), ".." + Path.DirectorySeparatorChar, projectInstanceName);
string targetDirectory = Path.Combine(Directory.GetCurrentDirectory(), ".." + Path.DirectorySeparatorChar,
projectInstanceName);
Debug.Log(targetDirectory);
if (Directory.Exists(targetDirectory))
{
@ -75,29 +71,21 @@ namespace TheGamedevGuru
Directory.CreateDirectory(targetDirectory);
List<string> subdirectories = new List<string>{"Assets", "Packages"};
if (includeProjectSettings)
{
subdirectories.Add("ProjectSettings");
}
var subdirectories = new List<string> { "Assets", "Packages" };
if (includeProjectSettings) subdirectories.Add("ProjectSettings");
foreach (var extraSubdirectory in extraSubdirectories.Split(','))
{
foreach (string extraSubdirectory in extraSubdirectories.Split(','))
subdirectories.Add(extraSubdirectory.Trim());
}
foreach (var subdirectory in subdirectories)
{
System.Diagnostics.Process.Start("CMD.exe",GetLinkCommand(subdirectory, targetDirectory));
}
foreach (string subdirectory in subdirectories)
Process.Start("CMD.exe", GetLinkCommand(subdirectory, targetDirectory));
EditorUtility.RevealInFinder(targetDirectory + Path.DirectorySeparatorChar + "Assets");
EditorUtility.DisplayDialog("Done!", $"Done! Feel free to add it as an existing project at: {targetDirectory}", "Ok :)");
EditorUtility.DisplayDialog("Done!",
$"Done! Feel free to add it as an existing project at: {targetDirectory}", "Ok :)");
}
static string GetLinkCommand(string subdirectory, string targetDirectory)
{
return $"/c mklink /J \"{targetDirectory}{Path.DirectorySeparatorChar}{subdirectory}\" \"{Directory.GetCurrentDirectory()}{Path.DirectorySeparatorChar}{subdirectory}\"";
}
private static string GetLinkCommand(string subdirectory, string targetDirectory) =>
$"/c mklink /J \"{targetDirectory}{Path.DirectorySeparatorChar}{subdirectory}\" \"{Directory.GetCurrentDirectory()}{Path.DirectorySeparatorChar}{subdirectory}\"";
}
}
}

42
Editor/EditorUtils.cs Normal file
View File

@ -0,0 +1,42 @@
using System;
using UnityEditor;
namespace NegUtils.Editor
{
public class EditorUtils
{
}
public class AssetPath
{
private readonly string filter;
private string path;
public AssetPath(string filter)
{
this.filter = filter;
TryFindPath();
}
public string Path
{
get
{
if (path != null)
return path;
TryFindPath();
return path;
}
}
private void TryFindPath()
{
string[] candidates = AssetDatabase.FindAssets(filter);
if (candidates.Length == 0)
throw new Exception("Missing layout asset!");
path = AssetDatabase.GUIDToAssetPath(candidates[0]);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a4848b69c1024df8ad6f76cfd4415d01
timeCreated: 1698258811

View File

@ -1,23 +1,18 @@
using UnityEditor;
using UnityEngine;
public class GUIDToAssetPath : EditorWindow
{
string guid = "";
string path = "";
[MenuItem("Tools/GUIDToAssetPath")]
static void CreateWindow()
{
GUIDToAssetPath window = (GUIDToAssetPath)EditorWindow.GetWindowWithRect(typeof(GUIDToAssetPath), new Rect(0, 0, 400, 120));
}
void OnGUI()
private string guid = "";
private string path = "";
private void OnGUI()
{
GUILayout.Label("Enter guid");
guid = GUILayout.TextField(guid);
GUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("Get Asset Path",GUILayout.Width(120)))
if (GUILayout.Button("Get Asset Path", GUILayout.Width(120)))
path = GetAssetPath(guid);
GUILayout.FlexibleSpace();
GUILayout.EndHorizontal();
@ -29,10 +24,17 @@ public class GUIDToAssetPath : EditorWindow
GUILayout.EndHorizontal();
GUILayout.Label(path);
}
static string GetAssetPath(string guid)
[MenuItem("Tools/GUIDToAssetPath")]
private static void CreateWindow()
{
var window = (GUIDToAssetPath)GetWindowWithRect(typeof(GUIDToAssetPath), new Rect(0, 0, 400, 120));
}
private static string GetAssetPath(string guid)
{
guid = guid.Replace("-", "");
string p = AssetDatabase.GUIDToAssetPath(guid);
Debug.Log(p);
if (p.Length == 0) p = "not found";

View File

@ -0,0 +1,46 @@
using System.IO;
using UnityEditor;
using UnityEngine;
namespace NEG.Utils.Editor
{
public static class MonoBehaviourExtensions
{
[MenuItem("CONTEXT/MonoBehaviour/Change Script")]
public static void ChangeScript(MenuCommand command)
{
if (command.context == null)
return;
var monoBehaviour = command.context as MonoBehaviour;
var monoScript = MonoScript.FromMonoBehaviour(monoBehaviour);
string scriptPath = AssetDatabase.GetAssetPath(monoScript);
string directoryPath = new FileInfo(scriptPath).Directory?.FullName;
// Allow the user to select which script to replace with
string newScriptPath = EditorUtility.OpenFilePanel("Select replacement script", directoryPath, "cs");
// Don't log anything if they cancelled the window
if (string.IsNullOrEmpty(newScriptPath)) return;
// Load the selected asset
string relativePath = "Assets\\" + Path.GetRelativePath(Application.dataPath, newScriptPath);
var chosenTextAsset = AssetDatabase.LoadAssetAtPath<TextAsset>(relativePath);
if (chosenTextAsset == null)
{
Debug.LogWarning($"Selected script couldn't be loaded ({relativePath})");
return;
}
Undo.RegisterCompleteObjectUndo(command.context, "Changing component script");
var so = new SerializedObject(monoBehaviour);
var scriptProperty = so.FindProperty("m_Script");
so.Update();
scriptProperty.objectReferenceValue = chosenTextAsset;
so.ApplyModifiedProperties();
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0501eedeaf1d4dcf8c4d0f1b7a0c1761
timeCreated: 1684519404

View File

@ -1,18 +1,18 @@
{
"name": "NEG.Utils.Editor",
"rootNamespace": "",
"references": [
"GUID:3c4294719a93e3c4e831a9ff0c261e8a"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
"name": "NEG.Utils.Editor",
"rootNamespace": "",
"references": [
"GUID:3c4294719a93e3c4e831a9ff0c261e8a"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,19 @@
using UnityEditor;
using UnityEngine;
namespace NEG.Utils.Editor
{
[CustomPropertyDrawer(typeof(ReadOnlyAttribute))]
public class ReadOnlyPropertyDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
GUI.enabled = false;
EditorGUI.PropertyField(position, property, label, true);
GUI.enabled = true;
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) =>
EditorGUI.GetPropertyHeight(property, label, true);
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1f837bac3a7b40528454a9fb9a46d0be
timeCreated: 1699371332

View File

@ -1,5 +1,6 @@
using UnityEngine;
using UnityEditor;
using UnityEngine;
/// <summary>
/// Drawer for the RequireInterface attribute.
/// </summary>
@ -10,7 +11,7 @@ namespace NEG.Utils
public class RequireInterfaceDrawer : PropertyDrawer
{
/// <summary>
/// Overrides GUI drawing for the attribute.
/// Overrides GUI drawing for the attribute.
/// </summary>
/// <param name="position">Position.</param>
/// <param name="property">Property.</param>
@ -21,11 +22,12 @@ namespace NEG.Utils
if (property.propertyType == SerializedPropertyType.ObjectReference)
{
// Get attribute parameters.
var requiredAttribute = this.attribute as RequireInterfaceAttribute;
var requiredAttribute = attribute as RequireInterfaceAttribute;
// Begin drawing property field.
EditorGUI.BeginProperty(position, label, property);
// Draw property field.
property.objectReferenceValue = EditorGUI.ObjectField(position, label, property.objectReferenceValue, requiredAttribute.requiredType, true);
property.objectReferenceValue = EditorGUI.ObjectField(position, label, property.objectReferenceValue,
requiredAttribute.requiredType, true);
// Finish drawing property field.
EditorGUI.EndProperty();
}

18
Editor/ScreenshotMaker.cs Normal file
View File

@ -0,0 +1,18 @@
using UnityEditor;
using UnityEngine;
namespace NEG.Editor
{
public static class ScreenshotMaker
{
[MenuItem("CONTEXT/Camera/Save view")]
public static void Capture(MenuCommand command)
{
string path = EditorUtility.SaveFilePanel("Save screenshot", "", "screen.png", "png");
if (path.Length == 0)
return;
ScreenCapture.CaptureScreenshot(path);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7dba407e8e8740a1bed3ee5fa833b13c
timeCreated: 1687881092

View File

@ -1,21 +1,16 @@
using System;
using UnityEditor;
namespace NEG.Utils.Serialization
{
public static class SerializationExtentions
{
public static SerializedProperty FindAutoProperty(this SerializedObject @this, string name)
{
return @this.FindProperty(GetBackingFieldName(name));
}
public static SerializedProperty FindAutoProperty(this SerializedObject @this, string name) =>
@this.FindProperty(GetBackingFieldName(name));
public static SerializedProperty FindAutoPropertyRelative(this SerializedProperty @this, string name)
{
return @this.FindPropertyRelative(GetBackingFieldName(name));
}
public static SerializedProperty FindAutoPropertyRelative(this SerializedProperty @this, string name) =>
@this.FindPropertyRelative(GetBackingFieldName(name));
private static string GetBackingFieldName(string name)
public static string GetBackingFieldName(string name)
{
#if NET_STANDARD || NET_STANDARD_2_1
return string.Create(1/*<*/ + name.Length + 16/*>k__BackingField*/, name, static (span, name) =>
@ -29,4 +24,4 @@ namespace NEG.Utils.Serialization
#endif
}
}
}
}

View File

@ -1,5 +1,4 @@
using System;
using System.IO;
using System.IO;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
@ -10,11 +9,34 @@ namespace NegUtils.Editor
[InitializeOnLoad]
public class ToolsWindowBase : EditorWindow
{
private const int UnitySceneExtensionLength = 6;
static ToolsWindowBase()
{
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
}
protected virtual void OnGUI()
{
if (GUILayout.Button("Select Scene"))
ShowScenesList(GUILayoutUtility.GetLastRect());
bool startFromSceneIndex0 = EditorPrefs.GetBool("StartFromSceneIndex0");
bool newVal = GUILayout.Toggle(startFromSceneIndex0, "Start from scene with index 0 on start");
if (newVal != startFromSceneIndex0) EditorPrefs.SetBool("StartFromSceneIndex0", newVal);
if (!startFromSceneIndex0)
return;
bool goToCurrentScene = EditorPrefs.GetBool("GoToCurrentSceneAfterPlay");
newVal = GUILayout.Toggle(goToCurrentScene, "Go to current scene after play");
if (newVal != goToCurrentScene) EditorPrefs.SetBool("GoToCurrentSceneAfterPlay", newVal);
bool goToFirstScene = EditorPrefs.GetBool("GoToFirstSceneAfterPlay");
newVal = GUILayout.Toggle(goToFirstScene, "Go to scene with index 1 after play");
if (newVal != goToFirstScene) EditorPrefs.SetBool("GoToFirstSceneAfterPlay", newVal);
}
[MenuItem("Tools/Show Tools Window")]
private static void ShowWindow()
{
@ -22,47 +44,39 @@ namespace NegUtils.Editor
window.Show();
}
protected virtual void OnGUI()
{
if (GUILayout.Button("Select Scene"))
ShowScenesList(GUILayoutUtility.GetLastRect());
bool startFromSceneIndex0 = EditorPrefs.GetBool("StartFromSceneIndex0");
bool newVal = GUILayout.Toggle(startFromSceneIndex0, "Start from scene with index 0 on start");
if (newVal != startFromSceneIndex0)
{
EditorPrefs.SetBool("StartFromSceneIndex0", newVal);
}
if (startFromSceneIndex0)
{
bool goToCurrentScene = EditorPrefs.GetBool("GoToCurrentSceneAfterPlay");
newVal = GUILayout.Toggle(goToCurrentScene, "Go to current scene after play");
if (newVal != goToCurrentScene)
{
EditorPrefs.SetBool("GoToCurrentSceneAfterPlay", newVal);
}
}
}
private static void ShowScenesList(Rect position)
{
var menu = new GenericMenu();
string path = Application.dataPath + "/Scenes/Production";
string[] fileInfo = Directory.GetFiles(path, "*.unity");
string path = Application.dataPath + "/Scenes";
AddFiles(path, path, menu);
foreach (string item in fileInfo)
{
string s = item;
menu.AddItem(new GUIContent(s.Remove(0, path.Length + 1).Remove(s.Length - path.Length - 7 ,6)), false, () => {
LoadScene(s);
});
menu.AddSeparator("");
}
menu.DropDown(position);
}
private static void AddFiles(string path, string basePath, GenericMenu menu)
{
string[] fileInfo = Directory.GetFiles(path, "*.unity");
for (int i = 0; i < fileInfo.Length; i++)
{
string s = fileInfo[i];
menu.AddItem(
new GUIContent(s.Remove(0, basePath.Length + 1)
.Remove(s.Length - basePath.Length - UnitySceneExtensionLength - 1, UnitySceneExtensionLength)
.Replace('\\', '/')), false, () =>
{
LoadScene(s);
});
if (i == fileInfo.Length) continue;
menu.AddSeparator("");
}
string[] dirInfo = Directory.GetDirectories(path);
foreach (string dir in dirInfo) AddFiles(dir, basePath, menu);
}
private static void LoadScene(string path)
{
EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo();
@ -71,11 +85,11 @@ namespace NegUtils.Editor
private static void OnPlayModeStateChanged(PlayModeStateChange state)
{
switch(state)
switch (state)
{
case PlayModeStateChange.ExitingEditMode:
{
if(!EditorPrefs.GetBool("StartFromSceneIndex0"))
if (!EditorPrefs.GetBool("StartFromSceneIndex0"))
return;
EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo();
EditorPrefs.SetString("LastOpenedScenePath", EditorSceneManager.GetSceneManagerSetup()[0].path);
@ -84,25 +98,26 @@ namespace NegUtils.Editor
break;
case PlayModeStateChange.EnteredPlayMode:
{
if(!EditorPrefs.GetBool("StartFromSceneIndex0"))
if (!EditorPrefs.GetBool("StartFromSceneIndex0"))
return;
if (EditorPrefs.GetBool("GoToCurrentSceneAfterPlay"))
{
EditorSceneManager.LoadSceneInPlayMode(EditorPrefs.GetString("LastOpenedScenePath"),
new LoadSceneParameters(LoadSceneMode.Single));
else
}
else if (EditorPrefs.GetBool("GoToFirstSceneAfterPlay"))
SceneManager.LoadScene(1);
}
break;
case PlayModeStateChange.EnteredEditMode:
{
if(!EditorPrefs.GetBool("StartFromSceneIndex0"))
if (!EditorPrefs.GetBool("StartFromSceneIndex0"))
return;
EditorSceneManager.OpenScene(EditorPrefs.GetString("LastOpenedScenePath"));
}
break;
}
}
}
}

14
Editor/TsvImporter.cs Normal file
View File

@ -0,0 +1,14 @@
using System.IO;
using UnityEditor.AssetImporters;
using UnityEngine;
[ScriptedImporter(1, "tsv")]
public class TsvImporter : ScriptedImporter
{
public override void OnImportAsset(AssetImportContext ctx)
{
var textAsset = new TextAsset(File.ReadAllText(ctx.assetPath));
ctx.AddObjectToAsset(Path.GetFileNameWithoutExtension(ctx.assetPath), textAsset);
ctx.SetMainObject(textAsset);
}
}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 88120b6e616164f489387a6a32a25dee
guid: b3f8438db4084014bab0a063e4675d3f
MonoImporter:
externalObjects: {}
serializedVersion: 2

50
KeyBasedFactory.cs Normal file
View File

@ -0,0 +1,50 @@
using JetBrains.Annotations;
using System;
using System.Collections.Generic;
using System.Reflection;
namespace NEG.Utils
{
public class KeyBasedFactory<T1, T2>
{
[PublicAPI] protected Dictionary<T1, Type> data;
public KeyBasedFactory()
{
data = new Dictionary<T1, Type>();
}
public void FireRegistration()
{
ScanAssembly(typeof(T2).Assembly);
if (typeof(T2).Assembly.GetType().Assembly == typeof(T2).Assembly)
return;
ScanAssembly(typeof(T2).Assembly.GetType().Assembly);
}
private static void ScanAssembly(Assembly assembly)
{
foreach (var type in assembly.GetTypes())
{
var methodFields =
type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
for (int i = 0; i < methodFields.Length; i++)
{
if (Attribute.GetCustomAttribute(methodFields[i], typeof(FactoryRegistration)) != null)
methodFields[i].Invoke(null, Array.Empty<object>());
}
}
}
public void Register(T1 key, Type type) => data.Add(key, type);
public T2 CreateInstance(T1 key, params object[] args) => (T2)Activator.CreateInstance(data[key], args);
}
[AttributeUsage(AttributeTargets.Method)]
public class FactoryRegistration : Attribute
{
}
}

3
KeyBasedFactory.cs.meta Normal file
View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2e0d3df1ddd34209bfb7262b4e51abfe
timeCreated: 1683917310

View File

@ -1,16 +1,16 @@
{
"name": "NEG.Utils",
"rootNamespace": "",
"references": [
"GUID:6055be8ebefd69e48b49212b09b47b2f"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
"name": "NEG.Utils",
"rootNamespace": "",
"references": [
"GUID:6055be8ebefd69e48b49212b09b47b2f"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -1,9 +1,11 @@
using NEG.UI.WindowSlot;
using NegUtils.NEG.UI;
namespace NEG.UI.Area
{
public interface IArea : ISlotsHolder, IUiElement
public interface IArea : ISlotsHolder, IControllable
{
void Open();
void Close();
}
}

18
NEG/UI/IControllable.cs Normal file
View File

@ -0,0 +1,18 @@
using System;
namespace NegUtils.NEG.UI
{
public interface IControllable
{
event Action<object> OnOpened;
event Action OnClosed;
event Action<BackUsed> OnBackUsed;
public void TryUseBack(ref BackUsed backUsed);
public class BackUsed
{
public bool Used { get; set; }
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 175798310e4048eda768cf40e6ee6de3
timeCreated: 1686596400

7
NEG/UI/IController.cs Normal file
View File

@ -0,0 +1,7 @@
namespace NegUtils.NEG.UI
{
public interface IController
{
IControllable Controllable { get; }
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b88f4a93020a4bc6bde40e0438d296a9
timeCreated: 1686595825

View File

@ -1,11 +0,0 @@
namespace NEG.UI
{
public interface IUiElement
{
/// <summary>
/// Sets only visible state of element
/// </summary>
/// <param name="setEnabled"></param>
void SetEnabled(bool setEnabled);
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 296bf6969a6347f8aea788a7bdd086af
timeCreated: 1670693177

View File

@ -1,14 +1,16 @@
{
"name": "NEG.UI",
"rootNamespace": "",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
"name": "NEG.UI",
"rootNamespace": "",
"references": [
"GUID:3c4294719a93e3c4e831a9ff0c261e8a"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -5,13 +5,14 @@ namespace NEG.UI.Popup
{
public class DefaultPopupData : PopupData
{
private readonly IDefaultPopup defaultPopup;
private readonly string title;
private readonly string content;
private readonly IDefaultPopup defaultPopup;
private readonly List<(string, Action)> options;
public DefaultPopupData(IDefaultPopup popup, string title, string content, List<(string, Action)> options) : base(popup)
private readonly string title;
public DefaultPopupData(IDefaultPopup popup, string title, string content, List<(string, Action)> options) :
base(popup)
{
defaultPopup = popup;
this.title = title;

View File

@ -6,11 +6,14 @@ namespace NEG.UI.Popup
public interface IDefaultPopup : IPopup
{
/// <summary>
/// Sets content based on provided data.
/// Sets content based on provided data.
/// </summary>
/// <param name="title">popup title</param>
/// <param name="content">popup content</param>
/// <param name="options">list of tuples (name, action on click), to set buttons. Do not pass here popup closing logic, implementing class should do it</param>
/// <param name="options">
/// list of tuples (name, action on click), to set buttons. Do not pass here popup closing logic,
/// implementing class should do it
/// </param>
public void SetContent(string title, string content, List<(string name, Action action)> options);
}
}

View File

@ -7,18 +7,18 @@ namespace NEG.UI.Popup
public interface IPopup
{
/// <summary>
/// Event to fire when popup is closed
/// Event to fire when popup is closed
/// </summary>
event Action<PopupData> OnPopupClosed;
/// <summary>
/// Show popup
/// Show popup
/// </summary>
/// <param name="data">data assigned to popup, used to give info that popup is closed</param>
public void Show(PopupData data);
/// <summary>
/// Close popup or mark as closed if not visible
/// Close popup or mark as closed if not visible
/// </summary>
/// <param name="silent">if true hide visually, without firing callbacks to properly close</param>
void Close(bool silent = false);

View File

@ -6,8 +6,25 @@ namespace NEG.UI.Popup
[PublicAPI]
public class PopupData
{
private readonly IPopup popup;
/// <summary>
/// Event that is fired on closing popup.
/// PopupData constructor.
/// </summary>
/// <param name="popup">attached to this data, can be used by different data instances</param>
public PopupData(IPopup popup)
{
this.popup = popup;
IsValid = true;
}
/// <summary>
/// Is this data is still valid. If set to false, popup will not show.
/// </summary>
public bool IsValid { get; protected set; }
/// <summary>
/// Event that is fired on closing popup.
/// </summary>
public event Action<PopupData> PopupClosedEvent
{
@ -16,35 +33,22 @@ namespace NEG.UI.Popup
}
/// <summary>
/// Is this data is still valid. If set to false, popup will not show.
/// </summary>
public bool IsValid { get; protected set; }
private readonly IPopup popup;
/// <summary>
/// PopupData constructor.
/// </summary>
/// <param name="popup">attached to this data, can be used by different data instances</param>
public PopupData(IPopup popup)
{
this.popup = popup;
IsValid = true;
}
/// <summary>
/// Show popup and pass needed data.
/// Show popup and pass needed data.
/// </summary>
public virtual void Show() => popup.Show(this);
/// <summary>
/// Hide popup. Close visuals without firing events;
/// Hide popup. Close visuals without firing events;
/// </summary>
public virtual void Hide() => popup.Close(true);
/// <summary>
/// Invalidate popup, <see cref="UiManager"/> will automatically skip this popup
/// Invalidate popup, <see cref="UiManager" /> will automatically skip this popup
/// </summary>
public virtual void Invalidate() => IsValid = false;
public virtual void Invalidate()
{
IsValid = false;
UiManager.Instance.RefreshPopups();
}
}
}

View File

@ -248,8 +248,10 @@ namespace System.Collections.Generic
public PriorityQueue(int initialCapacity, IComparer<TPriority>? comparer)
{
if (initialCapacity < 0)
{
throw new ArgumentOutOfRangeException(
nameof(initialCapacity), initialCapacity, SR.ArgumentOutOfRange_NeedNonNegNum);
}
_nodes = new (TElement, TPriority)[initialCapacity];
_comparer = InitializeComparer(comparer);
@ -489,7 +491,7 @@ namespace System.Collections.Generic
{
int i = 0;
(TElement, TPriority)[] nodes = _nodes;
foreach ((var element, var priority) in items)
foreach (var (element, priority) in items)
{
if (nodes.Length == i)
{
@ -508,9 +510,8 @@ namespace System.Collections.Generic
if (_size > 1) Heapify();
}
else
{
foreach ((var element, var priority) in items) Enqueue(element, priority);
}
foreach (var (element, priority) in items)
Enqueue(element, priority);
}
/// <summary>
@ -554,9 +555,8 @@ namespace System.Collections.Generic
if (i > 1) Heapify();
}
else
{
foreach (var element in elements) Enqueue(element, priority);
}
foreach (var element in elements)
Enqueue(element, priority);
}
/// <summary>
@ -684,11 +684,15 @@ namespace System.Collections.Generic
int lastParentWithChildren = GetParentIndex(_size - 1);
if (_comparer == null)
{
for (int index = lastParentWithChildren; index >= 0; --index)
MoveDownDefaultComparer(nodes[index], index);
}
else
{
for (int index = lastParentWithChildren; index >= 0; --index)
MoveDownCustomComparer(nodes[index], index);
}
}
/// <summary>
@ -715,9 +719,7 @@ namespace System.Collections.Generic
nodeIndex = parentIndex;
}
else
{
break;
}
}
nodes[nodeIndex] = node;
@ -748,9 +750,7 @@ namespace System.Collections.Generic
nodeIndex = parentIndex;
}
else
{
break;
}
}
nodes[nodeIndex] = node;
@ -894,8 +894,10 @@ namespace System.Collections.Generic
if (array.GetLowerBound(0) != 0) throw new ArgumentException(SR.Arg_NonZeroLowerBound, nameof(array));
if (index < 0 || index > array.Length)
{
throw new ArgumentOutOfRangeException(nameof(index), index,
SR.ArgumentOutOfRange_IndexMustBeLessOrEqual);
}
if (array.Length - index < _queue._size) throw new ArgumentException(SR.Argument_InvalidOffLen);

View File

@ -2,47 +2,28 @@ using JetBrains.Annotations;
using NEG.UI.Area;
using NEG.UI.Popup;
using NEG.UI.Window;
using NegUtils.NEG.UI;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace NEG.UI
{
[PublicAPI]
public abstract class UiManager
public abstract class UiManager : IDisposable
{
public static UiManager Instance { get; private set; }
/// <summary>
/// Current area shown on screen.
/// </summary>
public IArea CurrentArea
{
get => currentArea;
set
{
currentArea?.SetEnabled(false);
currentArea = value;
currentArea?.SetEnabled(true);
}
}
/// <summary>
/// Current window that is considered main (focused, lastly opened). Can be null.
/// </summary>
public IWindow CurrentMainWindow { get; protected set; }
private IArea currentArea;
private (PopupData data, int priority) currentShownPopup;
protected IDefaultPopup currentDefaultPopup;
private PriorityQueue<PopupData, int> popupsToShow = new();
private (PopupData data, int priority) currentShownPopup;
//TODO: localize
private string localizedYes = "Yes", localizedNo = "No", localizedOk = "Ok";
private List<IWindow> mainWindows;
private PriorityQueue<PopupData, int> popupsToShow = new();
protected UiManager(IArea startArea)
{
if (Instance != null)
@ -54,10 +35,39 @@ namespace NEG.UI
Instance = this;
CurrentArea = startArea;
mainWindows = new List<IWindow>();
}
public static UiManager Instance { get; protected set; }
/// <summary>
/// Current area shown on screen.
/// </summary>
public IArea CurrentArea
{
get => currentArea;
set
{
currentArea?.Close();
currentArea = value;
currentArea?.Open();
}
}
/// <summary>
/// Show popup if there is non other currently shown. Otherwise add current popup to ordered queue and show it later. It will be closed after pressing ok button.
/// Current window that is considered main (focused, lastly opened). Can be null.
/// </summary>
public IWindow CurrentMainWindow => mainWindows.LastOrDefault();
public PopupData CurrentPopup => currentShownPopup.data;
public virtual void Dispose() => Instance = null;
/// <summary>
/// Show popup if there is non other currently shown. Otherwise add current popup to ordered queue and show it later.
/// It will be closed after pressing ok button.
/// </summary>
/// <param name="title">popup title</param>
/// <param name="content">popup content</param>
@ -66,16 +76,18 @@ namespace NEG.UI
/// <param name="priority">priority of popup (lower number -> show first)</param>
/// <param name="forceShow">force show current popup only if currently shown has lower priority</param>
/// <returns>data for created popup, can be used to invalidate popup (will not show)</returns>
public PopupData ShowOkPopup(string title, string content, string okText = null, Action okPressed = null, int priority = 0, bool forceShow = false)
public PopupData ShowOkPopup(string title, string content, string okText = null, Action okPressed = null,
int priority = 0, bool forceShow = false)
{
var data = new DefaultPopupData(currentDefaultPopup, title, content,
new List<(string, Action)>() { (okText ?? localizedOk, okPressed) });
new List<(string, Action)> { (okText ?? localizedOk, okPressed) });
ShowPopup(data, priority, forceShow);
return data;
}
/// <summary>
/// Show popup if there is non other currently shown. Otherwise add current popup to ordered queue and show it later. It will be closed after pressing yes or no button.
/// Show popup if there is non other currently shown. Otherwise add current popup to ordered queue and show it later.
/// It will be closed after pressing yes or no button.
/// </summary>
/// <param name="title">popup title</param>
/// <param name="content">popup content</param>
@ -86,16 +98,21 @@ namespace NEG.UI
/// <param name="priority">priority of popup (lower number -> show first)</param>
/// <param name="forceShow">force show current popup only if currently shown has lower priority</param>
/// <returns>data for created popup, can be used to invalidate popup (will not show)</returns>
public PopupData ShowYesNoPopup(string title, string content, string yesText = null, string noText = null, Action yesPressed = null, Action noPressed = null, int priority = 0, bool forceShow = false)
public PopupData ShowYesNoPopup(string title, string content, string yesText = null, string noText = null,
Action yesPressed = null, Action noPressed = null, int priority = 0, bool forceShow = false)
{
var data = new DefaultPopupData(currentDefaultPopup, title, content,
new List<(string, Action)>() { (yesText ?? localizedYes, yesPressed), (noText ?? localizedNo, noPressed) });
new List<(string, Action)>
{
(yesText ?? localizedYes, yesPressed), (noText ?? localizedNo, noPressed)
});
ShowPopup(data, priority, forceShow);
return data;
}
/// <summary>
/// Show popup if there is non other currently shown. Otherwise add current popup to ordered queue and show it later. It will be closed after pressing any button.
/// Show popup if there is non other currently shown. Otherwise add current popup to ordered queue and show it later.
/// It will be closed after pressing any button.
/// </summary>
/// <param name="title">popup title</param>
/// <param name="content">popup content</param>
@ -103,15 +120,16 @@ namespace NEG.UI
/// <param name="priority">priority of popup (lower number -> show first)</param>
/// <param name="forceShow">force show current popup only if currently shown has lower priority</param>
/// <returns>data for created popup, can be used to invalidate popup (will not show)</returns>
public PopupData ShowPopup(string title, string content, List<(string, Action)> actions, int priority = 0, bool forceShow = false)
public PopupData ShowPopup(string title, string content, List<(string, Action)> actions, int priority = 0,
bool forceShow = false)
{
var data = new DefaultPopupData(currentDefaultPopup, title, content, actions);
ShowPopup(data, priority, forceShow);
return data;
}
/// <summary>
/// Show popup if there is non other currently shown. Otherwise add current popup to ordered queue and show it later.
/// Show popup if there is non other currently shown. Otherwise add current popup to ordered queue and show it later.
/// </summary>
/// <param name="data">popup data object</param>
/// <param name="priority">priority of popup (lower number -> show first)</param>
@ -121,35 +139,72 @@ namespace NEG.UI
popupsToShow.Enqueue(data, priority);
UpdatePopupsState(forceShow, priority, data);
}
public void UseBack()
{
IControllable.BackUsed backUsed = new();
CurrentMainWindow?.TryUseBack(ref backUsed);
if (backUsed.Used)
return;
CurrentArea.TryUseBack(ref backUsed);
}
public void RefreshPopups()
{
if (currentShownPopup.data is { IsValid: true })
return;
UpdatePopupsState(false);
}
public void SetMainWindow(IWindow window) => mainWindows.Add(window);
public void MainWindowClosed(IWindow window) => mainWindows.Remove(window);
public void OnWindowClosed(IWindow window) => MainWindowClosed(window);
//TODO: select new main window
protected void PopupClosed(PopupData data)
{
if (currentShownPopup.data != data)
{
Debug.LogError("Popup was not shown");
//Debug.LogError("Popup was not shown");
return;
}
UpdatePopupsState(false);
}
protected void SetDefaultPopup(IDefaultPopup popup) => currentDefaultPopup = popup;
private void UpdatePopupsState(bool forceShow, int priority = 0, PopupData data = null)
protected virtual void UpdatePopupsState(bool forceShow, int priority = 0, PopupData data = null)
{
if (forceShow)
{
if(currentShownPopup.priority <= priority)
if (currentShownPopup.data != null && currentShownPopup.priority >= priority)
return;
popupsToShow.Enqueue(currentShownPopup.data, currentShownPopup.priority);
ShowPopup(data, priority);
return;
}
if(!popupsToShow.TryDequeue(out var d, out int p))
return;
ShowPopup(d, p);
while (popupsToShow.TryDequeue(out var d, out int p))
{
if (d == null)
continue;
if (!d.IsValid)
continue;
if (d == currentShownPopup.data)
continue;
ShowPopup(d, p);
return;
}
if (currentShownPopup.data == null)
return;
currentShownPopup.data.PopupClosedEvent -= PopupClosed;
currentShownPopup.data.Hide();
currentShownPopup.data = null;
}
private void ShowPopup(PopupData data, int priority)
@ -159,11 +214,10 @@ namespace NEG.UI
currentShownPopup.data.PopupClosedEvent -= PopupClosed;
currentShownPopup.data.Hide();
}
currentShownPopup = (data, priority);
data.Show();
data.PopupClosedEvent += PopupClosed;
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More