com.amanotes.gdk 0.2.13 → 0.2.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.14 - 2023-08-08]
4
+ ### Support multiple embed remote config and load config by id
5
+ ### Clear cached remote config on new app update
6
+ ### Override remote config value in editor mode
7
+ ### New feature: Analytics Quality
8
+
3
9
  ## [0.2.13 - 2023-07-20]
4
10
  ### Fix error Remote Config JSON caused by special chars (", ', \)
5
11
 
@@ -0,0 +1,191 @@
1
+ using System;
2
+ using System.Text;
3
+ using Amanotes.Core.Internal;
4
+ using UnityEditor;
5
+ using UnityEngine;
6
+
7
+ using static Amanotes.Core.Internal.Logging;
8
+
9
+ namespace Amanotes.Editor
10
+ {
11
+ [CustomEditor(typeof(EmbedRemoteConfigAsset))]
12
+ internal class EmbedRemoteConfigAssetInspector : UnityEditor.Editor
13
+ {
14
+ private EmbedRemoteConfigAsset remoteConfigAsset;
15
+ private SerializedProperty overrideInEditorProperty;
16
+ private SerializedProperty overrideConfigProperty;
17
+ private SerializedProperty configsProperty;
18
+ private bool[] foldouts;
19
+
20
+ private void OnEnable()
21
+ {
22
+ remoteConfigAsset = (EmbedRemoteConfigAsset)target;
23
+ overrideInEditorProperty = serializedObject.FindProperty("overrideInEditor");
24
+ overrideConfigProperty = serializedObject.FindProperty("overrideConfig");
25
+ configsProperty = serializedObject.FindProperty("configs");
26
+ foldouts = new bool[configsProperty.arraySize];
27
+ }
28
+
29
+ public override void OnInspectorGUI()
30
+ {
31
+ EditorGUILayout.BeginHorizontal();
32
+ EditorGUILayout.LabelField("Active Id:");
33
+ EditorGUILayout.LabelField(remoteConfigAsset.activeId);
34
+ EditorGUILayout.EndHorizontal();
35
+
36
+ serializedObject.Update();
37
+
38
+ DrawPropertiesExcluding(serializedObject, "m_Script", "activeId", "configs", "overrideConfig");
39
+
40
+ if (overrideInEditorProperty.boolValue)
41
+ {
42
+ EditorGUILayout.PropertyField(overrideConfigProperty);
43
+ }
44
+
45
+ if (GUILayout.Button("Add a config set"))
46
+ {
47
+ configsProperty.arraySize++;
48
+ int lastIndex = configsProperty.arraySize - 1;
49
+ var config= configsProperty.GetArrayElementAtIndex(lastIndex);
50
+ config.FindPropertyRelative("id").stringValue = $"config{lastIndex}";
51
+ foldouts = AddElementToArray(foldouts, false);
52
+ }
53
+
54
+ for (int i = 0; i < configsProperty.arraySize; i++)
55
+ {
56
+ SerializedProperty configProperty = configsProperty.GetArrayElementAtIndex(i);
57
+ SerializedProperty idProperty = configProperty.FindPropertyRelative("id");
58
+ SerializedProperty fetchUrlProperty = configProperty.FindPropertyRelative("fetchUrl");
59
+ SerializedProperty itemsProperty = configProperty.FindPropertyRelative("items");
60
+
61
+ EditorGUILayout.BeginVertical(GUI.skin.box);
62
+ foldouts[i] = EditorGUILayout.Foldout(foldouts[i], idProperty.stringValue);
63
+ if (foldouts[i])
64
+ {
65
+ EditorGUI.indentLevel++;
66
+ EditorGUILayout.BeginHorizontal();
67
+ if (GUILayout.Button("Set active", GUILayout.MinWidth(100)))
68
+ {
69
+ remoteConfigAsset.SetActiveConfig(idProperty.stringValue);
70
+ }
71
+ if (GUILayout.Button("Fetch", GUILayout.MinWidth(80)))
72
+ {
73
+ var url = fetchUrlProperty.stringValue;
74
+ var currentConfigIndex = i;
75
+ if (string.IsNullOrWhiteSpace(url))
76
+ {
77
+ ShowError("Please fill the fetch URL first");
78
+ }
79
+ else
80
+ {
81
+ WebRequest.Get(url, onProgress: FetchProgress, onSuccess: s => FillEmbedConfig(currentConfigIndex, s), onError:ShowError);
82
+ }
83
+ }
84
+ if (GUILayout.Button("Remove", GUILayout.MinWidth(80)))
85
+ {
86
+ if (remoteConfigAsset.ActiveConfig?.id == idProperty.stringValue)
87
+ {
88
+ remoteConfigAsset.SetActiveConfig(string.Empty);
89
+ }
90
+ configsProperty.DeleteArrayElementAtIndex(i);
91
+ foldouts = RemoveElementAtIndex(foldouts, i);
92
+ }
93
+ if (GUILayout.Button("Copy", GUILayout.MinWidth(80)))
94
+ {
95
+ GUIUtility.systemCopyBuffer = KeyValueToText(itemsProperty);
96
+ }
97
+ if (GUILayout.Button("Paste", GUILayout.MinWidth(80)))
98
+ {
99
+ FillEmbedConfig(i, GUIUtility.systemCopyBuffer);
100
+ }
101
+ EditorGUILayout.EndHorizontal();
102
+ EditorGUILayout.PropertyField(idProperty);
103
+ EditorGUILayout.PropertyField(fetchUrlProperty);
104
+ EditorGUILayout.PropertyField(itemsProperty);
105
+
106
+ EditorGUI.indentLevel--;
107
+ }
108
+ EditorGUILayout.EndVertical();
109
+ }
110
+
111
+ serializedObject.ApplyModifiedProperties();
112
+ }
113
+
114
+ private T[] AddElementToArray<T>(T[] array, T element)
115
+ {
116
+ T[] newArray = new T[array.Length + 1];
117
+ array.CopyTo(newArray, 0);
118
+ newArray[array.Length] = element;
119
+ return newArray;
120
+ }
121
+
122
+ private T[] RemoveElementAtIndex<T>(T[] array, int index)
123
+ {
124
+ T[] newArray = new T[array.Length - 1];
125
+ for (int i = 0, j = 0; i < array.Length; i++)
126
+ {
127
+ if (i != index)
128
+ {
129
+ newArray[j] = array[i];
130
+ j++;
131
+ }
132
+ }
133
+ return newArray;
134
+ }
135
+
136
+ private void FetchProgress(float progress)
137
+ {
138
+ EditorUtility.DisplayProgressBar("Fetching default remote config", "Please wait...", progress);
139
+ }
140
+
141
+ private void FillEmbedConfig(int configIndex, string rawText)
142
+ {
143
+ try
144
+ {
145
+ string[] lines = rawText.Split("\n");
146
+ KeyValue[] arrKeyValue = new KeyValue[lines.Length];
147
+ for (int i = 0; i < lines.Length; i++)
148
+ {
149
+ var line = lines[i];
150
+ var idx = line.IndexOf('\t');
151
+ if (idx == -1)
152
+ {
153
+ LogWarning($"[RemoteConfig] Cannot get (key, value) at line {i+1}. Content:\n<{line}>");
154
+ continue;
155
+ }
156
+ arrKeyValue[i] = new KeyValue
157
+ {
158
+ key = line.Substring(0, idx),
159
+ value = line.Substring(idx + 1)
160
+ };
161
+ }
162
+ remoteConfigAsset.configs[configIndex].items = arrKeyValue;
163
+ }
164
+ catch (Exception e)
165
+ {
166
+ LogWarning(e.Message);
167
+ }
168
+ finally
169
+ {
170
+ EditorUtility.ClearProgressBar();
171
+ }
172
+ }
173
+
174
+ private string KeyValueToText(SerializedProperty items)
175
+ {
176
+ StringBuilder sb = new StringBuilder();
177
+ for (int i = 0; i < items.arraySize; i++)
178
+ {
179
+ SerializedProperty c = items.GetArrayElementAtIndex(i);
180
+ sb.AppendLine($"{c.FindPropertyRelative("key").stringValue}\t{c.FindPropertyRelative("value").stringValue}");
181
+ }
182
+ return sb.ToString();
183
+ }
184
+
185
+ private void ShowError(string error)
186
+ {
187
+ EditorUtility.ClearProgressBar();
188
+ EditorUtility.DisplayDialog("Error", error, "OK");
189
+ }
190
+ }
191
+ }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: cc3262a8477b7444e8ba53e7174756f6
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -1,7 +1,10 @@
1
1
  using System;
