Fix Extra Room catalog unlock handling

This commit is contained in:
gamer147
2026-07-28 13:14:28 -04:00
parent 93d9f58758
commit f4bf78d7d1
11 changed files with 539 additions and 56 deletions

View File

@@ -190,4 +190,153 @@ public class SharedProfileTests
Assert.Equal(42, jsonClone.Globals[0x900]);
Assert.Equal(0, jsonClone.SharedProfile.LoadInteger(0x900));
}
[Fact]
public void CatalogUnlockMarkersRoundTripBaseAndAppendResources()
{
var profile = new SharedProfile();
profile.ConfigureCatalogUnlockSlots(
6000,
new Dictionary<int, int> { [1] = 32 });
profile.MarkCatalogResourceOpened(5407);
profile.MarkCatalogResourceOpened(0x01000011);
SharedProfilePayload encoded = profile.Snapshot();
Assert.Equal(6002, encoded.CatalogCompatibilityValues.Count);
Assert.Equal(0x8791233au, encoded.CatalogCompatibilityValues[0]);
Assert.Equal(0x6868a8e1u, encoded.CatalogCompatibilityValues[5407 + 2]);
Assert.Equal(32u, encoded.ExtendedSelectorCounts[1]);
Assert.Equal(34, encoded.ExtendedValues.Count);
Assert.Equal(0x42d5ee4eu, encoded.ExtendedValues[17 + 2]);
var reloaded = new SharedProfile();
reloaded.Replace(encoded);
Assert.True(reloaded.IsCatalogResourceUnlocked(5407));
Assert.True(reloaded.IsCatalogResourceUnlocked(0x01000011));
Assert.False(reloaded.IsCatalogResourceUnlocked(5406));
Assert.False(reloaded.IsCatalogResourceUnlocked(0x01000012));
Assert.False(reloaded.IsCatalogResourceUnlocked(0x80000000));
}
[Fact]
public void CatalogUnlockOpcodeReturnsImportedProfilePredicate()
{
OpcodeTable table = OpcodeTableJson.Load(Paths.OpcodesJson);
Script script = ScriptAssembler.Assemble(table, "CATALOG_UNLOCK_QUERY", new List<(int, Operand[])>
{
(0x19d, [new Operand(GlobalInt, 0x900), new Operand(Immediate, 5407)]),
(0x19d, [new Operand(GlobalInt, 0x901), new Operand(Immediate, 5406)]),
(0x19d, [new Operand(GlobalInt, 0x902), new Operand(Immediate, 0x01000011)]),
(0x2, []),
}, []);
var profile = new SharedProfile();
profile.ConfigureCatalogUnlockSlots(6000, new Dictionary<int, int> { [1] = 32 });
profile.MarkCatalogResourceOpened(5407);
profile.MarkCatalogResourceOpened(0x01000011);
var vm = new VirtualMachine(
script, table, new RecordingHost(), sharedProfile: profile);
vm.Run();
Assert.Equal("exit", vm.HaltReason);
Assert.Equal(1, vm.Globals[0x900]);
Assert.Equal(0, vm.Globals[0x901]);
Assert.Equal(1, vm.Globals[0x902]);
}
[Fact]
public void TrackingStoreMarksOnlySuccessfulCatalogOpens()
{
var profile = new SharedProfile();
profile.ConfigureCatalogUnlockSlots(8);
var tracker = new CatalogTrackingAssetStore(
new SelectiveAssetStore(),
entry => profile.MarkCatalogResourceOpened(entry.PackedId));
var available = new AssetEntry("A.BIN", "DATA1.ALF", 0, 1, RawIndex: 3);
var missing = new AssetEntry("MISSING.BIN", "DATA1.ALF", 0, 1, RawIndex: 4);
Assert.Equal(new byte[] { 42 }, tracker.ReadAll(available));
Assert.Throws<FileNotFoundException>(() => tracker.ReadAll(missing));
Assert.True(profile.IsCatalogResourceUnlocked(3));
Assert.False(profile.IsCatalogResourceUnlocked(4));
}
[Fact]
public void InstalledSharedProfileUnlocksKnownCgAndHSceneResourcesWhenPresent()
{
string root = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Eushully", "姫狩りダンジョンマイスター", "SAVE");
if (!File.Exists(Path.Combine(root, "SAVE.DAT"))) return;
var store = new DirectoryNativeDatStore(
root,
new NativeSaveIdentity(
NativeSaveMagic.S4SD, 0x4a343234, "姫狩りダンジョンマイスター", 3, 10));
var profile = new SharedProfile();
Assert.True(profile.Load(store));
Assert.True(profile.IsCatalogResourceUnlocked(0x3324)); // ED.AGF
using var cgDocument = System.Text.Json.JsonDocument.Parse(
File.ReadAllBytes(Path.Combine(Paths.Build, "data", "CGINIT.json")));
uint[] cgIds = cgDocument.RootElement.GetProperty("records").EnumerateArray()
.Select(record => record.GetProperty("gallery_image_asset_id").GetUInt32())
.ToArray();
Assert.Equal(851, cgIds.Length);
Assert.All(cgIds, id => Assert.True(
profile.IsCatalogResourceUnlocked(id), $"CG resource 0x{id:x} should be unlocked"));
using var hDocument = System.Text.Json.JsonDocument.Parse(
File.ReadAllBytes(Path.Combine(Paths.Build, "data", "SPINIT.json")));
uint[] hSceneIds = hDocument.RootElement.GetProperty("records").EnumerateArray()
.SelectMany(page => page.GetProperty("script_resource_ids").EnumerateArray())
.Select(id => id.GetUInt32())
.Where(id => id != 0)
.ToArray();
Assert.Equal(118, hSceneIds.Length);
Assert.All(hSceneIds, id => Assert.True(
profile.IsCatalogResourceUnlocked(id), $"H-scene resource 0x{id:x} should be unlocked"));
int newlyOpenedId = Enumerable.Range(0, 13_208)
.First(id => !profile.IsCatalogResourceUnlocked(id));
profile.MarkCatalogResourceOpened(newlyOpenedId);
string roundTripRoot = Path.Combine(
Path.GetTempPath(), "age-installed-profile-unlocks-" + Guid.NewGuid().ToString("N"));
try
{
var roundTripStore = new DirectoryNativeDatStore(roundTripRoot, store.Identity);
profile.Save(roundTripStore, Timestamp, profile.AccumulatedPlaySeconds);
var reloaded = new SharedProfile();
Assert.True(reloaded.Load(roundTripStore));
Assert.True(reloaded.IsCatalogResourceUnlocked(newlyOpenedId));
Assert.All(cgIds, id => Assert.True(reloaded.IsCatalogResourceUnlocked(id)));
Assert.All(hSceneIds, id => Assert.True(reloaded.IsCatalogResourceUnlocked(id)));
}
finally
{
if (Directory.Exists(roundTripRoot)) Directory.Delete(roundTripRoot, recursive: true);
}
}
private sealed class SelectiveAssetStore : IAssetStore
{
public Stream Open(AssetEntry entry)
{
if (entry.Name == "MISSING.BIN") throw new FileNotFoundException();
return new MemoryStream([42], writable: false);
}
public byte[] ReadAll(AssetEntry entry)
{
using Stream stream = Open(entry);
var result = new byte[stream.Length];
stream.ReadExactly(result);
return result;
}
}
}