206 lines
9.1 KiB
C#
206 lines
9.1 KiB
C#
using System.Buffers.Binary;
|
|
using System.Text;
|
|
using Ludarium.Application;
|
|
using Ludarium.Domain;
|
|
|
|
namespace Ludarium.UnitTests;
|
|
|
|
/// <summary>
|
|
/// Platform identification for the container formats the players actually accept.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// A disc or cartridge that classifies as Unknown never becomes a launch candidate, so a format the
|
|
/// Dolphin policy or the browser matrix allows must also be identifiable here. These fixtures are
|
|
/// synthesized from the documented on-media headers, not from copyrighted dumps.
|
|
/// </remarks>
|
|
public sealed class PlatformIdentificationTests
|
|
{
|
|
[Fact]
|
|
public void GameCubeDiscsAreIdentifiedByTheirMagicWordNotTheirDirectory()
|
|
{
|
|
var classification = ArtifactAnalysis.Classify("library/unsorted/Disc.iso", GameCubeDisc());
|
|
|
|
Assert.Equal(MediaType.DiscImage, classification.MediaType);
|
|
Assert.Equal("gamecube", classification.Platform);
|
|
Assert.Equal(Confidence.Deterministic, classification.Confidence);
|
|
Assert.Contains(classification.Evidence, item => item.Kind == "signature" && item.Value == "GameCube disc");
|
|
Assert.Contains(classification.Evidence, item => item.Kind == "rom.gameCode" && item.Value == "GLZE");
|
|
Assert.Contains(classification.Evidence, item => item.Kind == "rom.region" && item.Value == "usa");
|
|
Assert.Contains(classification.Evidence, item => item.Kind == "rom.title" && item.Value == "LUDARIUM GAMECUBE FIXTURE");
|
|
}
|
|
|
|
[Fact]
|
|
public void WiiDiscsAreIdentifiedByTheirMagicWordNotTheirDirectory()
|
|
{
|
|
var classification = ArtifactAnalysis.Classify("library/unsorted/Disc.iso", WiiDisc());
|
|
|
|
Assert.Equal(MediaType.DiscImage, classification.MediaType);
|
|
Assert.Equal("wii", classification.Platform);
|
|
Assert.Equal(Confidence.Deterministic, classification.Confidence);
|
|
Assert.Contains(classification.Evidence, item => item.Kind == "signature" && item.Value == "Wii disc");
|
|
}
|
|
|
|
[Fact]
|
|
public void ADiscMagicOverridesAMisleadingPlatformDirectory()
|
|
{
|
|
var classification = ArtifactAnalysis.Classify("roms/gamecube/Disc.iso", WiiDisc(), "gamecube");
|
|
|
|
Assert.Equal("wii", classification.Platform);
|
|
Assert.Contains(classification.Evidence, item => item.Kind == "platform.conflict" &&
|
|
item.Value == "directory:gamecube;signature:wii");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("roms/gamecube/Game.gcm", "gamecube")]
|
|
[InlineData("roms/gamecube/Game.gcz", "gamecube")]
|
|
[InlineData("roms/wii/Game.wia", "wii")]
|
|
[InlineData("roms/wii/Game.rvz", "wii")]
|
|
[InlineData("roms/wii/Game.wbfs", "wii")]
|
|
public void EveryDolphinContainerFormatClassifiesAsAPlayableDiscImage(string path, string hint)
|
|
{
|
|
var classification = ArtifactAnalysis.Classify(path, [], hint);
|
|
|
|
Assert.Equal(MediaType.DiscImage, classification.MediaType);
|
|
Assert.Equal(hint, classification.Platform);
|
|
Assert.True(classification.Supported);
|
|
}
|
|
|
|
[Fact]
|
|
public void EveryFormatTheDolphinPolicyAcceptsIsIdentifiable()
|
|
{
|
|
foreach (var extension in NativeRemotePlayerRegistry.Dolphin.Extensions)
|
|
{
|
|
var classification = ArtifactAnalysis.Classify($"roms/wii/Game{extension}", [], "wii");
|
|
Assert.True(classification.Supported,
|
|
$"{extension} is launchable in Dolphin but is not identified by the scanner.");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void MegaDriveCartridgesAreIdentifiedAndSeparatedFromTheirVariants()
|
|
{
|
|
var genesis = ArtifactAnalysis.Classify("roms/unsorted/Game.md", SegaCartridge("SEGA MEGA DRIVE "));
|
|
var thirtyTwoX = ArtifactAnalysis.Classify("roms/unsorted/Game.32x", SegaCartridge("SEGA 32X "));
|
|
var segaCd = ArtifactAnalysis.Classify("roms/unsorted/Game.bin", SegaCartridge("SEGA SEGACD "));
|
|
|
|
Assert.Equal("genesis", genesis.Platform);
|
|
Assert.Equal(MediaType.Rom, genesis.MediaType);
|
|
Assert.Equal("sega32x", thirtyTwoX.Platform);
|
|
Assert.Equal("segacd", segaCd.Platform);
|
|
Assert.Contains(genesis.Evidence, item => item.Kind == "rom.internationalTitle" &&
|
|
item.Value == "LUDARIUM GENESIS FIXTURE");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0x40, "mastersystem")]
|
|
[InlineData(0x30, "mastersystem")]
|
|
[InlineData(0x50, "gamegear")]
|
|
[InlineData(0x60, "gamegear")]
|
|
[InlineData(0x70, "gamegear")]
|
|
public void TheEightBitSegaRegionNibbleSeparatesMasterSystemFromGameGear(int region, string expected)
|
|
{
|
|
var classification = ArtifactAnalysis.Classify("roms/unsorted/Game.sms", SegaEightBit(region));
|
|
|
|
Assert.Equal(expected, classification.Platform);
|
|
Assert.Equal(MediaType.Rom, classification.MediaType);
|
|
Assert.Equal(Confidence.Deterministic, classification.Confidence);
|
|
Assert.Contains(classification.Evidence, item => item.Kind == "rom.productCode" && item.Value == "50123");
|
|
Assert.Contains(classification.Evidence, item => item.Kind == "rom.revision" && item.Value == "1");
|
|
Assert.Contains(classification.Evidence, item => item.Kind == "rom.checksum" && item.Value == "1234");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("megadrive", "genesis")]
|
|
[InlineData("mega-drive", "genesis")]
|
|
[InlineData("MD", "genesis")]
|
|
[InlineData("sms", "mastersystem")]
|
|
[InlineData("game-gear", "gamegear")]
|
|
public void HardwareNamedDirectoriesResolveToTheCanonicalPlatform(string directory, string expected)
|
|
{
|
|
var hint = ArtifactAnalysis.InferPlatformHint("library", "/mnt/games", $"roms/{directory}/Game.md");
|
|
Assert.Equal(expected, hint);
|
|
Assert.Equal(expected, ArtifactAnalysis.Classify($"roms/{directory}/Game.md", [], hint).Platform);
|
|
}
|
|
|
|
[Fact]
|
|
public void PlatformValuesWrittenByEarlierVersionsStillResolve()
|
|
{
|
|
Assert.Equal("genesis", ArtifactAnalysis.CanonicalPlatform("sega-genesis"));
|
|
Assert.Equal("mastersystem", ArtifactAnalysis.CanonicalPlatform("sms"));
|
|
Assert.Equal("nes", ArtifactAnalysis.CanonicalPlatform("nes"));
|
|
Assert.Null(ArtifactAnalysis.CanonicalPlatform(null));
|
|
}
|
|
|
|
[Fact]
|
|
public void ARawTrackIsOnlyADiscImageInsideADiscLibrary()
|
|
{
|
|
var disc = ArtifactAnalysis.Classify("roms/psx/Game/Game (Track 1).bin", [], "psx");
|
|
var installer = ArtifactAnalysis.Classify("PC/Some Game/data1.bin", [], "windows");
|
|
|
|
Assert.Equal(MediaType.DiscImage, disc.MediaType);
|
|
Assert.Equal("psx", disc.Platform);
|
|
// An installer payload must not be promoted to a disc image on the strength of its extension.
|
|
Assert.Equal(MediaType.Unknown, installer.MediaType);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("roms/3ds/Game.3ds", "3ds")]
|
|
[InlineData("roms/3ds/Game.cia", "3ds")]
|
|
[InlineData("roms/wiiu/Game.wud", "wiiu")]
|
|
[InlineData("roms/psvita/Game.vpk", "psvita")]
|
|
public void FormatsForResearchedPlatformsAreIdentifiedEvenWithoutAPlayer(string path, string platform)
|
|
{
|
|
var classification = ArtifactAnalysis.Classify(path, []);
|
|
|
|
Assert.Equal(platform, classification.Platform);
|
|
Assert.True(classification.Supported);
|
|
}
|
|
|
|
private static byte[] GameCubeDisc()
|
|
{
|
|
var header = new byte[0x500];
|
|
Encoding.ASCII.GetBytes("GLZE").CopyTo(header, 0);
|
|
Encoding.ASCII.GetBytes("01").CopyTo(header, 4);
|
|
header[6] = 0;
|
|
header[7] = 1;
|
|
BinaryPrimitives.WriteUInt32BigEndian(header.AsSpan(0x1C), 0xC2339F3D);
|
|
Encoding.ASCII.GetBytes("LUDARIUM GAMECUBE FIXTURE").CopyTo(header, 0x20);
|
|
return header;
|
|
}
|
|
|
|
private static byte[] WiiDisc()
|
|
{
|
|
var header = new byte[0x500];
|
|
Encoding.ASCII.GetBytes("RLZP").CopyTo(header, 0);
|
|
Encoding.ASCII.GetBytes("01").CopyTo(header, 4);
|
|
BinaryPrimitives.WriteUInt32BigEndian(header.AsSpan(0x18), 0x5D1C9EA3);
|
|
Encoding.ASCII.GetBytes("LUDARIUM WII FIXTURE").CopyTo(header, 0x20);
|
|
return header;
|
|
}
|
|
|
|
private static byte[] SegaCartridge(string console)
|
|
{
|
|
var rom = new byte[0x200];
|
|
Encoding.ASCII.GetBytes(console.PadRight(16)[..16]).CopyTo(rom, 0x100);
|
|
Encoding.ASCII.GetBytes("(C)LUDARIUM 2026".PadRight(16)).CopyTo(rom, 0x110);
|
|
Encoding.ASCII.GetBytes("LUDARIUM GENESIS FIXTURE".PadRight(48)).CopyTo(rom, 0x120);
|
|
Encoding.ASCII.GetBytes("LUDARIUM GENESIS FIXTURE".PadRight(48)).CopyTo(rom, 0x150);
|
|
Encoding.ASCII.GetBytes("GM 00000000-00".PadRight(14)).CopyTo(rom, 0x180);
|
|
Encoding.ASCII.GetBytes("JUE").CopyTo(rom, 0x1F0);
|
|
return rom;
|
|
}
|
|
|
|
private static byte[] SegaEightBit(int region)
|
|
{
|
|
var rom = new byte[0x8000];
|
|
Encoding.ASCII.GetBytes("TMR SEGA").CopyTo(rom, 0x7FF0);
|
|
rom[0x7FFA] = 0x34; // checksum, low byte
|
|
rom[0x7FFB] = 0x12; // checksum, high byte
|
|
rom[0x7FFC] = 0x23; // product code, low BCD pair
|
|
rom[0x7FFD] = 0x01; // product code, high BCD pair
|
|
rom[0x7FFE] = 0x51; // top product digit and revision 1
|
|
rom[0x7FFF] = (byte)(region | 0x0C); // region nibble plus a 32 KiB size nibble
|
|
return rom;
|
|
}
|
|
}
|