2
2
  using System.Collections;
3
+ using System.Collections.Generic;
3
4
  using UnityEditor;
4
5
  using UnityEngine;
6
+ using UnityEngine.Networking;
7
+
5
8
  namespace Amanotes.Editor
6
9
  {
7
10
  internal static class Utils
@@ -31,4 +34,50 @@ namespace Amanotes.Editor
31
34
  EditorApplication.update += closureCallback;
32
35
  }
33
36
  }
37
+
38
+ internal static class WebRequest
39
+ {
40
+ internal static void Get(string url, Dictionary<string, string> headers = null, Action<float> onProgress = null, Action<string> onSuccess = null, Action<string> onError = null)
41
+ {
42
+ GetCoroutine(url, headers, onProgress, onSuccess, onError).StartEditorCoroutine();
43
+ }
44
+
45
+ private static IEnumerator GetCoroutine(string url, Dictionary<string, string> headers = null, Action<float> onProgress = null, Action<string> onSuccess = null, Action<string> onError = null)
46
+ {
47
+ using var request = UnityWebRequest.Get(url);
48
+ // Add headers to the request
49
+ if (headers != null)
50
+ {
51
+ foreach (var header in headers)
52
+ {
53
+ request.SetRequestHeader(header.Key, header.Value);
54
+ }
55
+ }
56
+
57
+ // Send the request
58
+ request.timeout = 60;
59
+ yield return request.SendWebRequest();
60
+
61
+ while (request.isDone == false)
62
+ {
63
+ onProgress?.Invoke(request.downloadProgress);
64
+ yield return null;
65
+ }
66
+
67
+ // Check for errors
68
+ #pragma warning disable CS0618
69
+ if (request.isNetworkError || request.isHttpError)
70
+ #pragma warning restore CS0618
71
+ {
72
+ Debug.LogError("Error: " + request.error);
73
+ onError?.Invoke(request.error);
74
+ }
75
+ else
76
+ {
77
+ // Process the response
78
+ string responseText = request.downloadHandler.text;
79
+ onSuccess?.Invoke(responseText);
80
+ }
81
+ }
82
+ }
34
83
  }
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -301,7 +301,8 @@ namespace Amanotes.Core
301
301
  internal static void ProcessEventQueue()
