Files
SabreTools/SabreTools.DatFiles/Formats/SabreJSON.cs

564 lines
21 KiB
C#
Raw Normal View History

2020-06-15 22:31:46 -07:00
using System;
using System.Collections.Generic;
using System.IO;
2020-12-09 00:42:22 -08:00
using System.Linq;
2020-06-15 22:31:46 -07:00
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
2020-12-08 13:23:59 -08:00
using SabreTools.Core;
using SabreTools.Core.Tools;
2020-12-08 15:15:41 -08:00
using SabreTools.DatItems;
2021-02-02 10:23:43 -08:00
using SabreTools.DatItems.Formats;
2020-06-15 22:31:46 -07:00
namespace SabreTools.DatFiles.Formats
2020-06-15 22:31:46 -07:00
{
/// <summary>
/// Represents parsing and writing of a reference SabreDAT JSON
2020-06-15 22:31:46 -07:00
/// </summary>
2020-09-07 22:40:27 -07:00
internal class SabreJSON : DatFile
2020-06-15 22:31:46 -07:00
{
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public SabreJSON(DatFile? datFile)
: base(datFile)
2020-06-15 22:31:46 -07:00
{
}
/// <inheritdoc/>
public override void ParseFile(string filename, int indexId, bool keep, bool statsOnly = false, bool throwOnError = false)
2020-06-16 11:27:36 -07:00
{
// Prepare all internal variables
StreamReader sr = new(System.IO.File.OpenRead(filename), new UTF8Encoding(false));
JsonTextReader jtr = new(sr);
2020-06-16 11:27:36 -07:00
// If we got a null reader, just return
if (jtr == null)
return;
// Otherwise, read the file to the end
try
{
jtr.Read();
while (!sr.EndOfStream)
{
// Skip everything not a property name
if (jtr.TokenType != JsonToken.PropertyName)
{
jtr.Read();
continue;
}
switch (jtr.Value)
{
// Header value
case "header":
2020-08-24 11:56:49 -07:00
ReadHeader(jtr);
2020-06-16 11:27:36 -07:00
jtr.Read();
break;
// Machine array
case "machines":
ReadMachines(jtr, statsOnly, filename, indexId);
2020-06-16 11:27:36 -07:00
jtr.Read();
break;
default:
jtr.Read();
break;
}
}
}
catch (Exception ex) when (!throwOnError)
2020-06-16 11:27:36 -07:00
{
logger.Warning($"Exception found while parsing '{filename}': {ex}");
2020-06-16 11:27:36 -07:00
}
jtr.Close();
}
/// <summary>
/// Read header information
/// </summary>
/// <param name="jtr">JsonTextReader to use to parse the header</param>
2020-08-24 11:56:49 -07:00
private void ReadHeader(JsonTextReader jtr)
2020-06-16 11:27:36 -07:00
{
// If the reader is invalid, skip
if (jtr == null)
return;
2020-08-24 11:56:49 -07:00
// Read in the header and apply any new fields
2020-06-16 11:27:36 -07:00
jtr.Read();
JsonSerializer js = new();
DatHeader? header = js.Deserialize<DatHeader>(jtr);
2020-08-24 11:56:49 -07:00
Header.ConditionalCopy(header);
2020-06-16 11:27:36 -07:00
}
/// <summary>
/// Read machine array information
/// </summary>
/// <param name="itr">JsonTextReader to use to parse the machine</param>
/// <param name="statsOnly">True to only add item statistics while parsing, false otherwise</param>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
private void ReadMachines(JsonTextReader jtr, bool statsOnly, string filename, int indexId)
2020-06-16 11:27:36 -07:00
{
// If the reader is invalid, skip
if (jtr == null)
return;
2020-08-24 14:29:00 -07:00
// Read in the machine array
2020-06-16 11:27:36 -07:00
jtr.Read();
JsonSerializer js = new();
JArray? machineArray = js.Deserialize<JArray>(jtr);
2020-06-16 11:27:36 -07:00
2020-08-24 14:29:00 -07:00
// Loop through each machine object and process
2024-02-28 19:19:50 -05:00
foreach (JObject machineObj in (machineArray ?? []).Cast<JObject>())
2020-08-24 14:29:00 -07:00
{
ReadMachine(machineObj, statsOnly, filename, indexId);
2020-06-16 11:27:36 -07:00
}
}
/// <summary>
2020-08-24 11:56:49 -07:00
/// Read machine object information
2020-06-16 11:27:36 -07:00
/// </summary>
2020-08-24 14:29:00 -07:00
/// <param name="machineObj">JObject representing a single machine</param>
/// <param name="statsOnly">True to only add item statistics while parsing, false otherwise</param>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
private void ReadMachine(JObject machineObj, bool statsOnly, string filename, int indexId)
2020-06-16 11:27:36 -07:00
{
2020-08-24 14:29:00 -07:00
// If object is invalid, skip it
if (machineObj == null)
2020-06-16 11:27:36 -07:00
return;
// Prepare internal variables
Machine? machine = null;
2020-06-16 11:27:36 -07:00
2020-08-24 14:29:00 -07:00
// Read the machine info, if possible
if (machineObj.ContainsKey("machine"))
machine = machineObj["machine"]?.ToObject<Machine>();
2020-06-16 11:27:36 -07:00
2020-08-24 14:29:00 -07:00
// Read items, if possible
if (machineObj.ContainsKey("items"))
ReadItems(machineObj["items"] as JArray, statsOnly, filename, indexId, machine);
2020-06-16 11:27:36 -07:00
}
/// <summary>
/// Read item array information
/// </summary>
2020-08-24 14:29:00 -07:00
/// <param name="itemsArr">JArray representing the items list</param>
/// <param name="statsOnly">True to only add item statistics while parsing, false otherwise</param>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
/// <param name="machine">Machine information to add to the parsed items</param>
2020-06-16 11:27:36 -07:00
private void ReadItems(
JArray? itemsArr,
bool statsOnly,
2020-06-16 11:27:36 -07:00
// Standard Dat parsing
string filename,
int indexId,
2020-06-16 11:27:36 -07:00
// Miscellaneous
Machine? machine)
2020-06-16 11:27:36 -07:00
{
2020-08-24 14:29:00 -07:00
// If the array is invalid, skip
if (itemsArr == null)
2020-06-16 11:27:36 -07:00
return;
2020-08-24 14:29:00 -07:00
// Loop through each datitem object and process
2024-02-28 19:19:50 -05:00
foreach (JObject itemObj in itemsArr.Cast<JObject>())
2020-06-16 11:27:36 -07:00
{
ReadItem(itemObj, statsOnly, filename, indexId, machine);
2020-06-16 11:27:36 -07:00
}
}
/// <summary>
/// Read item information
/// </summary>
2020-08-24 14:29:00 -07:00
/// <param name="machineObj">JObject representing a single datitem</param>
/// <param name="statsOnly">True to only add item statistics while parsing, false otherwise</param>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
/// <param name="machine">Machine information to add to the parsed items</param>
2020-06-16 11:27:36 -07:00
private void ReadItem(
2020-08-24 14:29:00 -07:00
JObject itemObj,
bool statsOnly,
2020-06-16 11:27:36 -07:00
// Standard Dat parsing
string filename,
int indexId,
2020-06-16 11:27:36 -07:00
// Miscellaneous
Machine? machine)
2020-06-16 11:27:36 -07:00
{
2020-08-24 14:29:00 -07:00
// If we have an empty item, skip it
if (itemObj == null)
2020-06-16 11:27:36 -07:00
return;
// Prepare internal variables
DatItem? datItem = null;
2020-06-16 11:27:36 -07:00
2020-08-24 14:29:00 -07:00
// Read the datitem info, if possible
if (itemObj.ContainsKey("datitem"))
2020-06-16 11:27:36 -07:00
{
JToken? datItemObj = itemObj["datitem"];
if (datItemObj == null)
return;
2024-03-05 15:24:11 -05:00
switch (datItemObj.Value<string>("type").AsEnumValue<ItemType>())
2020-06-16 11:27:36 -07:00
{
2020-09-01 11:34:52 -07:00
case ItemType.Adjuster:
datItem = datItemObj.ToObject<Adjuster>();
break;
case ItemType.Analog:
datItem = datItemObj.ToObject<Analog>();
break;
2020-08-24 14:29:00 -07:00
case ItemType.Archive:
datItem = datItemObj.ToObject<Archive>();
2020-06-16 11:27:36 -07:00
break;
2020-08-24 14:29:00 -07:00
case ItemType.BiosSet:
datItem = datItemObj.ToObject<BiosSet>();
break;
case ItemType.Blank:
datItem = datItemObj.ToObject<Blank>();
break;
2020-08-25 22:48:46 -07:00
case ItemType.Chip:
datItem = datItemObj.ToObject<Chip>();
break;
case ItemType.Condition:
datItem = datItemObj.ToObject<Condition>();
break;
2020-09-01 12:04:35 -07:00
case ItemType.Configuration:
datItem = datItemObj.ToObject<Configuration>();
break;
case ItemType.ConfLocation:
datItem = datItemObj.ToObject<ConfLocation>();
break;
case ItemType.ConfSetting:
datItem = datItemObj.ToObject<ConfSetting>();
break;
2020-09-02 23:02:06 -07:00
case ItemType.Control:
datItem = datItemObj.ToObject<Control>();
break;
2020-09-04 14:10:35 -07:00
case ItemType.DataArea:
datItem = datItemObj.ToObject<DataArea>();
break;
2020-09-02 17:09:19 -07:00
case ItemType.Device:
datItem = datItemObj.ToObject<Device>();
break;
2024-03-10 20:39:54 -04:00
case ItemType.DeviceRef:
datItem = datItemObj.ToObject<DeviceRef>();
break;
case ItemType.DipLocation:
datItem = datItemObj.ToObject<DipLocation>();
break;
case ItemType.DipValue:
datItem = datItemObj.ToObject<DipValue>();
break;
2020-09-01 13:36:32 -07:00
case ItemType.DipSwitch:
datItem = datItemObj.ToObject<DipSwitch>();
break;
2020-08-24 14:29:00 -07:00
case ItemType.Disk:
datItem = datItemObj.ToObject<Disk>();
break;
2020-09-04 14:10:35 -07:00
case ItemType.DiskArea:
datItem = datItemObj.ToObject<DiskArea>();
break;
2020-09-02 21:36:14 -07:00
case ItemType.Display:
datItem = datItemObj.ToObject<Display>();
break;
2020-09-02 15:38:10 -07:00
case ItemType.Driver:
datItem = datItemObj.ToObject<Driver>();
break;
2020-09-02 16:37:01 -07:00
case ItemType.Extension:
datItem = datItemObj.ToObject<Extension>();
break;
2020-09-02 13:31:50 -07:00
case ItemType.Feature:
datItem = datItemObj.ToObject<Feature>();
break;
2020-09-02 23:31:35 -07:00
case ItemType.Info:
datItem = datItemObj.ToObject<Info>();
break;
2020-09-02 21:59:26 -07:00
case ItemType.Input:
datItem = datItemObj.ToObject<Input>();
break;
2020-09-02 16:46:17 -07:00
case ItemType.Instance:
datItem = datItemObj.ToObject<Instance>();
break;
case ItemType.Media:
datItem = datItemObj.ToObject<Media>();
break;
2020-09-04 14:10:35 -07:00
case ItemType.Part:
datItem = datItemObj.ToObject<Part>();
break;
2020-09-03 13:20:56 -07:00
case ItemType.PartFeature:
datItem = datItemObj.ToObject<PartFeature>();
break;
2020-09-02 17:22:31 -07:00
case ItemType.Port:
datItem = datItemObj.ToObject<Port>();
break;
2020-09-01 11:34:52 -07:00
case ItemType.RamOption:
datItem = datItemObj.ToObject<RamOption>();
break;
2020-08-24 14:29:00 -07:00
case ItemType.Release:
datItem = datItemObj.ToObject<Release>();
break;
2023-04-19 12:26:54 -04:00
case ItemType.ReleaseDetails:
datItem = datItemObj.ToObject<ReleaseDetails>();
break;
2020-08-24 14:29:00 -07:00
case ItemType.Rom:
datItem = datItemObj.ToObject<Rom>();
break;
case ItemType.Sample:
datItem = datItemObj.ToObject<Sample>();
2020-08-20 21:15:37 -07:00
break;
2023-04-19 12:26:54 -04:00
case ItemType.Serials:
datItem = datItemObj.ToObject<Serials>();
break;
2024-03-10 20:39:54 -04:00
case ItemType.SharedFeat:
datItem = datItemObj.ToObject<SharedFeat>();
2020-09-03 00:48:07 -07:00
break;
2020-09-01 16:21:55 -07:00
case ItemType.Slot:
datItem = datItemObj.ToObject<Slot>();
break;
2020-09-02 22:44:54 -07:00
case ItemType.SlotOption:
datItem = datItemObj.ToObject<SlotOption>();
break;
2020-08-31 23:26:07 -07:00
case ItemType.SoftwareList:
2021-02-02 10:23:43 -08:00
datItem = datItemObj.ToObject<DatItems.Formats.SoftwareList>();
2020-08-31 23:26:07 -07:00
break;
2020-09-02 12:51:21 -07:00
case ItemType.Sound:
2020-09-02 13:31:50 -07:00
datItem = datItemObj.ToObject<Sound>();
2020-09-02 12:51:21 -07:00
break;
2023-04-19 12:26:54 -04:00
case ItemType.SourceDetails:
datItem = datItemObj.ToObject<SourceDetails>();
break;
2020-08-24 11:56:49 -07:00
}
2020-08-24 14:29:00 -07:00
}
2020-08-20 21:15:37 -07:00
2020-08-24 14:29:00 -07:00
// If we got a valid datitem, copy machine info and add
if (datItem != null)
{
datItem.CopyMachineInformation(machine);
2024-03-10 16:49:07 -04:00
datItem.SetFieldValue<Source?>(DatItem.SourceKey, new Source { Index = indexId, Name = filename });
ParseAddHelper(datItem, statsOnly);
2020-08-24 11:56:49 -07:00
}
}
2020-08-20 21:15:37 -07:00
/// <inheritdoc/>
public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false)
2020-08-24 11:56:49 -07:00
{
try
{
2021-02-03 11:22:09 -08:00
logger.User($"Writing to '{outfile}'...");
2023-04-17 13:22:35 -04:00
FileStream fs = System.IO.File.Create(outfile);
2020-08-24 11:56:49 -07:00
// If we get back null for some reason, just log and return
if (fs == null)
{
logger.Warning($"File '{outfile}' could not be created for writing! Please check to see if the file is writable");
2020-08-24 11:56:49 -07:00
return false;
}
2020-06-15 22:31:46 -07:00
StreamWriter sw = new(fs, new UTF8Encoding(false));
JsonTextWriter jtw = new(sw)
{
Formatting = Formatting.Indented,
IndentChar = '\t',
Indentation = 1
};
2020-06-15 22:31:46 -07:00
// Write out the header
WriteHeader(jtw);
// Write out each of the machines and roms
string? lastgame = null;
2020-06-15 22:31:46 -07:00
2020-07-26 21:00:30 -07:00
// Use a sorted list of games to output
2020-07-26 22:34:45 -07:00
foreach (string key in Items.SortedKeys)
2020-06-15 22:31:46 -07:00
{
ConcurrentList<DatItem> datItems = Items.FilteredItems(key);
2020-06-15 22:31:46 -07:00
2020-09-25 20:25:29 -07:00
// If this machine doesn't contain any writable items, skip
if (!ContainsWritable(datItems))
continue;
2020-06-15 22:31:46 -07:00
// Resolve the names in the block
2020-08-28 15:06:07 -07:00
datItems = DatItem.ResolveNames(datItems);
2020-06-15 22:31:46 -07:00
2020-08-28 15:06:07 -07:00
for (int index = 0; index < datItems.Count; index++)
2020-06-15 22:31:46 -07:00
{
2020-08-28 15:06:07 -07:00
DatItem datItem = datItems[index];
2020-06-15 22:31:46 -07:00
// If we have a different game and we're not at the start of the list, output the end of last item
if (lastgame != null && !string.Equals(lastgame, datItem.GetFieldValue<Machine>(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey), StringComparison.OrdinalIgnoreCase))
2024-02-28 19:19:50 -05:00
SabreJSON.WriteEndGame(jtw);
2020-06-15 22:31:46 -07:00
// If we have a new game, output the beginning of the new item
if (lastgame == null || !string.Equals(lastgame, datItem.GetFieldValue<Machine>(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey), StringComparison.OrdinalIgnoreCase))
2024-02-28 19:19:50 -05:00
SabreJSON.WriteStartGame(jtw, datItem);
2020-08-28 15:06:07 -07:00
// Check for a "null" item
datItem = ProcessNullifiedItem(datItem);
// Write out the item if we're not ignoring
if (!ShouldIgnore(datItem, ignoreblanks))
WriteDatItem(jtw, datItem);
2020-06-15 22:31:46 -07:00
// Set the new data to compare against
lastgame = datItem.GetFieldValue<Machine>(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey);
2020-06-15 22:31:46 -07:00
}
}
// Write the file footer out
2024-02-28 19:19:50 -05:00
SabreJSON.WriteFooter(jtw);
2020-06-15 22:31:46 -07:00
2021-02-03 11:22:09 -08:00
logger.User($"'{outfile}' written!{Environment.NewLine}");
2020-06-15 22:31:46 -07:00
jtw.Close();
fs.Dispose();
}
catch (Exception ex) when (!throwOnError)
2020-06-15 22:31:46 -07:00
{
logger.Error(ex);
2020-06-15 22:31:46 -07:00
return false;
}
return true;
}
/// <summary>
2020-08-24 20:23:57 -07:00
/// Write out DAT header using the supplied JsonTextWriter
2020-06-15 22:31:46 -07:00
/// </summary>
/// <param name="jtw">JsonTextWriter to output to</param>
private void WriteHeader(JsonTextWriter jtw)
2020-06-15 22:31:46 -07:00
{
jtw.WriteStartObject();
2020-06-15 22:31:46 -07:00
// Write the DatHeader
jtw.WritePropertyName("header");
JsonSerializer js = new() { Formatting = Formatting.Indented };
js.Serialize(jtw, Header);
2020-09-15 12:12:13 -07:00
jtw.WritePropertyName("machines");
jtw.WriteStartArray();
2020-06-15 22:31:46 -07:00
jtw.Flush();
2020-06-15 22:31:46 -07:00
}
/// <summary>
2020-08-24 20:23:57 -07:00
/// Write out Game start using the supplied JsonTextWriter
2020-06-15 22:31:46 -07:00
/// </summary>
/// <param name="jtw">JsonTextWriter to output to</param>
/// <param name="datItem">DatItem object to be output</param>
2024-02-28 19:19:50 -05:00
private static void WriteStartGame(JsonTextWriter jtw, DatItem datItem)
2020-06-15 22:31:46 -07:00
{
// No game should start with a path separator
if (!string.IsNullOrEmpty(datItem.GetFieldValue<Machine>(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey)))
datItem.GetFieldValue<Machine>(DatItem.MachineKey)!.SetFieldValue<string?>(Models.Metadata.Machine.NameKey, datItem.GetFieldValue<Machine>(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey)!.TrimStart(Path.DirectorySeparatorChar));
2020-06-15 22:31:46 -07:00
// Build the state
jtw.WriteStartObject();
2020-06-15 22:31:46 -07:00
// Write the Machine
jtw.WritePropertyName("machine");
JsonSerializer js = new() { Formatting = Formatting.Indented };
2024-03-10 16:49:07 -04:00
js.Serialize(jtw, datItem.GetFieldValue<Machine>(DatItem.MachineKey)!);
2020-08-20 22:42:04 -07:00
jtw.WritePropertyName("items");
jtw.WriteStartArray();
2020-06-15 22:31:46 -07:00
jtw.Flush();
2020-06-15 22:31:46 -07:00
}
/// <summary>
2020-08-24 20:23:57 -07:00
/// Write out Game end using the supplied JsonTextWriter
2020-06-15 22:31:46 -07:00
/// </summary>
/// <param name="jtw">JsonTextWriter to output to</param>
2024-02-28 19:19:50 -05:00
private static void WriteEndGame(JsonTextWriter jtw)
2020-06-15 22:31:46 -07:00
{
// End items
jtw.WriteEndArray();
2020-09-15 12:12:13 -07:00
// End machine
jtw.WriteEndObject();
2020-06-15 22:31:46 -07:00
jtw.Flush();
2020-06-15 22:31:46 -07:00
}
/// <summary>
2020-08-24 20:23:57 -07:00
/// Write out DatItem using the supplied JsonTextWriter
2020-06-15 22:31:46 -07:00
/// </summary>
/// <param name="jtw">JsonTextWriter to output to</param>
/// <param name="datItem">DatItem object to be output</param>
private void WriteDatItem(JsonTextWriter jtw, DatItem datItem)
2020-06-15 22:31:46 -07:00
{
// Pre-process the item name
ProcessItemName(datItem, true);
2020-06-15 22:31:46 -07:00
// Build the state
jtw.WriteStartObject();
// Write the DatItem
jtw.WritePropertyName("datitem");
JsonSerializer js = new() { ContractResolver = new BaseFirstContractResolver(), Formatting = Formatting.Indented };
js.Serialize(jtw, datItem);
2020-09-15 12:12:13 -07:00
// End item
jtw.WriteEndObject();
2020-06-15 22:31:46 -07:00
jtw.Flush();
2020-06-15 22:31:46 -07:00
}
/// <summary>
2020-08-24 20:23:57 -07:00
/// Write out DAT footer using the supplied JsonTextWriter
2020-06-15 22:31:46 -07:00
/// </summary>
/// <param name="jtw">JsonTextWriter to output to</param>
2024-02-28 19:19:50 -05:00
private static void WriteFooter(JsonTextWriter jtw)
2020-06-15 22:31:46 -07:00
{
// End items
jtw.WriteEndArray();
2020-06-15 22:31:46 -07:00
// End machine
jtw.WriteEndObject();
2020-06-15 22:31:46 -07:00
// End machines
jtw.WriteEndArray();
2020-09-15 12:12:13 -07:00
// End file
jtw.WriteEndObject();
2020-06-15 22:31:46 -07:00
jtw.Flush();
2020-06-15 22:31:46 -07:00
}
2020-12-09 00:42:22 -08:00
// https://github.com/dotnet/runtime/issues/728
private class BaseFirstContractResolver : DefaultContractResolver
{
public BaseFirstContractResolver()
{
NamingStrategy = new CamelCaseNamingStrategy();
}
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
return base.CreateProperties(type, memberSerialization)
.Where(p => p != null)
.OrderBy(p => BaseTypesAndSelf(p.DeclaringType).Count())
.ToList();
2020-12-09 00:42:22 -08:00
static IEnumerable<Type?> BaseTypesAndSelf(Type? t)
2020-12-09 00:42:22 -08:00
{
while (t != null)
{
yield return t;
t = t.BaseType;
}
}
}
}
2020-06-15 22:31:46 -07:00
}
}