diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index e3e25c0d5..4e2c76ba7 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -90,6 +90,7 @@ RequireEntityDestroyedRequestAuthority(root); RequireDroppedPickupCheckpointState(root); RequireTradePurchaseRequestAuthority(root); RequireInventoryProjectionSlotIdentity(root); +RequireInventoryValidationUsesManagedTypedDocuments(root); RequireInventoryLoadoutSaveRequestAuthority(root); RequireInventoryLoadoutRestoreRequestAuthority(root); RequireDockedCurrentShipRequestAuthority(root); @@ -6816,6 +6817,78 @@ static void RequireInventoryProjectionSlotIdentity(string root) } } +static void RequireInventoryValidationUsesManagedTypedDocuments(string root) +{ + var clientStatePath = Path.Combine( + root, + "Packages", + "org.gamecult.aetheria.state", + "Runtime", + "AetheriaClientState.cs"); + var inventoryMenuPath = Path.Combine(root, "Assets", "Scripts", "UI", "Menu", "InventoryMenu.cs"); + var inventoryPanelPath = Path.Combine(root, "Assets", "Scripts", "UI", "Menu", "InventoryPanel.cs"); + + if (!File.Exists(clientStatePath) || + !File.Exists(inventoryMenuPath) || + !File.Exists(inventoryPanelPath)) + { + throw new InvalidOperationException("Cannot verify managed inventory document access; expected client state and inventory UI sources are missing."); + } + + var clientState = File.ReadAllText(clientStatePath); + if (!clientState.Contains("public Task LatestInventoryAsync(int entityIndex)", StringComparison.Ordinal) || + !clientState.Contains("public AetheriaRuntimeInventoryDocument LatestInventory(int entityIndex)", StringComparison.Ordinal)) + { + throw new InvalidOperationException( + "AetheriaClientDetailState must expose managed typed latest access for indexed inventory documents."); + } + + var sources = new Dictionary + { + ["InventoryMenu.cs"] = File.ReadAllText(inventoryMenuPath), + ["InventoryPanel.cs"] = File.ReadAllText(inventoryPanelPath) + }; + foreach (var (name, source) in sources) + { + var compact = CompactSource(source); + var requiredSymbols = new[] + { + "var state = ResolveClient().Aetheria();", + "state.Latest()", + "state.Latest()", + "state.Details.LatestInventory(entityIndex)" + }; + var missingSymbols = requiredSymbols + .Where(symbol => !source.Contains(symbol, StringComparison.Ordinal)) + .ToArray(); + if (missingSymbols.Length > 0) + { + throw new InvalidOperationException( + $"{name} no longer validates inventory slots through managed typed client documents: " + + string.Join(", ", missingSymbols)); + } + + var forbiddenCompactedSymbols = new[] + { + ".Aetheria().Current.Entity.LatestAsync()", + ".Aetheria().StationRefit.LatestAsync()", + ".Details.Inventory(entityIndex).LatestAsync()" + } + .Select(CompactSource) + .ToArray(); + var hits = forbiddenCompactedSymbols + .Where(symbol => compact.Contains(symbol, StringComparison.Ordinal)) + .ToArray(); + if (hits.Length > 0) + { + throw new InvalidOperationException( + $"{name} still walks CultMesh document handles manually instead of using managed typed client state."); + } + } + + Console.WriteLine("Inventory validation: cargo/equipment checks read managed typed client documents instead of manual handle walks"); +} + static void RequireVerseHostSettingsAuthority(string root) { var settingsPath = Path.Combine(root, "Aetheria.State", "Documents", "AetheriaVerseHostSettings.cs"); @@ -14828,7 +14901,7 @@ static void RequireInventoryDoubleClickTransferRequestAuthority(string root) "TryValidateTypedCargoSlot(", "TryValidateTypedEquipmentSlot(", ".Details", - ".Inventory(entityIndex)", + ".LatestInventory(entityIndex)", "origin.Cargo.TryGetValue(item, out var originPosition)", "SourceIndex == cargoIndex", "SourceIndex == equipmentIndex", @@ -15823,6 +15896,11 @@ static bool ContainsUnitySettingsMember(string source, string memberName) source.Contains($"\nSettings.{memberName}", StringComparison.Ordinal); } +static string CompactSource(string source) +{ + return string.Concat((source ?? "").Where(character => !char.IsWhiteSpace(character))); +} + static string TryReadMethodName(string trimmedLine) { if (!StartsWithMethodAccessModifier(trimmedLine) || diff --git a/Assets/Scripts/UI/Menu/InventoryMenu.cs b/Assets/Scripts/UI/Menu/InventoryMenu.cs index 1fe85c809..612b38b14 100644 --- a/Assets/Scripts/UI/Menu/InventoryMenu.cs +++ b/Assets/Scripts/UI/Menu/InventoryMenu.cs @@ -838,13 +838,8 @@ public class InventoryMenu : MonoBehaviour try { - var current = ResolveClient() - .Aetheria() - .Current - .Entity - .LatestAsync() - .GetAwaiter() - .GetResult(); + var state = ResolveClient().Aetheria(); + var current = state.Latest(); if (current != null && string.Equals(current.EntityKey, entityKey, StringComparison.Ordinal)) { equipment = current.Equipment ?? Array.Empty(); @@ -852,12 +847,7 @@ public class InventoryMenu : MonoBehaviour return true; } - var refit = ResolveClient() - .Aetheria() - .StationRefit - .LatestAsync() - .GetAwaiter() - .GetResult(); + var refit = state.Latest(); var entityIndex = -1; if (refit != null) { @@ -872,13 +862,7 @@ public class InventoryMenu : MonoBehaviour if (entityIndex < 0) return false; - var inventory = ResolveClient() - .Aetheria() - .Details - .Inventory(entityIndex) - .LatestAsync() - .GetAwaiter() - .GetResult(); + var inventory = state.Details.LatestInventory(entityIndex); if (inventory == null || !string.Equals(inventory.EntityKey, entityKey, StringComparison.Ordinal)) return false; diff --git a/Assets/Scripts/UI/Menu/InventoryPanel.cs b/Assets/Scripts/UI/Menu/InventoryPanel.cs index c94b354e4..335f593db 100644 --- a/Assets/Scripts/UI/Menu/InventoryPanel.cs +++ b/Assets/Scripts/UI/Menu/InventoryPanel.cs @@ -1255,13 +1255,8 @@ private void Update() try { - var current = ResolveClient() - .Aetheria() - .Current - .Entity - .LatestAsync() - .GetAwaiter() - .GetResult(); + var state = ResolveClient().Aetheria(); + var current = state.Latest(); if (current != null && string.Equals(current.EntityKey, entityKey, StringComparison.Ordinal)) { equipment = current.Equipment ?? Array.Empty(); @@ -1269,7 +1264,7 @@ private void Update() return true; } - var refit = ResolveStationRefit(); + var refit = state.Latest(); var entityIndex = -1; if (refit != null) { @@ -1284,13 +1279,7 @@ private void Update() if (entityIndex < 0) return false; - var inventory = ResolveClient() - .Aetheria() - .Details - .Inventory(entityIndex) - .LatestAsync() - .GetAwaiter() - .GetResult(); + var inventory = state.Details.LatestInventory(entityIndex); if (inventory == null || !string.Equals(inventory.EntityKey, entityKey, StringComparison.Ordinal)) return false; diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClientState.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClientState.cs index 547bb8d20..198c3cfc2 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClientState.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClientState.cs @@ -461,6 +461,16 @@ namespace GameCult.Aetheria.State.Verse { return _inventory(entityIndex); } + + public Task LatestInventoryAsync(int entityIndex) + { + return Inventory(entityIndex).LatestAsync(); + } + + public AetheriaRuntimeInventoryDocument LatestInventory(int entityIndex) + { + return LatestInventoryAsync(entityIndex).ConfigureAwait(false).GetAwaiter().GetResult(); + } } public sealed class AetheriaClientStarbridgeState