com.amanotes.gdk 0.2.86 → 0.2.88

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/Editor/AmaGDKEditor.cs +3 -1
  3. package/Editor/Extra/GDKCIBuildCommand.cs +19 -2
  4. package/Editor/Extra/GDKLocalBuild.cs +25 -1
  5. package/Editor/Extra/GDKPostBuildAsset.cs +21 -3
  6. package/Editor/Utils/AmaGDKEditor.ExtractVersion.Max.cs +7 -0
  7. package/Extra/AmaGDKInstaller.unitypackage +0 -0
  8. package/Extra/AutoEventQC.unitypackage +0 -0
  9. package/Extra/CheckDiskSpace.unitypackage +0 -0
  10. package/Extra/Consent.unitypackage +0 -0
  11. package/Extra/CrashlyticHook.unitypackage +0 -0
  12. package/Extra/ForceUpdate.unitypackage +0 -0
  13. package/Extra/LegacyGDKUpdateHelper.unitypackage +0 -0
  14. package/Extra/PostProcessor.unitypackage +0 -0
  15. package/Packages/AmaGDKConfig.unitypackage +0 -0
  16. package/Packages/AmaGDKExample.unitypackage +0 -0
  17. package/Packages/AmaGDKTest.unitypackage +0 -0
  18. package/Packages/AppsFlyerAdapter.PurchaseConnector.unitypackage +0 -0
  19. package/Packages/AppsFlyerAdapter.unitypackage +0 -0
  20. package/Packages/FirebaseAnalyticsAdapter.unitypackage +0 -0
  21. package/Packages/FirebaseRemoteConfigAdapter.unitypackage +0 -0
  22. package/Packages/IronSourceAdapter.AdQuality.unitypackage +0 -0
  23. package/Packages/IronSourceAdapter.unitypackage +0 -0
  24. package/Packages/MaxAdNetworkAdapter.unitypackage +0 -0
  25. package/Packages/RevenueCatAdapter.unitypackage +0 -0
  26. package/Packages/SqliteAnalyticsAdapter.unitypackage +0 -0
  27. package/Runtime/Ad/AdLogic.cs +222 -147
  28. package/Runtime/AmaGDK.Analytics.cs +105 -65
  29. package/Runtime/AmaGDK.RemoteConfig.cs +50 -40
  30. package/Runtime/AmaGDK.cs +1 -1
  31. package/Runtime/AnalyticQualityAsset.cs +69 -38
  32. package/Runtime/AudioToolkit/Plugins/AudioDeviceDetector.cs +31 -17
  33. package/Runtime/Core/GDKSemVer.cs +23 -26
  34. package/Runtime/Fps/AmaFPSVisualizer.cs +48 -26
  35. package/Runtime/Internal/AmaGDK.Utils.cs +90 -46
  36. package/Runtime/Internal/ForceQuitMonitor.cs +43 -31
  37. package/Runtime/Klavar/Attributes/MinMaxAttribute.cs +12 -3
  38. package/Runtime/UI/ScrollView/GDKScrollView.cs +83 -48
  39. package/Runtime/Utils/GDKFileUtils.cs +2 -7
  40. package/Runtime/Utils/GDKUtils.cs +20 -8
  41. package/package.json +1 -1
@@ -458,42 +458,51 @@ namespace Amanotes.Core
458
458
  if (config.normalizeEventName) NormalizeEventName(ref eventNameToSend);
459
459
  config.quality.CheckEventQuality(eventParams.eventName, eventParams.parameters);
460
460
 
461
- var inUseAdapterIDs = config.enableEventStat ? new List<string>(listAdapters.Count) : null;
461
+ List<string> inUseAdapterIDs = config.enableEventStat ? new List<string>(listAdapters.Count) : null;
462
+ bool effective = ProcessAdapters(eventParams, eventNameToSend, inUseAdapterIDs);
463
+ HandleEventResult(effective, config, eventNameToSend, countInSession, totalCount, eventParams, inUseAdapterIDs);
464
+ }
465
+
466
+ private static bool ProcessAdapters(EventParams eventParams, string eventNameToSend, List<string> inUseAdapterIDs)
467
+ {
462
468
  var effective = false;
463
469
  foreach (AnalyticsAdapter m in listAdapters)
464
470
  {
465
- if (eventParams.overrideConfig)
466
- {
467
- // IMPORTANT NOTE:
468
- //
469
- // Once set overrideConfig, we should skip the adapter config completely!!!
470
- //
471
- if (eventParams.IsAdapterForbidden(m.adapterId)) continue;
472
- }
473
- else // Do not remove else
474
- {
475
- if (m.IsForbidden(eventParams.eventName)) continue;
476
- }
471
+ if (ShouldSkipAdapter(eventParams, m)) continue;
477
472
 
478
473
  effective = true;
479
474
  m.LogEvent(eventNameToSend, eventParams.parameters);
480
475
  inUseAdapterIDs?.Add(m.adapterId);
481
476
  }
477
+ return effective;
478
+ }
482
479
 
