302 lines
13 KiB
C#
302 lines
13 KiB
C#
#nullable enable
|
|
using Cysharp.Threading.Tasks;
|
|
using Newtonsoft.Json.Linq;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using UnityEngine;
|
|
using UVC.Data.Core;
|
|
using UVC.Extention;
|
|
|
|
namespace UVC.Data
|
|
{
|
|
public class UserSetting : OrderedDictionary<string, object>
|
|
{
|
|
// 사용자 데이터를 저장하는 컬렉션입니다.
|
|
private static Dictionary<string, UserSetting> _userDatas = new Dictionary<string, UserSetting>();
|
|
public static IReadOnlyDictionary<string, UserSetting> UserDatas => _userDatas;
|
|
|
|
/// <summary>
|
|
/// 사용자 정의 키를 지정된 세팅에 연결합니다.
|
|
/// </summary>
|
|
/// <remarks>동일한 키를 가진 세팅가 이미 있는 경우 새
|
|
/// 마스크로 대체됩니다.</remarks>
|
|
/// <param name="key">세팅의 고유 식별자입니다. <see langword="null"/>이거나 비어 있을 수 없습니다.</param>
|
|
/// <param name="setting">지정된 키와 연결할 <see cref="UserSetting"/> 객체입니다. <see langword="null"/>일 수 없습니다.</param>
|
|
public static void AddSetting(string key, UserSetting setting)
|
|
{
|
|
if (string.IsNullOrEmpty(key) || setting == null) return;
|
|
_userDatas[key] = setting;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 키와 연관된 사용자 마스크를 제거합니다.
|
|
/// </summary>
|
|
/// <remarks> <paramref name="key"/>가 null이거나 비어 있으면 메서드는 아무 작업도 수행하지 않습니다.
|
|
///</remarks>
|
|
/// <param name="key">제거할 사용자 마스크를 식별하는 키입니다. null이거나 비어 있으면 안 됩니다.</param>
|
|
public static void RemoveSetting(string key)
|
|
{
|
|
if (string.IsNullOrEmpty(key)) return;
|
|
_userDatas.Remove(key);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 키와 연관된 <see cref="UserSetting"/>를 검색합니다.
|
|
/// </summary>
|
|
/// <param name="key">연관된 <see cref="UserSetting"/>를 찾는 데 사용되는 키입니다. null이거나 비어 있을 수 없습니다.</param>
|
|
/// <returns>지정된 키와 연관된 <see cref="UserSetting"/> 또는 키를 찾을 수 없거나 null이거나 비어 있는 경우 <see langword="null"/>을 반환합니다.
|
|
///</returns>
|
|
public static UserSetting? Get(string key)
|
|
{
|
|
if (string.IsNullOrEmpty(key)) return null;
|
|
_userDatas.TryGetValue(key, out var setting);
|
|
return setting;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 내부 컬렉션에서 모든 사용자 세팅을 지웁니다.
|
|
/// </summary>
|
|
/// <remarks>이 메서드는 사용자 세팅 컬렉션에서 모든 항목을 제거하고 빈 상태로 재설정합니다.
|
|
/// 일반적으로 새 마스크를 적용하기 전에 이전에 적용된 마스크를 지우는 데 사용됩니다.
|
|
///</remarks>
|
|
public static void ClearSetting()
|
|
{
|
|
_userDatas.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 애플리케이션의 영구 데이터 디렉터리(AppData/LocalLow/Company Name/Product Name)에서 사용자 마스크 데이터를 로드합니다.
|
|
/// </summary>
|
|
/// <remarks>이 메서드는 애플리케이션의 영구 데이터 경로에 있는 "user" 하위 디렉터리에서 JSON 파일을 검색합니다.
|
|
/// 각 JSON 파일을 읽고 그 내용을 사용하여 <see cref="UserSetting"/>
|
|
/// 객체를 생성한 후 애플리케이션의 사용자 마스크 컬렉션에 추가합니다. 이 메서드는 메인 스레드를 차단하지 않도록 백그라운드
|
|
/// 스레드에서 실행됩니다.</remarks>
|
|
/// <returns></returns>
|
|
public static async UniTask LoadFromAppData()
|
|
{
|
|
//C:\Users\[user name]\AppData\LocalLow\[company name]\[product name]
|
|
string persistentDataPath = UnityEngine.Application.persistentDataPath;
|
|
#if UNITY_WEBGL && !UNITY_EDITOR
|
|
// WebGL: 스레드 사용 불가. 메인 스레드에서 파일 작업 수행하되 프레임 양보로 스톨 방지.
|
|
string folderPath = System.IO.Path.Combine(persistentDataPath, "user");
|
|
DirectoryInfo directory = new DirectoryInfo(folderPath);
|
|
if (!directory.Exists) return;
|
|
|
|
FileInfo[] files = directory.GetFiles("*.json");
|
|
foreach (FileInfo file in files)
|
|
{
|
|
// 프레임 양보
|
|
await UniTask.Yield();
|
|
|
|
string filePath = file.FullName;
|
|
string fileName = System.IO.Path.GetFileNameWithoutExtension(filePath);
|
|
if (string.IsNullOrEmpty(fileName)) continue;
|
|
string jsonString = System.IO.File.ReadAllText(filePath);
|
|
AddSetting(fileName, new UserSetting(jsonString));
|
|
}
|
|
#else
|
|
await UniTask.RunOnThreadPool(() =>
|
|
{
|
|
string folderPath = System.IO.Path.Combine(persistentDataPath, "user");
|
|
DirectoryInfo directory = new DirectoryInfo(folderPath);
|
|
if (!directory.Exists) return;
|
|
FileInfo[] files = directory.GetFiles("*.json");
|
|
foreach (FileInfo file in files)
|
|
{
|
|
string filePath = file.FullName;
|
|
string fileName = System.IO.Path.GetFileNameWithoutExtension(filePath);
|
|
if (string.IsNullOrEmpty(fileName)) continue;
|
|
string jsonString = System.IO.File.ReadAllText(filePath);
|
|
AddSetting(fileName, new UserSetting(jsonString));
|
|
}
|
|
});
|
|
#endif
|
|
}
|
|
|
|
/// <summary>
|
|
/// 사용자 세팅을 애플리케이션의 영구 데이터 경로(AppData/LocalLow/Company Name/Product Name)에 저장합니다.
|
|
/// </summary>
|
|
/// <remarks>이 메서드는 모든 사용자 세팅을 반복하며 각 마스크를 JSON 파일로 저장합니다.
|
|
/// 애플리케이션의 영구 데이터 경로에 있는 "user" 하위 디렉터리에 디렉터리가 없으면
|
|
/// 생성됩니다. 각 파일의 이름은 해당 키와 ".json" 확장자를 사용하여 지정됩니다. 저장 과정에서 발생하는 모든 오류는
|
|
/// Unity 콘솔에 기록됩니다.</remarks>
|
|
/// <returns></returns>
|
|
public static async UniTask SaveToAppData()
|
|
{
|
|
|
|
string persistentDataPath = UnityEngine.Application.persistentDataPath;
|
|
#if UNITY_WEBGL && !UNITY_EDITOR
|
|
foreach (var kv in _userDatas)
|
|
{
|
|
await UniTask.Yield(); // 긴 루프 스톨 방지
|
|
|
|
string key = kv.Key;
|
|
if (string.IsNullOrEmpty(key)) continue;
|
|
if (!_userDatas.TryGetValue(key, out var setting)) continue;
|
|
|
|
try
|
|
{
|
|
string folderPath = Path.Combine(persistentDataPath, "user");
|
|
if (!Directory.Exists(folderPath)) Directory.CreateDirectory(folderPath);
|
|
string filePath = Path.Combine(folderPath, $"{key}.json");
|
|
File.WriteAllText(filePath, setting.ToJsonString());
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogError($"SaveToAppData Error(WebGL): {ex.Message}");
|
|
}
|
|
}
|
|
#else
|
|
await UniTask.RunOnThreadPool(() =>
|
|
{
|
|
foreach (var keyValue in _userDatas)
|
|
{
|
|
string key = keyValue.Key;
|
|
if (string.IsNullOrEmpty(key)) continue;
|
|
if (!_userDatas.TryGetValue(key, out var setting)) return;
|
|
try
|
|
{
|
|
string folderPath = System.IO.Path.Combine(persistentDataPath, "user");
|
|
if (!Directory.Exists(folderPath)) Directory.CreateDirectory(folderPath);
|
|
string filePath = System.IO.Path.Combine(folderPath, $"{key}.json");
|
|
string jsonString = setting.ToJsonString();
|
|
System.IO.File.WriteAllText(filePath, jsonString);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogError($"SaveToAppData Error: {ex.Message}");
|
|
}
|
|
}
|
|
});
|
|
#endif
|
|
}
|
|
|
|
public static UserSetting? FromDataObject(DataObject dataObject)
|
|
{
|
|
if (dataObject == null) return null;
|
|
UserSetting setting = new UserSetting();
|
|
foreach (var item in dataObject)
|
|
{
|
|
setting[item.Key] = item.Key;
|
|
}
|
|
return setting;
|
|
}
|
|
|
|
public UserSetting() { }
|
|
|
|
public UserSetting(string jsonString)
|
|
{
|
|
if (string.IsNullOrEmpty(jsonString)) return;
|
|
JObject jObj = JObject.Parse(jsonString);
|
|
foreach (var property in jObj.Properties())
|
|
{
|
|
this[property.Name] = ConvertJTokenToObject(property);
|
|
}
|
|
}
|
|
|
|
public UserSetting(JObject jObj)
|
|
{
|
|
if (jObj == null) return;
|
|
foreach (var property in jObj.Properties())
|
|
{
|
|
this[property.Name] = ConvertJTokenToObject(property.Value);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// JToken을 실제 객체로 변환하는 헬퍼 메서드
|
|
/// </summary>
|
|
/// <param name="token">변환할 JToken 객체</param>
|
|
/// <returns>변환된 .NET 객체</returns>
|
|
/// <remarks>
|
|
/// 이 메서드는 JToken의 타입에 따라 적절한 .NET 타입의 객체로 변환합니다.
|
|
/// 객체는 DataObject로, 배열은 DataArray로 변환되며, 기본 타입은 해당하는 .NET 타입으로 변환됩니다.
|
|
/// </remarks>
|
|
private object ConvertJTokenToObject(JToken token)
|
|
{
|
|
if (token == null) return null;
|
|
|
|
switch (token.Type)
|
|
{
|
|
case JTokenType.Property:
|
|
JProperty prop = (JProperty)token;
|
|
return ConvertJTokenToObject(prop.Value);
|
|
case JTokenType.Object:
|
|
return new UserSetting((JObject)token);
|
|
case JTokenType.Array:
|
|
JArray array = (JArray)token;
|
|
List<UserSetting> settings = new List<UserSetting>();
|
|
if (array.All(item => item.Type == JTokenType.Object))
|
|
{
|
|
foreach (var item in array)
|
|
{
|
|
settings.Add(new UserSetting((JObject)item));
|
|
}
|
|
}
|
|
return settings;
|
|
case JTokenType.Integer:
|
|
return token.ToObject<int>();
|
|
case JTokenType.Float:
|
|
return token.ToObject<double>();
|
|
case JTokenType.String:
|
|
return token.ToObject<string>();
|
|
case JTokenType.Boolean:
|
|
return token.ToObject<bool>();
|
|
case JTokenType.Date:
|
|
return token.ToObject<DateTime>();
|
|
case JTokenType.Null:
|
|
return null;
|
|
default:
|
|
return token.ToObject<object>();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// JObject 타입과의 호환성을 위한 메서드입니다.
|
|
/// Dictionary에 저장된 모든 속성을 JObject 형식으로 반환합니다.
|
|
/// </summary>
|
|
/// <returns>현재 UserSetting의 내용을 담은 JObject</returns>
|
|
public JObject ToJObject()
|
|
{
|
|
JObject result = new JObject();
|
|
foreach (var pair in this)
|
|
{
|
|
if (pair.Value is UserSetting setting)
|
|
{
|
|
result[pair.Key] = ((UserSetting)pair.Value).ToJObject();
|
|
}
|
|
else
|
|
{
|
|
result[pair.Key] = JToken.FromObject(pair.Value);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// JObject에서 속성을 가져오기 위한 확장 메서드입니다.
|
|
/// </summary>
|
|
/// <param name="jObj">속성을 추출할 JObject</param>
|
|
/// <returns>JObject의 모든 속성</returns>
|
|
private IEnumerable<KeyValuePair<string, JToken>> GetProperties(JObject jObj)
|
|
{
|
|
foreach (var property in jObj.Properties())
|
|
{
|
|
yield return new KeyValuePair<string, JToken>(property.Name, property.Value);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Json 문자열로 변환합니다.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public string ToJsonString()
|
|
{
|
|
return ToJObject().ToString(Newtonsoft.Json.Formatting.Indented);
|
|
}
|
|
|
|
}
|
|
}
|