168 lines
5.6 KiB
C#
168 lines
5.6 KiB
C#
using UnityEngine;
|
|
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
|
|
public static class PathIndexer
|
|
{
|
|
private sealed class NodeMeta
|
|
{
|
|
public object Parent;
|
|
public string Segment; // 예: "production_system", "asrs[0]"
|
|
}
|
|
|
|
// 모델 클래스를 수정하지 않기 위해 외부 메타 테이블 사용
|
|
private static readonly ConditionalWeakTable<object, NodeMeta> _meta = new();
|
|
|
|
/// <summary>
|
|
/// 루트부터 전체 그래프를 순회하며 (Parent, Segment) 메타를 구축합니다.
|
|
/// 로직 데이터 로딩/디시리얼라이즈 직후 1회 호출을 권장합니다.
|
|
/// </summary>
|
|
public static void Build(object root)
|
|
{
|
|
if (root == null) throw new ArgumentNullException(nameof(root));
|
|
var visited = new HashSet<object>(ReferenceEqualityComparer.Instance);
|
|
Visit(root, parent: null, segment: null, visited);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 특정 객체 노드의 경로(노드까지)를 반환합니다. 예: "production_system.asrs[0]"
|
|
/// </summary>
|
|
public static string GetNodePath(object node, string rootPrefix = null)
|
|
{
|
|
if (node == null) throw new ArgumentNullException(nameof(node));
|
|
|
|
var segments = new List<string>();
|
|
var cur = node;
|
|
|
|
while (cur != null && _meta.TryGetValue(cur, out var m))
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(m.Segment))
|
|
segments.Add(m.Segment);
|
|
|
|
cur = m.Parent;
|
|
}
|
|
|
|
segments.Reverse();
|
|
|
|
var path = string.Join(".", segments.Where(s => !string.IsNullOrWhiteSpace(s)));
|
|
if (!string.IsNullOrWhiteSpace(rootPrefix))
|
|
{
|
|
// 서버가 production_system[0] 같은 추가 프리픽스를 요구하면 여기서 붙이세요.
|
|
// 예: rootPrefix = "production_system[0]" 또는 "data" 등
|
|
path = string.IsNullOrWhiteSpace(path) ? rootPrefix : $"{rootPrefix}.{path}";
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
/// <summary>
|
|
/// (변경된 객체) + (변경된 프로퍼티명)으로 최종 patch path를 만듭니다.
|
|
/// 예: GetPropertyPath(asrs0, "name") => "production_system.asrs[0].name"
|
|
/// </summary>
|
|
public static string GetPropertyPath(object node, string propertyName, string rootPrefix = null)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(propertyName))
|
|
throw new ArgumentException("propertyName is required.", nameof(propertyName));
|
|
|
|
var basePath = GetNodePath(node, rootPrefix);
|
|
return string.IsNullOrWhiteSpace(basePath) ? propertyName : $"{basePath}.{propertyName}";
|
|
}
|
|
|
|
// ----------------- 내부 구현 -----------------
|
|
|
|
private static void Visit(object node, object parent, string segment, HashSet<object> visited)
|
|
{
|
|
if (node == null) return;
|
|
|
|
var t = node.GetType();
|
|
if (IsTerminalType(t)) return;
|
|
if (!visited.Add(node)) return;
|
|
|
|
AttachMeta(node, parent, segment);
|
|
|
|
// 1) 프로퍼티 순회(기존)
|
|
var props = t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
|
.Where(p => p.CanRead && p.GetIndexParameters().Length == 0);
|
|
|
|
foreach (var p in props)
|
|
{
|
|
object child;
|
|
try { child = p.GetValue(node); }
|
|
catch { continue; }
|
|
|
|
VisitChild(node, p.Name, child, visited);
|
|
}
|
|
|
|
// 2) 필드 순회(추가)
|
|
var fields = t.GetFields(BindingFlags.Public | BindingFlags.Instance)
|
|
.Where(f => !f.IsStatic);
|
|
|
|
foreach (var f in fields)
|
|
{
|
|
object child;
|
|
try { child = f.GetValue(node); }
|
|
catch { continue; }
|
|
|
|
VisitChild(node, f.Name, child, visited);
|
|
}
|
|
}
|
|
|
|
private static void VisitChild(object parentNode, string memberName, object child, HashSet<object> visited)
|
|
{
|
|
if (child == null) return;
|
|
|
|
var ct = child.GetType();
|
|
if (IsTerminalType(ct)) return;
|
|
|
|
// IList면 원소에 memberName[i] 형태로 세그먼트 부여
|
|
if (child is IList list && child is not string)
|
|
{
|
|
for (int i = 0; i < list.Count; i++)
|
|
{
|
|
var elem = list[i];
|
|
if (elem == null) continue;
|
|
|
|
var et = elem.GetType();
|
|
if (IsTerminalType(et)) continue;
|
|
|
|
Visit(elem, parent: parentNode, segment: $"{memberName}[{i}]", visited);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 일반 객체
|
|
Visit(child, parent: parentNode, segment: memberName, visited);
|
|
}
|
|
|
|
private static void AttachMeta(object node, object parent, string segment)
|
|
{
|
|
// ConditionalWeakTable은 같은 키 add가 안 되므로 교체
|
|
_meta.Remove(node);
|
|
_meta.Add(node, new NodeMeta { Parent = parent, Segment = segment });
|
|
}
|
|
|
|
private static bool IsTerminalType(Type t)
|
|
{
|
|
if (t.IsPrimitive || t.IsEnum) return true;
|
|
if (t == typeof(string) || t == typeof(decimal)) return true;
|
|
if (t == typeof(DateTime) || t == typeof(DateTimeOffset) || t == typeof(Guid) || t == typeof(TimeSpan)) return true;
|
|
|
|
// Nullable<T> 처리
|
|
var nt = Nullable.GetUnderlyingType(t);
|
|
if (nt != null) return IsTerminalType(nt);
|
|
|
|
return false;
|
|
}
|
|
|
|
private sealed class ReferenceEqualityComparer : IEqualityComparer<object>
|
|
{
|
|
public static readonly ReferenceEqualityComparer Instance = new();
|
|
public new bool Equals(object x, object y) => ReferenceEquals(x, y);
|
|
public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj);
|
|
}
|
|
} |