480
+ private static bool ShouldSkipAdapter(EventParams eventParams, AnalyticsAdapter adapter)
481
+ {
482
+ // IMPORTANT NOTE:
483
+ //
484
+ // Once set overrideConfig, we should skip the adapter config completely!!!
485
+ //
486
+ return eventParams.overrideConfig
487
+ ? eventParams.IsAdapterForbidden(adapter.adapterId)
488
+ : adapter.IsForbidden(eventParams.eventName);
489
+ }
490
+
491
+ private static void HandleEventResult(bool effective, AnalyticsConfig config, string eventNameToSend, int countInSession, int totalCount, EventParams eventParams, List<string> inUseAdapterIDs)
492
+ {
483
493
  if (effective)
484
494
  {
485
495
  if (config.showAnalyticsLog)
486
496
  {
487
497
  ForceLog($"[Analytics] Event <{eventNameToSend}> (in session: {countInSession}, total: {totalCount})\n{JsonUtils.DictionaryToJson(eventParams.parameters, true)}");
488
498
  }
499
+ GDKUtils.SafeInvoke(OnEventSent, eventNameToSend, eventParams.parameters);
500
+ sessionStat?.OnEventSent(eventNameToSend, eventParams.parameters, inUseAdapterIDs);
489
501
  }
490
502
  else
491
503
  {
492
504
  LogError($"Event <{eventParams}> has been skipped (no matching Adapter)\n{JsonUtility.ToJson(eventParams)}");
493
505
  }
494
-
495
- GDKUtils.SafeInvoke(OnEventSent, eventNameToSend, eventParams.parameters);
496
- sessionStat?.OnEventSent(eventNameToSend, eventParams.parameters, inUseAdapterIDs);
497
506
  }
498
507
 
499
508
  internal static void LogEventImmediately(EventParams eventParams)
@@ -568,63 +577,89 @@ namespace Amanotes.Core
568
577
  var hasChanged = false;
569
578
  var trimKey = key.Trim();
570
579
 
571
- foreach (var c in trimKey)
572
- {
573
- var isLetter = IsAlphabet(c);
574
- var isNumber = char.IsDigit(c);
575
-
576
- if (c == '_' || isLetter || isNumber)
577
- {
578
- tempKey.Append(c);
579
- continue;
580
- }
581
-
582
- hasChanged = true;
583
- if (c == ' ' || c == '-')
584
- {
585
- tempKey.Append('_');
586
- }
587
- }
588
-
589
- if (hasChanged && willLog)
590
- {
591
- tempLog.Append(" - Name may only contain alphanumeric characters and underscores");
592
- }
580
+ hasChanged = ProcessCharacters(trimKey, tempKey, tempLog, willLog, hasChanged);
581
+ hasChanged = EnsureValidStart(tempKey, tempLog, willLog, hasChanged, key);
582
+ hasChanged = HandleReservedPrefixes(tempKey, tempLog, willLog, hasChanged, key);
583
+ hasChanged = ValidateLength(trimKey, tempKey, tempLog, willLog, hasChanged, key);
593
584
 
594
- if (tempKey.Length > 0 && !IsAlphabet(tempKey[0]))
595
- {
596
- hasChanged = true;
597
- tempKey.Insert(0, 'E');
598
- if (willLog) tempLog.Append($" - Name must start with a letter: <{key}>");
599
- }
585
+ ApplyChanges(dryRun, warningLog, tempLog, hasChanged, ref key, tempKey);
600
586
 
