Merge master into moddable-ships-wiring

#3 landed the same --top-speed fix this branch already carried; both
conflicting files keep this branch's version (same behavior, stronger test).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Nibu 2026-11-10 17:51:00 +00:00
commit 07cfa26d6c
6 changed files with 83 additions and 35 deletions

View file

@ -47,14 +47,7 @@ public class Faction
[Inspectable, CultReference(typeof(Faction), many: true), CultInspectorRange(0, 1), JsonProperty("allegiance"), Key(12)]
public Dictionary<CultRecordRef<Faction>, float> Allegiance = new Dictionary<CultRecordRef<Faction>, float>();
[Inspectable, JsonProperty("overworldMusic"), Key(13)]
public uint OverworldMusic;
[Inspectable, JsonProperty("combatMusic"), Key(14)]
public uint CombatMusic;
[Inspectable, JsonProperty("bossMusic"), Key(15)]
public uint BossMusic;
// Keys 13 to 15 (overworldMusic, combatMusic, bossMusic) held Wwise ids; retired, never reuse them.
// Key 16 is reserved for Relations (operator ruling faction-relations-field).

View file

@ -58,10 +58,14 @@ public class Galaxy
Seed = savedGame.Seed;
Factions = savedGame.Factions.Select(faction => cultCache.Get(faction)).ToArray();
for (var i = 0; i < Factions.Length; i++)
{
FactionRelationships[Factions[i]] = savedGame.Relationships[i];
}
if (savedGame.Standings != null)
foreach (var standing in savedGame.Standings)
FactionRelationships[cultCache.Get(standing.Faction)] = standing.Relationship;
else
// A save before Standings wrote Relationships in an order it did not record; it is right only for a
// galaxy that never reordered, and is all that save holds.
for (var i = 0; i < Factions.Length; i++)
FactionRelationships[Factions[i]] = savedGame.Relationships[i];
var savedZones = savedGame.Zones.Select(zone => cultCache.Get(zone)).ToArray();
Zones = savedZones.Select(zone =>

View file

@ -417,11 +417,7 @@ public abstract class EquippableItemData : CraftedItemData
[Inspectable, CultInspectorAssetGuid, JsonProperty("actionIcon"), Key(20)]
public string ActionBarIcon;
[Inspectable, JsonProperty("soundBank"), Key(21)]
public uint SoundBank;
[Inspectable, JsonProperty("audioStats"), Key(22)]
public List<AudioStat> AudioStats = new List<AudioStat>();
// Keys 21 (soundBank) and 22 (audioStats) held Wwise ids; audio is native Unity plus AquaSynth now. Retired; never reuse them.
[IgnoreMember]
public abstract HardpointType HardpointType { get; }
@ -455,16 +451,6 @@ public abstract class EquippableItemData : CraftedItemData
}
}
[Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn)]
public class AudioStat
{
[Inspectable, JsonProperty("parameter"), Key(0)]
public uint Parameter;
[Inspectable, JsonProperty("stat"), Key(1)]
public PerformanceStat Stat = new PerformanceStat();
}
[CultDocument("aetheria.geardata", "1"), Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn)]
public class GearData : EquippableItemData
{

View file

@ -45,6 +45,8 @@ public class SavedGame
[Key(11)]
public bool IsTutorial;
// Read only by a save written before Standings: its entries follow galaxy.Factions order, not Factions order, so
// Galaxy trusts it only when the two orders cannot differ (see Galaxy's save constructor).
[Key(12)]
public FactionRelationship[] Relationships;
@ -61,6 +63,21 @@ public class SavedGame
// gets Regions, Partition and Deal from its saved graph, boss zones, Exit and seed.
[Key(15)]
public List<SavedProvince> Provinces;
// Each faction's standing, keyed by the faction itself so no list order can misplace it. Written by RunSave.Capture;
// null in a save written before it, which then reads Relationships.
[Key(16)]
public List<SavedStanding> Standings;
}
[MessagePackObject]
public class SavedStanding
{
[Key(0)]
public CultRecordRef<Faction> Faction;
[Key(1)]
public FactionRelationship Relationship;
}
[MessagePackObject]
@ -87,7 +104,8 @@ public static class RunSave
DiscoveredZones = galaxy.DiscoveredZones.Select(dz => Array.IndexOf(galaxy.Zones, dz)).ToArray(),
Background = galaxy.Background,
Factions = factions.Select(f => cache.RefOf(f)).ToArray(),
Relationships = galaxy.Factions.Select(f => galaxy.FactionRelationships[f]).ToArray(),
Standings = galaxy.Factions.Select(f => new SavedStanding
{Faction = cache.RefOf(f), Relationship = galaxy.FactionRelationships[f]}).ToList(),
HomeZones = galaxy.HomeZones.ToDictionary(
x => Array.IndexOf(factions, x.Key),
x => Array.IndexOf(galaxy.Zones, x.Value)),

View file

