Files
XRLib/Assets/Scripts/Simulator/PatchIndexer.cs
2025-12-17 11:42:57 +09:00

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);
}
}