601
- foreach (var prefix in reservedPrefixes)
602
- {
603
- if (tempKey.Length < prefix.Key.Length || !StringStartsWith(tempKey, prefix.Key)) continue;
604
- hasChanged = true;
605
- tempKey.Remove(0, prefix.Key.Length);
606
- tempKey.Insert(0, prefix.Value);
607
- if (willLog) tempLog.AppendLine($" - <{key}> starts with reserved prefix <{prefix}>");
608
- }
587
+ GDKPool.Return(tempLog);
588
+ GDKPool.Return(tempKey);
589
+ }
590
+ Profiler.EndSample();
591
+ }
609
592
 
610
- if (trimKey.Length > 40)
593
+ private static bool ProcessCharacters(string trimKey, StringBuilder tempKey, StringBuilder tempLog, bool willLog, bool hasChanged)
594
+ {
595
+ foreach (var c in trimKey)
596
+ {
597
+ var isLetter = IsAlphabet(c);
598
+ var isNumber = char.IsDigit(c);
599
+
600
+ if (c == '_' || isLetter || isNumber)
611
601
  {
612
- hasChanged = true;
613
- if (willLog) tempLog.AppendLine($" - Name is too long ({key.Length} > 40): <{key}> ");
614
- tempKey.Length = 40;
602
+ tempKey.Append(c);
603
+ continue;
615
604
  }
616
605
 
617
- if (dryRun)
606
+ hasChanged = true;
607
+ if (c == ' ' || c == '-')
618
608
  {
619
- warningLog.Append(tempLog);
620
- } else {
621
- if (hasChanged) key = tempKey.ToString();
609
+ tempKey.Append('_');
622
610
  }
623
-
624
- GDKPool.Return(tempLog);
625
- GDKPool.Return(tempKey);
626
611
  }
627
- Profiler.EndSample();
612
+
613
+ if (hasChanged && willLog)
614
+ {
615
+ tempLog.Append(" - Name may only contain alphanumeric characters and underscores");
616
+ }
617
+ return hasChanged;
618
+ }
619
+
620
+ private static bool EnsureValidStart(StringBuilder tempKey, StringBuilder tempLog, bool willLog, bool hasChanged, string key)
621
+ {
622
+ if (tempKey.Length > 0 && !IsAlphabet(tempKey[0]))
623
+ {
624
+ hasChanged = true;
625
+ tempKey.Insert(0, 'E');
626
+ if (willLog) tempLog.Append($" - Name must start with a letter: <{key}>");
627
+ }
628
+ return hasChanged;
629
+ }
630
+
631
+ private static bool HandleReservedPrefixes(StringBuilder tempKey, StringBuilder tempLog, bool willLog, bool hasChanged, string key)
632
+ {
633
+ foreach (var prefix in reservedPrefixes)
634
+ {
635
+ if (tempKey.Length < prefix.Key.Length || !StringStartsWith(tempKey, prefix.Key)) continue;
636
+ hasChanged = true;
637
+ tempKey.Remove(0, prefix.Key.Length);
638
+ tempKey.Insert(0, prefix.Value);
639
+ if (willLog) tempLog.AppendLine($" - <{key}> starts with reserved prefix <{prefix}>");
640
+ }
641
+ return hasChanged;
642
+ }
643
+
644
+ private static bool ValidateLength(string trimKey, StringBuilder tempKey, StringBuilder tempLog, bool willLog, bool hasChanged, string key)
645
+ {
646
+ if (trimKey.Length > 40)
647
+ {
648
+ hasChanged = true;
649
+ if (willLog) tempLog.AppendLine($" - Name is too long ({key.Length} > 40): <{key}> ");
650
+ tempKey.Length = 40;
651
+ }
652
+ return hasChanged;
653
+ }
654
+
655
+ private static void ApplyChanges(bool dryRun, StringBuilder warningLog, StringBuilder tempLog, bool hasChanged, ref string key, StringBuilder tempKey)
656
+ {
657
+ if (dryRun)
658
+ {
659
+ warningLog.Append(tempLog);
660
+ } else {
661
+ if (hasChanged) key = tempKey.ToString();
662
+ }
628
663
  }
629
664
 
630
665
  private static bool StringStartsWith(StringBuilder stringBuilder, string prefix)
@@ -1069,11 +1104,16 @@ namespace Amanotes.Core
1069
1104
  h.Add(mId);
