1
Fork 0
mirror of https://github.com/xivdev/EXDSchema.git synced 2025-06-05 23:57:46 +00:00

Remove tools. Add schemas folder for schema storage

This commit is contained in:
Liam 2023-11-05 00:11:40 -05:00
parent ed9d6560e2
commit 40e47bdab8
2036 changed files with 12 additions and 2164 deletions

View file

@ -4,6 +4,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SchemaConverter", "SchemaCo
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SchemaValidator", "SchemaValidator\SchemaValidator.csproj", "{22B92DA2-D46B-4219-A0E7-CC55F9213BAD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BasicSchemaGenerator", "BasicSchemaGenerator\BasicSchemaGenerator.csproj", "{370F2E2A-C6CF-4E82-8CC7-B6093026CE3A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnknownFixer", "UnknownFixer\UnknownFixer.csproj", "{63AE5933-C11C-4397-A5AB-877CE217AEBB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -18,5 +22,13 @@ Global
{22B92DA2-D46B-4219-A0E7-CC55F9213BAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{22B92DA2-D46B-4219-A0E7-CC55F9213BAD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{22B92DA2-D46B-4219-A0E7-CC55F9213BAD}.Release|Any CPU.Build.0 = Release|Any CPU
{370F2E2A-C6CF-4E82-8CC7-B6093026CE3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{370F2E2A-C6CF-4E82-8CC7-B6093026CE3A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{370F2E2A-C6CF-4E82-8CC7-B6093026CE3A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{370F2E2A-C6CF-4E82-8CC7-B6093026CE3A}.Release|Any CPU.Build.0 = Release|Any CPU
{63AE5933-C11C-4397-A5AB-877CE217AEBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{63AE5933-C11C-4397-A5AB-877CE217AEBB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{63AE5933-C11C-4397-A5AB-877CE217AEBB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{63AE5933-C11C-4397-A5AB-877CE217AEBB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

BIN
NewDefinitions_post.7z Normal file

Binary file not shown.

View file

@ -1,34 +0,0 @@
using Lumina.Data.Structs.Excel;
using SchemaConverter.New;
namespace SchemaConverter;
public class ColumnInfo
{
public int BitOffset { get; set; }
public string Name { get; set; }
public int Index;
public string? Type { get; set; } // icon, color etc
public ExcelColumnDataType DataType { get; set; }
public bool IsArrayMember { get; set; }
public int? ArrayIndex { get; set; }
public Condition? Condition { get; set; }
public List<string>? Targets { get; set; }
public ColumnInfo() { }
public ColumnInfo(Old.Definition def, int index, bool isArrayMember, int? arrayIndex, Condition? condition, List<string>? targets)
{
var converterType = def.Converter?.Type;
var nameSuffix = isArrayMember ? $"[{arrayIndex}]" : "";
Name = Util.StripDefinitionName(def.Name);// + nameSuffix;
Index = index;
Type = converterType;
IsArrayMember = isArrayMember;
ArrayIndex = arrayIndex;
Condition = condition;
Targets = targets;
}
public override string ToString() => $"{Name} ({Index}@{BitOffset / 8}&{BitOffset % 8}) {Type} {IsArrayMember} {ArrayIndex}";
}

View file

@ -1,65 +0,0 @@
// ReSharper disable UnusedMember.Global
// ReSharper disable InconsistentNaming
using System.ComponentModel;
using SharpYaml;
using SharpYaml.Serialization;
namespace SchemaConverter.New;
public enum FieldType
{
Scalar,
Array,
Icon,
ModelId,
Color,
Link,
}
public class Sheet
{
[YamlMember(0)]
public string Name { get; set; }
[YamlMember(1)]
public string? DisplayField { get; set; }
[YamlMember(2)]
public List<Field> Fields { get; set; }
}
public class Field
{
[YamlMember(0)]
public string? Name { get; set; }
[YamlMember(1)]
public int? Count { get; set; }
[YamlMember(2)]
[DefaultValue(FieldType.Scalar)]
public FieldType Type { get; set; }
[YamlMember(3)]
public string? Comment { get; set; }
[YamlMember(4)]
public List<Field>? Fields { get; set; }
[YamlMember(5)]
public Condition? Condition { get; set; }
[YamlMember(6)]
[YamlStyle(YamlStyle.Flow)]
public List<string>? Targets { get; set; }
}
public class Condition
{
[YamlMember(0)]
public string? Switch { get; set; }
[YamlMember(1)]
public Dictionary<int, List<string>>? Cases { get; set; }
}

View file

@ -1,54 +0,0 @@
using Newtonsoft.Json;
namespace SchemaConverter.Old;
public class When
{
[JsonProperty( "key" )] public string Key { get; set; }
[JsonProperty( "value" )] public int Value { get; set; }
}
public class Link
{
[JsonProperty( "when" )] public When When { get; set; }
[JsonProperty( "project" )] public string Project { get; set; }
[JsonProperty( "key" )] public string Key { get; set; }
[JsonProperty( "sheet" )] public string LinkedSheet { get; set; }
[JsonProperty( "sheets" )] public List< string > Sheets { get; set; }
}
public class Converter
{
[JsonProperty( "type" )] public string Type { get; set; }
[JsonProperty( "target" )] public string Target { get; set; }
[JsonProperty( "links" )] public List< Link > Links { get; set; }
[JsonProperty( "targets" )] public List< string > Targets { get; set; }
}
public class Definition
{
[JsonProperty( "index" )] public uint Index { get; set; }
[JsonProperty( "name" )] public string? Name { get; set; }
[JsonProperty( "converter" )] public Converter Converter { get; set; }
[JsonProperty( "type" )] public string Type { get; set; }
[JsonProperty( "count" )] public int Count { get; set; }
// Valid for repeats only
[JsonProperty( "definition" )] public Definition RepeatDefinition { get; set; }
// Valid for groups only
[JsonProperty( "members" )] public List< Definition > GroupDefinitions { get; set; }
public string GetName()
{
return Name ?? $"Unknown{Type.FirstCharToUpper()}{Index}";
}
}
public class Sheet
{
[JsonProperty( "sheet" )] public string SheetName { get; set; }
[JsonProperty( "defaultColumn" )] public string? DefaultColumn { get; set; }
[JsonProperty( "isGenericReferenceTarget" )] public bool IsGenericReferenceTarget { get; set; }
[JsonProperty( "definitions" )] public List< Definition > Definitions { get; set; }
}

View file

@ -1,275 +0,0 @@
using Lumina;
using Lumina.Data.Files.Excel;
using Lumina.Data.Structs.Excel;
using Newtonsoft.Json;
using SchemaConverter.New;
using SchemaConverter.Old;
namespace SchemaConverter;
public class SchemaConverter
{
private static readonly List<string> _genericReferenceLink = new();
private static void EnumerateGenericReferenceTargets(string oldSchemaDir)
{
var targets = new List<string>();
foreach (var oldSchemaPath in Directory.GetFiles(oldSchemaDir, "*.json"))
{
var oldSchema = JsonConvert.DeserializeObject<Old.Sheet>(File.ReadAllText(oldSchemaPath));
if (oldSchema is { IsGenericReferenceTarget: true })
{
targets.Add(oldSchema.SheetName);
}
}
_genericReferenceLink.AddRange(targets);
}
public static void Main(string[] args)
{
// we need 3 args
if (args.Length != 3)
{
Console.WriteLine("Usage: SchemaConverter.exe <game install directory> <schema directory> <output directory>");
return;
}
var gameDir = args[0];
var oldSchemaDir = args[1];
var newSchemaDir = args[2];
var gameData = new GameData(gameDir);
EnumerateGenericReferenceTargets(oldSchemaDir);
foreach (var oldSchemaPath in Directory.GetFiles(oldSchemaDir, "*.json"))
{
var sheetName = Path.GetFileNameWithoutExtension(oldSchemaPath);
var newSchemaPath = Path.Combine(newSchemaDir, $"{sheetName}.yml");
Directory.CreateDirectory(Path.GetDirectoryName(newSchemaPath));
var exh = gameData.GetFile<ExcelHeaderFile>($"exd/{sheetName}.exh");
if (exh == null)
{
Console.Error.WriteLine($"Sheet {sheetName} does not exist!");
continue;
}
var result = Convert(exh, oldSchemaPath, newSchemaPath);
var strResult = result ? "succeeded!" : "failed...";
// Console.WriteLine($"Conversion of {sheetName} {strResult}");
}
}
public static bool Convert(ExcelHeaderFile exh, string oldSchemaPath, string newSchemaPath)
{
// Loading and validation
var oldSchema = JsonConvert.DeserializeObject<Old.Sheet>(File.ReadAllText(oldSchemaPath));
if (oldSchema == null)
{
Console.Error.WriteLine($"Failed to parse old schema for {exh.FilePath}!");
return false;
}
if (oldSchema.Definitions.Count == 0)
{
// Console.WriteLine($"{exh.FilePath.Path} has no column definitions in old schema!");
}
// Load and parse the old schema to supplement exh information
var columnInfos = new List<ColumnInfo>();
Emit(columnInfos, oldSchema);
var definedColumnMap = columnInfos.ToDictionary(c => c.Index, c => c);
for (int i = 0; i < exh.ColumnDefinitions.Length; i++)
{
var definition = exh.ColumnDefinitions[i];
if (definedColumnMap.TryGetValue(i, out var columnInfo))
{
columnInfo.BitOffset = Util.GetBitOffset(exh.ColumnDefinitions[i].Offset, exh.ColumnDefinitions[i].Type);
columnInfo.DataType = definition.IsBoolType ? ExcelColumnDataType.Bool : definition.Type;
// fixup ulongs lol
if (columnInfo.DataType == ExcelColumnDataType.Int64)
columnInfo.Type = "quad";
}
else
{
columnInfos.Add(
new ColumnInfo
{
Name = $"Unknown{i}",
Index = i,
Type = definition.Type == ExcelColumnDataType.Int64 ? "quad" : "null",
DataType = definition.IsBoolType ? ExcelColumnDataType.Bool : definition.Type,
BitOffset = Util.GetBitOffset(definition.Offset, definition.Type)
});
}
}
columnInfos.Sort((c1, c2) => c1.BitOffset.CompareTo(c2.BitOffset));
var columnCountsByName = new Dictionary<string, int>();
for (int i = 0; i < columnInfos.Count; i++)
{
if (columnCountsByName.TryGetValue(columnInfos[i].Name, out var count))
columnCountsByName[columnInfos[i].Name] = count + 1;
else
columnCountsByName[columnInfos[i].Name] = 1;
}
if (columnCountsByName.Any(c => c.Value > 1))
{
Console.WriteLine($"{oldSchema.SheetName} is a shitty fucking stupid sheet!");
}
var name = oldSchema?.SheetName;
if (name == null)
name = Path.GetFileNameWithoutExtension(exh.FilePath.Path).FirstCharToUpper();
var newSchema = new New.Sheet
{
Name = name,
DisplayField = oldSchema?.DefaultColumn,
Fields = new List<New.Field>(),
};
foreach (var col in columnInfos)
{
var field = new New.Field
{
Name = col.Name,
Type = col.Type switch
{
"quad" => FieldType.ModelId,
"icon" => FieldType.Icon,
"color" => FieldType.Color,
_ => FieldType.Scalar,
},
Condition = col.Condition,
Targets = col.Targets,
};
if (field.Condition != null || field.Targets != null)
field.Type = FieldType.Link;
newSchema.Fields.Add(field);
}
// PlaceArray(newSchema);
var newSchemaStr = SerializeUtil.Serialize(newSchema);
File.WriteAllText(newSchemaPath, newSchemaStr);
// Console.WriteLine(newSchemaStr);
return true;
}
// private static void PlaceArray(New.Sheet sheet)
// {
// var seenColumns = new Dictionary<string, int>();
//
// for (int i = 0; i < sheet.Fields.Count; i++)
// {
// var field = sheet.Fields[i];
//
// // How many times does it occur?
// var occurrences = sheet.Fields.Count(f => f.Name == field.Name);
//
// // When does it occur?
// var firstOccurrence = seenColumns.
// var distance = sheet.Fields.FindIndex(i + 1, f => f.Name == field.Name) - i;
//
//
// }
// }
private static void Emit(List<ColumnInfo> infos, Old.Sheet sheet)
{
var index = 0;
foreach (var definition in sheet.Definitions)
{
if (index != definition.Index)
{
for (int i = index; i < definition.Index; i++)
{
infos.Add(new ColumnInfo {Name = $"Unknown{i}", Index = i });
}
// Console.WriteLine($"{sheet.SheetName}: skipped and generated {definition.Index - index} columns from {index} to {definition.Index}");
}
index = (int)definition.Index;
if (definition.Type == null)
EmitSingle(infos, definition, false, null, ref index);
else if (definition.Type == "repeat")
EmitRepeat(infos, definition, ref index);
else if (definition.Type == "group")
EmitGroup(infos, definition, ref index);
else
throw new Exception($"Unknown type {definition.Type}!");
}
}
private static void EmitSingle(List<ColumnInfo> infos, Old.Definition definition, bool isArray, int? arrayIndex, ref int index)
{
var link = ConvertLink(definition.Converter);
infos.Add(new ColumnInfo(definition, index++, isArray, arrayIndex, link.condition, link.targets));
}
private static void EmitRepeat(List<ColumnInfo> infos, Old.Definition definition, ref int index)
{
for (int i = 0; i < definition.Count; i++)
{
if (definition.RepeatDefinition.Type == null)
EmitSingle(infos, definition.RepeatDefinition, true, i, ref index);
else if (definition.RepeatDefinition.Type == "repeat")
EmitRepeat(infos, definition.RepeatDefinition, ref index);
else if (definition.RepeatDefinition.Type == "group")
EmitGroup(infos, definition.RepeatDefinition, ref index);
else
throw new Exception($"Unknown repeat type {definition.Type}!");
}
}
private static void EmitGroup(List<ColumnInfo> infos, Old.Definition definition, ref int index)
{
foreach (var member in definition.GroupDefinitions)
{
if (member.Type == null)
EmitSingle(infos, member, false, null, ref index);
else if (member.Type == "repeat")
EmitRepeat(infos, member, ref index);
else if (member.Type == "group")
EmitGroup(infos, member, ref index);
else
throw new Exception($"Unknown group member type {member.Type}!");
}
}
private static (Condition? condition, List<string>? targets) ConvertLink(Old.Converter oldLink)
{
if (oldLink == null) return (null, null);
if (oldLink.Type == "generic")
{
return (null, _genericReferenceLink);
}
else if (oldLink.Type == "link")
{
return (null, new List<string> {oldLink.Target});
}
else if (oldLink.Type == "multiref")
{
return (null, oldLink.Targets);
}
else if (oldLink.Type == "complexlink")
{
if (oldLink.Links[0].Project != null)
{
return (null, null);
}
var condition = new Condition();
condition.Switch = Util.StripDefinitionName(oldLink.Links[0].When.Key);
condition.Cases = new Dictionary<int, List<string>>();
foreach (var oldLinkLink in oldLink.Links)
condition.Cases.Add(oldLinkLink.When.Value, oldLinkLink.LinkedSheet == null ? oldLinkLink.Sheets : new List<string> { oldLinkLink.LinkedSheet });
return (condition, null);
}
else
{
return (null, null);
}
}
}

View file

@ -1,17 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Lumina" Version="3.11.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="SharpYaml" Version="2.1.0" />
</ItemGroup>
</Project>

View file

@ -1,53 +0,0 @@
using SharpYaml;
using SharpYaml.Events;
using SharpYaml.Serialization;
using SharpYaml.Serialization.Serializers;
namespace SchemaConverter;
public static class SerializeUtil
{
private static readonly Serializer _serializer;
static SerializeUtil()
{
var settings = new SerializerSettings
{
EmitAlias = false,
EmitDefaultValues = false,
NamingConvention = new CamelCaseNamingConvention(),
IgnoreNulls = true,
};
settings.RegisterSerializer(typeof(Dictionary<int, List<string>>), new CustomDictionarySerializer());
settings.RegisterSerializer(typeof(New.FieldType), new CustomFieldTypeSerializer());
_serializer = new Serializer(settings);
}
public static string Serialize(object o)
{
return _serializer.Serialize(o);
}
}
internal class CustomDictionarySerializer : DictionarySerializer
{
protected override void WriteDictionaryItem(ref ObjectContext objectContext, KeyValuePair<object, object?> keyValue, KeyValuePair<Type, Type> types)
{
objectContext.SerializerContext.WriteYaml(keyValue.Key, types.Key);
objectContext.SerializerContext.WriteYaml(keyValue.Value, types.Value, YamlStyle.Flow);
}
}
internal class CustomFieldTypeSerializer : ScalarSerializerBase
{
public override object? ConvertFrom(ref ObjectContext context, Scalar fromScalar)
{
return Enum.Parse<New.FieldType>(new PascalNamingConvention().Convert(fromScalar.Value));
}
public override string ConvertTo(ref ObjectContext objectContext)
{
return objectContext.Settings.NamingConvention.Convert(objectContext.Instance.ToString());
}
}

View file

@ -1,62 +0,0 @@
using Lumina.Data.Structs.Excel;
namespace SchemaConverter;
public static class Util
{
public static string FirstCharToUpper(this string input) =>
input switch
{
null => throw new ArgumentNullException(nameof(input)),
"" => throw new ArgumentException($"{nameof(input)} cannot be empty", nameof(input)),
_ => string.Concat(input[0].ToString().ToUpper(), input.AsSpan(1))
};
public static string StripDefinitionName(string str)
{
if( string.IsNullOrWhiteSpace(str))
return null;
str = str
.Replace("<", "")
.Replace(">", "")
.Replace("{", "")
.Replace("}", "")
.Replace("(", "")
.Replace(")", "")
.Replace("/", "")
.Replace("[", "")
.Replace("]", "")
.Replace(" ", "")
.Replace("'", "")
.Replace("-", "")
.Replace("%", "Pct");
if(char.IsDigit(str[0]))
{
var index = str[0] - '0';
var words = new[] {"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"};
str = $"{words[index]}{str[1..]}";
}
return str;
}
public static int GetBitOffset(int offset, ExcelColumnDataType dataType)
{
var bitOffset = offset * 8;
return dataType switch
{
ExcelColumnDataType.PackedBool0 => bitOffset + 0,
ExcelColumnDataType.PackedBool1 => bitOffset + 1,
ExcelColumnDataType.PackedBool2 => bitOffset + 2,
ExcelColumnDataType.PackedBool3 => bitOffset + 3,
ExcelColumnDataType.PackedBool4 => bitOffset + 4,
ExcelColumnDataType.PackedBool5 => bitOffset + 5,
ExcelColumnDataType.PackedBool6 => bitOffset + 6,
ExcelColumnDataType.PackedBool7 => bitOffset + 7,
_ => bitOffset,
};
}
}

View file

@ -1,46 +0,0 @@
using Lumina.Data.Structs.Excel;
using SchemaValidator.New;
namespace SchemaValidator;
public class DefinedColumn
{
public ExcelColumnDefinition Definition { get; set; }
public Field Field { get; set; }
private int? _bitOffset;
public int BitOffset {
get
{
if (_bitOffset == null)
_bitOffset = CalculateBitOffset(Definition.Offset, Definition.Type);
return _bitOffset.Value;
}
}
public override string ToString() => $"{Field} @ 0x{BitOffset / 8:X}&{BitOffset % 8}";
public static int CalculateBitOffset(ExcelColumnDefinition def)
{
return CalculateBitOffset(def.Offset, def.Type);
}
public static int CalculateBitOffset(int offset, ExcelColumnDataType type)
{
var bitOffset = offset * 8;
return type switch
{
ExcelColumnDataType.PackedBool0 => bitOffset + 0,
ExcelColumnDataType.PackedBool1 => bitOffset + 1,
ExcelColumnDataType.PackedBool2 => bitOffset + 2,
ExcelColumnDataType.PackedBool3 => bitOffset + 3,
ExcelColumnDataType.PackedBool4 => bitOffset + 4,
ExcelColumnDataType.PackedBool5 => bitOffset + 5,
ExcelColumnDataType.PackedBool6 => bitOffset + 6,
ExcelColumnDataType.PackedBool7 => bitOffset + 7,
_ => bitOffset,
};
}
}

View file

@ -1,60 +0,0 @@
// ReSharper disable UnusedMember.Global
// ReSharper disable InconsistentNaming
using System.ComponentModel;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace SchemaValidator.New;
public enum FieldType
{
Scalar,
Array,
Icon,
ModelId,
Color,
Link,
}
public class Sheet
{
public string Name { get; set; }
public string? DisplayField { get; set; }
public List<Field> Fields { get; set; }
}
public class Field
{
public string? Name { get; set; }
public int? Count { get; set; }
[DefaultValue(FieldType.Scalar)]
[JsonConverter(typeof(StringEnumConverter), true)]
public FieldType Type { get; set; }
public string? Comment { get; set; }
public List<Field>? Fields { get; set; }
public Condition? Condition { get; set; }
public List<string>? Targets { get; set; }
public override string ToString()
{
var arraySuffix = Count.HasValue ? $"[{Count}]" : "";
var name = Name != null ? $"{Name}{arraySuffix}" : "Unknown";
return $"{name} ({Type})";
}
public override bool Equals(object? obj)
{
if (obj is not Field other)
return false;
var fieldsEqual = (Fields == null && other.Fields == null) || (Fields != null && other.Fields != null && Fields.SequenceEqual(other.Fields));
var targetsEqual = (Targets == null && other.Targets == null) || (Targets != null && other.Targets != null && Targets.SequenceEqual(other.Targets));
return Name == other.Name && Count == other.Count && Type == other.Type && Comment == other.Comment && Condition == other.Condition && fieldsEqual && targetsEqual;
}
}
public class Condition
{
public string? Switch { get; set; }
public Dictionary<int, List<string>>? Cases { get; set; }
}

View file

@ -1,213 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "#/definitions/Sheet",
"type": "object",
"additionalProperties": false,
"properties": {
"sheet": {
"$ref": "#/definitions/Sheet"
}
},
"definitions": {
"Sheet": {
"title": "Sheet",
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"description": "The name of the sheet.",
"type": "string"
},
"displayField": {
"description": "The name of the field to use for displaying a reference to this sheet in a cell. Useful only for UI-based consumption.",
"type": "string"
},
"fields": {
"description": "The fields of the sheet. Sheets must specify all fields present in the EXH file for that sheet, meaning they all must have at least one field.",
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/definitions/Field"
}
},
"comment": {
"type": "string"
}
},
"oneOf": [
{ "required": ["name", "fields"] }
]
},
"Field": {
"description": "A field in a sheet. Describes one or more columns.",
"title": "Field",
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"description": "The name of the field.",
"type": "string"
},
"type": {
"description": "Defines the type of the field. Scalar should be assumed by default, and has no meaning. The only other type that affects parsing is array.",
"type": "string",
"enum": ["scalar", "link", "array", "icon", "modelId", "color"]
},
"count": {
"description": "Only valid for array types. Defines the number of elements in the array.",
"type": "integer"
},
"targets": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
},
"condition": {
"$ref": "#/definitions/Condition"
},
"fields": {
"description": "Only valid for array types. Defines the fields of the array. Fields are not available on non-array types because grouping non-array types is meaningless. They should be defined at the top-level.",
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/definitions/Field"
}
},
"comment": {
"type": "string"
}
},
"allOf": [
{
"description": "Arrays require a count.",
"if": {
"required": ["type"],
"properties": {
"type": {
"const": "array"
}
}
},
"then": {
"required": ["count"]
},
"else": {
"not": {
"required": ["count"]
}
}
},
{
"description": "Fields with a fields list must be an array.",
"if": {
"required": ["fields"]
},
"then": {
"required": ["type"],
"properties": {
"type": {
"const": "array"
}
}
}
},
{
"description": "Fields with a count must be an array.",
"if": {
"required": ["count"]
},
"then": {
"required": ["type"],
"properties": {
"type": {
"const": "array"
}
}
}
},
{
"description": "Fields can have only one of condition or targets.",
"allOf": [
{
"description": "Fields with targets cannot have a condition.",
"if": {
"required": ["targets"]
},
"then": {
"not": {
"required": ["condition"]
}
}
},
{
"description": "Fields with a condition cannot have targets.",
"if": {
"required": ["condition"]
},
"then": {
"not": {
"required": ["targets"]
}
}
},
{
"description": "Arrays can have neither condition or targets.",
"not": {
"required": ["condition", "targets"]
}
}
]
},
{
"description": "Fields with a link type must have a condition or targets.",
"if": {
"required": ["type"],
"properties": {
"type": {
"const": "link"
}
}
},
"then": {
"oneOf": [
{
"required": ["condition"],
"not": {
"required": ["targets"]
}
},
{
"required": ["targets"],
"not": {
"required": ["condition"]
}
}
]
}
}
]
},
"Condition": {
"title": "Condition",
"type": "object",
"additionalProperties": false,
"properties": {
"switch": {
"type": "string"
},
"cases": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"required": ["cases", "switch"]
}
}
}

View file

@ -1,114 +0,0 @@
using Lumina;
using Lumina.Data.Files.Excel;
using SchemaValidator.New;
using SchemaValidator.Util;
using SchemaValidator.Validation;
using SchemaValidator.Validation.Validators;
namespace SchemaValidator;
public class SchemaValidator
{
public static void Main(string[] args)
{
// we need 3 args
if (args.Length != 3)
{
Console.WriteLine("Usage: SchemaValidator.exe <game install directory> <json schema file> <schema directory>");
return;
}
var gameDir = args[0];
var schemaFile = args[1];
var schemaDir = args[2];
var gameData = new GameData(gameDir);
var schemaText = File.ReadAllText(schemaFile);
var testDict = new Dictionary<string, List<int>>()
{
{"all", new() {0}},
};
var validators = new List<Validator>
{
new SchemaFileValidator(gameData, schemaText),
new ColumnCountValidator(gameData),
new IconTypeValidator(gameData),
new NamedInnerNamedOuterValidator(gameData),
new FieldNameValidator(gameData),
new ModelIdTypeValidator(gameData),
new ColorTypeValidator(gameData),
new IconPathExistsValidator(gameData),
new SingleLinkRefValidator(gameData, testDict),
new MultiLinkRefValidator(gameData, testDict),
new ConditionValidator(gameData),
new ConditionRefValidator(gameData, testDict),
new DuplicateFieldNameValidator(gameData),
};
var exl = gameData.GetFile<ExcelListFile>("exd/root.exl");
var existingSheets = exl.ExdMap.Select(s => s.Key).ToHashSet();
var results = new ValidationResults();
foreach (var schemaPath in Directory.GetFiles(schemaDir, "*.yml"))
{
var sheetName = Path.GetFileNameWithoutExtension(schemaPath);
var exh = gameData.GetFile<ExcelHeaderFile>($"exd/{sheetName}.exh");
if (exh == null)
{
results.Add(ValidationResult.Error(sheetName, "SheetExistsValidator", "Schema exists but sheet does not!"));
}
Sheet sheet;
try
{
sheet = SerializeUtil.Deserialize<Sheet>(File.ReadAllText(schemaPath));
}
catch (Exception e)
{
Console.Error.WriteLine($"Sheet {sheetName} encountered an exception when deserializing!");
Console.Error.WriteLine(e.Message);
Console.Error.WriteLine(e.StackTrace);
continue;
}
if (sheet == null)
{
Console.Error.WriteLine($"Sheet {sheetName} could not be deserialized!");
continue;
}
// SerializeUtil.EvaluateSchema(schemaPath);
foreach (var validator in validators)
results.Add(validator.Validate(exh, sheet));
existingSheets.Remove(sheetName);
}
foreach (var sheet in existingSheets)
{
if (sheet.Contains('/')) continue;
results.Add(ValidationResult.Error(sheet, "SchemaDefinedValidator", "Sheet exists but has no schema!"));
}
// ---
foreach (var result in results.Results.Where(r => r.Status == ValidationStatus.Warning))
{
var msgfmt = result.Message == "" ? "" : $" - ";
Console.WriteLine($"{result.Status}: {result.SheetName} - {result.ValidatorName}{msgfmt}{result.Message}");
}
foreach (var result in results.Results.Where(r => r.Status == ValidationStatus.Error))
{
var msgfmt = result.Message == "" ? "" : $" - ";
Console.WriteLine($"{result.Status}: {result.SheetName} - {result.ValidatorName}{msgfmt}{result.Message}");
}
var successCount = results.Results.Count(r => r.Status == ValidationStatus.Success);
var warningCount = results.Results.Count(r => r.Status == ValidationStatus.Warning);
var errorCount = results.Results.Count(r => r.Status == ValidationStatus.Error);
Console.WriteLine($"{successCount} success, {warningCount} warnings, {errorCount} errors");
}
}

View file

@ -1,21 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JsonSchema.Net" Version="5.2.5" />
<PackageReference Include="Lumina" Version="3.11.0" />
<PackageReference Include="NJsonSchema" Version="10.9.0" />
<PackageReference Include="SharpYaml" Version="2.1.0" />
</ItemGroup>
</Project>

View file

@ -1,106 +0,0 @@
using Lumina.Data.Files.Excel;
using SchemaValidator.New;
namespace SchemaValidator.Util;
/// <summary>
/// Useful methods for working with the EXDSchema object model.
/// </summary>
public static class SchemaUtil
{
public static int GetColumnCount(Sheet sheet)
{
var total = 0;
foreach (var field in sheet.Fields)
total += GetFieldCount(field);
return total;
}
public static List<DefinedColumn> Flatten(ExcelHeaderFile exh, Sheet sheet)
{
var fields = new List<DefinedColumn>();
foreach (var field in sheet.Fields)
Emit(fields, field);
var exhDefList = exh.ColumnDefinitions.ToList();
exhDefList.Sort((c1, c2) => DefinedColumn.CalculateBitOffset(c1).CompareTo(DefinedColumn.CalculateBitOffset(c2)));
var min = Math.Min(exhDefList.Count, fields.Count);
for(int i = 0; i < min; i++)
{
var field = fields[i];
field.Definition = exhDefList[i];
}
return fields;
}
private static void Emit(List<DefinedColumn> list, Field field, string nameOverride = "")
{
if (field.Type != FieldType.Array)
{
// Single field
list.Add(new DefinedColumn { Field = CreateField(field, nameOverride) });
}
else if (field.Type == FieldType.Array)
{
// We can have an array without fields, it's just scalars
if (field.Fields == null)
{
for (int i = 0; i < field.Count.Value; i++)
{
list.Add(new DefinedColumn { Field = CreateField(field, "") });
}
}
else
{
for (int i = 0; i < field.Count.Value; i++)
{
foreach (var nestedField in field.Fields)
{
Emit(list, nestedField, field.Name);
}
}
}
}
}
private static Field CreateField(Field baseField, string nameOverride)
{
var addedField = new Field
{
Name = baseField.Name,
Comment = baseField.Comment,
Count = null,
Type = baseField.Type == FieldType.Array ? FieldType.Scalar : baseField.Type,
Fields = null,
Condition = baseField.Condition,
Targets = baseField.Targets,
};
// This is for unnamed inner fields of arrays such as arrays of links
// We don't want to override the name of unnamed scalars though
if (baseField.Name == null && baseField.Type != FieldType.Scalar && nameOverride != "")
addedField.Name = nameOverride;
return addedField;
}
private static int GetFieldCount(Field field)
{
if (field.Type == FieldType.Array)
{
var total = 0;
if (field.Fields != null)
{
foreach (var nestedField in field.Fields)
total += GetFieldCount(nestedField);
}
else
{
total = 1;
}
return total * field.Count.Value;
}
return 1;
}
}

View file

@ -1,44 +0,0 @@
using System.Text.Json.Nodes;
using Json.Schema;
using Newtonsoft.Json;
using SchemaValidator.New;
using SharpYaml;
using SharpYaml.Events;
using SharpYaml.Serialization;
using SharpYaml.Serialization.Serializers;
using JsonSchema = Json.Schema.JsonSchema;
namespace SchemaValidator.Util;
public static class SerializeUtil
{
private static readonly Serializer _serializer;
static SerializeUtil()
{
var settings = new SerializerSettings
{
EmitAlias = false,
EmitDefaultValues = false,
NamingConvention = new CamelCaseNamingConvention(),
IgnoreNulls = true,
};
_serializer = new Serializer(settings);
}
public static string Serialize(object o)
{
return _serializer.Serialize(o);
}
public static T? Deserialize<T>(string s)
{
return _serializer.Deserialize<T>(s);
}
public static object? Deserialize(string s)
{
return _serializer.Deserialize(s);
}
}

View file

@ -1,8 +0,0 @@
using Newtonsoft.Json.Converters;
namespace SchemaValidator.Util;
public class StringToEnumCamelCaseConverter : StringEnumConverter
{
}

View file

@ -1,92 +0,0 @@
namespace SchemaValidator.Validation;
public enum ValidationStatus
{
Success,
Error,
Warning,
Failed,
Info,
}
public class ValidationResults
{
public List<ValidationResult> Results { get; set; } = new();
public ValidationResults() { }
public ValidationResults(ValidationResult result) => Results.Add(result);
public void Add(ValidationResult result) => Results.Add(result);
public void Add(ValidationResults results) => Results.AddRange(results.Results);
public static ValidationResults Success(string sheetName, string validatorName, string message = "") => new(ValidationResult.Success(sheetName, validatorName, message));
public static ValidationResults Error(string sheetName, string validatorName, string message = "") => new(ValidationResult.Error(sheetName, validatorName, message));
public static ValidationResults Warning(string sheetName, string validatorName, string message = "") => new(ValidationResult.Warning(sheetName, validatorName, message));
public static ValidationResults Failed(string sheetName, string validatorName, string message = "") => new(ValidationResult.Failed(sheetName, validatorName, message));
public static ValidationResults Info(string sheetName, string validatorName, string message = "") => new(ValidationResult.Info(sheetName, validatorName, message));
}
public class ValidationResult
{
public ValidationStatus Status { get; set; }
public string SheetName { get; set; }
public string ValidatorName { get; set; }
public string Message { get; set; }
private ValidationResult() {}
public static ValidationResult Success(string sheetName, string validatorName, string message = "")
{
return new ValidationResult
{
SheetName = sheetName,
ValidatorName = validatorName,
Status = ValidationStatus.Success,
Message = message,
};
}
public static ValidationResult Error(string sheetName, string validatorName, string message = "")
{
return new ValidationResult
{
SheetName = sheetName,
ValidatorName = validatorName,
Status = ValidationStatus.Error,
Message = message,
};
}
public static ValidationResult Warning(string sheetName, string validatorName, string message = "")
{
return new ValidationResult
{
SheetName = sheetName,
ValidatorName = validatorName,
Status = ValidationStatus.Warning,
Message = message,
};
}
public static ValidationResult Failed(string sheetName, string validatorName, string message = "")
{
return new ValidationResult
{
SheetName = sheetName,
ValidatorName = validatorName,
Status = ValidationStatus.Failed,
Message = message,
};
}
public static ValidationResult Info(string sheetName, string validatorName, string message = "")
{
return new ValidationResult
{
SheetName = sheetName,
ValidatorName = validatorName,
Status = ValidationStatus.Info,
Message = message,
};
}
}

View file

@ -1,42 +0,0 @@
using Lumina;
using Lumina.Data.Files.Excel;
using Lumina.Data.Structs.Excel;
using Lumina.Excel;
using SchemaValidator.New;
namespace SchemaValidator.Validation;
public abstract class Validator
{
protected GameData GameData;
public Validator(GameData gameData)
{
GameData = gameData;
}
public abstract string ValidatorName();
public abstract ValidationResults Validate(ExcelHeaderFile exh, Sheet sheet);
protected long? ReadColumnIntegerValue(RawExcelSheet sheet, RowParser parser, DefinedColumn column)
{
var offset = column.Definition.Offset;
var type = column.Definition.Type;
Int128? value = type switch
{
ExcelColumnDataType.Int8 => parser.ReadOffset<sbyte>(offset),
ExcelColumnDataType.UInt8 => parser.ReadOffset<byte>(offset),
ExcelColumnDataType.Int16 => parser.ReadOffset<short>(offset),
ExcelColumnDataType.UInt16 => parser.ReadOffset<ushort>(offset),
ExcelColumnDataType.Int32 => parser.ReadOffset<int>(offset),
ExcelColumnDataType.UInt32 => parser.ReadOffset<uint>(offset),
ExcelColumnDataType.Int64 => parser.ReadOffset<long>(offset),
ExcelColumnDataType.UInt64 => parser.ReadOffset<ulong>(offset),
_ => null,
};
if (value != null)
return (long)value;
return null;
}
}

View file

@ -1,32 +0,0 @@
using Lumina;
using Lumina.Data.Files.Excel;
using Lumina.Data.Structs.Excel;
using SchemaValidator.New;
using SchemaValidator.Util;
namespace SchemaValidator.Validation.Validators;
public class ColorTypeValidator : Validator
{
public override string ValidatorName() => "ColorTypeValidator";
public ColorTypeValidator(GameData gameData) : base(gameData) { }
public override ValidationResults Validate(ExcelHeaderFile exh, Sheet sheet)
{
var results = new ValidationResults();
var fields = SchemaUtil.Flatten(exh, sheet);
foreach (var field in fields)
{
if (field.Field.Type == FieldType.Color && field.Definition.Type != ExcelColumnDataType.UInt32)
{
var msg = $"Column {field.Field.Name}@0x{field.Definition.Offset:X} type {field.Definition.Type} is not valid for type 'color'.";
results.Results.Add(ValidationResult.Error(sheet.Name, ValidatorName(), msg));
}
}
if (results.Results.Count == 0)
return ValidationResults.Success(sheet.Name, ValidatorName());
return results;
}
}

View file

@ -1,21 +0,0 @@
using Lumina;
using Lumina.Data.Files.Excel;
using SchemaValidator.New;
using SchemaValidator.Util;
namespace SchemaValidator.Validation.Validators;
public class ColumnCountValidator : Validator
{
public override string ValidatorName() => "ColumnCountValidator";
public ColumnCountValidator(GameData gameData) : base(gameData) { }
public override ValidationResults Validate(ExcelHeaderFile exh, Sheet sheet)
{
var colCount = SchemaUtil.GetColumnCount(sheet);
if (colCount != exh.ColumnDefinitions.Length)
return ValidationResults.Error(sheet.Name, ValidatorName(), $"Column count mismatch! exh count {exh.ColumnDefinitions.Length} != schema count {colCount}");
return ValidationResults.Success(sheet.Name, ValidatorName());
}
}

View file

@ -1,161 +0,0 @@
using System.Diagnostics;
using Lumina;
using Lumina.Data.Files.Excel;
using Lumina.Excel;
using SchemaValidator.New;
using SchemaValidator.Util;
namespace SchemaValidator.Validation.Validators;
public class ConditionRefValidator : Validator
{
public override string ValidatorName() => "ConditionRefValidator";
private Dictionary<string, List<int>> _ignoredValues = new();
public ConditionRefValidator(GameData gameData) : base(gameData) { }
public ConditionRefValidator(GameData gameData, Dictionary<string, List<int>> ignoredValues) : base(gameData)
{
_ignoredValues = ignoredValues;
}
private class LinkTargetData
{
public string SourceSheet { get; set; }
public DefinedColumn Source { get; set; }
public string SwitchField { get; set; }
public int SwitchValue { get; set; }
public HashSet<string> TargetSheets { get; set; }
public HashSet<long> TargetKeys { get; set; }
public override bool Equals(object? obj)
{
if (obj is not LinkTargetData other)
return false;
return SourceSheet == other.SourceSheet && Source.Field.Equals(other.Source.Field) && TargetSheets == other.TargetSheets && TargetKeys.SetEquals(other.TargetKeys);
}
}
public override ValidationResults Validate(ExcelHeaderFile exh, Sheet sheet)
{
var results = new ValidationResults();
var fields = SchemaUtil.Flatten(exh, sheet);
var dataFile = GameData.Excel.GetSheetRaw($"{sheet.Name}");
if (dataFile == null)
{
var msg = $"Failed to obtain sheet {sheet.Name} from game data.";
results.Results.Add(ValidationResult.Failed(sheet.Name, ValidatorName(), msg));
return results;
}
// Get all link fields with condition
var linkFields = fields.Where(f => f.Field is { Type: FieldType.Link, Condition: not null, Targets: null }).ToList();
var linkVals = new List<LinkTargetData>();
foreach (var field in linkFields)
{
var offset = field.Definition.Offset;
var switchOn = field.Field.Condition.Switch;
var switchField = fields.First(f => f.Field.Name == switchOn);
// var definedSwitchColumnValues = field.Field.Condition.Cases.Keys.ToHashSet();
// store the column values for each switch value
var columnValues = new Dictionary<long, HashSet<long>>();
foreach (var row in dataFile.GetRowParsers())
{
var columnValue = ReadColumnIntegerValue(dataFile, row, field);
var switchColumnValue = ReadColumnIntegerValue(dataFile, row, switchField);
if (columnValue == null || switchColumnValue == null)
{
var msg = $"Column {field.Field.Name}@0x{offset:X} type {field.Definition.Type} is not valid for type 'link' condition switch, failed to read.";
results.Results.Add(ValidationResult.Failed(sheet.Name, ValidatorName(), msg));
break; // don't spam the same error
}
if (columnValues.TryGetValue(switchColumnValue.Value, out var values))
{
values.Add(columnValue.Value);
}
else
{
columnValues.Add(switchColumnValue.Value, new HashSet<long> { columnValue.Value });
}
}
foreach (var valueSet in columnValues)
{
// Skip if we don't have a defined target set for this value of the switch
if (!field.Field.Condition.Cases.TryGetValue((int)valueSet.Key, out var switchTargets))
continue;
var targetData = new LinkTargetData
{
SourceSheet = sheet.Name,
Source = field,
TargetSheets = switchTargets.ToHashSet(),
TargetKeys = valueSet.Value,
SwitchField = switchField.Field.Name,
SwitchValue = (int)valueSet.Key,
};
linkVals.Add(targetData);
}
}
foreach (var linkData in linkVals.Distinct())
{
var sheetNames = linkData.TargetSheets;
var sheetKeys = linkData.TargetKeys;
var dataFiles = new Dictionary<string, RawExcelSheet>();
foreach (var sheetName in sheetNames)
{
var tmpDataFile = GameData.Excel.GetSheetRaw(sheetName);
if (tmpDataFile == null)
{
var msg = $"Source {linkData.SourceSheet} field {linkData.Source.Field.Name}@0x{linkData.Source.Definition.Offset:X} references non-existent sheet {sheetName}.";
results.Add(ValidationResult.Error(sheet.Name, ValidatorName(), msg));
continue;
}
dataFiles.Add(sheetName, tmpDataFile);
}
foreach (var key in sheetKeys)
{
if (_ignoredValues.TryGetValue("all", out var ignoredKeys) && ignoredKeys.Contains((int)key))
{
sheetKeys.Remove(key);
continue;
}
foreach (var linkedDataFile in dataFiles)
{
if (_ignoredValues.TryGetValue(linkedDataFile.Key, out ignoredKeys) && ignoredKeys.Contains((int)key))
{
sheetKeys.Remove(key);
}
else if (linkedDataFile.Value.GetRow((uint)key) != null)
{
// Console.WriteLine($"removing {key} because of {linkedDataFile.Key}");
sheetKeys.Remove(key);
}
}
}
if (sheetKeys.Count > 0)
{
var contents = string.Join(", ", sheetKeys);
var display = $"{linkData.Source.Field.Name}@0x{linkData.Source.Definition.Offset:X}";
var msg = $"Source {linkData.SourceSheet} field {display} contains key references {contents} for case {linkData.SwitchField} = {linkData.SwitchValue} which do not exist in any linked sheet.";
results.Add(ValidationResult.Warning(sheet.Name, ValidatorName(), msg));
}
}
if (results.Results.Count == 0)
return ValidationResults.Success(sheet.Name, ValidatorName());
return results;
}
}

View file

@ -1,67 +0,0 @@
using System.Diagnostics;
using Lumina;
using Lumina.Data.Files.Excel;
using SchemaValidator.New;
using SchemaValidator.Util;
namespace SchemaValidator.Validation.Validators;
public class ConditionValidator : Validator
{
public override string ValidatorName() => "ConditionValidator";
public ConditionValidator(GameData gameData) : base(gameData) { }
public override ValidationResults Validate(ExcelHeaderFile exh, Sheet sheet)
{
var results = new ValidationResults();
var fields = SchemaUtil.Flatten(exh, sheet);
var dataFile = GameData.Excel.GetSheetRaw($"{sheet.Name}");
// Get all link fields with null targets (filters out target-based links)
var conditionFields = fields.Where(f => f.Field is { Type: FieldType.Link, Condition: not null, Targets: null }).ToList();
foreach (var field in conditionFields)
{
var offset = field.Definition.Offset;
var switchOn = field.Field.Condition.Switch;
var switchField = fields.First(f => f.Field.Name == switchOn);
var definedSwitchColumnValues = field.Field.Condition.Cases.Keys.ToHashSet();
var switchColumnValues = new HashSet<long>();
foreach (var row in dataFile.GetRowParsers())
{
var columnValue = ReadColumnIntegerValue(dataFile, row, switchField);
if (columnValue == null)
{
var msg = $"Column {field.Field.Name}@0x{offset:X} type {field.Definition.Type} is not valid for type 'link' condition switch, failed to read.";
results.Results.Add(ValidationResult.Failed(sheet.Name, ValidatorName(), msg));
break; // don't spam the same error
}
switchColumnValues.Add(columnValue.Value);
}
foreach (var switchColumnValue in switchColumnValues)
{
if (!definedSwitchColumnValues.Contains((int)switchColumnValue))
{
var msg = $"Column {field.Field.Name}@0x{offset:X} type {field.Definition.Type} is not valid for type 'link' condition switch, switch column {switchOn} value {switchColumnValue} is not defined in the schema.";
results.Add(ValidationResult.Warning(sheet.Name, ValidatorName(), msg));
}
}
foreach (var definedSwitchColumnValue in definedSwitchColumnValues)
{
if (!switchColumnValues.Contains(definedSwitchColumnValue))
{
var msg = $"Column {field.Field.Name}@0x{offset:X} type {field.Definition.Type} is not valid for type 'link' condition switch, switch column {switchOn} value {definedSwitchColumnValue} is not present in the column values.";
results.Add(ValidationResult.Warning(sheet.Name, ValidatorName(), msg));
}
}
}
if (results.Results.Count == 0)
return ValidationResults.Success(sheet.Name, ValidatorName());
return results;
}
}

View file

@ -1,53 +0,0 @@
using Lumina;
using Lumina.Data.Files.Excel;
using SchemaValidator.New;
using SchemaValidator.Util;
namespace SchemaValidator.Validation.Validators;
public class DuplicateFieldNameValidator : Validator
{
public override string ValidatorName() => "DuplicateFieldNameValidator";
public DuplicateFieldNameValidator(GameData gameData) : base(gameData) { }
private string _sheetName;
public override ValidationResults Validate(ExcelHeaderFile exh, Sheet sheet)
{
_sheetName = sheet.Name;
var fieldNames = new HashSet<string>();
var results = new ValidationResults();
foreach (var field in sheet.Fields)
{
results.Add(Validate(field));
if (fieldNames.Contains(field.Name))
return ValidationResults.Error(sheet.Name, ValidatorName(), $"Duplicate field name {field.Name}");
fieldNames.Add(field.Name);
}
if (results.Results.Count == 0)
return ValidationResults.Success(sheet.Name, ValidatorName());
return results;
}
private ValidationResults Validate(Field field)
{
var results = new ValidationResults();
var fieldNames = new HashSet<string>();
if (field.Fields != null)
{
foreach (var subField in field.Fields)
{
Validate(subField);
if (fieldNames.Contains(subField.Name))
results.Add(ValidationResult.Error(_sheetName, ValidatorName(), $"Duplicate field name {subField.Name}"));
fieldNames.Add(subField.Name);
}
}
return results;
}
}

View file

@ -1,43 +0,0 @@
using System.Text.RegularExpressions;
using Lumina;
using Lumina.Data.Files.Excel;
using SchemaValidator.New;
using SchemaValidator.Util;
namespace SchemaValidator.Validation.Validators;
public partial class FieldNameValidator : Validator
{
public override string ValidatorName() => "FieldNameValidator";
private string _sheetName = "";
[GeneratedRegex("^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.Compiled)]
private static partial Regex _nameRegex();
public override ValidationResults Validate(ExcelHeaderFile exh, Sheet sheet)
{
_sheetName = sheet.Name;
// I just don't have the brainpower to recurse right now
var flat = SchemaUtil.Flatten(exh, sheet);
var results = new ValidationResults();
foreach (var fieldName in flat.Select(d => d.Field.Name).Distinct())
{
if (string.IsNullOrEmpty(fieldName))
{
results.Add(ValidationResult.Error(_sheetName, ValidatorName(), "Field name is empty."));
continue;
}
if (!_nameRegex().IsMatch(fieldName))
results.Results.Add(ValidationResult.Error(_sheetName, ValidatorName(), $"Field name {fieldName} is not a valid name."));
}
if (results.Results.Count == 0)
return ValidationResults.Success(sheet.Name, ValidatorName());
return results;
}
public FieldNameValidator(GameData gameData) : base(gameData) { }
}

View file

@ -1,55 +0,0 @@
using Lumina;
using Lumina.Data.Files.Excel;
using SchemaValidator.New;
using SchemaValidator.Util;
namespace SchemaValidator.Validation.Validators;
public class IconPathExistsValidator : Validator
{
public override string ValidatorName() => "IconPathExistsValidator";
public IconPathExistsValidator(GameData gameData) : base(gameData) { }
public override ValidationResults Validate(ExcelHeaderFile exh, Sheet sheet)
{
var results = new ValidationResults();
var fields = SchemaUtil.Flatten(exh, sheet);
var dataFile = GameData.Excel.GetSheetRaw($"{sheet.Name}");
foreach (var field in fields)
{
if (field.Field.Type == FieldType.Icon)
{
var offset = field.Definition.Offset;
foreach (var row in dataFile.GetRowParsers())
{
var columnValue = ReadColumnIntegerValue(dataFile, row, field);
if (columnValue == null)
{
var msg = $"Column {field.Field.Name}@0x{offset:X} type {field.Definition.Type} is not valid for type 'icon', failed to read.";
results.Results.Add(ValidationResult.Failed(sheet.Name, ValidatorName(), msg));
break; // don't spam the same error
}
if (!IconPathExists(columnValue.Value))
{
var msg = $"Column {field.Field.Name}@0x{offset:X} type {field.Definition.Type} at row {row.RowId} icon '{columnValue}' does not exist.";
results.Results.Add(ValidationResult.Warning(sheet.Name, ValidatorName(), msg));
break; // don't spam the same error
}
}
}
}
if (results.Results.Count == 0)
return ValidationResults.Success(sheet.Name, ValidatorName());
return results;
}
private bool IconPathExists(long iconId)
{
var path = $"ui/icon/{iconId / 1000 * 1000:000000}/{iconId:000000}.tex";
return GameData.FileExists(path);
}
}

View file

@ -1,39 +0,0 @@
using Lumina;
using Lumina.Data.Files.Excel;
using Lumina.Data.Structs.Excel;
using SchemaValidator.New;
using SchemaValidator.Util;
namespace SchemaValidator.Validation.Validators;
public class IconTypeValidator : Validator
{
public override string ValidatorName() => "IconTypeValidator";
private readonly HashSet<ExcelColumnDataType> _validTypes = new()
{
ExcelColumnDataType.UInt32,
ExcelColumnDataType.Int32,
ExcelColumnDataType.UInt16,
};
public IconTypeValidator(GameData gameData) : base(gameData) { }
public override ValidationResults Validate(ExcelHeaderFile exh, Sheet sheet)
{
var results = new ValidationResults();
var fields = SchemaUtil.Flatten(exh, sheet);
foreach (var field in fields)
{
if (field.Field.Type == FieldType.Icon && !_validTypes.Contains(field.Definition.Type))
{
var msg = $"Column {field.Field.Name}@0x{field.Definition.Offset:X} type {field.Definition.Type} is not valid for type 'icon'.";
results.Results.Add(ValidationResult.Error(sheet.Name, ValidatorName(), msg));
}
}
if (results.Results.Count == 0)
return ValidationResults.Success(sheet.Name, ValidatorName());
return results;
}
}

View file

@ -1,38 +0,0 @@
using Lumina;
using Lumina.Data.Files.Excel;
using Lumina.Data.Structs.Excel;
using SchemaValidator.New;
using SchemaValidator.Util;
namespace SchemaValidator.Validation.Validators;
public class ModelIdTypeValidator : Validator
{
public override string ValidatorName() => "ModelIdTypeValidator";
private readonly HashSet<ExcelColumnDataType> _validTypes = new()
{
ExcelColumnDataType.UInt32,
ExcelColumnDataType.UInt64,
};
public ModelIdTypeValidator(GameData gameData) : base(gameData) { }
public override ValidationResults Validate(ExcelHeaderFile exh, Sheet sheet)
{
var results = new ValidationResults();
var fields = SchemaUtil.Flatten(exh, sheet);
foreach (var field in fields)
{
if (field.Field.Type == FieldType.ModelId && !_validTypes.Contains(field.Definition.Type))
{
var msg = $"Column {field.Field.Name}@0x{field.Definition.Offset:X} type {field.Definition.Type} is not valid for type 'modelId'.";
results.Results.Add(ValidationResult.Error(sheet.Name, ValidatorName(), msg));
}
}
if (results.Results.Count == 0)
return ValidationResults.Success(sheet.Name, ValidatorName());
return results;
}
}

View file

@ -1,136 +0,0 @@
using System.Diagnostics;
using Lumina;
using Lumina.Data.Files.Excel;
using Lumina.Excel;
using SchemaValidator.New;
using SchemaValidator.Util;
namespace SchemaValidator.Validation.Validators;
public class MultiLinkRefValidator : Validator
{
public override string ValidatorName() => "MultiLinkRefValidator";
private Dictionary<string, List<int>> _ignoredValues = new();
public MultiLinkRefValidator(GameData gameData) : base(gameData) { }
public MultiLinkRefValidator(GameData gameData, Dictionary<string, List<int>> ignoredValues) : base(gameData)
{
_ignoredValues = ignoredValues;
}
private class LinkTargetData
{
public string SourceSheet { get; set; }
public DefinedColumn Source { get; set; }
public HashSet<string> TargetSheets { get; set; }
public HashSet<long> TargetKeys { get; set; }
public override bool Equals(object? obj)
{
if (obj is not LinkTargetData other)
return false;
return SourceSheet == other.SourceSheet && Source.Field.Equals(other.Source.Field) && TargetSheets == other.TargetSheets && TargetKeys.SetEquals(other.TargetKeys);
}
}
public override ValidationResults Validate(ExcelHeaderFile exh, Sheet sheet)
{
var results = new ValidationResults();
var fields = SchemaUtil.Flatten(exh, sheet);
var dataFile = GameData.Excel.GetSheetRaw($"{sheet.Name}");
if (dataFile == null)
{
var msg = $"Failed to obtain sheet {sheet.Name} from game data.";
results.Results.Add(ValidationResult.Failed(sheet.Name, ValidatorName(), msg));
return results;
}
// Get all link fields with null condition, > 1 target
var linkFields = fields.Where(f => f.Field is { Type: FieldType.Link, Condition: null, Targets.Count: > 1 }).ToList();
var linkVals = new List<LinkTargetData>();
foreach (var field in linkFields)
{
var offset = field.Definition.Offset;
var columnValues = new HashSet<long>();
foreach (var row in dataFile.GetRowParsers())
{
var columnValue = ReadColumnIntegerValue(dataFile, row, field);
if (columnValue == null)
{
var msg = $"Row {row.RowId} column {field.Field.Name}@0x{offset:X} type {field.Definition.Type} failed to read properly.";
results.Results.Add(ValidationResult.Failed(sheet.Name, ValidatorName(), msg));
break; // don't spam the same error
}
columnValues.Add(columnValue.Value);
}
var targetData = new LinkTargetData
{
SourceSheet = sheet.Name,
Source = field,
TargetSheets = field.Field.Targets.ToHashSet(),
TargetKeys = columnValues
};
linkVals.Add(targetData);
}
foreach (var linkData in linkVals.Distinct())
{
var sheetNames = linkData.TargetSheets;
var sheetKeys = linkData.TargetKeys;
var dataFiles = new Dictionary<string, RawExcelSheet>();
foreach (var sheetName in sheetNames)
{
var tmpDataFile = GameData.Excel.GetSheetRaw(sheetName);
if (tmpDataFile == null)
{
var msg = $"Source {linkData.SourceSheet} field {linkData.Source.Field.Name}@0x{linkData.Source.Definition.Offset:X} references non-existent sheet {sheetName}.";
results.Add(ValidationResult.Error(sheet.Name, ValidatorName(), msg));
continue;
}
dataFiles.Add(sheetName, tmpDataFile);
}
foreach (var key in sheetKeys)
{
if (_ignoredValues.TryGetValue("all", out var ignoredKeys) && ignoredKeys.Contains((int)key))
{
sheetKeys.Remove(key);
continue;
}
foreach (var linkedDataFile in dataFiles)
{
if (_ignoredValues.TryGetValue(linkedDataFile.Key, out ignoredKeys) && ignoredKeys.Contains((int)key))
{
sheetKeys.Remove(key);
}
else if (linkedDataFile.Value.GetRow((uint)key) != null)
{
// Console.WriteLine($"removing {key} because of {linkedDataFile.Key}");
sheetKeys.Remove(key);
}
}
}
if (sheetKeys.Count > 0)
{
var contents = string.Join(", ", sheetKeys);
var display = $"{linkData.Source.Field.Name}@0x{linkData.Source.Definition.Offset:X}";
var msg = $"Source {linkData.SourceSheet} field {display} contains key references {contents} which do not exist in any linked sheet.";
results.Add(ValidationResult.Warning(sheet.Name, ValidatorName(), msg));
}
}
if (results.Results.Count == 0)
return ValidationResults.Success(sheet.Name, ValidatorName());
return results;
}
}

View file

@ -1,56 +0,0 @@
using Lumina;
using Lumina.Data.Files.Excel;
using SchemaValidator.New;
namespace SchemaValidator.Validation.Validators;
public class NamedInnerNamedOuterValidator : Validator
{
public override string ValidatorName() => "NamedInnerNamedOuterValidator";
private string _sheetName = "";
public override ValidationResults Validate(ExcelHeaderFile exh, Sheet sheet)
{
_sheetName = sheet.Name;
var results = new ValidationResults();
foreach (var field in sheet.Fields)
{
CheckNames(results, null, field);
}
if (results.Results.Count == 0)
return ValidationResults.Success(sheet.Name, ValidatorName());
return results;
}
public NamedInnerNamedOuterValidator(GameData gameData) : base(gameData) { }
private void CheckNames(ValidationResults results, Field? parentField, Field field)
{
if (parentField != null)
{
if (parentField.Fields.Count == 1 && !string.IsNullOrEmpty(parentField.Name) && !string.IsNullOrEmpty(field.Name) && field.Type == FieldType.Scalar)
{
var msg = $"Parent field {parentField.Name} has a single named field {field.Name}.";
results.Results.Add(ValidationResult.Warning(_sheetName, ValidatorName(), msg));
}
}
if (field.Type != FieldType.Array)
{
// Single field
return;
}
if (field.Type == FieldType.Array)
{
if (field.Fields != null)
{
foreach (var nestedField in field.Fields)
{
CheckNames(results, field, nestedField);
}
}
}
}
}

View file

@ -1,43 +0,0 @@
using System.Text.Json.Nodes;
using Json.Schema;
using Lumina;
using Lumina.Data.Files.Excel;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using SchemaValidator.New;
namespace SchemaValidator.Validation.Validators;
public class SchemaFileValidator : Validator
{
public override string ValidatorName() => "SchemaFileValidator";
private readonly JsonSerializerSettings _settings = new()
{
NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Ignore,
ContractResolver = new CamelCasePropertyNamesContractResolver(),
};
private readonly JsonSchema _schema;
public SchemaFileValidator(GameData gameData, string schemaText) : base(gameData)
{
_schema = JsonSchema.FromText(schemaText);
}
public override ValidationResults Validate(ExcelHeaderFile exh, Sheet sheet)
{
// Re-serialize the yml sheet into json
var json = JsonConvert.SerializeObject(sheet, _settings);
if (json == null) return ValidationResults.Failed(sheet.Name, ValidatorName(), "Json serialization returned null.");
var node = JsonNode.Parse(json);
var schemaResult = _schema.Evaluate(node);
if (schemaResult == null) return ValidationResults.Failed(sheet.Name, ValidatorName(), "Schema validation returned null.");
if (schemaResult.IsValid) return ValidationResults.Success(sheet.Name, ValidatorName());
return ValidationResults.Error(sheet.Name, ValidatorName());
}
}

View file

@ -1,114 +0,0 @@
using System.Diagnostics;
using Lumina;
using Lumina.Data.Files.Excel;
using SchemaValidator.New;
using SchemaValidator.Util;
namespace SchemaValidator.Validation.Validators;
public class SingleLinkRefValidator : Validator
{
public override string ValidatorName() => "SingleLinkRefValidator";
private Dictionary<string, List<int>> _ignoredValues = new();
public SingleLinkRefValidator(GameData gameData) : base(gameData) { }
public SingleLinkRefValidator(GameData gameData, Dictionary<string, List<int>> ignoredValues) : base(gameData)
{
_ignoredValues = ignoredValues;
}
private class LinkTargetData
{
public string SourceSheet { get; set; }
public DefinedColumn Source { get; set; }
public string TargetSheet { get; set; }
public HashSet<long> TargetKeys { get; set; }
public override bool Equals(object? obj)
{
if (obj is not LinkTargetData other)
return false;
return SourceSheet == other.SourceSheet && Source.Field.Equals(other.Source.Field) && TargetSheet == other.TargetSheet && TargetKeys.SetEquals(other.TargetKeys);
}
}
public override ValidationResults Validate(ExcelHeaderFile exh, Sheet sheet)
{
var results = new ValidationResults();
var fields = SchemaUtil.Flatten(exh, sheet);
var dataFile = GameData.Excel.GetSheetRaw($"{sheet.Name}");
if (dataFile == null)
{
var msg = $"Failed to obtain sheet {sheet.Name} from game data.";
results.Results.Add(ValidationResult.Failed(sheet.Name, ValidatorName(), msg));
return results;
}
// Get all link fields with null condition, 1 target
var linkFields = fields.Where(f => f.Field is { Type: FieldType.Link, Condition: null, Targets.Count: 1 }).ToList();
var linkVals = new List<LinkTargetData>();
foreach (var field in linkFields)
{
var offset = field.Definition.Offset;
var columnValues = new HashSet<long>();
foreach (var row in dataFile.GetRowParsers())
{
var columnValue = ReadColumnIntegerValue(dataFile, row, field);
if (columnValue == null)
{
var msg = $"Row {row.RowId} column {field.Field.Name}@0x{offset:X} type {field.Definition.Type} failed to read properly.";
results.Results.Add(ValidationResult.Failed(sheet.Name, ValidatorName(), msg));
break; // don't spam the same error
}
columnValues.Add(columnValue.Value);
}
// our filtering ensures we have exactly one target
var targetData = new LinkTargetData
{
SourceSheet = sheet.Name,
Source = field,
TargetSheet = field.Field.Targets[0],
TargetKeys = columnValues,
};
linkVals.Add(targetData);
}
foreach (var linkData in linkVals.Distinct())
{
var sheetName = linkData.TargetSheet;
var sheetKeys = linkData.TargetKeys;
var linkedDataFile = GameData.Excel.GetSheetRaw(sheetName);
if (linkedDataFile == null)
{
var msg = $"Source {linkData.SourceSheet} field {linkData.Source.Field.Name}@0x{linkData.Source.Definition.Offset:X} references non-existent sheet {sheetName}.";
results.Add(ValidationResult.Error(sheet.Name, ValidatorName(), msg));
continue;
}
foreach (var key in sheetKeys)
{
if (_ignoredValues.TryGetValue(sheetName, out var ignoredKeys) && ignoredKeys.Contains((int)key)) continue;
if (_ignoredValues.TryGetValue("all", out ignoredKeys) && ignoredKeys.Contains((int)key)) continue;
if (linkedDataFile.GetRow((uint)key) == null)
{
var display = $"{linkData.Source.Field.Name}@0x{linkData.Source.Definition.Offset:X}";
var msg = $"Source {linkData.SourceSheet} field {display} references {sheetName} key '{key}' which does not exist.";
results.Add(ValidationResult.Warning(sheet.Name, ValidatorName(), msg));
}
}
}
if (results.Results.Count == 0)
return ValidationResults.Success(sheet.Name, ValidatorName());
return results;
}
}

Some files were not shown because too many files have changed in this diff Show more