diff --git a/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs b/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs
index 7150e47a1..6b899ee35 100644
--- a/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs
+++ b/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs
@@ -198,6 +198,7 @@ public interface IPowerConsumer
Union(49, typeof(LaunchSuppressorData)),
Union(50, typeof(ListenerData)),
Union(51, typeof(ScannerData)),
+ Union(52, typeof(LockWeaponData)),
JsonObject(MemberSerialization.OptIn)]
public abstract class BehaviorData
{
diff --git a/Assets/Scripts/ServerShared/Behaviors/Weapon.cs b/Assets/Scripts/ServerShared/Behaviors/Weapon.cs
index 12166d46c..cb42d6bd4 100644
--- a/Assets/Scripts/ServerShared/Behaviors/Weapon.cs
+++ b/Assets/Scripts/ServerShared/Behaviors/Weapon.cs
@@ -11,12 +11,7 @@ using Newtonsoft.Json;
using CultMath;
using static CultMath.math;
-[Inspectable,
- Union(0, typeof(InstantWeaponData)),
- Union(1, typeof(LauncherData)),
- Union(2, typeof(ConstantWeaponData)),
- Union(3, typeof(ChargedWeaponData)),
- RuntimeInspectable]
+[Inspectable, RuntimeInspectable]
public abstract class WeaponData : BehaviorData
{
[Inspectable, JsonProperty("damageType"), Key(1), RuntimeInspectable]
diff --git a/Assets/Scripts/ServerShared/Environment.cs b/Assets/Scripts/ServerShared/Environment.cs
index 737033e4e..9d4994fa3 100644
--- a/Assets/Scripts/ServerShared/Environment.cs
+++ b/Assets/Scripts/ServerShared/Environment.cs
@@ -72,6 +72,8 @@ public class NoiseSettings
[Union(0, typeof(PowerBrush)),
Union(1, typeof(SimplexBrush)),
+ Union(2, typeof(AnimatedSimplexBrush)),
+ Union(3, typeof(RadialWaveBrush)),
Serializable, MessagePackObject, JsonObject(MemberSerialization.OptIn)]
public abstract class Brush
{
diff --git a/Assets/Scripts/ServerShared/ShipAuthoring.cs b/Assets/Scripts/ServerShared/ShipAuthoring.cs
index eafbacd2d..3962fa468 100644
--- a/Assets/Scripts/ServerShared/ShipAuthoring.cs
+++ b/Assets/Scripts/ServerShared/ShipAuthoring.cs
@@ -136,10 +136,14 @@ public static class ShipAuthoringStore
{
RefuseLegacyEmbeddedHull(path);
try { return LoadRecords(path); }
- catch (MessagePackSerializationException error)
+ catch (Exception error) when (error is InvalidDataException or MessagePackSerializationException)
{
throw new InvalidOperationException($"{path}: a record does not decode as its schema: {error.Message}", error);
}
+ catch (NotSupportedException error)
+ {
+ throw new InvalidOperationException($"{path}: not a readable ship file: {error.Message}", error);
+ }
}
// Key 1 of ShipAuthoring held the whole hull before S1. The type no longer has that member, so a deserialized record
@@ -152,7 +156,7 @@ public static class ShipAuthoringStore
if (bytes.Length == 0) return;
CultPersistedStoreSnapshot snapshot;
try { snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(bytes); }
- catch (MessagePackSerializationException error)
+ catch (Exception error) when (error is MessagePackSerializationException or NotSupportedException)
{
throw new InvalidOperationException($"{path}: not a readable ship file: {error.Message}", error);
}
diff --git a/Directory.Build.props b/Directory.Build.props
index 190e89780..05d5e4d22 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -2,12 +2,12 @@
$(CULTLIB_ROOT)
$(MSBuildThisFileDirectory)..\CultLib
-
- 45c2f4006f5f7dab357426cabd3197e8d0a17f66
+
+ b8fc5eb07720a187a0421424ac2cb3aa2966ca1d
$(CULTMATH_ROOT)
$(CultLibRoot)
-
- 9c1b06cd703bfa44352050e819b0d0659cf5978a
+
+ 7a8933b085468e1fc6f9d3bce4a85e7d101aaca3
C:\Program Files\Git\cmd\git.exe
git
diff --git a/Packages/manifest.json b/Packages/manifest.json
index 2dd2a3bb3..7693bd94f 100644
--- a/Packages/manifest.json
+++ b/Packages/manifest.json
@@ -52,8 +52,8 @@
"com.unity.modules.vr": "1.0.0",
"com.unity.modules.wind": "1.0.0",
"com.unity.modules.xr": "1.0.0",
- "org.gamecult.caching.unity": "https://github.com/GameCult/CultLib.git?path=/src/GameCult.Unity/Assets/Caching#caching-unity-v1.4.0",
- "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.60",
- "org.gamecult.cultmath": "https://github.com/GameCult/CultLib.git?path=/packages/cultmath/unity/org.gamecult.cultmath#cultmath-unity-v0.4.1"
+ "org.gamecult.caching.unity": "https://github.com/GameCult/CultLib.git?path=/src/GameCult.Unity/Assets/Caching#caching-unity-v2.0.0",
+ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v2.0.1",
+ "org.gamecult.cultmath": "https://github.com/GameCult/CultLib.git?path=/packages/cultmath/unity/org.gamecult.cultmath#cultmath-unity-v0.5.0"
}
}
diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json
index 5d26bd587..919cf94d1 100644
--- a/Packages/packages-lock.json
+++ b/Packages/packages-lock.json
@@ -256,27 +256,29 @@
"url": "https://packages.unity.com"
},
"org.gamecult.caching.unity": {
- "version": "https://github.com/GameCult/CultLib.git?path=/src/GameCult.Unity/Assets/Caching#caching-unity-v1.4.0",
+ "version": "https://github.com/GameCult/CultLib.git?path=/src/GameCult.Unity/Assets/Caching#caching-unity-v2.0.0",
"depth": 0,
"source": "git",
"dependencies": {
- "org.gamecult.cultlib": "1.0.60"
+ "org.gamecult.cultlib": "2.0.0"
},
- "hash": "45c2f4006f5f7dab357426cabd3197e8d0a17f66"
+ "hash": "42dce3611a81adc5c5fac54cadbaa5637d6022d3"
},
"org.gamecult.cultlib": {
- "version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.60",
+ "version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v2.0.1",
"depth": 0,
"source": "git",
- "dependencies": {},
- "hash": "45c2f4006f5f7dab357426cabd3197e8d0a17f66"
+ "dependencies": {
+ "org.gamecult.cultmath": "0.5.0"
+ },
+ "hash": "b8fc5eb07720a187a0421424ac2cb3aa2966ca1d"
},
"org.gamecult.cultmath": {
- "version": "https://github.com/GameCult/CultLib.git?path=/packages/cultmath/unity/org.gamecult.cultmath#cultmath-unity-v0.4.1",
+ "version": "https://github.com/GameCult/CultLib.git?path=/packages/cultmath/unity/org.gamecult.cultmath#cultmath-unity-v0.5.0",
"depth": 0,
"source": "git",
"dependencies": {},
- "hash": "9c1b06cd703bfa44352050e819b0d0659cf5978a"
+ "hash": "7a8933b085468e1fc6f9d3bce4a85e7d101aaca3"
},
"com.unity.modules.accessibility": {
"version": "1.0.0",
diff --git a/tests/Aetheria.Shared.Tests/CultLibAdoptionTests.cs b/tests/Aetheria.Shared.Tests/CultLibAdoptionTests.cs
new file mode 100644
index 000000000..bfe379d36
--- /dev/null
+++ b/tests/Aetheria.Shared.Tests/CultLibAdoptionTests.cs
@@ -0,0 +1,38 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using GameCult.Caching;
+using GameCult.Caching.MessagePack;
+using MessagePack;
+using Xunit;
+
+// CultLib 2.0 refuses a union subtype no arm names (it used to write nil). These keep that refusal a build-time failure.
+public sealed class CultLibAdoptionTests
+{
+ [Fact]
+ public void EveryConcreteSubtypeOfAUnionBaseIsAnArmOfIt()
+ {
+ var types = typeof(BehaviorData).Assembly.GetTypes();
+ var missing = new System.Collections.Generic.List();
+ foreach (var root in types)
+ {
+ var arms = root.GetCustomAttributes(false).Select(u => u.SubType).ToHashSet();
+ if (arms.Count == 0) continue;
+ foreach (var subtype in types.Where(t => t != root && !t.IsAbstract && !t.IsInterface && root.IsAssignableFrom(t)))
+ if (!arms.Contains(subtype)) missing.Add($"{subtype.Name} is not an arm of {root.Name}");
+ }
+ Assert.Empty(missing);
+ }
+
+ [Fact]
+ public void NoShippedGearHoldsANullBehavior()
+ {
+ using var cache = new CultCache(TestCatalog.Registry());
+ cache.AddBackingStore(new SingleFileMessagePackBackingStore(TestCatalog.Repo, true), AetheriaStores.CatalogTypes);
+ var gear = cache.GetAll().ToArray();
+ Assert.NotEmpty(gear);
+ var bad = gear.Where(g => g.Behaviors != null && g.Behaviors.Any(b => b == null)).Select(g => g.Name).ToArray();
+ Assert.Empty(bad);
+ }
+}
diff --git a/tests/Aetheria.Shared.Tests/RunSaveCompatibilityTests.cs b/tests/Aetheria.Shared.Tests/RunSaveCompatibilityTests.cs
new file mode 100644
index 000000000..8b8c98217
--- /dev/null
+++ b/tests/Aetheria.Shared.Tests/RunSaveCompatibilityTests.cs
@@ -0,0 +1,33 @@
+using System;
+using System.IO;
+using System.Linq;
+using GameCult.Caching;
+using GameCult.Caching.MessagePack;
+using Xunit;
+
+// tests/vectors/cultlib-1.0.60/run.cc is a run.cc written by Aetheria on CultLib 1.0.60 (64 zones, factions, one entity).
+// A player's save outlives the runtime that wrote it, so the current runtime must open it as compatible drift.
+public sealed class RunSaveCompatibilityTests : IDisposable
+{
+ private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-runcompat-" + Guid.NewGuid().ToString("N"));
+
+ public RunSaveCompatibilityTests() => Directory.CreateDirectory(_root);
+
+ public void Dispose() => Directory.Delete(_root, true);
+
+ [Fact]
+ public void ARunWrittenByCultLib1060OpensWithEveryZoneAndFactionResolving()
+ {
+ var run = Path.Combine(_root, "run.cc");
+ File.Copy(Path.Combine(RunStartTests.FindRepoRoot(), "tests", "vectors", "cultlib-1.0.60", "run.cc"), run);
+ using var cache = new CultCache(TestCatalog.Registry());
+ cache.AddBackingStore(new SingleFileMessagePackBackingStore(TestCatalog.Repo, true), AetheriaStores.CatalogTypes);
+ cache.AddBackingStore(new SingleFileMessagePackBackingStore(run), AetheriaStores.RunTypes);
+ var saved = cache.GetGlobal();
+ Assert.NotNull(saved);
+ Assert.NotEmpty(saved.Zones);
+ Assert.NotEmpty(saved.Factions);
+ Assert.All(saved.Zones, zone => Assert.NotNull(cache.Get(zone)));
+ Assert.All(saved.Factions, faction => Assert.NotNull(cache.Get(faction)));
+ }
+}
diff --git a/tests/Aetheria.Shared.Tests/ShipAuthoringTests.cs b/tests/Aetheria.Shared.Tests/ShipAuthoringTests.cs
index eced4c6f7..850e83dfd 100644
--- a/tests/Aetheria.Shared.Tests/ShipAuthoringTests.cs
+++ b/tests/Aetheria.Shared.Tests/ShipAuthoringTests.cs
@@ -173,7 +173,47 @@ public sealed class ShipAuthoringTests
RewritePayload(path, "aetheria.hulldata", slots => ((object[])((object[])slots[5])[0])[2] = new object[] { 5, false, true, false });
var error = Assert.Throws(() => ShipAuthoringStore.Load(path));
Assert.Contains(path, error.Message);
- Assert.IsAssignableFrom(error.InnerException);
+ // The pull names the record key and schema; the MessagePack failure is its cause.
+ var pull = Assert.IsType(error.InnerException);
+ Assert.Contains("Record '" + ShipModCatalog.HullKey("mod.skiff") + "'", pull.Message);
+ Assert.Contains("schema", pull.Message);
+ Assert.IsAssignableFrom(pull.InnerException);
+ }
+
+ // A store header with a nil format stamp is refused by the runtime with NotSupportedException; Load names the file.
+ [Fact]
+ public void LoadNamesTheFileWhoseFormatStampIsNil()
+ {
+ using var directory = new TempDirectory();
+ var path = Path.Combine(directory.Path, "nilstamp.cc");
+ File.WriteAllBytes(path, CultDocumentMessagePackSerialization.SerializeSnapshot(new CultPersistedStoreSnapshot
+ {
+ FormatVersion = null,
+ SchemaCatalog = Array.Empty(),
+ Records = Array.Empty(),
+ }));
+ var error = Assert.Throws(() => ShipAuthoringStore.Load(path));
+ Assert.Contains(path, error.Message);
+ Assert.IsType(error.InnerException);
+ }
+
+ // A store format this runtime does not know is refused on read with NotSupportedException from the attach; Load names the file.
+ [Fact]
+ public async Task LoadNamesTheFileWhoseStoreFormatIsNewer()
+ {
+ using var directory = new TempDirectory();
+ var path = Path.Combine(directory.Path, "newer.cc");
+ using (var cache = ShipAuthoringStore.Open(path, writable: true))
+ {
+ ShipAuthoringStore.Write(cache, Fixture().Hull, Fixture().Visual);
+ await cache.FlushAsync();
+ }
+ var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path));
+ snapshot.FormatVersion = "cultcache.store.v9";
+ File.WriteAllBytes(path, CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot));
+ var error = Assert.Throws(() => ShipAuthoringStore.Load(path));
+ Assert.Contains(path, error.Message);
+ Assert.IsType(error.InnerException);
}
// Decodes one record's payload of the named schema to its slot array, lets the edit change it, and stores it again.
diff --git a/tests/vectors/cultlib-1.0.60/run.cc b/tests/vectors/cultlib-1.0.60/run.cc
new file mode 100644
index 000000000..fc2a4e76e
Binary files /dev/null and b/tests/vectors/cultlib-1.0.60/run.cc differ
diff --git a/tools/blender/aetheria_ships/ship_cc.py b/tools/blender/aetheria_ships/ship_cc.py
index dd3ca78be..b7dca307c 100644
--- a/tools/blender/aetheria_ships/ship_cc.py
+++ b/tools/blender/aetheria_ships/ship_cc.py
@@ -119,7 +119,10 @@ class ShipFile(NamedTuple):
def read(path: str, cultlib_packages: str) -> ShipFile:
cultcache, msgpack = _libraries(cultlib_packages)
store = cultcache.SingleFileMessagePackBackingStore(path)
- envelopes = store.pull_all()
+ try:
+ envelopes = store.pull_all()
+ except OSError as error:
+ raise ValueError(f"The ship file cannot be read: {error.strerror}") from None
ships = [envelope for envelope in envelopes if envelope.type == SCHEMA]
hulls = [envelope for envelope in envelopes if envelope.type == HULL_SCHEMA]
# Before S1 the hull lived inside the ship record. Saving such a file would preserve the stale hull under a ship that