1070
1105
  }
1071
1106
 
1107
+ eventData.overrideConfig = true;
1072
1108
  if (h.Count > 0)
1073
1109
  {
1074
- eventData.overrideConfig = true;
1075
1110
  eventData.allowAdapterIds ??= h;
1076
1111
  }
1112
+ else
1113
+ {
1114
+ // No valid adapters found, create empty set to ensure event is skipped
1115
+ eventData.allowAdapterIds ??= GDKPool.GetHashSet();
1116
+ }
1077
1117
 
1078
1118
  return ap;
1079
1119
  }
@@ -259,43 +259,9 @@ namespace Amanotes.Core
259
259
  try
260
260
  {
261
261
  DictConfig.Clear();
262
- using (StreamReader sr = new StreamReader(GDKFileUtils.GetPath(fileName)))
263
- {
264
- //Check new app update
265
- string firstLine = sr.ReadLine();
266
- if (firstLine == null)
267
- {
268
- NextState(false);
269
- return;
270
- }
271
- if (ShouldClearCache(firstLine))
272
- {
273
- GDKFileUtils.Delete(fileName);
274
- string oldVersion = firstLine.StartsWith(VERSION_PREFIX) ? firstLine : "<none>";
275
- Log($"[RemoteConfig] Cached remote config cleared due to embed version changed:\n{oldVersion} -> {EmbedVersion}");
276
- NextState(false);
277
- return;
278
- }
279
-
280
- //Read key-value
281
- string line;
282
- int lineNo = 1;
283
- while ((line = sr.ReadLine()) != null)
284
- {
285
- lineNo++;
286
- var idx = line.IndexOf('\t');
287
- if (idx == -1)
288
- {
289
- LogWarning($"[RemoteConfig] Cannot get (key, value) at line {lineNo} of cache. Content:\n<{line}>");
290
- continue;
291
- }
292
- string key = line.Substring(0, idx);
293
- string value = line.Substring(idx + 1);
294
- DictConfig.Add(key, Decode(value));
295
- }
296
- }
297
- Source = RemoteConfigSource.LocalCache;
298
- Log("[RemoteConfig] Load from cache");
262
+ bool success = ReadCacheFile();
263
+ Profiler.EndSample();
264
+ NextState(success);
299
265
  }
300
266
  catch (Exception ex)
301
267
  {
@@ -303,11 +269,55 @@ namespace Amanotes.Core
303
269
  _cacheExisted = false;
304
270
  Profiler.EndSample();
305
271
  NextState(false);
306
- return;
307
272
  }
273
+ }
308
274
 
309
- Profiler.EndSample();
310
- NextState(true);
275
+ static bool ReadCacheFile()
276
+ {
277
+ using (StreamReader sr = new StreamReader(GDKFileUtils.GetPath(fileName)))
278
+ {
279
+ string firstLine = sr.ReadLine();
280
+ if (firstLine == null) return false;
281
+
282
+ if (!ValidateCacheVersion(firstLine)) return false;
283
+
284
+ ReadConfigKeyValues(sr);
285
+ }
286
+
287
+ Source = RemoteConfigSource.LocalCache;
288
+ Log("[RemoteConfig] Load from cache");
289
+ return true;
290
+ }
291
+
292
+ static bool ValidateCacheVersion(string firstLine)
293
+ {
294
+ if (ShouldClearCache(firstLine))
295
+ {
296
+ GDKFileUtils.Delete(fileName);
297
+ string oldVersion = firstLine.StartsWith(VERSION_PREFIX) ? firstLine : "<none>";
298
+ Log($"[RemoteConfig] Cached remote config cleared due to embed version changed:\n{oldVersion} -> {EmbedVersion}");
299
+ return false;
300
+ }
301
+ return true;
302
+ }
303
+
304
+ static void ReadConfigKeyValues(StreamReader sr)
305
+ {
306
+ string line;
307
+ int lineNo = 1;
308
+ while ((line = sr.ReadLine()) != null)
309
+ {
310
+ lineNo++;
311
+ var idx = line.IndexOf('\t');
312
+ if (idx == -1)
313
+ {
314
+ LogWarning($"[RemoteConfig] Cannot get (key, value) at line {lineNo} of cache. Content:\n<{line}>");
315
+ continue;
316
+ }
317
+ string key = line.Substring(0, idx);
318
+ string value = line.Substring(idx + 1);
319
+ DictConfig.Add(key, Decode(value));
320
+ }
311
321
  }
