mirror of
https://github.com/xivdev/EXDSchema.git
synced 2025-06-06 16:17:46 +00:00
Update schema, code, and docs to reflect the list
type and new list declaration method.
This commit is contained in:
parent
b4e19ad8fb
commit
a448444c39
5 changed files with 99 additions and 53 deletions
|
@ -32,7 +32,7 @@ The schema includes the following:
|
||||||
- Maps out-of-the-box to a very simple object mapping
|
- Maps out-of-the-box to a very simple object mapping
|
||||||
- JSON schema for the schema itself, providing IDE completion and error-checking
|
- JSON schema for the schema itself, providing IDE completion and error-checking
|
||||||
- Example code
|
- Example code
|
||||||
- A C# project that perform the conversion from the
|
- A C# project that perform a partial conversion from the
|
||||||
common JSON schemas ("SC Schema") to the EXDSchema format
|
common JSON schemas ("SC Schema") to the EXDSchema format
|
||||||
- A C# project that will validate EXDSchemas against a given game installation
|
- A C# project that will validate EXDSchemas against a given game installation
|
||||||
- A tool for editing schemas and viewing the results of parsing on-the-fly
|
- A tool for editing schemas and viewing the results of parsing on-the-fly
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using Lumina.Data.Structs.Excel;
|
using Lumina.Data.Structs.Excel;
|
||||||
|
using SchemaConverter.New;
|
||||||
|
|
||||||
namespace SchemaConverter;
|
namespace SchemaConverter;
|
||||||
|
|
||||||
|
@ -11,11 +12,12 @@ public class ColumnInfo
|
||||||
public ExcelColumnDataType DataType { get; set; }
|
public ExcelColumnDataType DataType { get; set; }
|
||||||
public bool IsArrayMember { get; set; }
|
public bool IsArrayMember { get; set; }
|
||||||
public int? ArrayIndex { get; set; }
|
public int? ArrayIndex { get; set; }
|
||||||
public New.Link? Link { get; set; }
|
public Condition? Condition { get; set; }
|
||||||
|
public List<string>? Targets { get; set; }
|
||||||
|
|
||||||
public ColumnInfo() { }
|
public ColumnInfo() { }
|
||||||
|
|
||||||
public ColumnInfo(Old.Definition def, int index, bool isArrayMember, int? arrayIndex, New.Link link)
|
public ColumnInfo(Old.Definition def, int index, bool isArrayMember, int? arrayIndex, Condition? condition, List<string>? targets)
|
||||||
{
|
{
|
||||||
var converterType = def.Converter?.Type;
|
var converterType = def.Converter?.Type;
|
||||||
var nameSuffix = isArrayMember ? $"[{arrayIndex}]" : "";
|
var nameSuffix = isArrayMember ? $"[{arrayIndex}]" : "";
|
||||||
|
@ -24,8 +26,11 @@ public class ColumnInfo
|
||||||
Type = converterType;
|
Type = converterType;
|
||||||
IsArrayMember = isArrayMember;
|
IsArrayMember = isArrayMember;
|
||||||
ArrayIndex = arrayIndex;
|
ArrayIndex = arrayIndex;
|
||||||
Link = link;
|
Condition = condition;
|
||||||
|
Targets = targets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public override string ToString() => $"{Name} ({Index}@{BitOffset / 8}&{BitOffset % 8}) {Type} {IsArrayMember} {ArrayIndex}";
|
public override string ToString() => $"{Name} ({Index}@{BitOffset / 8}&{BitOffset % 8}) {Type} {IsArrayMember} {ArrayIndex}";
|
||||||
}
|
}
|
|
@ -14,6 +14,7 @@ public enum FieldType
|
||||||
Icon,
|
Icon,
|
||||||
ModelId,
|
ModelId,
|
||||||
Color,
|
Color,
|
||||||
|
Link,
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Sheet
|
public class Sheet
|
||||||
|
@ -47,24 +48,18 @@ public class Field
|
||||||
public List<Field>? Fields { get; set; }
|
public List<Field>? Fields { get; set; }
|
||||||
|
|
||||||
[YamlMember(5)]
|
[YamlMember(5)]
|
||||||
public Link? Link { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Link
|
|
||||||
{
|
|
||||||
[YamlMember(0)]
|
|
||||||
public Condition? Condition { get; set; }
|
public Condition? Condition { get; set; }
|
||||||
|
|
||||||
[YamlMember(1)]
|
[YamlMember(6)]
|
||||||
[YamlStyle(YamlStyle.Flow)]
|
[YamlStyle(YamlStyle.Flow)]
|
||||||
public List<string> Target { get; set; }
|
public List<string>? Targets { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Condition
|
public class Condition
|
||||||
{
|
{
|
||||||
[YamlMember(0)]
|
[YamlMember(0)]
|
||||||
public string Switch { get; set; }
|
public string? Switch { get; set; }
|
||||||
|
|
||||||
[YamlMember(1)]
|
[YamlMember(1)]
|
||||||
public Dictionary<int, List<string>> Cases { get; set; }
|
public Dictionary<int, List<string>>? Cases { get; set; }
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
using Lumina;
|
using Lumina;
|
||||||
using Lumina.Data.Files.Excel;
|
using Lumina.Data.Files.Excel;
|
||||||
using Lumina.Data.Structs.Excel;
|
using Lumina.Data.Structs.Excel;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
@ -9,7 +9,7 @@ namespace SchemaConverter;
|
||||||
|
|
||||||
public class SchemaConverter
|
public class SchemaConverter
|
||||||
{
|
{
|
||||||
private static readonly New.Link _genericReferenceLink = new() {Target = new List<string>()};
|
private static readonly List<string> _genericReferenceLink = new();
|
||||||
|
|
||||||
private static void EnumerateGenericReferenceTargets(string oldSchemaDir)
|
private static void EnumerateGenericReferenceTargets(string oldSchemaDir)
|
||||||
{
|
{
|
||||||
|
@ -22,7 +22,7 @@ public class SchemaConverter
|
||||||
targets.Add(oldSchema.SheetName);
|
targets.Add(oldSchema.SheetName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_genericReferenceLink.Target.AddRange(targets);
|
_genericReferenceLink.AddRange(targets);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
|
@ -128,8 +128,11 @@ public class SchemaConverter
|
||||||
"color" => FieldType.Color,
|
"color" => FieldType.Color,
|
||||||
_ => FieldType.Scalar,
|
_ => FieldType.Scalar,
|
||||||
},
|
},
|
||||||
Link = col.Link,
|
Condition = col.Condition,
|
||||||
|
Targets = col.Targets,
|
||||||
};
|
};
|
||||||
|
if (field.Condition != null || field.Targets != null)
|
||||||
|
field.Type = FieldType.Link;
|
||||||
newSchema.Fields.Add(field);
|
newSchema.Fields.Add(field);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,7 +169,8 @@ public class SchemaConverter
|
||||||
|
|
||||||
private static void EmitSingle(List<ColumnInfo> infos, Old.Definition definition, bool isArray, int? arrayIndex, ref int index)
|
private static void EmitSingle(List<ColumnInfo> infos, Old.Definition definition, bool isArray, int? arrayIndex, ref int index)
|
||||||
{
|
{
|
||||||
infos.Add(new ColumnInfo(definition, index++, isArray, arrayIndex, ConvertLink(definition.Converter)));
|
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)
|
private static void EmitRepeat(List<ColumnInfo> infos, Old.Definition definition, ref int index)
|
||||||
|
@ -199,40 +203,38 @@ public class SchemaConverter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static New.Link ConvertLink(Old.Converter oldLink)
|
private static (Condition? condition, List<string>? targets) ConvertLink(Old.Converter oldLink)
|
||||||
{
|
{
|
||||||
if (oldLink == null) return null;
|
if (oldLink == null) return (null, null);
|
||||||
|
|
||||||
var newLink = new New.Link();
|
|
||||||
if (oldLink.Type == "generic")
|
if (oldLink.Type == "generic")
|
||||||
{
|
{
|
||||||
return _genericReferenceLink;
|
return (null, _genericReferenceLink);
|
||||||
}
|
}
|
||||||
else if (oldLink.Type == "link")
|
else if (oldLink.Type == "link")
|
||||||
{
|
{
|
||||||
newLink.Target = new List<string>() {oldLink.Target};
|
return (null, new List<string> {oldLink.Target});
|
||||||
}
|
}
|
||||||
else if (oldLink.Type == "multiref")
|
else if (oldLink.Type == "multiref")
|
||||||
{
|
{
|
||||||
newLink.Target = oldLink.Targets;
|
return (null, oldLink.Targets);
|
||||||
}
|
}
|
||||||
else if (oldLink.Type == "complexlink")
|
else if (oldLink.Type == "complexlink")
|
||||||
{
|
{
|
||||||
if (oldLink.Links[0].Project != null)
|
if (oldLink.Links[0].Project != null)
|
||||||
{
|
{
|
||||||
return null;
|
return (null, null);
|
||||||
}
|
}
|
||||||
newLink.Condition = new Condition();
|
var condition = new Condition();
|
||||||
newLink.Condition.Switch = oldLink.Links[0].When.Key;
|
condition.Switch = oldLink.Links[0].When.Key;
|
||||||
newLink.Condition.Cases = new Dictionary<int, List<string>>();
|
condition.Cases = new Dictionary<int, List<string>>();
|
||||||
foreach (var oldLinkLink in oldLink.Links)
|
foreach (var oldLinkLink in oldLink.Links)
|
||||||
newLink.Condition.Cases.Add(oldLinkLink.When.Value, oldLinkLink.Sheets);
|
condition.Cases.Add(oldLinkLink.When.Value, oldLinkLink.LinkedSheet == null ? oldLinkLink.Sheets : new List<string> { oldLinkLink.LinkedSheet });
|
||||||
|
return (condition, null);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return null;
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return newLink;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
84
Usage.md
84
Usage.md
|
@ -50,7 +50,7 @@ fields:
|
||||||
This schema is valid because it is accurate in structure. It defines a field for each column in the EXH file as of 6.48.
|
This schema is valid because it is accurate in structure. It defines a field for each column in the EXH file as of 6.48.
|
||||||
|
|
||||||
### Types
|
### Types
|
||||||
Valid types for fields in a schema are `scalar`, `array`, `icon`, `modelId`, and `color`.
|
Valid types for fields in a schema are `scalar`, `link`, `array`, `icon`, `modelId`, and `color`.
|
||||||
|
|
||||||
#### scalar
|
#### scalar
|
||||||
The default type. If the `type` is omitted from a field, it will be assumed to be a `scalar`. Effectively does nothing except tell consumers that
|
The default type. If the `type` is omitted from a field, it will be assumed to be a `scalar`. Effectively does nothing except tell consumers that
|
||||||
|
@ -96,7 +96,37 @@ Some fields contain an RGB value for color in the ARGB format with no alpha. Thi
|
||||||
columns' fields as actual colors rather than the raw value.
|
columns' fields as actual colors rather than the raw value.
|
||||||
|
|
||||||
#### array
|
#### array
|
||||||
Array fields provide the ability to group and repeat nested structures. For example, the notorious SpecialShop sheet:
|
Array fields provide the ability to group and repeat nested structures. These are the methods of declaring an array:
|
||||||
|
```yml
|
||||||
|
name: ExampleSheet
|
||||||
|
fields:
|
||||||
|
- name: Array of scalars
|
||||||
|
comment: This array is just an array of scalars
|
||||||
|
type: array
|
||||||
|
count: 2
|
||||||
|
- name: Erroneous array
|
||||||
|
comment: This array fails schema validation because it contains the fields key with no fields
|
||||||
|
type: array
|
||||||
|
count: 2
|
||||||
|
fields: []
|
||||||
|
- name: Array of single explicit column
|
||||||
|
comment: Schema consumers should consider this an array of scalars that are also a link
|
||||||
|
type: array
|
||||||
|
count: 2
|
||||||
|
fields:
|
||||||
|
- type: link
|
||||||
|
targets: [Item]
|
||||||
|
- name: Array of structs
|
||||||
|
comment: This array is a list of structs
|
||||||
|
type: array
|
||||||
|
count: 2
|
||||||
|
fields:
|
||||||
|
- type: scalar
|
||||||
|
- type: scalar
|
||||||
|
```
|
||||||
|
The comment on each array declaration describes what the array is declaring.
|
||||||
|
|
||||||
|
For a more concrete example, let's look at `SpecialShop`:
|
||||||
```yml
|
```yml
|
||||||
name: SpecialShop
|
name: SpecialShop
|
||||||
fields:
|
fields:
|
||||||
|
@ -114,17 +144,29 @@ fields:
|
||||||
- name: Item
|
- name: Item
|
||||||
type: array
|
type: array
|
||||||
count: 2
|
count: 2
|
||||||
|
fields:
|
||||||
|
- type: link
|
||||||
|
targets: [Item]
|
||||||
- name: Category
|
- name: Category
|
||||||
type: array
|
type: array
|
||||||
count: 2
|
count: 2
|
||||||
|
fields:
|
||||||
|
- type: link
|
||||||
|
targets: [SpecialShopItemCategory]
|
||||||
- name: ItemCost
|
- name: ItemCost
|
||||||
type: array
|
type: array
|
||||||
count: 3
|
count: 3
|
||||||
- name: Quest
|
- name: Quest
|
||||||
type: array
|
type: array
|
||||||
count: 2
|
count: 2
|
||||||
|
fields:
|
||||||
|
- type: link
|
||||||
|
targets: [Quest]
|
||||||
- name: Unknown
|
- name: Unknown
|
||||||
- name: AchievementUnlock
|
- name: AchievementUnlock
|
||||||
|
fields:
|
||||||
|
- type: link
|
||||||
|
targets: [Achievement]
|
||||||
- name: CollectabilityCost
|
- name: CollectabilityCost
|
||||||
type: array
|
type: array
|
||||||
count: 3
|
count: 3
|
||||||
|
@ -138,6 +180,8 @@ fields:
|
||||||
type: array
|
type: array
|
||||||
count: 3
|
count: 3
|
||||||
- name: Quest
|
- name: Quest
|
||||||
|
type: link
|
||||||
|
targets: [Quest]
|
||||||
- type: scalar
|
- type: scalar
|
||||||
- type: scalar
|
- type: scalar
|
||||||
- name: CompleteText
|
- name: CompleteText
|
||||||
|
@ -179,29 +223,29 @@ struct SpecialShop
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
As you can see, the overall schema is similar to defining structures in YML but omitting the actual data type.
|
As you can see, the overall schema is similar to defining structures in YML but omitting the actual data type.
|
||||||
This nested capability allows you to define complex structures. However, to cut down on overall parsing complexity,
|
This nested capability allows you to define complex structures. From experience, we have seen that
|
||||||
based on existing knowledge of the EXH data, **you may only nest twice.**
|
you should not need to nest more than 2 levels deep, but schema consumers should still support this.
|
||||||
|
|
||||||
### Linking
|
### Linking
|
||||||
The sheets that power the game are relational in nature, so the schema supports a few different kinds of linking.
|
The sheets that power the game are relational in nature, so the schema supports a few different kinds of linking.
|
||||||
|
|
||||||
#### Single Link
|
#### Single Link
|
||||||
To define a link, simply add a link object:
|
To define a single link, set the `type` to `link` and define the `targets` array:
|
||||||
```yml
|
```yml
|
||||||
- name: Quest
|
- name: Quest
|
||||||
link:
|
type: link
|
||||||
target: [Quest]
|
targets: [Quest]
|
||||||
```
|
```
|
||||||
Note that the link target is an array of strings. They must be sheet names, and there must be at least one sheet. To link to one sheet, leave a single sheet in the array.
|
Note that the link targets is an array of strings. They must be sheet names, and there must be at least one sheet. To link to one sheet, leave a single sheet in the array.
|
||||||
|
|
||||||
#### Multi Link
|
#### Multi Link
|
||||||
A sheet's single column can link to multiple columns:
|
A sheet's single column can link to multiple columns:
|
||||||
```yml
|
```yml
|
||||||
- name: Requirement
|
- name: Requirement
|
||||||
link:
|
type: link
|
||||||
target: [Quest, GrandCompany]
|
targets: [Quest, GrandCompany]
|
||||||
```
|
```
|
||||||
In this case, disparate sheet key ranges will provide the ability to determine which sheet a link should resolve to.
|
In this case, disparate sheet key ranges will provide the ability for consumers to determine which sheet a link should resolve to.
|
||||||
For example, if a row's `Requirement` is `2`, it will resolve to `GrandCompany`, because row `2` exists in `GrandCompany` and not in `Quest.`
|
For example, if a row's `Requirement` is `2`, it will resolve to `GrandCompany`, because row `2` exists in `GrandCompany` and not in `Quest.`
|
||||||
The same thing happens in the other direction: if `Requirement` is `69208`, it will link to `Quest` and not `GrandCompany` for the same reason.
|
The same thing happens in the other direction: if `Requirement` is `69208`, it will link to `Quest` and not `GrandCompany` for the same reason.
|
||||||
|
|
||||||
|
@ -210,14 +254,14 @@ A sheet's single column can link to multiple columns depending on another field
|
||||||
```yml
|
```yml
|
||||||
- name: Location
|
- name: Location
|
||||||
comment: PlaceName when LocationKey is 1, ContentFinderCondition when LocationKey is 4
|
comment: PlaceName when LocationKey is 1, ContentFinderCondition when LocationKey is 4
|
||||||
link:
|
type: link
|
||||||
condition:
|
condition:
|
||||||
switch: LocationKey
|
switch: LocationKey
|
||||||
cases:
|
cases:
|
||||||
1: [PlaceName]
|
1: [PlaceName]
|
||||||
4: [ContentFinderCondition]
|
4: [ContentFinderCondition]
|
||||||
```
|
```
|
||||||
The targets array is not required for conditional links.
|
The targets array is not required for conditional links, and if both are specified, the file will fail schema validation.
|
||||||
When defining the link, add a `condition` object with a `switch` key that defines the field to switch on the value of.
|
When defining the link, add a `condition` object with a `switch` key that defines the field to switch on the value of.
|
||||||
The `cases` dictionary contains arrays of the sheet to reference when the case matches.
|
The `cases` dictionary contains arrays of the sheet to reference when the case matches.
|
||||||
|
|
||||||
|
@ -242,7 +286,7 @@ Yes, the `case` dictionary may contain an *array*. This means that each case can
|
||||||
32: [Orchestrion]
|
32: [Orchestrion]
|
||||||
36: [SubmarinePart]
|
36: [SubmarinePart]
|
||||||
```
|
```
|
||||||
The `AdditionalData` column does a lot of heavy lifting. We can assume during game execution that the use of the field is heavily based on context,
|
The `AdditionalData` column in `Item` does a lot of heavy lifting. We can assume during game execution that the use of the field is heavily based on context,
|
||||||
but for research and data exploration, having the ability to define the exact sheet is very useful. Here, we can see that when `FilterGroup` is `14`,
|
but for research and data exploration, having the ability to define the exact sheet is useful. Here, we can see that when `FilterGroup` is `14`,
|
||||||
we can link to any of `HousingExterior`, `HousingInterior`, `HousingYardObject`, `HousingFurniture`, `HousingPreset`, or finally `HousingUnitedExterior`.
|
we can link to any of `HousingExterior`, `HousingInterior`, `HousingYardObject`, `HousingFurniture`, `HousingPreset`, or finally `HousingUnitedExterior`.
|
||||||
This works because the value for `AdditionalData` are distinct ranges, even when `FilterGroup` is `14`, thus allowing the definition here to behave like a multi link.
|
This works because the value for `AdditionalData` are distinct ranges, even when `FilterGroup` is `14`, thus allowing the definition here to behave like a multi link.
|
Loading…
Add table
Reference in a new issue