using System;
using System.Collections.Generic;
using System.Linq;
using Northbound.Data;
using UnityEditor;
using UnityEngine;
namespace Northbound.Editor
{
///
/// ScriptableObject를 단일 원본으로 사용하는 게임 데이터의 참조와 값 범위를 검사합니다.
///
public static class ScriptableObjectDataValidator
{
private const string DataRoot = "Assets/Data/ScriptableObjects";
[MenuItem("Tools/Northbound/Data/Validate All ScriptableObjects")]
public static void ValidateAll()
{
int errors = 0;
int warnings = 0;
List towers = LoadAll();
List monsters = LoadAll();
List creeps = LoadAll();
List players = LoadAll();
List upgrades = LoadAll();
List settings = LoadAll();
ValidateUniqueIds(towers, data => data.id, "Tower", ref errors);
ValidateUniqueIds(monsters, data => data.id, "Monster", ref errors);
ValidateUniqueIds(creeps, data => data.id, "Creep", ref errors);
ValidateUniqueIds(players, data => data.id, "Player", ref errors);
ValidateUniqueIds(upgrades, data => data.id, "Upgrade", ref errors);
ValidateUniqueIds(settings, data => data.id, "DefaultSettings", ref errors);
ValidateTowers(towers, ref errors, ref warnings);
ValidateUnits(monsters, data => data.id, data => data.maxHp, data => data.moveSpeed,
data => data.cost, data => data.weight, "Monster", ref errors);
ValidateUnits(creeps, data => data.id, data => data.maxHp, data => data.moveSpeed,
data => data.cost, data => data.weight, "Creep", ref errors);
ValidatePlayers(players, ref errors);
ValidateUpgrades(upgrades, ref errors, ref warnings);
ValidateSettings(settings, ref errors, ref warnings);
string summary = $"[SO Data Validation] {towers.Count} towers, {monsters.Count} monsters, " +
$"{creeps.Count} creeps, {players.Count} players, {upgrades.Count} upgrades, " +
$"{settings.Count} settings — {errors} errors, {warnings} warnings";
if (errors > 0)
Debug.LogError(summary);
else if (warnings > 0)
Debug.LogWarning(summary);
else
Debug.Log(summary);
}
private static List LoadAll() where T : ScriptableObject
{
return AssetDatabase.FindAssets($"t:{typeof(T).Name}", new[] { DataRoot })
.Select(AssetDatabase.GUIDToAssetPath)
.Select(AssetDatabase.LoadAssetAtPath)
.Where(asset => asset != null)
.OrderBy(asset => asset.name)
.ToList();
}
private static void ValidateUniqueIds(IEnumerable assets, Func getId,
string dataType, ref int errors) where T : UnityEngine.Object
{
var ids = new Dictionary();
foreach (T asset in assets)
{
int id = getId(asset);
if (id <= 0)
{
Error($"{dataType} ID는 1 이상이어야 합니다: {asset.name}", asset, ref errors);
continue;
}
if (ids.TryGetValue(id, out T existing))
Error($"{dataType} ID {id}가 중복됩니다: {existing.name}, {asset.name}", asset, ref errors);
else
ids.Add(id, asset);
}
}
private static void ValidateTowers(List towers, ref int errors, ref int warnings)
{
var towerIds = towers.Select(data => data.id).ToHashSet();
foreach (TowerData data in towers)
{
if (string.IsNullOrWhiteSpace(data.buildingName))
Error($"Tower {data.id}의 표시 이름이 비어 있습니다.", data, ref errors);
if (data.prefab == null)
Error($"Tower {data.id}의 프리팹이 없습니다.", data, ref errors);
if (data.mana < 0 || data.manpower < 0)
Error($"Tower {data.id}의 비용은 음수일 수 없습니다.", data, ref errors);
if (data.maxHp <= 0)
Error($"Tower {data.id}의 체력은 1 이상이어야 합니다.", data, ref errors);
if (data.sizeX <= 0 || data.sizeY <= 0 || data.sizeZ <= 0)
Error($"Tower {data.id}의 배치 크기는 모두 1 이상이어야 합니다.", data, ref errors);
if (data.upgradeTo > 0 && !towerIds.Contains(data.upgradeTo))
Error($"Tower {data.id}의 업그레이드 대상 {data.upgradeTo}가 없습니다.", data, ref errors);
if (data.atkDamage > 0 && data.atkRange <= 0)
Warn($"Tower {data.id}는 공격력이 있지만 사거리가 없습니다.", data, ref warnings);
}
}
private static void ValidateUnits(IEnumerable units, Func getId,
Func getHealth, Func getSpeed, Func getCost,
Func getWeight, string dataType, ref int errors) where T : UnityEngine.Object
{
foreach (T data in units)
{
int id = getId(data);
if (getHealth(data) <= 0)
Error($"{dataType} {id}의 체력은 1 이상이어야 합니다.", data, ref errors);
if (getSpeed(data) < 0)
Error($"{dataType} {id}의 이동 속도는 음수일 수 없습니다.", data, ref errors);
if (getCost(data) <= 0 || getWeight(data) <= 0)
Error($"{dataType} {id}의 생성 비용과 가중치는 0보다 커야 합니다.", data, ref errors);
}
}
private static void ValidatePlayers(IEnumerable players, ref int errors)
{
foreach (PlayerData data in players)
{
if (data.maxHp <= 0 || data.moveSpeed <= 0)
Error($"Player {data.id}의 체력과 이동 속도는 0보다 커야 합니다.", data, ref errors);
}
}
private static void ValidateUpgrades(List upgrades, ref int errors, ref int warnings)
{
var ids = upgrades.Select(data => data.id).ToHashSet();
foreach (UpgradeData data in upgrades)
{
if (string.IsNullOrWhiteSpace(data.memo))
Warn($"Upgrade {data.id}의 표시용 메모가 비어 있습니다.", data, ref warnings);
if (data.mana < 0)
Error($"Upgrade {data.id}의 비용은 음수일 수 없습니다.", data, ref errors);
if (data.effectStatList.Count != data.effectOpList.Count ||
data.effectStatList.Count != data.effectValueList.Count)
Error($"Upgrade {data.id}의 효과 목록 길이가 서로 다릅니다.", data, ref errors);
foreach (int prerequisiteId in data.requireUpgradeId)
{
if (prerequisiteId == data.id)
Error($"Upgrade {data.id}가 자기 자신을 선행 조건으로 참조합니다.", data, ref errors);
else if (!ids.Contains(prerequisiteId))
Error($"Upgrade {data.id}의 선행 조건 {prerequisiteId}가 없습니다.", data, ref errors);
}
}
}
private static void ValidateSettings(IEnumerable settings,
ref int errors, ref int warnings)
{
var keys = new HashSet(StringComparer.Ordinal);
foreach (DefaultSettingsData data in settings)
{
if (string.IsNullOrWhiteSpace(data.key))
{
Warn($"DefaultSettings {data.id}의 키가 비어 있습니다.", data, ref warnings);
continue;
}
string scopedKey = $"{data.scene}:{data.key}";
if (!keys.Add(scopedKey))
Error($"DefaultSettings 키가 중복됩니다: {scopedKey}", data, ref errors);
}
}
private static void Error(string message, UnityEngine.Object context, ref int count)
{
count++;
Debug.LogError($"[SO Data Validation] {message}", context);
}
private static void Warn(string message, UnityEngine.Object context, ref int count)
{
count++;
Debug.LogWarning($"[SO Data Validation] {message}", context);
}
}
}