312
322
 
313
323
  static void LoadEmbedConfig()
package/Runtime/AmaGDK.cs CHANGED
@@ -27,7 +27,7 @@ namespace Amanotes.Core
27
27
  {
28
28
  public partial class AmaGDK : MonoBehaviour
29
29
  {
30
- public const string VERSION = "0.2.86";
30
+ public const string VERSION = "0.2.88";
31
31
 
32
32
  internal static Status _status = Status.None;
33
33
  internal static ConfigAsset _config = null;
@@ -54,31 +54,27 @@ namespace Amanotes.Core
54
54
  }
55
55
  }
56
56
 
57
+ private static readonly HashSet<Type> NumericTypes = new HashSet<Type>
58
+ {
59
+ typeof(int), typeof(long), typeof(short), typeof(byte),
60
+ typeof(double), typeof(float), typeof(decimal),
61
+ typeof(uint), typeof(ulong), typeof(ushort), typeof(sbyte)
62
+ };
63
+
57
64
  private bool IsNumericType(Type type)
58
65
  {
59
- return type == typeof(int) ||
60
- type == typeof(long) ||
61
- type == typeof(short) ||
62
- type == typeof(byte) ||
63
- type == typeof(double) ||
64
- type == typeof(float) ||
65
- type == typeof(decimal) ||
66
- type == typeof(uint) ||
67
- type == typeof(ulong) ||
68
- type == typeof(ushort) ||
69
- type == typeof(sbyte);
66
+ return NumericTypes.Contains(type);
70
67
  }
71
68
 
69
+ private static readonly HashSet<Type> IntegerTypes = new HashSet<Type>
70
+ {
71
+ typeof(sbyte), typeof(byte), typeof(short), typeof(ushort),
72
+ typeof(int), typeof(uint), typeof(long), typeof(ulong)
73
+ };
74
+
72
75
  private bool IsIntegerType(Type type)
73
76
  {
74
- return type == typeof(sbyte) ||
75
- type == typeof(byte) ||
76
- type == typeof(short) ||
77
- type == typeof(ushort) ||
78
- type == typeof(int) ||
79
- type == typeof(uint) ||
80
- type == typeof(long) ||
81
- type == typeof(ulong);
77
+ return IntegerTypes.Contains(type);
82
78
  }
83
79
 
84
80
  internal void ValidateParams(string eventName, Dictionary<string, object> eventParams)
@@ -89,6 +85,19 @@ namespace Amanotes.Core
89
85
  var isValid = true;
90
86
  HashSet<string> checkedParameters = GDKPool.GetHashSet();
91
87
 
88
+ isValid &= ValidateRequiredParams(eventParams, logBuilder, checkedParameters);
89
+ isValid &= ValidateExtraParams(eventParams, logBuilder, checkedParameters);
90
+
91
+ if (!isValid) LogWarningOnce(logBuilder.ToString());
92
+
93
+ GDKPool.Return(logBuilder);
94
+ GDKPool.Return(checkedParameters);
95
+ }
96
+
97
+ private bool ValidateRequiredParams(Dictionary<string, object> eventParams, StringBuilder logBuilder, HashSet<string> checkedParameters)
98
+ {
99
+ bool isValid = true;
100
+
92
101
  foreach (var kvp in DicParam)
93
102
  {
94
103
  if (!eventParams.TryGetValue(kvp.Key, out object paramValue))
@@ -109,7 +118,14 @@ namespace Amanotes.Core
109
118
 
110
119
  checkedParameters.Add(kvp.Key);
111
120
  }
121
+
122
+ return isValid;
123
+ }
112
124
 
125
+ private bool ValidateExtraParams(Dictionary<string, object> eventParams, StringBuilder logBuilder, HashSet<string> checkedParameters)
126
+ {
127
+ bool isValid = true;
128
+
113
129
  foreach (var parameter in eventParams)
114
130
  {
115
131
  if (checkedParameters.Contains(parameter.Key)) continue;
@@ -118,10 +134,7 @@ namespace Amanotes.Core
118
134
  logBuilder.AppendLine($"[+] {parameter.Key}");
119
135
  }
120
136
 
121
- if (!isValid) LogWarningOnce(logBuilder.ToString());
122
-
123
- GDKPool.Return(logBuilder);
124
- GDKPool.Return(checkedParameters);
137
+ return isValid;
125
138
  }