302
302
  {
303
303
  if (eventQueue.Count <= 0) return;
304
- bool showAnalyticsLog = Config.common.showAnalyticsLog;
304
+ SDKConfig common = Config.common;
305
+ bool showAnalyticsLog = common.showAnalyticsLog;
305
306
  sessionStat.ClearLastFrameEventInfo();
306
307
 
307
308
  while (eventQueue.Count > 0)
@@ -317,9 +318,11 @@ namespace Amanotes.Core
317
318
  }
318
319
 
319
320
  string eventNameToSend = GetEventNameToSend(eventData.eventName, countInSession, totalCount);
320
- if (Config.common.normalizeEventName) NormalizeEventName(ref eventNameToSend);
321
+ if (common.normalizeEventName) NormalizeEventName(ref eventNameToSend);
321
322
 
322
- if (showAnalyticsLog) Debug.Log($"AmaGDK | Event <{eventNameToSend}> (in session: {countInSession}, total: {totalCount})\n{JsonUtils.DictionaryToJson(eventData.parameters, true)}");
323
+ if (showAnalyticsLog) Debug.Log($"AmaGDK | Event <{eventNameToSend}> (in session: {countInSession}, total: {totalCount})\n{JsonUtils.DictionaryToJson(eventData.parameters, true)}");
324
+
325
+ common.quality.CheckEventQuality(eventData.eventName, eventData.parameters);
323
326
 
324
327
  var inUseAdapterIDs = new List<string>();
325
328
  foreach (AdapterContext m in listAdapters)
@@ -731,12 +734,28 @@ namespace Amanotes.Core
731
734
 
732
735
  namespace Amanotes.Core.Internal
