using System.Security.Cryptography; using System.Text.Json; using XFEEliteDangerous.Client.Storage; namespace XFEEliteDangerous.Core; /// /// Explicit private starter candidate for the pinned build. Actual resource slot metadata is combined /// with the sourced private starter fit; this does not assert that a world, spawn or transport is ready. /// This factory is never called by public registration or a native read. /// public static class NativeSidewinderSaveFactory { private const string MetadataHash = "9e154c97e7a0cd2d924902dd33e936ce3449e8ac025545196191fb2dcfc85732"; internal sealed record Slot(string Name, bool FixedModule, string ResourceDefault); internal sealed record Metadata(string Version, string NativeSha256, string SourceContract, Slot[] Slots); private static readonly Lazy metadata = new(ReadMetadata); internal static IReadOnlyList NativeSlotNames => metadata.Value.Slots.Select(slot=>slot.Name).ToArray(); public static NativeCommanderSave Create(uint commanderId, uint vesselId, long credits, NativeFullSavegameProfile profile, NativeLocationStringValue location) { ArgumentNullException.ThrowIfNull(profile); if (commanderId == 0) throw new ArgumentOutOfRangeException(nameof(commanderId)); var starter = NativeStarterShipDefinition.Sidewinder(vesselId, profile.Name); var slots = metadata.Value.Slots; var bindings = starter.Modules.ToDictionary(module => module.Slot, StringComparer.Ordinal); if (bindings.Keys.Any(slot => !slots.Any(definition => definition.Name == slot && !definition.FixedModule))) throw new InvalidDataException("Private starter fit does not match the pinned native slot table."); NativeModuleSnapshot Module(uint id, string name) => new() { Id = id, Name = name, Value = 0, Excess = 0, On = true, Priority = 0, HealthMillion = 1_000_000, Free = true }; //25E2610 initializes every resource slot before overlaying the fit, // including ordinary slots with built-in defaults. In particular the // VesselVoice slot must retain VoicePack_Verity; setting all unbound // ordinary slots to null incorrectly drops this resource default. var ordinary = slots.Where(slot => !slot.FixedModule).Select(slot => new NativeSlotSnapshot(slot.Name, bindings.TryGetValue(slot.Name, out var binding) ? Module(binding.CatalogueId, binding.Symbol) : string.IsNullOrEmpty(slot.ResourceDefault) ? null : Module(0, slot.ResourceDefault))).ToArray(); var fixedModules = slots.Where(slot => slot.FixedModule).Select(slot => // Native resource-derived defaults themselves use zero catalog identity. // Retain exact slot/name, never claim zero is an official market ID. new NativeSlotSnapshot(slot.Name, Module(0, slot.ResourceDefault))).ToArray(); var catalogue = new NativeSavegameCatalogue { Source = "Pinned native64-slot defaults overlaid by sourced15-module private gift; uncatalogued built-ins id0; gameplay acceptance pending", PinnedVersion = metadata.Value.Version, ValidationPending = true, Ships = [new("SideWinder", ordinary.Select(slot => new NativeSlotDefinition(slot.Name, slot.Module is not null, slot.Module is {Id:0} ? slot.Module.Name : null)).ToArray(), fixedModules.Select(slot => new NativeSlotDefinition(slot.Name, true)).ToArray())], Modules = ordinary.Concat(fixedModules).Where(slot=>slot.Module is not null) .Select(slot=>new NativeModuleDefinition(slot.Module!.Id,slot.Module.Name)) .DistinctBy(module => module.Name, StringComparer.Ordinal).ToArray() }; var save = new NativeCommanderSave { Catalogue = catalogue, Profile = profile, Snapshot = new() { CommanderNativeId = commanderId, Credits = credits, Alive = true, Location = location, CurrentVesselId = vesselId, ParentVesselId = vesselId, Vessels = [new() { Id = vesselId, Name = "SideWinder", Alive = true, CockpitBreached = false, Excess = 0, OxygenRemainingMilliseconds = starter.Physical.OxygenMilliseconds, RadarRangeSelection = 0, WeaponMode = 3, Fuel = new(starter.Physical.FuelCapacity, starter.Physical.FuelLevel, starter.Physical.ReserveLevel), Value = new(0, 0, 0), Health = new(1_000_000, 1_000_000, true, 1_000_000, 1_000_000), Location = location, LastLocation = location, Modules = ordinary, FixedModules = fixedModules, // Explicit initial private configuration. Weapon assignments are not guessed from resource indices. Firegroups = [], Capacitors = [new(0, 4, 1_000_000), new(1, 4, 1_000_000), new(2, 4, 1_000_000)], Cargo = [] }] } }; // Enforce the same account/system/field rules as the actual import endpoint before returning any candidate. _ = save.CreateInventory(new AccountState { NativeUserId = commanderId }, 0); return save; } private static Metadata ReadMetadata() { var assembly = typeof(NativeSidewinderSaveFactory).Assembly; var name = assembly.GetManifestResourceNames().Single(resource => resource.EndsWith(".native-sidewinder-slots.json", StringComparison.Ordinal)); using var stream = assembly.GetManifestResourceStream(name)!; using var bytes = new MemoryStream(); stream.CopyTo(bytes); if (Convert.ToHexStringLower(SHA256.HashData(bytes.GetBuffer().AsSpan(0, checked((int)bytes.Length)))) != MetadataHash) throw new InvalidDataException("Pinned native Sidewinder slot metadata changed."); var value = JsonSerializer.Deserialize(bytes.GetBuffer().AsSpan(0, checked((int)bytes.Length)), Wire.Json) ?? throw new InvalidDataException("Missing native Sidewinder slot metadata."); if (value.Version != "2026.09.03.332841" || value.NativeSha256 != "e6be8bbe04e6a7ae226d4318945af7f367de13dc5a007a261964d9ba8144e988" || value.Slots.Length != 64 || value.Slots.Count(slot => slot.FixedModule) != 21 || value.Slots.Select(slot => slot.Name).Distinct(StringComparer.Ordinal).Count() != 64 || value.Slots.Any(slot => string.IsNullOrWhiteSpace(slot.Name) || slot.FixedModule && string.IsNullOrWhiteSpace(slot.ResourceDefault))) throw new InvalidDataException("Native Sidewinder slot metadata does not match the pinned contract."); return value; } }