diff --git a/AttractMode.cs b/AttractMode.cs
new file mode 100644
index 00000000..ab008487
--- /dev/null
+++ b/AttractMode.cs
@@ -0,0 +1,14 @@
+namespace SabreTools.Serialization
+{
+ ///
+ /// Separated value serializer/deserializer for AttractMode romlists
+ ///
+ public static class AttractMode
+ {
+ public const string HeaderWithoutRomname = "#Name;Title;Emulator;CloneOf;Year;Manufacturer;Category;Players;Rotation;Control;Status;DisplayCount;DisplayType;AltRomname;AltTitle;Extra;Buttons";
+ public const int HeaderWithoutRomnameCount = 17;
+
+ public const string HeaderWithRomname = "#Romname;Title;Emulator;Cloneof;Year;Manufacturer;Category;Players;Rotation;Control;Status;DisplayCount;DisplayType;AltRomname;AltTitle;Extra;Buttons;Favourite;Tags;PlayedCount;PlayedTime;FileIsAvailable";
+ public const int HeaderWithRomnameCount = 22;
+ }
+}
\ No newline at end of file
diff --git a/CrossModel/AttractMode.Deserializer.cs b/CrossModel/AttractMode.Deserializer.cs
new file mode 100644
index 00000000..979d57b3
--- /dev/null
+++ b/CrossModel/AttractMode.Deserializer.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Linq;
+using SabreTools.Models.AttractMode;
+
+namespace SabreTools.Serialization.CrossModel
+{
+ public partial class AttractMode : IModelSerializer
+ {
+ ///
+#if NET48
+ public MetadataFile Deserialize(Models.Metadata.MetadataFile obj)
+#else
+ public MetadataFile? Deserialize(Models.Metadata.MetadataFile? obj)
+#endif
+ {
+ if (obj == null)
+ return null;
+
+ var header = obj.Read(Models.Metadata.MetadataFile.HeaderKey);
+ var metadataFile = header != null ? ConvertHeaderFromInternalModel(header) : new MetadataFile();
+
+ var machines = obj.Read(Models.Metadata.MetadataFile.MachineKey);
+ if (machines != null && machines.Any())
+ {
+ metadataFile.Row = machines
+ .Where(m => m != null)
+ .SelectMany(ConvertMachineFromInternalModel)
+ .ToArray();
+ }
+
+ return metadataFile;
+ }
+
+ ///
+ /// Convert from to
+ ///
+ private static MetadataFile ConvertHeaderFromInternalModel(Models.Metadata.Header item)
+ {
+ var metadataFile = new MetadataFile
+ {
+ Header = item.ReadStringArray(Models.Metadata.Header.HeaderKey),
+ };
+ return metadataFile;
+ }
+
+ ///
+ /// Convert from to an array of
+ ///
+ private static Row[] ConvertMachineFromInternalModel(Models.Metadata.Machine item)
+ {
+ var roms = item.Read(Models.Metadata.Machine.RomKey);
+ if (roms == null || !roms.Any())
+ return Array.Empty();
+
+ return roms
+ .Where(r => r != null)
+ .Select(rom => ConvertFromInternalModel(rom, item))
+ .ToArray();
+ }
+
+ ///
+ /// Convert from to
+ ///
+ private static Row ConvertFromInternalModel(Models.Metadata.Rom item, Models.Metadata.Machine parent)
+ {
+ var row = new Row
+ {
+ Name = parent.ReadString(Models.Metadata.Machine.NameKey),
+ Title = item.ReadString(Models.Metadata.Rom.NameKey),
+ Emulator = parent.ReadString(Models.Metadata.Machine.EmulatorKey),
+ CloneOf = parent.ReadString(Models.Metadata.Machine.CloneOfKey),
+ Year = parent.ReadString(Models.Metadata.Machine.YearKey),
+ Manufacturer = parent.ReadString(Models.Metadata.Machine.ManufacturerKey),
+ Category = parent.ReadString(Models.Metadata.Machine.CategoryKey),
+ Players = parent.ReadString(Models.Metadata.Machine.PlayersKey),
+ Rotation = parent.ReadString(Models.Metadata.Machine.RotationKey),
+ Control = parent.ReadString(Models.Metadata.Machine.ControlKey),
+ Status = parent.ReadString(Models.Metadata.Machine.StatusKey),
+ DisplayCount = parent.ReadString(Models.Metadata.Machine.DisplayCountKey),
+ DisplayType = parent.ReadString(Models.Metadata.Machine.DisplayTypeKey),
+ AltRomname = item.ReadString(Models.Metadata.Rom.AltRomnameKey),
+ AltTitle = item.ReadString(Models.Metadata.Rom.AltTitleKey),
+ Extra = parent.ReadString(Models.Metadata.Machine.ExtraKey),
+ Buttons = parent.ReadString(Models.Metadata.Machine.ButtonsKey),
+ Favorite = parent.ReadString(Models.Metadata.Machine.FavoriteKey),
+ Tags = parent.ReadString(Models.Metadata.Machine.TagsKey),
+ PlayedCount = parent.ReadString(Models.Metadata.Machine.PlayedCountKey),
+ PlayedTime = parent.ReadString(Models.Metadata.Machine.PlayedTimeKey),
+ FileIsAvailable = item.ReadString(Models.Metadata.Rom.FileIsAvailableKey),
+ };
+ return row;
+ }
+ }
+}
\ No newline at end of file
diff --git a/CrossModel/AttractMode.Serializer.cs b/CrossModel/AttractMode.Serializer.cs
new file mode 100644
index 00000000..3617c4b5
--- /dev/null
+++ b/CrossModel/AttractMode.Serializer.cs
@@ -0,0 +1,92 @@
+using System.Linq;
+using SabreTools.Models.AttractMode;
+
+namespace SabreTools.Serialization.CrossModel
+{
+ public partial class AttractMode : IModelSerializer
+ {
+ ///
+#if NET48
+ public Models.Metadata.MetadataFile Serialize(MetadataFile obj)
+#else
+ public Models.Metadata.MetadataFile? Serialize(MetadataFile? obj)
+#endif
+ {
+ if (obj == null)
+ return null;
+
+ var metadataFile = new Models.Metadata.MetadataFile
+ {
+ [Models.Metadata.MetadataFile.HeaderKey] = ConvertHeaderToInternalModel(obj),
+ };
+
+ if (obj?.Row != null && obj.Row.Any())
+ {
+ metadataFile[Models.Metadata.MetadataFile.MachineKey] = obj.Row
+ .Where(r => r != null)
+ .Select(ConvertMachineToInternalModel)
+ .ToArray();
+ }
+
+ return metadataFile;
+ }
+
+ ///
+ /// Convert from to
+ ///
+ private static Models.Metadata.Header ConvertHeaderToInternalModel(MetadataFile item)
+ {
+ var header = new Models.Metadata.Header
+ {
+ [Models.Metadata.Header.HeaderKey] = item.Header,
+ };
+ return header;
+ }
+
+ ///
+ /// Convert from to
+ ///
+ private static Models.Metadata.Machine ConvertMachineToInternalModel(Row item)
+ {
+ var machine = new Models.Metadata.Machine
+ {
+ [Models.Metadata.Machine.NameKey] = item.Name,
+ [Models.Metadata.Machine.EmulatorKey] = item.Emulator,
+ [Models.Metadata.Machine.CloneOfKey] = item.CloneOf,
+ [Models.Metadata.Machine.YearKey] = item.Year,
+ [Models.Metadata.Machine.ManufacturerKey] = item.Manufacturer,
+ [Models.Metadata.Machine.CategoryKey] = item.Category,
+ [Models.Metadata.Machine.PlayersKey] = item.Players,
+ [Models.Metadata.Machine.RotationKey] = item.Rotation,
+ [Models.Metadata.Machine.ControlKey] = item.Control,
+ [Models.Metadata.Machine.StatusKey] = item.Status,
+ [Models.Metadata.Machine.DisplayCountKey] = item.DisplayCount,
+ [Models.Metadata.Machine.DisplayTypeKey] = item.DisplayType,
+ [Models.Metadata.Machine.ExtraKey] = item.Extra,
+ [Models.Metadata.Machine.ButtonsKey] = item.Buttons,
+ [Models.Metadata.Machine.FavoriteKey] = item.Favorite,
+ [Models.Metadata.Machine.TagsKey] = item.Tags,
+ [Models.Metadata.Machine.PlayedCountKey] = item.PlayedCount,
+ [Models.Metadata.Machine.PlayedTimeKey] = item.PlayedTime,
+ [Models.Metadata.Machine.PlayedTimeKey] = item.PlayedTime,
+ [Models.Metadata.Machine.RomKey] = ConvertToInternalModel(item),
+ };
+ return machine;
+ }
+
+ ///
+ /// Convert from to
+ ///
+ private static Models.Metadata.Rom ConvertToInternalModel(Row item)
+ {
+ var rom = new Models.Metadata.Rom
+ {
+ [Models.Metadata.Rom.NameKey] = item.Title,
+ [Models.Metadata.Rom.AltRomnameKey] = item.AltRomname,
+ [Models.Metadata.Rom.AltTitleKey] = item.AltTitle,
+ [Models.Metadata.Rom.FileIsAvailableKey] = item.FileIsAvailable,
+ };
+ return rom;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Files/AttractMode.Deserializer.cs b/Files/AttractMode.Deserializer.cs
new file mode 100644
index 00000000..2407e92d
--- /dev/null
+++ b/Files/AttractMode.Deserializer.cs
@@ -0,0 +1,20 @@
+using SabreTools.Models.AttractMode;
+
+namespace SabreTools.Serialization.Files
+{
+ public partial class AttractMode : IFileSerializer
+ {
+ ///
+#if NET48
+ public MetadataFile Deserialize(string path)
+#else
+ public MetadataFile? Deserialize(string? path)
+#endif
+ {
+ using (var stream = PathProcessor.OpenStream(path))
+ {
+ return new Streams.AttractMode().Deserialize(stream);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Files/AttractMode.Serializer.cs b/Files/AttractMode.Serializer.cs
new file mode 100644
index 00000000..d47a6529
--- /dev/null
+++ b/Files/AttractMode.Serializer.cs
@@ -0,0 +1,29 @@
+using System.IO;
+using SabreTools.Models.AttractMode;
+
+namespace SabreTools.Serialization.Files
+{
+ public partial class AttractMode : IFileSerializer
+ {
+ ///
+#if NET48
+ public bool Serialize(MetadataFile obj, string path)
+#else
+ public bool Serialize(MetadataFile? obj, string? path)
+
+#endif
+ {
+ using (var stream = new Streams.AttractMode().Serialize(obj))
+ {
+ if (stream == null)
+ return false;
+
+ using (var fs = File.OpenWrite(path))
+ {
+ stream.CopyTo(fs);
+ return true;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streams/AttractMode.Deserializer.cs b/Streams/AttractMode.Deserializer.cs
new file mode 100644
index 00000000..38771813
--- /dev/null
+++ b/Streams/AttractMode.Deserializer.cs
@@ -0,0 +1,110 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using SabreTools.IO.Readers;
+using SabreTools.Models.AttractMode;
+
+namespace SabreTools.Serialization.Streams
+{
+ public partial class AttractMode : IStreamSerializer
+ {
+ ///
+#if NET48
+ public MetadataFile Deserialize(Stream data)
+#else
+ public MetadataFile? Deserialize(Stream? data)
+#endif
+ {
+ // If the stream is null
+ if (data == null)
+ return default;
+
+ // Setup the reader and output
+ var reader = new SeparatedValueReader(data, Encoding.UTF8)
+ {
+ Separator = ';',
+ VerifyFieldCount = false,
+ };
+ var dat = new MetadataFile();
+
+ // Read the header values first
+ if (!reader.ReadHeader() || reader.HeaderValues == null)
+ return null;
+
+ dat.Header = reader.HeaderValues.ToArray();
+
+ // Loop through the rows and parse out values
+ var rows = new List();
+ while (!reader.EndOfStream)
+ {
+ // If we have no next line
+ if (!reader.ReadNextLine() || reader.Line == null)
+ break;
+
+ // Parse the line into a row
+ Row row;
+ if (reader.Line.Count < Serialization.AttractMode.HeaderWithRomnameCount)
+ {
+ row = new Row
+ {
+ Name = reader.Line[0],
+ Title = reader.Line[1],
+ Emulator = reader.Line[2],
+ CloneOf = reader.Line[3],
+ Year = reader.Line[4],
+ Manufacturer = reader.Line[5],
+ Category = reader.Line[6],
+ Players = reader.Line[7],
+ Rotation = reader.Line[8],
+ Control = reader.Line[9],
+ Status = reader.Line[10],
+ DisplayCount = reader.Line[11],
+ DisplayType = reader.Line[12],
+ AltRomname = reader.Line[13],
+ AltTitle = reader.Line[14],
+ Extra = reader.Line[15],
+ Buttons = reader.Line[16],
+ };
+
+ // If we have additional fields
+ if (reader.Line.Count > Serialization.AttractMode.HeaderWithoutRomnameCount)
+ row.ADDITIONAL_ELEMENTS = reader.Line.Skip(Serialization.AttractMode.HeaderWithoutRomnameCount).ToArray();
+ }
+ else
+ {
+ row = new Row
+ {
+ Name = reader.Line[0],
+ Title = reader.Line[1],
+ Emulator = reader.Line[2],
+ CloneOf = reader.Line[3],
+ Year = reader.Line[4],
+ Manufacturer = reader.Line[5],
+ Category = reader.Line[6],
+ Players = reader.Line[7],
+ Rotation = reader.Line[8],
+ Control = reader.Line[9],
+ Status = reader.Line[10],
+ DisplayCount = reader.Line[11],
+ DisplayType = reader.Line[12],
+ AltRomname = reader.Line[13],
+ AltTitle = reader.Line[14],
+ Extra = reader.Line[15],
+ Buttons = reader.Line[16],
+ };
+
+ // If we have additional fields
+ if (reader.Line.Count > Serialization.AttractMode.HeaderWithRomnameCount)
+ row.ADDITIONAL_ELEMENTS = reader.Line.Skip(Serialization.AttractMode.HeaderWithRomnameCount).ToArray();
+ }
+
+ rows.Add(row);
+ }
+
+ // Assign the rows to the Dat and return
+ dat.Row = rows.ToArray();
+ return dat;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streams/AttractMode.Serializer.cs b/Streams/AttractMode.Serializer.cs
new file mode 100644
index 00000000..c8272ee0
--- /dev/null
+++ b/Streams/AttractMode.Serializer.cs
@@ -0,0 +1,91 @@
+using System.IO;
+using System.Linq;
+using System.Text;
+using SabreTools.IO.Writers;
+using SabreTools.Models.AttractMode;
+
+namespace SabreTools.Serialization.Streams
+{
+ public partial class AttractMode : IStreamSerializer
+ {
+ ///
+#if NET48
+ public Stream Serialize(MetadataFile obj)
+#else
+ public Stream? Serialize(MetadataFile? obj)
+#endif
+ {
+ // If the metadata file is null
+ if (obj == null)
+ return null;
+
+ // Setup the writer and output
+ var stream = new MemoryStream();
+ var writer = new SeparatedValueWriter(stream, Encoding.UTF8)
+ {
+ Separator = ';',
+ Quotes = false,
+ VerifyFieldCount = false,
+ };
+
+ // TODO: Include flag to write out long or short header
+ // Write the short header
+ writer.WriteString(Serialization.AttractMode.HeaderWithoutRomname); // TODO: Convert to array of values
+
+ // Write out the rows, if they exist
+ WriteRows(obj.Row, writer);
+
+ // Return the stream
+ stream.Seek(0, SeekOrigin.Begin);
+ return stream;
+ }
+
+ ///
+ /// Write rows information to the current writer
+ ///
+ /// Array of Row objects representing the rows information
+ /// SeparatedValueWriter representing the output
+#if NET48
+ private static void WriteRows(Row[] rows, SeparatedValueWriter writer)
+#else
+ private static void WriteRows(Row[]? rows, SeparatedValueWriter writer)
+#endif
+ {
+ // If the games information is missing, we can't do anything
+ if (rows == null || !rows.Any())
+ return;
+
+ // Loop through and write out the rows
+ foreach (var row in rows)
+ {
+#if NET48
+ var rowArray = new string[]
+#else
+ var rowArray = new string?[]
+#endif
+ {
+ row.Name,
+ row.Title,
+ row.Emulator,
+ row.CloneOf,
+ row.Year,
+ row.Manufacturer,
+ row.Category,
+ row.Players,
+ row.Rotation,
+ row.Control,
+ row.Status,
+ row.DisplayCount,
+ row.DisplayType,
+ row.AltRomname,
+ row.AltTitle,
+ row.Extra,
+ row.Buttons,
+ };
+
+ writer.WriteValues(rowArray);
+ writer.Flush();
+ }
+ }
+ }
+}
\ No newline at end of file