733
736
  {
737
+ [Serializable]
738
+ public class AnalyticQualityConfig
739
+ {
740
+ public bool enable = false;
741
+ public AnalyticQualityAsset qualityAsset;
742
+
743
+ public void CheckEventQuality(string eventName, Dictionary<string, object> parameters)
744
+ {
745
+ if (enable && qualityAsset != null)
746
+ {
747
+ qualityAsset.CheckEventQuality(eventName, parameters);
748
+ }
749
+ }
750
+ }
751
+
734
752
  public partial class SDKConfig
735
753
  {
736
754
  public bool showAnalyticsLog = true;
737
755
  public bool normalizeEventName = true;
738
756
  public bool checkMigrationIntegrity = true;
739
757
  public bool enableEventStat;
758
+ public AnalyticQualityConfig quality;
740
759
  }
741
760
 
742
761
  public interface IAnalyticAdapter
@@ -16,8 +16,7 @@ namespace Amanotes.Core
16
16
  {
17
17
  public enum RemoteConfigSource
18
18
  {
19
- DefaultAsset,
20
- DefaultFile,
19
+ EmbedConfig,
21
20
  Cache,
22
21
  Internet
23
22
  }
@@ -26,7 +25,7 @@ namespace Amanotes.Core
26
25
  public partial class RemoteConfig //Public
27
26
  {
28
27
  public static Action<bool> onFetchCompleted;
29
- public static Dictionary<string, string> DictConfig { get { return dictConfig; } }
28
+ public static Dictionary<string, string> DictConfig => dictConfig;
30
29
  public static RemoteConfigSource Source { private set; get; }
31
30
  static bool? _cacheExisted = null;
32
31
  public static bool HasCache
@@ -64,6 +63,15 @@ namespace Amanotes.Core
64
63
 
65
64
  public static T Get<T>(string key, T defaultValue = default(T))
66
65
  {
66
+ #if UNITY_EDITOR
67
+ //Override value in editor environment
68
+ if (adapterConfig.embedRemoteConfig.overrideInEditor)
69
+ {
70
+ foreach (var c in adapterConfig.embedRemoteConfig.overrideConfig)
71
+ if (c.key == key)
72
+ return (T)Convert.ChangeType(c.value, typeof(T));
73
+ }
74
+ #endif
67
75
  if (dictConfig.ContainsKey(key))
68
76
  return (T)Convert.ChangeType(dictConfig[key], typeof(T));
69
77
  return defaultValue;
@@ -78,6 +86,13 @@ namespace Amanotes.Core
78
86
  {
79
87
  return JsonUtils.DictionaryToJson(dictConfig);
80
88
  }
89
+
90
+ public static void SetActiveEmbedConfig(string id)
91
+ {
92
+ if (adapterConfig == null || adapterConfig.embedRemoteConfig == null)
93
+ LogWarning("[RemoteConfig] No embed remote config available");
94
+ adapterConfig.embedRemoteConfig.SetActiveConfig(id);
95
+ }
81
96
  }
82
97
 
83
98
  public partial class RemoteConfig //Internal
@@ -91,7 +106,7 @@ namespace Amanotes.Core
91
106
  SaveCache,
92
107
 
93
108
  LoadCache,
94
- LoadDefaultAsset,
109
+ LoadEmbedConfig,
95
110
 
96
111
  Success,
97
112
  Fail
@@ -100,9 +115,15 @@ namespace Amanotes.Core
100
115
  static Dictionary<string, string> dictConfig = new Dictionary<string, string>();
101
116
  static string fileName = $"{nameof(RemoteConfig)}.kv";
102
117
  static string base64Prefix = "base64:";
118
+ const string VERSION_PREFIX = "version:";
119
+ static int EmbedVersion =>
120
+ adapterConfig != null && adapterConfig.embedRemoteConfig != null
121
+ ? adapterConfig.embedRemoteConfig.version
122
+ : 1;
103
123
 
104
124
  static FetchState _state = FetchState.None;
105
- internal static FetchState State { get { return _state; } }
125
+ internal static FetchState State => _state;
126
+
106
127
  static readonly Dictionary<FetchState, Action[]> fetchSM = new Dictionary<FetchState, Action[]>
107
128
  {
108
129
  { FetchState.None, new Action[]{ ResolveDependency }},
@@ -110,8 +131,8 @@ namespace Amanotes.Core
110
131
  { FetchState.CheckInternet, new Action[]{ LoadCache, Fetch}},
111
132
  { FetchState.Fetch, new Action[]{ LoadCache, SaveCache}},
112
133
  { FetchState.SaveCache, new Action[]{ SetDone }},
113
- { FetchState.LoadCache, new Action[]{ LoadDefaultAsset, SetDone}},
114
- { FetchState.LoadDefaultAsset, new Action[]{ SetFail, SetDone}}
134
+ { FetchState.LoadCache, new Action[]{ LoadEmbedConfig, SetDone}},
135
+ { FetchState.LoadEmbedConfig, new Action[]{ SetFail, SetDone}}
115
136
  };
116
137
 
117
138
  internal static IRemoteConfig adapter;
@@ -177,27 +198,14 @@ namespace Amanotes.Core
177
198
  {
178
199
  _state = FetchState.SaveCache;
179
200
  StringBuilder sb = new StringBuilder();
180
-
181
-
182
- bool needUpdateDefault = Application.isEditor && adapterConfig.autoUpdateDefaultValue;
183
- List<KeyValue> list = needUpdateDefault ? new List<KeyValue>() : null;
184
201
 
202
+ //Add embedded remote config version to force load from embedded config after app update
203
+ sb.AppendLine($"{VERSION_PREFIX}{EmbedVersion}");
204
+
185
205
  foreach (var c in dictConfig)
186
206
  {
187
207
  sb.AppendLine($"{c.Key}\t{Encode(c.Value)}");
188
- if (!needUpdateDefault) continue;
189
- list.Add(new KeyValue() { key = c.Key, value = c.Value });
190
- }
191
-
192
- #if UNITY_EDITOR
193
- if (needUpdateDefault)
194
- {
195
- list.Sort((item1, item2) => string.Compare(item1.key, item2.key, StringComparison.Ordinal));
196
- adapterConfig.defaultValue = list.ToArray();
197
- EditorUtility.SetDirty(_config);
198
- Log($"[RemoteConfig] Updated default remote config!");
199
208
  }
200
- #endif
201
209
 
202
210
  string filePath = GDKFileUtils.GetPath(fileName);
203
211
  GDKFileUtils.Save(filePath, sb.ToString());
@@ -220,15 +228,32 @@ namespace Amanotes.Core
220
228
  dictConfig.Clear();
221
229
  using (StreamReader sr = new StreamReader(GDKFileUtils.GetPath(fileName)))
222
230
  {
231
+ //Check new app update
232
+ string firstLine = sr.ReadLine();
233
+ if (firstLine == null)
234
+ {
235
+ NextState(false);
236
+ return;
237
+ }
238
+ if (ShouldClearCache(firstLine))
239
+ {
240
+ GDKFileUtils.Delete(fileName);
241
+ string oldVersion = firstLine.StartsWith(VERSION_PREFIX) ? firstLine : "<none>";
242
+ Log($"[RemoteConfig] Cached remote config cleared due to embed version changed:\n{oldVersion} -> {EmbedVersion}");
243
+ NextState(false);
244
+ return;
245
+ }
246
+
247
+ //Read key-value
223
248
  string line;
224
- int lineIdx = 0;
249
+ int lineNo = 1;
225
250
  while ((line = sr.ReadLine()) != null)
226
251
  {
227
- lineIdx++;
252
+ lineNo++;
228
253
  var idx = line.IndexOf('\t');
229
254
  if (idx == -1)
230
255
  {
231
- LogWarning($"[RemoteConfig] Cannot get (key, value) at line {lineIdx} of cache");
256
+ LogWarning($"[RemoteConfig] Cannot get (key, value) at line {lineNo} of cache. Content:\n<{line}>");
232
257
  continue;
233
258
  }
234
259
  string key = line.Substring(0, idx);
@@ -250,24 +275,30 @@ namespace Amanotes.Core
250
275
  NextState(true);
251
276
  }
252
277
 
253
- static void LoadDefaultAsset()
278
+ static void LoadEmbedConfig()
254
279
  {
255
- _state = FetchState.LoadDefaultAsset;
280
+ _state = FetchState.LoadEmbedConfig;
281
+
282
+ if (adapterConfig == null || adapterConfig.embedRemoteConfig == null)
283
+ {
284
+ NextState(false);
285
+ return;
286
+ }
256
287
 
257
- var defaultValue = ((AdapterContext)adapter).GetAdapterConfig<RemoteConfigConfig>().defaultValue;
288
+ var config = adapterConfig.embedRemoteConfig.ActiveConfig;
258
289
 
259
- if (defaultValue.Length == 0)
290
+ if (config == null || config.items.Length == 0)
260
291
  {
261
292
  NextState(false);
262
293
  return;
263
294
  }
264
295
 
265
- foreach (var item in defaultValue)
296
+ foreach (var item in config.items)
266
297
  {
267
298
  dictConfig.Add(item.key, item.value);
268
299
  }
269
- Source = RemoteConfigSource.DefaultAsset;
270
- Log("[RemoteConfig] Load from default asset");
300
+ Source = RemoteConfigSource.EmbedConfig;
301
+ Log("[RemoteConfig] Load from embed asset");
271
302
  NextState(true);
272
303
  }
273
304
 
@@ -301,6 +332,12 @@ namespace Amanotes.Core
301
332
  s = s.Remove(0, base64Prefix.Length);
302
333
  return Encoding.UTF8.GetString(Convert.FromBase64String(s));
303
334
  }
335
+
336
+ static bool ShouldClearCache(string cacheVersion)
337
+ {
338
+ if (!cacheVersion.StartsWith(VERSION_PREFIX)) return true;
339
+ return cacheVersion != VERSION_PREFIX + EmbedVersion;
340
+ }
304
341
  }
305
342
  }
306
343
 
@@ -324,19 +361,12 @@ namespace Amanotes.Core.Internal
324
361
  [Serializable]
325
362
  public class RemoteConfigConfig
326
363
  {
327
- public bool autoUpdateDefaultValue;
328
- public KeyValue[] defaultValue;
364
+ [SerializeField]
365
+ internal EmbedRemoteConfigAsset embedRemoteConfig;
329
366
  public float firstFetchTimeOutInSeconds = 10;
330
367
  public float defaultTimeOutInSeconds = 3;
331
368
  }
332
-
333
- [Serializable]
334
- public class KeyValue
335
- {
336
- public string key;
337
- public string value;
338
- }
339
-
369
+
340
370
  public interface IRemoteConfig
341
371
  {
342
372
  void ResolveDependencies(Action<bool> onComplete);
package/Runtime/AmaGDK.cs CHANGED
@@ -14,7 +14,7 @@ namespace Amanotes.Core
14
14
  {
15
15
  public partial class AmaGDK : MonoBehaviour
16
16
  {
17
- public const string VERSION = "0.2.13";
17
+ public const string VERSION = "0.2.14";
18
18
 
19
19
  internal static AmaGDK _instance;
20
20
  internal static Status _status = Status.None;
@@ -0,0 +1,218 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.Linq;
4
+ using System.Text;
5
+ using UnityEngine;
6
+ using static Amanotes.Core.Internal.Logging;
7
+ #if UNITY_EDITOR
8
+ using UnityEditor;
9
+ #endif
10
+
11
+ namespace Amanotes.Core
12
+ {
13
+ public enum ParamType
14
+ {
15
+ String,
16
+ Integer,
17
+ RealNumber
18
+ }
19
+
20
+ [Serializable]
21
+ public class EventParam
22
+ {
23
+ public string paramName;
24
+ public ParamType type;
25
+ public bool isOptional;
26
+ }
27
+
28
+ [Serializable]
29
+ public class EventQuality
30
+ {
31
+ public string eventName;
32
+ public List<EventParam> lstParam;
33
+
34
+ private Dictionary<string, EventParam> dicParam = null;
35
+
36
+ internal Dictionary<string, EventParam> DicParam
37
+ {
38
+ get
39
+ {
40
+ if (dicParam == null) BuildCache();
41
+ return dicParam;
42
+ }
43
+ }
44
+
45
+ private void BuildCache()
46
+ {
47
+ dicParam = new Dictionary<string, EventParam>();
48
+ foreach (var param in lstParam)
49
+ {
50
+ if (dicParam.ContainsKey(param.paramName)) continue;
51
+
52
+ dicParam.Add(param.paramName, param);
53
+ }
54
+ }
55
+
56
+ private bool IsNumericType(Type type)
57
+ {
58
+ return type == typeof(int) ||
59
+ type == typeof(long) ||
60
+ type == typeof(short) ||
61
+ type == typeof(byte) ||
62
+ type == typeof(double) ||
63
+ type == typeof(float) ||
64
+ type == typeof(decimal) ||
65
+ type == typeof(uint) ||
66
+ type == typeof(ulong) ||
67
+ type == typeof(ushort) ||
68
+ type == typeof(sbyte);
69
+ }
70
+
71
+ private bool IsIntegerType(Type type)
72
+ {
73
+ return type == typeof(sbyte) ||
74
+ type == typeof(byte) ||
75
+ type == typeof(short) ||
76
+ type == typeof(ushort) ||
77
+ type == typeof(int) ||
78
+ type == typeof(uint) ||
79
+ type == typeof(long) ||
80
+ type == typeof(ulong);
81
+ }
82
+
83
+ internal void ValidateParams(string eventName, Dictionary<string, object> eventParams)
84
+ {
85
+ StringBuilder logBuilder = new StringBuilder();
86
+ logBuilder.AppendLine($"Analytics quality violation found for event <{eventName}>");
87
+ bool isValid = true;
88
+ HashSet<string> checkedParameters = new HashSet<string>();
89
+
90
+ foreach (var kvp in DicParam)
91
+ {
92
+ if (!eventParams.ContainsKey(kvp.Key))
93
+ {
94
+ if (!kvp.Value.isOptional)
95
+ {
96
+ isValid = false;
97
+ logBuilder.AppendLine($"[-] {kvp.Key}");
98
+ }
99
+ continue;
100
+ }
101
+
102
+ object paramValue = eventParams[kvp.Key];
103
+
104
+ if (!ValidateParamType(paramValue, kvp.Value.type))
105
+ {
106
+ isValid = false;
107
+ logBuilder.AppendLine($"[X] {kvp.Key}. Input type {paramValue.GetType()}, Expected type: {kvp.Value.type.ToString()}.");
108
+ }
109
+
110
+ checkedParameters.Add(kvp.Key);
111
+ }
112
+
113
+ foreach (var parameter in eventParams)
114
+ {
115
+ if (checkedParameters.Contains(parameter.Key)) continue;
116
+
117
+ isValid = false;
118
+ logBuilder.AppendLine($"[+] {parameter.Key}");
119
+ }
120
+
121
+ if (!isValid) LogWarningOnce(logBuilder.ToString());
122
+ }
123
+
124
+ private bool ValidateParamType(object paramValue, ParamType paramType)
125
+ {
126
+ bool isValidType = false;
127
+
128
+ switch (paramType)
129
+ {
130
+ case ParamType.String:
131
+ isValidType = paramValue is string;
132
+ break;
133
+ case ParamType.Integer:
134
+ isValidType = IsIntegerType(paramValue.GetType());
135
+ break;
136
+ case ParamType.RealNumber:
137
+ isValidType = IsNumericType(paramValue.GetType());
138
+ break;
139
+ default:
140
+ LogWarningOnce($"Unsupported EventParam type: + {paramType}");
141
+ break;
142
+ }
143
+
144
+ return isValidType;
145
+ }
146
+ }
147
+
148
+ [CreateAssetMenu(fileName = "AmaGDKAnalyticsQuality", menuName = "Ama GDK/Analytics Quality", order = 3)]
149
+ [Serializable]
150
+ public class AnalyticQualityAsset : ScriptableObject
151
+ {
152
+ [SerializeField] private List<EventQuality> lstEvent = new List<EventQuality>();
153
+
154
+ private Dictionary<string, EventQuality> dictEvent = null;
155
+ private void BuildCache()
156
+ {
157
+ dictEvent = new Dictionary<string, EventQuality>();
158
+ foreach (var evt in lstEvent)
159
+ {
160
+ if (dictEvent.ContainsKey(evt.eventName))
161
+ {
162
+ LogWarningOnce($"Duplicate event: {evt.eventName}");
163
+ continue;
164
+ }
165
+
166
+ dictEvent.Add(evt.eventName, evt);
167
+ }
168
+ }
169
+
170
+ private Dictionary<string, EventQuality> DictEvent
171
+ {
172
+ get
173
+ {
174
+ if (dictEvent == null) BuildCache();
175
+ return dictEvent;
176
+ }
177
+ }
178
+ #if UNITY_EDITOR
179
+ [ContextMenu("Validate Unique EventNames")]
180
+ private void ValidateUniqueEventNames()
181
+ {
182
+ HashSet<string> eventNames = new HashSet<string>();
183
+
184
+ lstEvent = lstEvent.OrderBy(evt => evt.eventName).ToList();
185
+
186
+ foreach (EventQuality eventQuality in lstEvent)
187
+ {
188
+ if (string.IsNullOrWhiteSpace(eventQuality.eventName)) Debug.LogWarning("Event name cannot be empty or whitespace.");
189
+
190
+ if (!eventNames.Add(eventQuality.eventName)) Debug.LogWarning($"Duplicate event name found: {eventQuality.eventName}. Event names must be unique.");
191
+
192
+ HashSet<string> paramNames = new HashSet<string>();
193
+
194
+ eventQuality.lstParam = eventQuality.lstParam.OrderBy(param => param.paramName).ToList();
195
+
196
+ foreach (var param in eventQuality.lstParam)
197
+ {
198
+ if (string.IsNullOrWhiteSpace(param.paramName)) Debug.LogWarning("Event parameter name cannot be empty or whitespace.");
199
+
200
+ if (!paramNames.Add(param.paramName)) Debug.LogWarning($"Duplicate parameter name found in event {eventQuality.eventName}: {param.paramName}. Parameter names must be unique within an event.");
201
+ }
202
+ }
203
+ EditorUtility.SetDirty(this);
204
+ }
205
+ #endif
206
+
207
+ public void CheckEventQuality(string eventName, Dictionary<string, object> parameters)
208
+ {
209
+ if (!DictEvent.ContainsKey(eventName))
210
+ {
211
+ LogWarningOnce($"Event not found in AnalyticQuality: {eventName}");
212
+ return;
213
+ }
214
+
215
+ DictEvent[eventName].ValidateParams(eventName, parameters);
216
+ }
217
+ }
218
+ }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: fc7588fb4889749c8ba1469aeaf0d42d
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -0,0 +1,59 @@
1
+ using System;
2
+ using System.Linq;
3
+ using UnityEngine;
4
+
5
+ namespace Amanotes.Core.Internal
6
+ {
7
+ [CreateAssetMenu(fileName = "AmaGDKEmbedRemoteConfig", menuName = "Ama GDK/Embed Remote Config", order = 2)]
8
+ [Serializable]
9
+ internal class EmbedRemoteConfigAsset : ScriptableObject
10
+ {
11
+ public int version = 1;
12
+ [SerializeField]
13
+ internal string activeId;
14
+ public bool overrideInEditor;
15
+ public KeyValue[] overrideConfig;
16
+ [SerializeField]
17
+ internal EmbedConfig[] configs;
18
+ private EmbedConfig activeConfig;
19
+ internal EmbedConfig ActiveConfig
20
+ {
21
+ get
22
+ {
23
+ if (activeConfig != null) return activeConfig;
24
+ activeConfig = GetConfigById(activeId);
25
+ return activeConfig;
26
+ }
27
+ }
28
+
29
+ internal EmbedConfig GetConfigById(string id)
30
+ {
31
+ var config = configs?.FirstOrDefault(a => a.id == id);
32
+ if (config == null)
33
+ Logging.LogWarningOnce($"Not found embedded config <{id}>");
34
+ return config;
35
+ }
36
+
37
+ internal void SetActiveConfig(string id)
38
+ {
39
+ activeId = id;
40
+ activeConfig = null;
41
+ }
42
+ }
43
+
44
+ [Serializable]
45
+ internal class EmbedConfig
46
+ {
47
+ public string id;
48
+ public string fetchUrl;
49
+ public KeyValue[] items;
50
+ }
51
+
52
+ [Serializable]
53
+ internal class KeyValue
54
+ {
55
+ public string key;
56
+ public string value;
57
+ }
58
+ }
59
+
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: c9b85abfe1c1641eb8627956b9ef33af
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "com.amanotes.gdk",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "displayName": "AmaGDK",
5
5
  "description": "Amanotes Game Development Kit",
6
6
  "unity": "2020.3",