126
139
 
127
140
  private bool ValidateParamType(object paramValue, ParamType paramType)
@@ -188,26 +201,44 @@ namespace Amanotes.Core
188
201
 
189
202
  foreach (EventQuality eventQuality in lstEvent)
190
203
  {
191
- if (string.IsNullOrWhiteSpace(eventQuality.eventName)) Debug.LogWarning("Event name cannot be empty or whitespace.");
204
+ ValidateEventName(eventQuality.eventName, eventNames);
205
+ ValidateEventParameters(eventQuality);
206
+ }
207
+
208
+ GDKPool.Return(eventNames);
209
+ EditorUtility.SetDirty(this);
210
+ }
192
211
 
193
- if (!eventNames.Add(eventQuality.eventName)) Debug.LogWarning($"Duplicate event name found: {eventQuality.eventName}. Event names must be unique.");
212
+ private void ValidateEventName(string eventName, HashSet<string> eventNames)
213
+ {
214
+ if (string.IsNullOrWhiteSpace(eventName))
215
+ Debug.LogWarning("Event name cannot be empty or whitespace.");
194
216
 
195
- HashSet<string> paramNames = GDKPool.GetHashSet();
196
-
197
- eventQuality.lstParam = eventQuality.lstParam.OrderBy(param => param.paramName).ToList();
198
-
199
- foreach (var param in eventQuality.lstParam)
200
- {
201
- if (string.IsNullOrWhiteSpace(param.paramName)) Debug.LogWarning("Event parameter name cannot be empty or whitespace.");
217
+ if (!eventNames.Add(eventName))
218
+ Debug.LogWarning($"Duplicate event name found: {eventName}. Event names must be unique.");
219
+ }
202
220
 
203
- 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.");
204
- }
205
-
206
- GDKPool.Return(paramNames);
221
+ private void ValidateEventParameters(EventQuality eventQuality)
222
+ {
223
+ HashSet<string> paramNames = GDKPool.GetHashSet();
224
+
225
+ eventQuality.lstParam = eventQuality.lstParam.OrderBy(param => param.paramName).ToList();
226
+
227
+ foreach (var param in eventQuality.lstParam)
228
+ {
229
+ ValidateParameterName(param.paramName, eventQuality.eventName, paramNames);
207
230
  }
208
231
 
209
- GDKPool.Return(eventNames);
210
- EditorUtility.SetDirty(this);
232
+ GDKPool.Return(paramNames);
233
+ }
234
+
235
+ private void ValidateParameterName(string paramName, string eventName, HashSet<string> paramNames)
236
+ {
237
+ if (string.IsNullOrWhiteSpace(paramName))
238
+ Debug.LogWarning("Event parameter name cannot be empty or whitespace.");
239
+
240
+ if (!paramNames.Add(paramName))
241
+ Debug.LogWarning($"Duplicate parameter name found in event {eventName}: {paramName}. Parameter names must be unique within an event.");
211
242
  }
212
243
  #endif
213
244
 
@@ -162,10 +162,7 @@ namespace Amanotes.Core
162
162
 
163
163
  private static string RemoveSurrogatePairs(string str, string replacementCharacter = "?")