@ -224,9 +224,11 @@ at the same commit.
stat); `SoundBank` is catalog key 21 (`:403-404`).
- Gone since B5: the `AetherDrive` audio calls and its `RpmAudioParameter` and
`TorqueRatioAudioParameter` fields (grep 0), and the `EntityInstance` block that read
`item.Data.SoundBank` (grep 0). Follow-up `wwise-catalog-fields` still names both; its live
set is `SoundBank` (key 21), `AudioStats` (key 22) and `Faction.OverworldMusic`,
`CombatMusic`, `BossMusic` (`Corporations.cs:52-59`, keys 13 to 15).
`item.Data.SoundBank` (grep 0). Follow-up `wwise-catalog-fields` retired the rest: `SoundBank`
(key 21), `AudioStats` (key 22, with `AudioStat`) and `Faction.OverworldMusic`, `CombatMusic`,
`BossMusic` (keys 13 to 15) are deleted, their keys left as never-reuse comments. The shipped
`GameData/Aetheria.cc` still carries the old values (29 equippables with a `SoundBank`, 2 factions
with music); the reader skips unknown keys and the next catalog write drops them.
- `WwiseMetadata.cs` (91 lines) and `WwiseSoundBinding.cs` (21 lines) are used only by that
surface; both have `.meta` files. A repo-wide grep outside `docs` finds the Wwise symbols in
exactly these files: `ChargedWeapon.cs`, `InstantWeapon.cs`, `Reactor.cs`, `Thruster.cs`,

View file

@ -150,6 +150,48 @@ public sealed class RunSaveTests : IDisposable
}
}
// Galaxy.Factions and Galaxy.HomeZones.Keys are two orders of the same factions. A saved standing belongs to its
// faction, whichever order the galaxy lists them in.
[Fact]
public void StandingsSurviveSaveAndContinueWhenFactionOrderDiffersFromHomeZoneOrder()
{
using (var cache = AetheriaStores.Open(Catalog, catalogWritable: true))
{
cache.Upsert(new Faction { Name = "Borealis", ShortName = "BOR" });
cache.Upsert(new Faction { Name = "Corvid", ShortName = "COR" });
cache.FlushAsync().Wait();
}
using (var cache = Open())
{
var factions = cache.GetAll<Faction>().OrderBy(f => f.Name, StringComparer.Ordinal).ToArray();
var game = Game(cache, factions);
RunSave.Commit(cache, game, Zones(2, null, -1), new ProvenanceLedger());
}
var expected = new Dictionary<string, FactionRelationship>
{
{ "Adrasteia", FactionRelationship.Hated }, { "Borealis", FactionRelationship.Friendly }, { "Corvid", FactionRelationship.Beloved }
};
using (var cache = Open())
{
var galaxy = new Galaxy(cache, cache.GetGlobal<SavedGame>(), _ => { });
Assert.NotEqual(galaxy.Factions, galaxy.HomeZones.Keys);
foreach (var faction in galaxy.Factions) galaxy.FactionRelationships[faction] = expected[faction.Name];
var itemManager = new ItemManager(1u, cache, new ProvenanceLedger(), TestSettings(), _ => { });
galaxy.Zones[0].Contents = new Zone(itemManager, new PlanetSettings(), new ZonePack(), galaxy.Zones[0], galaxy);
var (game, zones) = RunSave.Capture(cache, galaxy, galaxy.Zones[0].Contents, null, new SavedActionBarBinding[0], 0);
RunSave.Commit(cache, game, zones, RunSave.Lots(cache));
}
using (var cache = Open())
{
var galaxy = new Galaxy(cache, cache.GetGlobal<SavedGame>(), _ => { });
Assert.Equal(expected, galaxy.Factions.ToDictionary(f => f.Name, f => galaxy.FactionRelationships[f]));
}
}
// The run's credits ride the save: what Capture is given is what a later Continue reads from the run store.
[Fact]
public void CreditsSurviveContinue()
@ -753,11 +795,14 @@ public sealed class RunSaveTests : IDisposable
return new ZonePack { Orbits = { root, moon }, Planets = { body } };
}
private static SavedGame Game(CultCache cache) => new SavedGame
private static SavedGame Game(CultCache cache) => Game(cache, cache.GetAll<Faction>().Single());
// HomeZones lists the factions in reverse of their saved order.
private static SavedGame Game(CultCache cache, params Faction[] factions) => new SavedGame
{
Factions = new[] { cache.RefOf(cache.GetAll<Faction>().Single()) },
Relationships = new[] { FactionRelationship.Neutral },
HomeZones = new Dictionary<int, int> { { 0, 0 } },
Factions = factions.Select(f => cache.RefOf(f)).ToArray(),
Relationships = factions.Select(_ => FactionRelationship.Neutral).ToArray(),
HomeZones = factions.Select((_, i) => factions.Length - 1 - i).ToDictionary(i => i, _ => 0),
BossZones = new Dictionary<int, int>(),
DiscoveredZones = new[] { 0 },
ActionBarBindings = new SavedActionBarBinding[0],