164
164
  {
165
- if (str == null)
166
- {
167
- return null;
168
- }
165
+ if (str == null) return null;
169
166
 
170
167
  StringBuilder sb = null;
171
168
  for (var i = 0; i < str.Length; i++)
@@ -174,19 +171,8 @@ namespace Amanotes.Core
174
171
 
175
172
  if (char.IsSurrogate(ch))
176
173
  {
177
- if (sb == null)
178
- {
179
- sb = GDKPool.GetStringBuilder();
180
- sb.Append(str, 0, i);
181
- }
182
-
183
- sb.Append(replacementCharacter);
184
-
185
- // If there is a high+low surrogate, skip the low surrogate
186
- if (i + 1 < str.Length && char.IsHighSurrogate(ch) && char.IsLowSurrogate(str[i + 1]))
187
- {
188
- i++;
189
- }
174
+ sb = HandleSurrogateCharacter(str, i, sb, replacementCharacter, out int skipCount);
175
+ i += skipCount;
190
176
  }
191
177
  else
192
178
  {
@@ -196,6 +182,34 @@ namespace Amanotes.Core
196
182
 
197
183
  return sb == null ? str : GDKPool.ReturnString(sb);
198
184
  }
185
+
186
+ private static StringBuilder HandleSurrogateCharacter(string str, int index, StringBuilder sb, string replacementCharacter, out int skipCount)
187
+ {
188
+ skipCount = 0;
189
+
190
+ if (sb == null)
191
+ {
192
+ sb = GDKPool.GetStringBuilder();
193
+ sb.Append(str, 0, index);
194
+ }
195
+
196
+ sb.Append(replacementCharacter);
197
+
198
+ // If there is a high+low surrogate, skip the low surrogate
199
+ if (ShouldSkipLowSurrogate(str, index))
200
+ {
201
+ skipCount = 1;
202
+ }
203
+
204
+ return sb;
205
+ }
206
+
207
+ private static bool ShouldSkipLowSurrogate(string str, int index)
208
+ {
209
+ return index + 1 < str.Length &&
210
+ char.IsHighSurrogate(str[index]) &&
211
+ char.IsLowSurrogate(str[index + 1]);
212
+ }
199
213
  }
200
214
 
201
215
  internal enum AudioDeviceType
@@ -32,38 +32,35 @@ namespace Amanotes.Core.Internal
32
32
  // Constructor for string-based version initialization
33
33
  public GDKSemVer(string version)
34
34
  {
35
- // Default values
36
- major = 0;
37
- minor = 0;
38
- patch = 0;
39
- extra = -1;
35
+ var parsed = ParseVersionString(version);
36
+ major = parsed.major;
37
+ minor = parsed.minor;
38
+ patch = parsed.patch;
39
+ extra = parsed.extra;
40
+ }
40
41
 
41
- if (string.IsNullOrEmpty(version)) return;
42
+ private static (int major, int minor, int patch, int extra) ParseVersionString(string version)
43
+ {
44
+ if (string.IsNullOrEmpty(version)) return (0, 0, 0, -1);
42
45
 
43
46
  Profiler.BeginSample("GDKSemVer()");
47
+ version = version.Trim();
48
+ Match match = VERSION_PATTERN.Match(version);
49
+
50
+ if (!match.Success)
44
51
  {
45
- version = version.Trim();
46
- Match match = VERSION_PATTERN.Match(version);
47
- if (!match.Success)
48
- {
49
- LogWarning($"[GDKSemVer] Invalid version string: [{version}]");
50
- Profiler.EndSample();
51
- return;
52
- }
53
-
54
- // Parse the version components using TryParse for safety
55
- major = int.TryParse(match.Groups[1].Value, out int parsedMajor) ? parsedMajor : 0;
56
- minor = int.TryParse(match.Groups[2].Value, out int parsedMinor) ? parsedMinor : 0;
57
- if (match.Groups.Count > 3 && !int.TryParse(match.Groups[3].Value, out patch))
58
- {
59
- patch = 0;
60
- }
61
- if (match.Groups.Count > 4 && !int.TryParse(match.Groups[4].Value, out extra))
62
- {
63
- extra = -1;
64
- }
52
+ LogWarning($"[GDKSemVer] Invalid version string: [{version}]");
53
+ Profiler.EndSample();
54
+ return (0, 0, 0, -1);
65
55
  }
56
+
57
+ var major = int.TryParse(match.Groups[1].Value, out int parsedMajor) ? parsedMajor : 0;
58
+ var minor = int.TryParse(match.Groups[2].Value, out int parsedMinor) ? parsedMinor : 0;
59
+ var patch = match.Groups.Count > 3 && int.TryParse(match.Groups[3].Value, out int parsedPatch) ? parsedPatch : 0;
60
+ var extra = match.Groups.Count > 4 && int.TryParse(match.Groups[4].Value, out int parsedExtra) ? parsedExtra : -1;
61
+
66
62
  Profiler.EndSample();
63
+ return (major, minor, patch, extra);
67
64
  }
68
65
 
69
66
  // Property to check if the version is valid