From bf173147650efe37accd9dd5e5563fba604fa160 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 13:44:48 +0200 Subject: [PATCH 001/120] Move every LonginusX consumer to the Longinus: scenarios, tests, tools, the starting hull Duel, Arcs, Launcher Angles and Starved Reactor fit and place the Longinus (thrusters, Core Power or MoveOnPro at (2,4), radiators relaid); the modes start a Longinus (Settings.asset StartingHullName; the Aether multipliers leave it). The drive's tests, its Steering Axis probe and its Brownout fixtures go with their subject; ship_cc.py and the live-game checklist drop the drive and the hull. The drive itself and the catalog records are deleted in the following commits. Co-Authored-By: Claude Sonnet 5.5 --- Assets/Resources/Settings.asset | 4 +- Assets/Scripts/ServerShared/Scenarios/Arcs.cs | 19 ++-- Assets/Scripts/ServerShared/Scenarios/Duel.cs | 17 ++-- .../ServerShared/Scenarios/LauncherAngles.cs | 19 ++-- .../ServerShared/Scenarios/StarvedReactor.cs | 17 ++-- docs/merge-to-master-checklist.md | 8 +- tests/Aetheria.Shared.Tests/BrownoutTests.cs | 88 ++----------------- .../ConditionRatioTests.cs | 2 +- .../FireControlCut124Tests.cs | 3 +- .../FireControlCut7Tests.cs | 2 +- tests/Aetheria.Shared.Tests/RunStartTests.cs | 2 +- tests/Aetheria.Shared.Tests/ScenarioTests.cs | 14 +-- .../ShipAuthoringTests.cs | 2 +- .../ShipValidationTests.cs | 2 +- tests/Aetheria.Shared.Tests/SteeringTests.cs | 9 +- tests/mutation_tests_addressables_cut.py | 14 +-- tools/blender/aetheria_ships/ship_cc.py | 2 +- 17 files changed, 79 insertions(+), 145 deletions(-) diff --git a/Assets/Resources/Settings.asset b/Assets/Resources/Settings.asset index 692fb08fe..e1b1933e3 100644 --- a/Assets/Resources/Settings.asset +++ b/Assets/Resources/Settings.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b2e346f533ccee5d2c655b3d8e0817e3f93e9234af06557ee9c6ec13cecb9acd -size 13137 +oid sha256:070f5797cb93cad308c38f3c87e05a729cfb21b87004fcdc2e146dba336045d6 +size 13073 diff --git a/Assets/Scripts/ServerShared/Scenarios/Arcs.cs b/Assets/Scripts/ServerShared/Scenarios/Arcs.cs index d1ceb61e0..bbbb867be 100644 --- a/Assets/Scripts/ServerShared/Scenarios/Arcs.cs +++ b/Assets/Scripts/ServerShared/Scenarios/Arcs.cs @@ -16,18 +16,21 @@ public sealed class Arcs : Scenario public override void Stage(ScenarioStage stage) { - var fighter = stage.Fit("LonginusX", + var fighter = stage.Fit("Longinus", ("Cockpit 2x2", int2(2, 6), None), - ("Traction", int2(2, 3), None), - ("Core Power", int2(2, 1), None), + ("Large Drive", int2(1, 0), Reversed), + ("Large Drive", int2(3, 0), Reversed), + ("Talaria", int2(2, 14), CounterClockwise), + ("Talaria", int2(3, 14), Clockwise), + ("Core Power", int2(2, 4), None), ("GT 3K", int2(0, 5), None), ("GT 3K", int2(5, 5), None), ("FastBlast+-", int2(1, 8), None), ("FastBlast+-", int2(4, 8), None), - ("Iapyx", int2(1, 2), CounterClockwise), - ("Iapyx", int2(4, 2), Clockwise), + ("Iapyx", int2(2, 2), CounterClockwise), + ("Iapyx", int2(3, 2), Clockwise), ("not if i see you first", int2(3, 10), None), - ("Fire Control Array", int2(2, 5), None)); + ("Fire Control Array", int2(1, 6), None)); var turret = stage.Fit("Turret", ("ClearPath", int2(1, 2), None), ("ClearPath", int2(5, 2), None), @@ -39,8 +42,8 @@ public sealed class Arcs : Scenario ("Fire Control Array", int2(3, 6), None)); stage.Player(fighter, float2(0, 0), facing: float2(0, 1)); - stage.Place(stage.Bare("LonginusX"), Bow); - stage.Place(stage.Bare("LonginusX"), Stern); + stage.Place(stage.Bare("Longinus"), Bow); + stage.Place(stage.Bare("Longinus"), Stern); stage.Place(turret, Turret, stance: ScenarioStance.Hostile); } } diff --git a/Assets/Scripts/ServerShared/Scenarios/Duel.cs b/Assets/Scripts/ServerShared/Scenarios/Duel.cs index fa270a4bd..457197186 100644 --- a/Assets/Scripts/ServerShared/Scenarios/Duel.cs +++ b/Assets/Scripts/ServerShared/Scenarios/Duel.cs @@ -4,7 +4,7 @@ using static ItemRotation; public sealed class Duel : Scenario { public override string Name => "Duel"; - public override string Brief => "One piloted, hostile LonginusX with slow GT 3K missiles and a Spectra in its hold. Verify: rolled " + + public override string Brief => "One piloted, hostile Longinus with slow GT 3K missiles and a Spectra in its hold. Verify: rolled " + "hits and near-misses, subsystem aim, beam reads above bow, armour takes the missile, loot keeps its brand."; public override uint Seed => 104; public override bool Ambient => false; @@ -13,19 +13,22 @@ public sealed class Duel : Scenario public override void Stage(ScenarioStage stage) { - var fighter = stage.Fit("LonginusX", + var fighter = stage.Fit("Longinus", ("Cockpit 2x2", int2(2, 6), None), - ("Traction", int2(2, 3), None), - ("Core Power", int2(2, 1), None), + ("Large Drive", int2(1, 0), Reversed), + ("Large Drive", int2(3, 0), Reversed), + ("Talaria", int2(2, 14), CounterClockwise), + ("Talaria", int2(3, 14), Clockwise), + ("Core Power", int2(2, 4), None), ("GT 3K", int2(0, 5), None), ("GT 3K", int2(5, 5), None), ("FastBlast+-", int2(1, 8), None), ("FastBlast+-", int2(4, 8), None), - ("Iapyx", int2(1, 2), CounterClockwise), - ("Iapyx", int2(4, 2), Clockwise), + ("Iapyx", int2(2, 2), CounterClockwise), + ("Iapyx", int2(3, 2), Clockwise), ("not if i see you first", int2(3, 10), None), ("PotaT+-", int2(1, 5), None), - ("Fire Control Array", int2(2, 5), None), + ("Fire Control Array", int2(1, 6), None), ("Store-All Plus", int2(2, 8), None)); stage.Player(fighter, float2(0, 0)); diff --git a/Assets/Scripts/ServerShared/Scenarios/LauncherAngles.cs b/Assets/Scripts/ServerShared/Scenarios/LauncherAngles.cs index a734be6e1..e5439d3cc 100644 --- a/Assets/Scripts/ServerShared/Scenarios/LauncherAngles.cs +++ b/Assets/Scripts/ServerShared/Scenarios/LauncherAngles.cs @@ -4,7 +4,7 @@ using static ItemRotation; public sealed class LauncherAngles : Scenario { public override string Name => "Launcher Angles"; - public override string Brief => "A GT 3K, a pswarm and two FastBlasts; a hostile LonginusX broadside on, a bare Longinus beside it. " + + public override string Brief => "A GT 3K, a pswarm and two FastBlasts; a hostile bare Longinus broadside on, another beside it. " + "Verify: hits pulse only the facing edge; a pswarm round whose target dies fades unexploded."; public override uint Seed => 105; public override bool Ambient => false; @@ -13,21 +13,24 @@ public sealed class LauncherAngles : Scenario public override void Stage(ScenarioStage stage) { - var launcher = stage.Fit("LonginusX", + var launcher = stage.Fit("Longinus", ("Cockpit 2x2", int2(2, 6), None), - ("Traction", int2(2, 3), None), - ("Core Power", int2(2, 1), None), + ("Large Drive", int2(1, 0), Reversed), + ("Large Drive", int2(3, 0), Reversed), + ("Talaria", int2(2, 14), CounterClockwise), + ("Talaria", int2(3, 14), Clockwise), + ("Core Power", int2(2, 4), None), ("GT 3K", int2(0, 5), None), ("pswarm", int2(5, 5), None), ("FastBlast+-", int2(1, 8), None), ("FastBlast+-", int2(4, 8), None), - ("Iapyx", int2(1, 2), CounterClockwise), - ("Iapyx", int2(4, 2), Clockwise), + ("Iapyx", int2(2, 2), CounterClockwise), + ("Iapyx", int2(3, 2), Clockwise), ("not if i see you first", int2(3, 10), None), - ("Fire Control Array", int2(2, 5), None)); + ("Fire Control Array", int2(1, 6), None)); stage.Player(launcher, float2(0, 0)); - stage.Place(stage.Bare("LonginusX"), float2(0, 900), facing: float2(1, 0), stance: ScenarioStance.Hostile); + stage.Place(stage.Bare("Longinus"), float2(0, 900), facing: float2(1, 0), stance: ScenarioStance.Hostile); stage.Place(stage.Bare("Longinus"), float2(300, 700)); } } diff --git a/Assets/Scripts/ServerShared/Scenarios/StarvedReactor.cs b/Assets/Scripts/ServerShared/Scenarios/StarvedReactor.cs index ccbf9baa8..4c57c3b36 100644 --- a/Assets/Scripts/ServerShared/Scenarios/StarvedReactor.cs +++ b/Assets/Scripts/ServerShared/Scenarios/StarvedReactor.cs @@ -13,17 +13,20 @@ public sealed class StarvedReactor : Scenario public override void Stage(ScenarioStage stage) { - var starved = stage.Fit("LonginusX", + var starved = stage.Fit("Longinus", ("Cockpit 2x2", int2(2, 6), None), - ("Traction", int2(2, 3), None), - ("MoveOnPro", int2(2, 1), None), + ("Large Drive", int2(1, 0), Reversed), + ("Large Drive", int2(3, 0), Reversed), + ("Talaria", int2(2, 14), CounterClockwise), + ("Talaria", int2(3, 14), Clockwise), + ("MoveOnPro", int2(2, 4), None), ("Spectra", int2(1, 8), None), ("Spectra", int2(4, 8), None), - ("Iapyx", int2(1, 2), CounterClockwise), - ("Iapyx", int2(4, 2), Clockwise), + ("Iapyx", int2(2, 2), CounterClockwise), + ("Iapyx", int2(3, 2), Clockwise), ("not if i see you first", int2(3, 10), None), - ("Fire Control Array", int2(2, 5), None)); + ("Fire Control Array", int2(1, 6), None)); stage.Player(starved, float2(0, 0)); - stage.Place(stage.Bare("LonginusX"), float2(0, 400)); + stage.Place(stage.Bare("Longinus"), float2(0, 400)); } } diff --git a/docs/merge-to-master-checklist.md b/docs/merge-to-master-checklist.md index 292ad913c..d5d70e17b 100644 --- a/docs/merge-to-master-checklist.md +++ b/docs/merge-to-master-checklist.md @@ -33,7 +33,7 @@ Compiled 2026-09-29 by Eyes from the maps on the branch. Pointers are `doc:line` ### 2b. Fresh launch (no `run.cc`, no `player.cc`) 3. **Launch to main menu.** Expect no Addressables errors, and the Groups window shows `Assets/Content` under `Default Local Group` (`addressables-cut.md:301`). `player.cc` appears and Continue is disabled (`cultcache-migration-cut.md:2746`). Proves: Addressables Cut 1, CultCache cutover. The 09-17 smoke covered the last two on the old tree; this is a cheap recheck on the new schema. -4. **New Game (tutorial).** Expect the galaxy generates and the start ship is LonginusX (`settings-globals-cut.md:498`, an expectation, not a recorded check for this stack). Expect a Zenith station with a docking bay in the entrance zone every time (`locomotion-cut.md:375`). Proves: locomotion Cut 1 station rule. +4. **New Game (tutorial).** Expect the galaxy generates and the start ship is the Longinus (`settings-globals-cut.md:498`, an expectation, not a recorded check for this stack). Expect a Zenith station with a docking bay in the entrance zone every time (`locomotion-cut.md:375`). Proves: locomotion Cut 1 station rule. 5. **Addressables, "Use Asset Database".** Ships, turret and Zenith spawn; thruster and aether-drive particles show; fire an instant weapon (Autocannon) and a charged one (ChargeBlast SG); action bar icons show (`addressables-cut.md:362-366`). Proves: Addressables Cut 2. ### 2c. Flight, power and economy @@ -44,19 +44,19 @@ Compiled 2026-09-29 by Eyes from the maps on the branch. Pointers are `doc:line` 9. **Brownout and refill (O6, O4).** Brownout reads as degradation, not breakage (`:774`, `:990`). A weapon refilling under brownout stutters instead of firing full rate and cooking the ship (`:729-730`). Proves: power-supply term, brownout, input capacitors. 10. **Memory across zones (O1).** Play across several zones with kills, open the Profiler or Task Manager, and confirm memory does not climb (`:672-673`, `:980-982`). Proves: stat resolver, the entity-leak fix (map Cut 2). Combine with steps 11 and 12. -### 2d. Combat (against a LonginusX-class AI), then save and Studio +### 2d. Combat (against a Longinus-class AI), then save and Studio 11. **Fire-control Cut 1 arcs.** Side-mounted weapons still fire at targets abeam; a turret tracks all the way round; nothing fires through the hull (`fire-control-cut.md:397-398`). 12. **Fire-control Cut 3 and shots.** Shots show rolled impacts and misses; a miss reads as a near-miss, not a bug; damage matches the HUD; aiming at a revealed subsystem concentrates damage; kill time is in the same order as before (`:659-661`). Reveal and selection work, kills drop loot, pickup stores it, the tractor beam still pulls (`headless-playground-cut.md:789-794`). No crash or frozen hot ship when the target vanishes (the 2026-09-20 failure, `fire-control-cut.md:1464-1470`). Recheck after the fixes: unrecorded. 13. **12.2, bow then beam.** The HUD `hull` factor reads higher from the beam. Turn your armoured face into a slow missile and watch where it lands (`:2380-2383`). 14. **12.3, launcher into a bow, then a flank.** Use a GT 3K or pswarm launcher. The schematic display pulses the facing edge only (`:2519-2520`). -15. **12.4, scratch catalog.** Watch a proximity airburst beside a LonginusX and a delayed penetrator into its nose. Damage must land where the model shows the blast; this confirms the centre-of-mass anchor. Bow is already confirmed +y (`:3095-3097`, `:2028-2031`). Proves: 12.4(b) `Detonate`. +15. **12.4, scratch catalog.** Watch a proximity airburst beside a Longinus and a delayed penetrator into its nose. Damage must land where the model shows the blast; this confirms the centre-of-mass anchor. Bow is already confirmed +y (`:3095-3097`, `:2028-2031`). Proves: 12.4(b) `Detonate`. 16. **Wormhole, save, Continue.** Take a wormhole, quit, Continue: same items, same tier and brand (`item-provenance-cut.md:536-537`). 17. **Die.** Continue is disabled and `run.cc` holds no ledger (`:540-541`). 18. **Shield panel, `FieldShieldTest`.** Follow the numbered look in `shield-panel-cut.md:683-700`. Nothing visible on load. Click the nose: a patch grows and the field ripple runs too. Click the flank: the panel lies flat, not tilted. Click the same spot twice: it strikes the same panel. Spam far-apart clicks: at most 12 panels, oldest replaced. Let them fade: none left. Then the Cut 5 payoff: strong hits dice the patch, and repeated hits on one spot break through on a later hit (`:737-739`). Proves: shield-panel Cuts 3, 4, 5. 19. **Studio click-through.** - Open `Aetheria.cc`. The fire-control behaviour union renders (`fire-control-cut.md:503-506`). - - `LonginusX` schematic still underlays the shape drawer (`addressables-cut.md:370-371`). + - `Longinus` schematic still underlays the shape drawer (`addressables-cut.md:370-371`). - The restored hull records open (`locomotion-cut.md:501`; the map gives no checklist beyond "the Studio click-through"). - The scratch catalog's new `Fuse` and `BlastRadius` fields edit (from 2a; no map line). - Open `GameData/run.cc` after step 16. `aetheria.provenanceledger` renders `Lots` with `Attributed` origins and per-role lists. If Studio cannot draw an int-keyed dictionary of unions, that is a Studio defect to raise, not a schema change (`item-provenance-cut.md:541-544`). diff --git a/tests/Aetheria.Shared.Tests/BrownoutTests.cs b/tests/Aetheria.Shared.Tests/BrownoutTests.cs index 660b4106b..b4a691574 100644 --- a/tests/Aetheria.Shared.Tests/BrownoutTests.cs +++ b/tests/Aetheria.Shared.Tests/BrownoutTests.cs @@ -10,7 +10,7 @@ using static CultMath.math; // Cut 7 (docs/stats-and-power-target.md): "Continuous consumers brown out through a power supply curve on // their performance stats. An exponent is enough." Cut 6 wired the curve itself (PowerCurveTests.cs) and the // bus already writes a real fractional grant (PowerBusTests.cs, PowerTierTests.cs), but every continuous -// consumer still gated its own effect on Item.PowerSupply >= 1f (Thruster.Execute, AetherDrive.Execute, +// consumer still gated its own effect on Item.PowerSupply >= 1f (Thruster.Execute, // Radiator.Execute, ConstantWeapon.Execute, EnergyDraw.Execute) -- so a partial grant read exactly like none, // the flicking-off bug the ruling names. This file pins each behaviour's own gate removal: a partial grant now // produces a reduced effect, a full grant is unchanged, a zero grant still produces nothing, the degradation @@ -18,12 +18,11 @@ using static CultMath.math; // rule, Cut 4) is untouched by any of it. Each rule has a matching mutation in // tests/mutation_tests_stats_power_cut7.py. // -// F6 (docs/stats-and-power-cut.md, Soul pass 2026-09-19) surfaced a real conflict this file's Radiator and -// AetherDrive fixtures happened to sit on top of, unnoticed because the pre-F6 registry only named each +// F6 (docs/stats-and-power-cut.md, Soul pass 2026-09-19) surfaced a real conflict this file's Radiator +// fixture happened to sit on top of, unnoticed because the pre-F6 registry only named each // consumer's own top-level request field. Radiator.PowerRequest's own early-out reads PumpedHeat AND WasteHeat -// (to decide whether the pump can keep up this tick); AetherDrive.PowerRequest's spin-up arithmetic reads -// Torque, LambdaMultiplier, MaximumRpm and PassiveCoupling. Cut 7 asks for the brownout curve to live on exactly -// the stats these two behaviours actually curve (PumpedHeat, Torque), but F6 forbade a PowerSupply term on any +// (to decide whether the pump can keep up this tick). Cut 7 asks for the brownout curve to live on exactly +// the stats the behaviour actually curves (PumpedHeat), but F6 forbade a PowerSupply term on any // stat a request reads at all, on pain of the exact oscillation Soul measured against the shipped catalog's own // "OK Disperser" radiator (SOUL_RadiatorRequestDependsOnItsOwnGrantAndOscillates, docs/stats-and-power-cut.md). // The six tests below that authored one were marked Skip rather than deleted or silently reworked to hide the @@ -32,8 +31,8 @@ using static CultMath.math; // // Nominal-request ruling (docs/stats-and-power-cut.md, operator ruling 2026-09-19) is that future pass: a power // request is evaluated nominally -- what the item wants at full supply, not what it is currently managing -// (EquippedItem.EvaluateNominalPower, Entity.cs). Radiator.PowerRequest and AetherDrive.PowerRequest now read -// PumpedHeat/Torque (and every other field in StatValidation.PowerRequestFields) through that nominal read, so +// (EquippedItem.EvaluateNominalPower, Entity.cs). Radiator.PowerRequest now reads +// PumpedHeat (and every other field in StatValidation.PowerRequestFields) through that nominal read, so // the same stat can carry a PowerSupply term (curved for Execute's real effect) AND be a request field, without // the request depending on its own answer -- there is no more oscillation to reproduce, because the request no // longer reads the live grant at all. StatValidation.ValidateNoPowerSupplyOnRequest (the static check F6 added @@ -64,14 +63,6 @@ public sealed class BrownoutTests : IDisposable private static PerformanceStat Curved(float max, float exponent) => new PerformanceStat { Min = 0, Max = max, Terms = { new StatTerm { Source = StatSource.PowerSupply, Exponent = exponent } } }; - // A flat Bezier (two keys, equal value, zero tangents): AetherDrive's TorqueProfile multiplies potential - // torque by this curve's value at the current rpm ratio -- flat at 1 keeps that multiplier out of the way of - // the PowerSupply curve under test. - private static BezierCurve FlatCurve(float value) => new BezierCurve - { - Keys = new[] { new float4(0, value, 0, 0), new float4(1, value, 0, 0) } - }; - private static EquippableItem Mint(CultCache cache, ItemManager items, ItemData design) => new EquippableItem { Data = cache.RefOf(design), @@ -180,55 +171,6 @@ public sealed class BrownoutTests : IDisposable Assert.Equal(.025f, half, 3); } - // --- AetherDrive: PassiveCoupling = 1 engages the rotor regardless of axis input, so the drive can be - // --- exercised without threading Ship's own movement/torque classification -- Axis stays at Ship.Update's - // --- own default (0,0,Turn), and Turn is 0 by default. Rpm starts at 0, - // --- so decay(0, ...) contributes nothing and the whole tick's Rpm gain is exactly this behaviour's own - // --- spin-up under test. --- - private (float rpmGain, AetherDrive drive) RunAetherDrive(float reactorCharge, float exponent = 1) - { - using var cache = OpenCatalog(new AetherDriveData - { - RotorDiameter = float3(1, 1, 1), RotorMass = float3(1, 1, 1), - MaximumRpm = Constant(100), CouplingLambda = float3(1, 1, 1), LambdaMultiplier = Constant(1), - CouplingEfficiency = Constant(1), Torque = Curved(30, exponent), TorqueProfile = FlatCurve(1), - EnergyDraw = Constant(50), PassiveCoupling = Constant(1) - }); - var ship = BuildShip(cache, reactorCharge); - ship.Update(1f); - var drive = ship.GetBehavior(); - return (drive.Rpm.x, drive); - } - - // Nominal-request ruling (docs/stats-and-power-cut.md, operator ruling 2026-09-19, see file header): Torque - // is a registered AetherDrive request field (PowerRequest's spin-up arithmetic reads it), but PowerRequest now - // reads it through EvaluateNominalPower (full-supply, undegraded), so Curved(...) on Torque is a legal - // catalog shape again -- Execute's own real, curved Evaluate is what actually degrades the spin-up below. - [Fact] - public void AetherDriveAtHalfGrantProducesReducedNotZeroNotFullSpinUp() - { - var (full, _) = RunAetherDrive(reactorCharge: 1000); // demand ~50, generation far exceeds it -> ratio 1 - var (half, _) = RunAetherDrive(reactorCharge: 25); // demand ~50, generation 25 -> ratio .5 - - Assert.True(half > 0f && half < full, $"half={half} should sit strictly between 0 and full={full}"); - } - - [Fact] - public void AetherDriveAtFullGrantIsUnchangedFromToday() - { - var (full, drive) = RunAetherDrive(reactorCharge: 1000); - Assert.Equal(1f, drive.Item.PowerSupply, 3); - Assert.True(full > 0f); - } - - [Fact] - public void AetherDriveAtZeroGrantProducesNothing() - { - var (zero, drive) = RunAetherDrive(reactorCharge: 0); - Assert.Equal(0f, drive.Item.PowerSupply, 3); - Assert.Equal(0f, zero, 4); - } - // --- Radiator: no explicit power gate survives Cut 7 at all -- PumpedHeat above is a plain Evaluate() read, // --- so a curved PumpedHeat already pumps less under a partial grant, and waste heat is unaffected, so a // --- starved radiator falls behind (the reduced-performance failure the ruling asks for) instead of the @@ -383,10 +325,10 @@ public sealed class BrownoutTests : IDisposable // --- Nominal-request ruling (docs/stats-and-power-cut.md, operator ruling 2026-09-19): a direct probe of // --- EquippedItem.EvaluateNominalPower itself, decoupled from any behaviour's own physical state (Radiator's - // --- temperature, AetherDrive's Rpm) so the only thing that can make it diverge from the ordinary Evaluate + // --- temperature) so the only thing that can make it diverge from the ordinary Evaluate // --- is the item's own PowerSupply grant. Thruster's EnergyUsage carries the term here purely as a probe -- // --- not a shape BrownoutTests' other fixtures author -- because Thruster has no other tick-to-tick state - // --- that could confound the comparison the way Radiator's RadiatorTemperature or AetherDrive's Rpm would. + // --- that could confound the comparison the way Radiator's RadiatorTemperature would. // --- Backs tests/mutation_tests_stats_power_nominal_request.py's own mutation of this exact method. --- [Fact] public void EvaluateNominalPowerIgnoresTheItemsCurrentPowerSupplyGrant() @@ -444,16 +386,4 @@ public sealed class BrownoutTests : IDisposable $"expected tick 2's gate to stay open on the nominal PumpedHeat/WasteHeat ratio, got demand {secondDemand}"); } - // --- The same wiring check attempted on AetherDrive.PowerRequest's own Torque read does not carry over - // --- cleanly: torqueRatio (actualRpmDelta/potentialRpmDelta) self-cancels Torque's own magnitude whenever - // --- the rotor is not yet rpm-capped (both numerator and denominator scale with Torque, so the ratio -- and - // --- so the final draw -- comes out identical whether Torque is read nominally or through the curved - // --- Evaluate; verified empirically while building this fixture: reverting AetherDrive.PowerRequest to - // --- Evaluate produced byte-identical TotalDemand across two ticks, including once the rotor was pushed - // --- into its rpm cap). A black-box demand comparison cannot discriminate this call site the way it can - // --- Radiator's binary gate above. Its correctness rests instead on the identical pattern to Radiator's - // --- (same EvaluateNominalPower call, same EquippedItem mechanism EvaluateNominalPowerIgnoresTheItemsCurrent - // --- PowerSupplyGrant already pins) and on AetherDriveAtHalfGrantProducesReducedNotZeroNotFullSpinUp/ - // --- AtFullGrantIsUnchangedFromToday/AtZeroGrantProducesNothing above, which exercise the real end-to-end - // --- Execute path this ruling exists to keep legal. Not part of the mutation harness for that reason. } diff --git a/tests/Aetheria.Shared.Tests/ConditionRatioTests.cs b/tests/Aetheria.Shared.Tests/ConditionRatioTests.cs index 9781f0ec9..13b5f281e 100644 --- a/tests/Aetheria.Shared.Tests/ConditionRatioTests.cs +++ b/tests/Aetheria.Shared.Tests/ConditionRatioTests.cs @@ -12,7 +12,7 @@ using static CultMath.math; // Should multiply that by actual performance." EquippedItem.ConditionRatio (Entity.cs) is the one number that // answers "how healthy does this item actually look": the item's resolved value for a stat against what the // same item, same lot, same modifiers would produce with Heat/Durability/PowerSupply pinned to their identity. -// Thruster.Condition and AetherDrive.Condition (Thruster.cs/AetherDrive.cs) are that ratio read against each +// Thruster.Condition (Thruster.cs) is that ratio read against each // behaviour's own governing stat (Thrust, Torque); ShipInstance.cs (Unity-side, not covered here) multiplies // presentation intent by it instead of recomputing any durability/heat/power arithmetic of its own. // Each rule here has a matching mutation in tests/mutation_tests_condition_ratio_cut8.py. diff --git a/tests/Aetheria.Shared.Tests/FireControlCut124Tests.cs b/tests/Aetheria.Shared.Tests/FireControlCut124Tests.cs index c6b1bb00c..c515d8469 100644 --- a/tests/Aetheria.Shared.Tests/FireControlCut124Tests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlCut124Tests.cs @@ -469,10 +469,9 @@ public sealed class FireControlCut124Tests : IDisposable } // The same rule on the shipped hulls: a blast just grazing the corner farthest from the centre of mass. The - // corners of the shipped Longinus and LonginusX reach past the old cull's box-half-diagonal bound. + // corner of the shipped Longinus reaches past the old cull's box-half-diagonal bound. [Theory] [InlineData("Longinus")] - [InlineData("LonginusX")] public void AGrazingBlastAtAShippedHullsFarthestCornerLands(string hullName) { Shape shape; diff --git a/tests/Aetheria.Shared.Tests/FireControlCut7Tests.cs b/tests/Aetheria.Shared.Tests/FireControlCut7Tests.cs index 63578287d..aca0f24ba 100644 --- a/tests/Aetheria.Shared.Tests/FireControlCut7Tests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlCut7Tests.cs @@ -212,7 +212,7 @@ public sealed class FireControlCut7Tests : IDisposable Assert.NotEmpty(equippable); Assert.Contains(equippable, e => e is WeaponItemData); - var hull = cache.GetByName("LonginusX"); + var hull = cache.GetByName("Longinus"); Assert.NotNull(hull); var settings = new GameplaySettings diff --git a/tests/Aetheria.Shared.Tests/RunStartTests.cs b/tests/Aetheria.Shared.Tests/RunStartTests.cs index 755dd91bb..3cb507f4e 100644 --- a/tests/Aetheria.Shared.Tests/RunStartTests.cs +++ b/tests/Aetheria.Shared.Tests/RunStartTests.cs @@ -670,7 +670,7 @@ public sealed partial class RunStartTests : IDisposable public void TheRealHullsMaterializeBareFromABrandedProduct() { var available = new LoadoutGenerator(ref _items.Random, _items, _galaxy, _galaxy.Entrance, null, 2); - foreach (var hull in new[] { Hull("Djinni"), Hull("LonginusX"), TurretHull() }) + foreach (var hull in new[] { Hull("Djinni"), Hull("Longinus"), TurretHull() }) { var failures = new List(); var entity = Loadouts.Materialize(_items, null, BareLoadout(hull), available.IsAvailable, failures); diff --git a/tests/Aetheria.Shared.Tests/ScenarioTests.cs b/tests/Aetheria.Shared.Tests/ScenarioTests.cs index cfd5cc174..2ee1f96be 100644 --- a/tests/Aetheria.Shared.Tests/ScenarioTests.cs +++ b/tests/Aetheria.Shared.Tests/ScenarioTests.cs @@ -129,7 +129,7 @@ public sealed partial class RunStartTests [Fact] public void ModesMatchOldNewGame() { - Assert.Equal("LonginusX", _startingHull); + Assert.Equal("Longinus", _startingHull); var tutorial = Launch(new TutorialGalaxy(), Inputs(() => GalaxySeed)); Assert.Empty(tutorial.failures); @@ -285,8 +285,8 @@ public sealed partial class RunStartTests var failures = Refused(Arena(true), new Scripted(true, stage => { stage.Player(stage.Bare("Djinni"), float2(0, 0)); - stage.Place(stage.Generated("LonginusX"), float2(300, 0), stance: ScenarioStance.Hostile, piloted: true); - stage.Place(stage.Fit("LonginusX", ("No Such Gun", int2(1, 8), ItemRotation.None)), float2(-300, 0)); + stage.Place(stage.Generated("Longinus"), float2(300, 0), stance: ScenarioStance.Hostile, piloted: true); + stage.Place(stage.Fit("Longinus", ("No Such Gun", int2(1, 8), ItemRotation.None)), float2(-300, 0)); })); Assert.Contains("No Such Gun", Assert.Single(failures)); } @@ -301,8 +301,8 @@ public sealed partial class RunStartTests var staged = Stage(zone, new Scripted(true, stage => { stage.Player(stage.Bare("Djinni"), float2(0, 0)); - stage.Place(stage.Generated("LonginusX"), float2(300, 0), stance: ScenarioStance.Hostile, piloted: true); - stage.Place(stage.Bare("LonginusX"), float2(-300, 0), stance: ScenarioStance.Neutral, piloted: false); + stage.Place(stage.Generated("Longinus"), float2(300, 0), stance: ScenarioStance.Hostile, piloted: true); + stage.Place(stage.Bare("Longinus"), float2(-300, 0), stance: ScenarioStance.Neutral, piloted: false); })); var player = staged.Player; var hostile = staged.Entities[0]; @@ -336,8 +336,8 @@ public sealed partial class RunStartTests var staged = Stage(zone, new Scripted(true, stage => { stage.Player(stage.Bare("Djinni"), float2(0, 0)); - stage.Place(stage.Bare("LonginusX"), float2(300, 0), stance: ScenarioStance.Hostile); - stage.Place(stage.Bare("LonginusX"), float2(-300, 0), stance: ScenarioStance.Hostile); + stage.Place(stage.Bare("Longinus"), float2(300, 0), stance: ScenarioStance.Hostile); + stage.Place(stage.Bare("Longinus"), float2(-300, 0), stance: ScenarioStance.Hostile); })); var player = staged.Player; var hostile = staged.Entities[0]; diff --git a/tests/Aetheria.Shared.Tests/ShipAuthoringTests.cs b/tests/Aetheria.Shared.Tests/ShipAuthoringTests.cs index f969ff187..98eeaf5f7 100644 --- a/tests/Aetheria.Shared.Tests/ShipAuthoringTests.cs +++ b/tests/Aetheria.Shared.Tests/ShipAuthoringTests.cs @@ -251,7 +251,7 @@ public sealed class ShipAuthoringTests [Theory] [InlineData(HardpointType.Hull), InlineData(HardpointType.Tool), InlineData(HardpointType.Thermal), InlineData(HardpointType.WarpDrive), InlineData(HardpointType.Reactor), InlineData(HardpointType.Shield), InlineData(HardpointType.Sensors), - InlineData(HardpointType.ControlModule), InlineData(HardpointType.AetherDrive)] + InlineData(HardpointType.ControlModule)] public void AnInternalHardpointCarriesNoAnchor(HardpointType type) { WithOnly(type).Validate(); diff --git a/tests/Aetheria.Shared.Tests/ShipValidationTests.cs b/tests/Aetheria.Shared.Tests/ShipValidationTests.cs index c67f3e6d3..804ff723f 100644 --- a/tests/Aetheria.Shared.Tests/ShipValidationTests.cs +++ b/tests/Aetheria.Shared.Tests/ShipValidationTests.cs @@ -33,7 +33,7 @@ public sealed class ShipValidationTests var ship = ShipAuthoringTests.Fixture(); ship.Hull.Hardpoints[0].Armor = 5; ship.Hull.Hardpoints[0].FiringArc = 360; - ship.Hull.Hardpoints[0].Type = HardpointType.AetherDrive; + ship.Hull.Hardpoints[0].Type = HardpointType.ControlModule; ship.Visual.Anchors.RemoveAll(anchor => anchor.Id == "thruster.port"); ship.Hull.Hardpoints[0].Rotation = ItemRotation.Clockwise; ship.Validate(); diff --git a/tests/Aetheria.Shared.Tests/SteeringTests.cs b/tests/Aetheria.Shared.Tests/SteeringTests.cs index b87e91f71..e6c8604a6 100644 --- a/tests/Aetheria.Shared.Tests/SteeringTests.cs +++ b/tests/Aetheria.Shared.Tests/SteeringTests.cs @@ -241,7 +241,7 @@ public sealed class SteeringTests } // The turn bound has one owner, the actuators: Ship.Update hands Turn to them unclamped, Thruster.Axis saturates to - // 0..1 and AetherDrive.Axis clamps to -1..1. A Turn of 3 therefore rotates the hull as a Turn of 1. + // 0..1. A Turn of 3 therefore rotates the hull as a Turn of 1. [Fact] public void TheActuatorsBoundATurnBeyondOne() { @@ -276,13 +276,6 @@ public sealed class SteeringTests thruster.Axis = -3f; Assert.Equal(0f, thruster.Axis); } - - // Axis is a pure property; no catalog hull carries a drive, so the instance is made without its item. - var drive = (AetherDrive)System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(typeof(AetherDrive)); - drive.Axis = float3(3f, -3f, 3f); - Assert.Equal(float3(1f, -1f, 1f), drive.Axis); - drive.Axis = float3(0f, 0f, -3f); - Assert.Equal(-1f, drive.Axis.z); } // The size of a turn, computed from the hull and not read back from the code under test: each rotation thruster diff --git a/tests/mutation_tests_addressables_cut.py b/tests/mutation_tests_addressables_cut.py index 3194b47cb..953121448 100644 --- a/tests/mutation_tests_addressables_cut.py +++ b/tests/mutation_tests_addressables_cut.py @@ -33,7 +33,7 @@ UNITY_EXE = r"C:\Program Files\Unity\Hub\Editor\6000.3.24f1\Editor\Unity.exe" ITEM_DATA_CS = REPO_ROOT / "Assets" / "Scripts" / "ServerShared" / "ItemData.cs" DIRECTORY_BUILD_PROPS = REPO_ROOT / "Directory.Build.props" -# A known-good record to mutate: HullData "LonginusX", whose Prefab already resolves (see the Cut 2 +# A known-good record to mutate: HullData "Longinus", whose Prefab already resolves (see the Cut 2 # migration report). Texture2D guid is its own Schematic (schema_Longinus.png), still a real, # addressable asset, just the wrong type for Prefab (which wants a GameObject+EntityInstance). TEXTURE_GUID = "51702555e534a7a4fb39eb105b80bbaf" @@ -109,7 +109,7 @@ using System.Linq; using System.Reflection; using GameCult.Caching; -// Scratch-only (not landed): sets HullData "LonginusX".Prefab to an arbitrary string in a copy of +// Scratch-only (not landed): sets HullData "Longinus".Prefab to an arbitrary string in a copy of // the catalog, for tests/mutation_tests.py to feed to EngineAssetCheck and prove it fails. internal static class Program { @@ -122,12 +122,12 @@ internal static class Program { var stored = cache.AllStoredDocuments.First(s => s.Descriptor.DocumentType.Name == "HullData" && - (string)s.Descriptor.DocumentType.GetField("Name").GetValue(s.Document) == "LonginusX"); + (string)s.Descriptor.DocumentType.GetField("Name").GetValue(s.Document) == "Longinus"); var field = stored.Descriptor.DocumentType.GetField("Prefab", BindingFlags.Public | BindingFlags.Instance); field.SetValue(stored.Document, newValue); cache.UpsertAsync(stored.Descriptor.DocumentType, stored.Document, stored.Key).Wait(); cache.FlushAllBackingStores(); - Console.WriteLine($"Set HullData \"LonginusX\".Prefab = \"{newValue}\""); + Console.WriteLine($"Set HullData \"Longinus\".Prefab = \"{newValue}\""); return 0; } finally @@ -217,11 +217,11 @@ def main(): if code == 0: failures.append(f"{name}: EngineAssetCheck exited 0; the mutation was not caught") continue - if "LonginusX" not in text: - failures.append(f"{name}: failure did not name the record (LonginusX)") + if "Longinus" not in text: + failures.append(f"{name}: failure did not name the record (Longinus)") continue assert_contains(text, expect, name) - print(f"OK: caught, exit {code}, named LonginusX, mentioned {expect!r}") + print(f"OK: caught, exit {code}, named Longinus, mentioned {expect!r}") except SystemExit as exc: failures.append(f"{name}: {exc}") finally: diff --git a/tools/blender/aetheria_ships/ship_cc.py b/tools/blender/aetheria_ships/ship_cc.py index f16fe72a5..6018d6a96 100644 --- a/tools/blender/aetheria_ships/ship_cc.py +++ b/tools/blender/aetheria_ships/ship_cc.py @@ -33,7 +33,7 @@ HARDPOINT_MEMBERS = ("Type", "Position", "Shape", "Transform", "Rotation", "Armo ANCHOR_MEMBERS = ("Id", "Role", "ModelNodeId", "ParentId", "Order") # Enum member names in value order (HardpointType, ItemRotation). HARDPOINT_TYPE_NAMES = ("Hull", "Tool", "Thermal", "Thruster", "WarpDrive", "Reactor", "Radiator", - "Shield", "Sensors", "Energy", "Ballistic", "Launcher", "ControlModule", "AetherDrive") + "Shield", "Sensors", "Energy", "Ballistic", "Launcher", "ControlModule") ROTATION_NAMES = ("None", "CounterClockwise", "Reversed", "Clockwise") From f53f45fdf1f81548d093e3750cf77a076b6a35ca Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 13:47:23 +0200 Subject: [PATCH 002/120] AetherDb retire-longinusx: the one-shot that deletes the hull, the drive and their products Dry run unless "apply". Refuses unless exactly the four records are present, refuses when any other record's bytes name one of their ids or the names LonginusX or Traction, and finds nothing on a second apply. Deleted again once the catalog lands. Co-Authored-By: Claude Sonnet 5.5 --- tools/AetherDb/Program.cs | 64 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tools/AetherDb/Program.cs b/tools/AetherDb/Program.cs index c65f4c3f2..5f2fc61bd 100644 --- a/tools/AetherDb/Program.cs +++ b/tools/AetherDb/Program.cs @@ -33,6 +33,7 @@ public static class Program case "brownout-migrate": return BrownoutMigrate(args.Contains("apply")); case "roles-migrate": return RolesMigrate(args.Contains("apply")); case "firing-arc-migrate": return FiringArcMigrate(args.Contains("apply")); + case "retire-longinusx": return RetireLonginusX(args.Contains("apply")); case "targeting-catalog": return TargetingCatalog(args.Contains("apply")); case "targeting-catalog-6c": return TargetingCatalog6c(args.Contains("apply")); case "targeting-catalog-6d": return TargetingCatalog6d(args.Contains("apply")); @@ -1085,6 +1086,69 @@ public static class Program return 0; } + // retire-longinusx (docs/aetheria-release-map.md, "Retiring the LonginusX and parking the aether drive"): deletes + // HullData LonginusX, GearData Traction and the two FactionProductData that sell them, in one Commit. Dry run unless + // passed "apply". It refuses unless exactly those four records are present, and refuses when any other record's + // serialized bytes name one of their ids or the names LonginusX or Traction. A second apply finds nothing and refuses. + private static int RetireLonginusX(bool apply) + { + var hullId = "445d911a-9951-40a3-874e-0b7ce8b35129"; + var gearId = "42d1ffaa-f764-4a65-ba03-43cc4e6fd2bc"; + var gearProductId = "007ed2b1-8fdf-473d-9ebf-e55fcd1a6903"; + var db = AetherDb.Open(catalogWritable: apply); + + var stored = db.Cache.AllStoredDocuments.ToArray(); + var hull = stored.FirstOrDefault(d => d.Document is HullData && string.Equals(d.Key.Value, hullId, StringComparison.OrdinalIgnoreCase)); + var gear = stored.FirstOrDefault(d => d.Document is GearData && string.Equals(d.Key.Value, gearId, StringComparison.OrdinalIgnoreCase)); + if (hull == null || gear == null) + { + Console.WriteLine("Nothing to retire: the LonginusX hull and the Traction gear are not both present."); + return 1; + } + + var doomed = new List { hull, gear }; + doomed.AddRange(stored.Where(d => d.Document is FactionProductData product && + (product.Design.Key.Value == hull.Key.Value || product.Design.Key.Value == gear.Key.Value))); + if (doomed.Count != 4 || doomed.Count(d => string.Equals(d.Key.Value, gearProductId, StringComparison.OrdinalIgnoreCase)) != 1) + { + Console.WriteLine($"Refused: expected the hull, the gear and their two products, found {doomed.Count} records."); + return 1; + } + + var needles = doomed.Select(d => d.Key.Value).Concat(new[] { "LonginusX", "Traction" }) + .Select(System.Text.Encoding.UTF8.GetBytes).ToArray(); + var doomedKeys = new HashSet(doomed.Select(d => d.Key.Value)); + var named = 0; + foreach (var other in stored.Where(d => !doomedKeys.Contains(d.Key.Value))) + { + var options = CultDocumentMessagePackSerialization.OptionsFor(other.Document.GetType().Assembly); + var bytes = MessagePack.MessagePackSerializer.Serialize(other.Document.GetType(), other.Document, options); + if (!needles.Any(needle => bytes.AsSpan().IndexOf(needle) >= 0)) continue; + Console.WriteLine($"Named by {other.Document.GetType().Name} {NameOf(other.Document) ?? other.Key.Value}"); + named++; + } + if (named > 0) + { + Console.WriteLine($"Refused: {named} other records name the retiring records."); + return 1; + } + + foreach (var d in doomed) + Console.WriteLine($"{(apply ? "Deleting" : "Would delete")} {d.Document.GetType().Name} {NameOf(d.Document)} {d.Key.Value}"); + if (!apply) + { + Console.WriteLine("Dry run. Pass \"apply\" to delete 4 records."); + return 0; + } + + db.Cache.Commit(batch => + { + foreach (var d in doomed) batch.Remove(d.Key); + }); + Console.WriteLine("Deleted 4 records from Aetheria.cc"); + return 0; + } + // Fire control Cut 6a (docs/fire-control-cut.md, Cut 2's "Catalog:" bullet). Cut 2 shipped the behaviour and // never shipped the data: no targeting-system design exists, so tools/AetherDb loadout throws // InvalidLoadoutException ("No compatible targeting system found for entity!") for every armed hull. This From 7109ce9e573111165ecdda8d10201a6fa8b6ca9e Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 13:55:55 +0200 Subject: [PATCH 003/120] retire-longinusx command: import its serializer namespace; keep the Python enum names equal to the enum until the enum member goes The previous commit did not compile AetherDb (missing using) and dropped the Python name while HardpointType.AetherDrive still existed, which ShipSchemaPinTests pins. Both repaired; the name leaves with the enum member. Co-Authored-By: Claude Sonnet 5.5 --- tools/AetherDb/Program.cs | 1 + tools/blender/aetheria_ships/ship_cc.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/AetherDb/Program.cs b/tools/AetherDb/Program.cs index 5f2fc61bd..4c2c40d7a 100644 --- a/tools/AetherDb/Program.cs +++ b/tools/AetherDb/Program.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Reflection; using CultMath; using GameCult.Caching; +using GameCult.Caching.MessagePack; using Random = CultMath.Random; // Commands over the game database, run with: dotnet run --project tools/AetherDb -- diff --git a/tools/blender/aetheria_ships/ship_cc.py b/tools/blender/aetheria_ships/ship_cc.py index 6018d6a96..f16fe72a5 100644 --- a/tools/blender/aetheria_ships/ship_cc.py +++ b/tools/blender/aetheria_ships/ship_cc.py @@ -33,7 +33,7 @@ HARDPOINT_MEMBERS = ("Type", "Position", "Shape", "Transform", "Rotation", "Armo ANCHOR_MEMBERS = ("Id", "Role", "ModelNodeId", "ParentId", "Order") # Enum member names in value order (HardpointType, ItemRotation). HARDPOINT_TYPE_NAMES = ("Hull", "Tool", "Thermal", "Thruster", "WarpDrive", "Reactor", "Radiator", - "Shield", "Sensors", "Energy", "Ballistic", "Launcher", "ControlModule") + "Shield", "Sensors", "Energy", "Ballistic", "Launcher", "ControlModule", "AetherDrive") ROTATION_NAMES = ("None", "CounterClockwise", "Reversed", "Clockwise") From 8038b46aec1cf7358353cc14314c37ae983aa830 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 13:57:31 +0200 Subject: [PATCH 004/120] retire-longinusx: a record names the retiring ones only by a whole string, not by a substring The first dry run was refused by NameFile USA, whose name list holds longer strings containing the words. A reference is a string equal to the id or the name. Co-Authored-By: Claude Sonnet 5.5 --- tools/AetherDb/Program.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/AetherDb/Program.cs b/tools/AetherDb/Program.cs index 4c2c40d7a..07e931f3e 100644 --- a/tools/AetherDb/Program.cs +++ b/tools/AetherDb/Program.cs @@ -1116,8 +1116,9 @@ public static class Program return 1; } + // A needle is the whole serialized string: a name inside a longer string (a name list entry) is not a reference. var needles = doomed.Select(d => d.Key.Value).Concat(new[] { "LonginusX", "Traction" }) - .Select(System.Text.Encoding.UTF8.GetBytes).ToArray(); + .Select(text => MessagePack.MessagePackSerializer.Serialize(text)).ToArray(); var doomedKeys = new HashSet(doomed.Select(d => d.Key.Value)); var named = 0; foreach (var other in stored.Where(d => !doomedKeys.Contains(d.Key.Value))) From 2226190d7a5c88978291dccb19fe3c43fd19ab01 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 13:59:36 +0200 Subject: [PATCH 005/120] Scenario fits: the Fire Control Array sits on free Longinus cells (0,4) The first relaying put it at (1,6), where its 2x1 footprint met the cockpit. Co-Authored-By: Claude Sonnet 5.5 --- Assets/Scripts/ServerShared/Scenarios/Arcs.cs | 2 +- Assets/Scripts/ServerShared/Scenarios/Duel.cs | 2 +- Assets/Scripts/ServerShared/Scenarios/LauncherAngles.cs | 2 +- Assets/Scripts/ServerShared/Scenarios/StarvedReactor.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Assets/Scripts/ServerShared/Scenarios/Arcs.cs b/Assets/Scripts/ServerShared/Scenarios/Arcs.cs index bbbb867be..585ebe64b 100644 --- a/Assets/Scripts/ServerShared/Scenarios/Arcs.cs +++ b/Assets/Scripts/ServerShared/Scenarios/Arcs.cs @@ -30,7 +30,7 @@ public sealed class Arcs : Scenario ("Iapyx", int2(2, 2), CounterClockwise), ("Iapyx", int2(3, 2), Clockwise), ("not if i see you first", int2(3, 10), None), - ("Fire Control Array", int2(1, 6), None)); + ("Fire Control Array", int2(0, 4), None)); var turret = stage.Fit("Turret", ("ClearPath", int2(1, 2), None), ("ClearPath", int2(5, 2), None), diff --git a/Assets/Scripts/ServerShared/Scenarios/Duel.cs b/Assets/Scripts/ServerShared/Scenarios/Duel.cs index 457197186..5fafd63e4 100644 --- a/Assets/Scripts/ServerShared/Scenarios/Duel.cs +++ b/Assets/Scripts/ServerShared/Scenarios/Duel.cs @@ -28,7 +28,7 @@ public sealed class Duel : Scenario ("Iapyx", int2(3, 2), Clockwise), ("not if i see you first", int2(3, 10), None), ("PotaT+-", int2(1, 5), None), - ("Fire Control Array", int2(1, 6), None), + ("Fire Control Array", int2(0, 4), None), ("Store-All Plus", int2(2, 8), None)); stage.Player(fighter, float2(0, 0)); diff --git a/Assets/Scripts/ServerShared/Scenarios/LauncherAngles.cs b/Assets/Scripts/ServerShared/Scenarios/LauncherAngles.cs index e5439d3cc..3c0bb2f01 100644 --- a/Assets/Scripts/ServerShared/Scenarios/LauncherAngles.cs +++ b/Assets/Scripts/ServerShared/Scenarios/LauncherAngles.cs @@ -27,7 +27,7 @@ public sealed class LauncherAngles : Scenario ("Iapyx", int2(2, 2), CounterClockwise), ("Iapyx", int2(3, 2), Clockwise), ("not if i see you first", int2(3, 10), None), - ("Fire Control Array", int2(1, 6), None)); + ("Fire Control Array", int2(0, 4), None)); stage.Player(launcher, float2(0, 0)); stage.Place(stage.Bare("Longinus"), float2(0, 900), facing: float2(1, 0), stance: ScenarioStance.Hostile); diff --git a/Assets/Scripts/ServerShared/Scenarios/StarvedReactor.cs b/Assets/Scripts/ServerShared/Scenarios/StarvedReactor.cs index 4c57c3b36..4cd9d3daa 100644 --- a/Assets/Scripts/ServerShared/Scenarios/StarvedReactor.cs +++ b/Assets/Scripts/ServerShared/Scenarios/StarvedReactor.cs @@ -25,7 +25,7 @@ public sealed class StarvedReactor : Scenario ("Iapyx", int2(2, 2), CounterClockwise), ("Iapyx", int2(3, 2), Clockwise), ("not if i see you first", int2(3, 10), None), - ("Fire Control Array", int2(1, 6), None)); + ("Fire Control Array", int2(0, 4), None)); stage.Player(starved, float2(0, 0)); stage.Place(stage.Bare("Longinus"), float2(0, 400)); } From 4656857420f2ad2a6f3542ce402a4c9ef52ec936 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 13:59:45 +0200 Subject: [PATCH 006/120] Catalog: HullData LonginusX, GearData Traction and their two FactionProductData are gone Produced by AetherDb retire-longinusx apply on this branch's base catalog (63250cf2 in the LFS store); dry run listed exactly the four records, a second apply is refused, and AetherDb dangling reports 0. Co-Authored-By: Claude Sonnet 5.5 --- GameData/Aetheria.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GameData/Aetheria.cc b/GameData/Aetheria.cc index 96135d923..6e5d39090 100644 --- a/GameData/Aetheria.cc +++ b/GameData/Aetheria.cc @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63250cf21ae9abeec657f0603336bb11b4d50b66560903ef45a8cceb01a7ae25 -size 12703489 +oid sha256:1e474a1d862135072369f0177dac29ec583e860567d6908fdf346e2f60ab0922 +size 12701513 From 2cfdc72853a093de1219b0cf383a6047dcbc23a9 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 14:04:28 +0200 Subject: [PATCH 007/120] Delete the aether drive: behaviour, union 12, hardpoint type, aggregates, settings, presenters, prefab AetherDriveData and AetherDrive, Union(12) (now a retired-do-not-reuse comment, as 26 is), HardpointType.AetherDrive (last member, nothing shifts), Ship's drive sets, aggregates and Axis write, the five request-field rows, GameplaySettings Aether multipliers, ShipInstance's drive particles, SchematicDisplay's RPM display and AetherDriveUi, the undock refusal's drive clause, the drive prefab and material, and AetherDb's drive roles and brownout row. The design is kept at parked/aether-drive. Adds EveryShippedShipHullHasATopSpeed. ARPG.unity still holds stale serialized references to the removed SchematicDisplay fields and the RPM Display object; the next commit removes them. Co-Authored-By: Claude Sonnet 5.5 --- .../Prefabs/Thrusters/Aether Drive.prefab | 4819 ----------------- .../Thrusters/Aether Drive.prefab.meta | 7 - .../Dithered Particle Trail AetherDrive.mat | 78 - ...thered Particle Trail AetherDrive.mat.meta | 8 - Assets/Scripts/Gameplay/ActionGameManager.cs | 2 +- Assets/Scripts/Gameplay/ShipInstance.cs | 36 - .../ServerShared/Behaviors/AetherDrive.cs | 185 - .../Behaviors/AetherDrive.cs.meta | 3 - .../ServerShared/Behaviors/Behaviors.cs | 2 +- Assets/Scripts/ServerShared/Entity.cs | 6 +- Assets/Scripts/ServerShared/Enums.cs | 3 +- Assets/Scripts/ServerShared/ItemData.cs | 8 +- Assets/Scripts/ServerShared/PowerTiers.cs | 2 +- Assets/Scripts/ServerShared/Settings.cs | 4 +- Assets/Scripts/ServerShared/Ship.cs | 39 - Assets/Scripts/UI/HUD/SchematicDisplay.cs | 23 - .../FireControlCut12Tests.cs | 2 +- .../FireControlCut9Tests.cs | 2 +- .../RestoredHullsTests.cs | 17 + tools/AetherDb/Program.cs | 22 +- tools/blender/aetheria_ships/ship_cc.py | 2 +- 21 files changed, 36 insertions(+), 5234 deletions(-) delete mode 100644 Assets/Content/Prefabs/Thrusters/Aether Drive.prefab delete mode 100644 Assets/Content/Prefabs/Thrusters/Aether Drive.prefab.meta delete mode 100644 Assets/Materials/Particles/Dithered Particle Trail AetherDrive.mat delete mode 100644 Assets/Materials/Particles/Dithered Particle Trail AetherDrive.mat.meta delete mode 100644 Assets/Scripts/ServerShared/Behaviors/AetherDrive.cs delete mode 100644 Assets/Scripts/ServerShared/Behaviors/AetherDrive.cs.meta diff --git a/Assets/Content/Prefabs/Thrusters/Aether Drive.prefab b/Assets/Content/Prefabs/Thrusters/Aether Drive.prefab deleted file mode 100644 index 4a566b534..000000000 --- a/Assets/Content/Prefabs/Thrusters/Aether Drive.prefab +++ /dev/null @@ -1,4819 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!1 &3270076511764283730 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 5152310125488819541} - - component: {fileID: 8928170315538518364} - - component: {fileID: 8974554360790158463} - m_Layer: 0 - m_Name: Aether Drive - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!4 &5152310125488819541 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 3270076511764283730} - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_Children: [] - m_Father: {fileID: 0} - m_RootOrder: 0 - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!198 &8928170315538518364 -ParticleSystem: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 3270076511764283730} - serializedVersion: 7 - lengthInSec: 5 - simulationSpeed: 2 - stopAction: 0 - cullingMode: 0 - ringBufferMode: 0 - ringBufferLoopRange: {x: 0, y: 1} - looping: 1 - prewarm: 0 - playOnAwake: 1 - useUnscaledTime: 0 - autoRandomSeed: 1 - useRigidbodyForVelocity: 0 - startDelay: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - moveWithTransform: 2 - moveWithCustomTransform: {fileID: 0} - scalingMode: 2 - randomSeed: 0 - InitialModule: - serializedVersion: 3 - enabled: 1 - startLifetime: - serializedVersion: 2 - minMaxState: 0 - scalar: 0.5 - minScalar: 5 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - startSpeed: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 5 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - startColor: - serializedVersion: 2 - minMaxState: 0 - minColor: {r: 1, g: 1, b: 1, a: 1} - maxColor: {r: 1, g: 1, b: 1, a: 1} - maxGradient: - serializedVersion: 2 - key0: {r: 1, g: 1, b: 1, a: 1} - key1: {r: 1, g: 1, b: 1, a: 1} - key2: {r: 0, g: 0, b: 0, a: 0} - key3: {r: 0, g: 0, b: 0, a: 0} - key4: {r: 0, g: 0, b: 0, a: 0} - key5: {r: 0, g: 0, b: 0, a: 0} - key6: {r: 0, g: 0, b: 0, a: 0} - key7: {r: 0, g: 0, b: 0, a: 0} - ctime0: 0 - ctime1: 65535 - ctime2: 0 - ctime3: 0 - ctime4: 0 - ctime5: 0 - ctime6: 0 - ctime7: 0 - atime0: 0 - atime1: 65535 - atime2: 0 - atime3: 0 - atime4: 0 - atime5: 0 - atime6: 0 - atime7: 0 - m_Mode: 0 - m_NumColorKeys: 2 - m_NumAlphaKeys: 2 - minGradient: - serializedVersion: 2 - key0: {r: 1, g: 1, b: 1, a: 1} - key1: {r: 1, g: 1, b: 1, a: 1} - key2: {r: 0, g: 0, b: 0, a: 0} - key3: {r: 0, g: 0, b: 0, a: 0} - key4: {r: 0, g: 0, b: 0, a: 0} - key5: {r: 0, g: 0, b: 0, a: 0} - key6: {r: 0, g: 0, b: 0, a: 0} - key7: {r: 0, g: 0, b: 0, a: 0} - ctime0: 0 - ctime1: 65535 - ctime2: 0 - ctime3: 0 - ctime4: 0 - ctime5: 0 - ctime6: 0 - ctime7: 0 - atime0: 0 - atime1: 65535 - atime2: 0 - atime3: 0 - atime4: 0 - atime5: 0 - atime6: 0 - atime7: 0 - m_Mode: 0 - m_NumColorKeys: 2 - m_NumAlphaKeys: 2 - startSize: - serializedVersion: 2 - minMaxState: 0 - scalar: 0.25 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - startSizeY: - serializedVersion: 2 - minMaxState: 0 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - startSizeZ: - serializedVersion: 2 - minMaxState: 0 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - startRotationX: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - startRotationY: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - startRotation: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - randomizeRotationDirection: 0 - maxNumParticles: 1000 - size3D: 0 - rotation3D: 0 - gravityModifier: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - ShapeModule: - serializedVersion: 6 - enabled: 1 - type: 0 - angle: 25 - length: 5 - boxThickness: {x: 0, y: 0, z: 0} - radiusThickness: 1 - donutRadius: 0.2 - m_Position: {x: 0, y: 0, z: 0} - m_Rotation: {x: 0, y: 0, z: 0} - m_Scale: {x: 1, y: 1, z: 1} - placementMode: 2 - m_MeshMaterialIndex: 0 - m_MeshNormalOffset: 0 - m_MeshSpawn: - mode: 0 - spread: 0 - speed: - serializedVersion: 2 - minMaxState: 0 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - m_Mesh: {fileID: 0} - m_MeshRenderer: {fileID: 0} - m_SkinnedMeshRenderer: {fileID: 0} - m_Sprite: {fileID: 0} - m_SpriteRenderer: {fileID: 0} - m_UseMeshMaterialIndex: 0 - m_UseMeshColors: 0 - alignToDirection: 0 - m_Texture: {fileID: 0} - m_TextureClipChannel: 3 - m_TextureClipThreshold: 0 - m_TextureUVChannel: 0 - m_TextureColorAffectsParticles: 1 - m_TextureAlphaAffectsParticles: 1 - m_TextureBilinearFiltering: 0 - randomDirectionAmount: 0 - sphericalDirectionAmount: 0 - randomPositionAmount: 0 - radius: - value: 10 - mode: 0 - spread: 0 - speed: - serializedVersion: 2 - minMaxState: 0 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - arc: - value: 360 - mode: 0 - spread: 0 - speed: - serializedVersion: 2 - minMaxState: 0 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - EmissionModule: - enabled: 1 - serializedVersion: 4 - rateOverTime: - serializedVersion: 2 - minMaxState: 0 - scalar: 75 - minScalar: 10 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - rateOverDistance: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - m_BurstCount: 0 - m_Bursts: [] - SizeModule: - enabled: 1 - curve: - serializedVersion: 2 - minMaxState: 1 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 1 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 1 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - y: - serializedVersion: 2 - minMaxState: 1 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 1 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 1 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - z: - serializedVersion: 2 - minMaxState: 1 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 1 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 1 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - separateAxes: 0 - RotationModule: - enabled: 0 - x: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - y: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - curve: - serializedVersion: 2 - minMaxState: 0 - scalar: 0.7853982 - minScalar: 0.7853982 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - separateAxes: 0 - ColorModule: - enabled: 1 - gradient: - serializedVersion: 2 - minMaxState: 1 - minColor: {r: 1, g: 1, b: 1, a: 1} - maxColor: {r: 1, g: 1, b: 1, a: 1} - maxGradient: - serializedVersion: 2 - key0: {r: 0, g: 0.54509807, b: 1, a: 1} - key1: {r: 0.122641504, g: 0, b: 0.121027805, a: 0.45490196} - key2: {r: 0.122641504, g: 0, b: 0.121027805, a: 0} - key3: {r: 0, g: 0, b: 0, a: 0} - key4: {r: 0, g: 0, b: 0, a: 0} - key5: {r: 0, g: 0, b: 0, a: 0} - key6: {r: 0, g: 0, b: 0, a: 0} - key7: {r: 0, g: 0, b: 0, a: 0} - ctime0: 0 - ctime1: 65535 - ctime2: 65535 - ctime3: 0 - ctime4: 0 - ctime5: 0 - ctime6: 0 - ctime7: 0 - atime0: 0 - atime1: 65535 - atime2: 0 - atime3: 0 - atime4: 0 - atime5: 0 - atime6: 0 - atime7: 0 - m_Mode: 0 - m_NumColorKeys: 2 - m_NumAlphaKeys: 2 - minGradient: - serializedVersion: 2 - key0: {r: 1, g: 1, b: 1, a: 1} - key1: {r: 1, g: 1, b: 1, a: 1} - key2: {r: 0, g: 0, b: 0, a: 0} - key3: {r: 0, g: 0, b: 0, a: 0} - key4: {r: 0, g: 0, b: 0, a: 0} - key5: {r: 0, g: 0, b: 0, a: 0} - key6: {r: 0, g: 0, b: 0, a: 0} - key7: {r: 0, g: 0, b: 0, a: 0} - ctime0: 0 - ctime1: 65535 - ctime2: 0 - ctime3: 0 - ctime4: 0 - ctime5: 0 - ctime6: 0 - ctime7: 0 - atime0: 0 - atime1: 65535 - atime2: 0 - atime3: 0 - atime4: 0 - atime5: 0 - atime6: 0 - atime7: 0 - m_Mode: 0 - m_NumColorKeys: 2 - m_NumAlphaKeys: 2 - UVModule: - serializedVersion: 2 - enabled: 0 - mode: 0 - timeMode: 0 - fps: 30 - frameOverTime: - serializedVersion: 2 - minMaxState: 1 - scalar: 0.9999 - minScalar: 0.9999 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 1 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 1 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - startFrame: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - speedRange: {x: 0, y: 1} - tilesX: 1 - tilesY: 1 - animationType: 0 - rowIndex: 0 - cycles: 1 - uvChannelMask: -1 - rowMode: 1 - sprites: - - sprite: {fileID: 0} - flipU: 0 - flipV: 0 - VelocityModule: - enabled: 0 - x: - serializedVersion: 2 - minMaxState: 1 - scalar: 99.9 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - y: - serializedVersion: 2 - minMaxState: 1 - scalar: 1 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - z: - serializedVersion: 2 - minMaxState: 1 - scalar: 50 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: -1 - inSlope: 2 - outSlope: 2 - tangentMode: 0 - weightedMode: 0 - inWeight: 0 - outWeight: 0 - - serializedVersion: 3 - time: 1 - value: -0.013513565 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0 - outWeight: 0 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - orbitalX: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - orbitalY: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - orbitalZ: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - orbitalOffsetX: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - orbitalOffsetY: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - orbitalOffsetZ: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - radial: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - speedModifier: - serializedVersion: 2 - minMaxState: 0 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - inWorldSpace: 0 - InheritVelocityModule: - enabled: 0 - m_Mode: 0 - m_Curve: - serializedVersion: 2 - minMaxState: 0 - scalar: 2 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - LifetimeByEmitterSpeedModule: - enabled: 0 - m_Curve: - serializedVersion: 2 - minMaxState: 1 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: -0.8 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0.2 - inSlope: -0.8 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - m_Range: {x: 0, y: 1} - ForceModule: - enabled: 1 - x: - serializedVersion: 2 - minMaxState: 1 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: -1 - inSlope: 3.072868 - outSlope: 3.072868 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.11519607 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - y: - serializedVersion: 2 - minMaxState: 1 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: -1 - inSlope: 3.0251138 - outSlope: 3.0251138 - tangentMode: 0 - weightedMode: 0 - inWeight: 0 - outWeight: 0.09068627 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.12531352 - outWeight: 0 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - z: - serializedVersion: 2 - minMaxState: 1 - scalar: 50 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: -1 - inSlope: 3.0335512 - outSlope: 3.0335512 - tangentMode: 0 - weightedMode: 0 - inWeight: 0 - outWeight: 0.09354838 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0 - outWeight: 0 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - inWorldSpace: 1 - randomizePerFrame: 0 - ExternalForcesModule: - serializedVersion: 2 - enabled: 0 - multiplierCurve: - serializedVersion: 2 - minMaxState: 0 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - influenceFilter: 0 - influenceMask: - serializedVersion: 2 - m_Bits: 4294967295 - influenceList: [] - ClampVelocityModule: - enabled: 0 - x: - serializedVersion: 2 - minMaxState: 0 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - y: - serializedVersion: 2 - minMaxState: 0 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - z: - serializedVersion: 2 - minMaxState: 0 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - magnitude: - serializedVersion: 2 - minMaxState: 0 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - separateAxis: 0 - inWorldSpace: 0 - multiplyDragByParticleSize: 1 - multiplyDragByParticleVelocity: 1 - dampen: 0 - drag: - serializedVersion: 2 - minMaxState: 0 - scalar: 33.08 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - NoiseModule: - enabled: 1 - strength: - serializedVersion: 2 - minMaxState: 0 - scalar: 25 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: -2 - outSlope: -2 - tangentMode: 0 - weightedMode: 0 - inWeight: 0 - outWeight: 0 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0 - outWeight: 0 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - strengthY: - serializedVersion: 2 - minMaxState: 0 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - strengthZ: - serializedVersion: 2 - minMaxState: 0 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - separateAxes: 0 - frequency: 0.1 - damping: 0 - octaves: 3 - octaveMultiplier: 0.5 - octaveScale: 2 - quality: 2 - scrollSpeed: - serializedVersion: 2 - minMaxState: 0 - scalar: 1 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - remap: - serializedVersion: 2 - minMaxState: 1 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: -1 - inSlope: 0 - outSlope: 2 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 2 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - remapY: - serializedVersion: 2 - minMaxState: 1 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: -1 - inSlope: 0 - outSlope: 2 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 2 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - remapZ: - serializedVersion: 2 - minMaxState: 1 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: -1 - inSlope: 0 - outSlope: 2 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 2 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - remapEnabled: 0 - positionAmount: - serializedVersion: 2 - minMaxState: 0 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - rotationAmount: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - sizeAmount: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - SizeBySpeedModule: - enabled: 0 - curve: - serializedVersion: 2 - minMaxState: 1 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 1 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 1 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - y: - serializedVersion: 2 - minMaxState: 1 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 1 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 1 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - z: - serializedVersion: 2 - minMaxState: 1 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 1 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 1 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - range: {x: 0, y: 1} - separateAxes: 0 - RotationBySpeedModule: - enabled: 0 - x: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - y: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - curve: - serializedVersion: 2 - minMaxState: 0 - scalar: 0.7853982 - minScalar: 0.7853982 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - separateAxes: 0 - range: {x: 0, y: 1} - ColorBySpeedModule: - enabled: 0 - gradient: - serializedVersion: 2 - minMaxState: 1 - minColor: {r: 1, g: 1, b: 1, a: 1} - maxColor: {r: 1, g: 1, b: 1, a: 1} - maxGradient: - serializedVersion: 2 - key0: {r: 1, g: 1, b: 1, a: 1} - key1: {r: 1, g: 1, b: 1, a: 1} - key2: {r: 0, g: 0, b: 0, a: 0} - key3: {r: 0, g: 0, b: 0, a: 0} - key4: {r: 0, g: 0, b: 0, a: 0} - key5: {r: 0, g: 0, b: 0, a: 0} - key6: {r: 0, g: 0, b: 0, a: 0} - key7: {r: 0, g: 0, b: 0, a: 0} - ctime0: 0 - ctime1: 65535 - ctime2: 0 - ctime3: 0 - ctime4: 0 - ctime5: 0 - ctime6: 0 - ctime7: 0 - atime0: 0 - atime1: 65535 - atime2: 0 - atime3: 0 - atime4: 0 - atime5: 0 - atime6: 0 - atime7: 0 - m_Mode: 0 - m_NumColorKeys: 2 - m_NumAlphaKeys: 2 - minGradient: - serializedVersion: 2 - key0: {r: 1, g: 1, b: 1, a: 1} - key1: {r: 1, g: 1, b: 1, a: 1} - key2: {r: 0, g: 0, b: 0, a: 0} - key3: {r: 0, g: 0, b: 0, a: 0} - key4: {r: 0, g: 0, b: 0, a: 0} - key5: {r: 0, g: 0, b: 0, a: 0} - key6: {r: 0, g: 0, b: 0, a: 0} - key7: {r: 0, g: 0, b: 0, a: 0} - ctime0: 0 - ctime1: 65535 - ctime2: 0 - ctime3: 0 - ctime4: 0 - ctime5: 0 - ctime6: 0 - ctime7: 0 - atime0: 0 - atime1: 65535 - atime2: 0 - atime3: 0 - atime4: 0 - atime5: 0 - atime6: 0 - atime7: 0 - m_Mode: 0 - m_NumColorKeys: 2 - m_NumAlphaKeys: 2 - range: {x: 0, y: 1} - CollisionModule: - enabled: 0 - serializedVersion: 4 - type: 0 - collisionMode: 0 - colliderForce: 0 - multiplyColliderForceByParticleSize: 0 - multiplyColliderForceByParticleSpeed: 0 - multiplyColliderForceByCollisionAngle: 1 - m_Planes: - - {fileID: 0} - - {fileID: 0} - - {fileID: 0} - - {fileID: 0} - - {fileID: 0} - - {fileID: 0} - m_Dampen: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - m_Bounce: - serializedVersion: 2 - minMaxState: 0 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - m_EnergyLossOnCollision: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minKillSpeed: 0 - maxKillSpeed: 10000 - radiusScale: 1 - collidesWith: - serializedVersion: 2 - m_Bits: 4294967295 - maxCollisionShapes: 256 - quality: 0 - voxelSize: 0.5 - collisionMessages: 0 - collidesWithDynamic: 1 - interiorCollisions: 0 - TriggerModule: - enabled: 0 - serializedVersion: 2 - inside: 1 - outside: 0 - enter: 0 - exit: 0 - colliderQueryMode: 0 - radiusScale: 1 - primitives: - - {fileID: 0} - - {fileID: 0} - - {fileID: 0} - - {fileID: 0} - - {fileID: 0} - - {fileID: 0} - SubModule: - serializedVersion: 2 - enabled: 0 - subEmitters: - - serializedVersion: 3 - emitter: {fileID: 0} - type: 0 - properties: 0 - emitProbability: 1 - LightsModule: - enabled: 0 - ratio: 0 - light: {fileID: 0} - randomDistribution: 1 - color: 1 - range: 1 - intensity: 1 - rangeCurve: - serializedVersion: 2 - minMaxState: 0 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - intensityCurve: - serializedVersion: 2 - minMaxState: 0 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - maxLights: 20 - TrailModule: - enabled: 1 - mode: 0 - ratio: 1 - lifetime: - serializedVersion: 2 - minMaxState: 0 - scalar: 1 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minVertexDistance: 0.5 - textureMode: 0 - ribbonCount: 1 - shadowBias: 0.5 - worldSpace: 0 - dieWithParticles: 0 - sizeAffectsWidth: 0 - sizeAffectsLifetime: 0 - inheritParticleColor: 0 - generateLightingData: 0 - splitSubEmitterRibbons: 0 - attachRibbonsToTransform: 0 - colorOverLifetime: - serializedVersion: 2 - minMaxState: 0 - minColor: {r: 1, g: 1, b: 1, a: 1} - maxColor: {r: 1, g: 1, b: 1, a: 1} - maxGradient: - serializedVersion: 2 - key0: {r: 1, g: 1, b: 1, a: 1} - key1: {r: 1, g: 1, b: 1, a: 1} - key2: {r: 0, g: 0, b: 0, a: 0} - key3: {r: 0, g: 0, b: 0, a: 0} - key4: {r: 0, g: 0, b: 0, a: 0} - key5: {r: 0, g: 0, b: 0, a: 0} - key6: {r: 0, g: 0, b: 0, a: 0} - key7: {r: 0, g: 0, b: 0, a: 0} - ctime0: 0 - ctime1: 65535 - ctime2: 0 - ctime3: 0 - ctime4: 0 - ctime5: 0 - ctime6: 0 - ctime7: 0 - atime0: 0 - atime1: 65535 - atime2: 0 - atime3: 0 - atime4: 0 - atime5: 0 - atime6: 0 - atime7: 0 - m_Mode: 0 - m_NumColorKeys: 2 - m_NumAlphaKeys: 2 - minGradient: - serializedVersion: 2 - key0: {r: 1, g: 1, b: 1, a: 1} - key1: {r: 1, g: 1, b: 1, a: 1} - key2: {r: 0, g: 0, b: 0, a: 0} - key3: {r: 0, g: 0, b: 0, a: 0} - key4: {r: 0, g: 0, b: 0, a: 0} - key5: {r: 0, g: 0, b: 0, a: 0} - key6: {r: 0, g: 0, b: 0, a: 0} - key7: {r: 0, g: 0, b: 0, a: 0} - ctime0: 0 - ctime1: 65535 - ctime2: 0 - ctime3: 0 - ctime4: 0 - ctime5: 0 - ctime6: 0 - ctime7: 0 - atime0: 0 - atime1: 65535 - atime2: 0 - atime3: 0 - atime4: 0 - atime5: 0 - atime6: 0 - atime7: 0 - m_Mode: 0 - m_NumColorKeys: 2 - m_NumAlphaKeys: 2 - widthOverTrail: - serializedVersion: 2 - minMaxState: 1 - scalar: 0.25 - minScalar: 1 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 6.705435 - outSlope: 6.705435 - tangentMode: 0 - weightedMode: 0 - inWeight: 0 - outWeight: 0.034313723 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0 - outWeight: 0 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - colorOverTrail: - serializedVersion: 2 - minMaxState: 1 - minColor: {r: 1, g: 1, b: 1, a: 1} - maxColor: {r: 1, g: 1, b: 1, a: 1} - maxGradient: - serializedVersion: 2 - key0: {r: 0.2830189, g: 0, b: 0, a: 0} - key1: {r: 0.12415449, g: 0.26510748, b: 0.8490566, a: 1} - key2: {r: 0, g: 0, b: 0, a: 0} - key3: {r: 0, g: 0, b: 0, a: 0} - key4: {r: 0, g: 0, b: 0, a: 0} - key5: {r: 0, g: 0, b: 0, a: 0} - key6: {r: 0, g: 0, b: 0, a: 0} - key7: {r: 0, g: 0, b: 0, a: 0} - ctime0: 0 - ctime1: 65535 - ctime2: 0 - ctime3: 0 - ctime4: 0 - ctime5: 0 - ctime6: 0 - ctime7: 0 - atime0: 129 - atime1: 31802 - atime2: 65535 - atime3: 65535 - atime4: 0 - atime5: 0 - atime6: 0 - atime7: 0 - m_Mode: 0 - m_NumColorKeys: 2 - m_NumAlphaKeys: 3 - minGradient: - serializedVersion: 2 - key0: {r: 1, g: 1, b: 1, a: 1} - key1: {r: 1, g: 1, b: 1, a: 1} - key2: {r: 0, g: 0, b: 0, a: 0} - key3: {r: 0, g: 0, b: 0, a: 0} - key4: {r: 0, g: 0, b: 0, a: 0} - key5: {r: 0, g: 0, b: 0, a: 0} - key6: {r: 0, g: 0, b: 0, a: 0} - key7: {r: 0, g: 0, b: 0, a: 0} - ctime0: 0 - ctime1: 65535 - ctime2: 0 - ctime3: 0 - ctime4: 0 - ctime5: 0 - ctime6: 0 - ctime7: 0 - atime0: 0 - atime1: 65535 - atime2: 0 - atime3: 0 - atime4: 0 - atime5: 0 - atime6: 0 - atime7: 0 - m_Mode: 0 - m_NumColorKeys: 2 - m_NumAlphaKeys: 2 - CustomDataModule: - enabled: 1 - mode0: 1 - vectorComponentCount0: 2 - color0: - serializedVersion: 2 - minMaxState: 0 - minColor: {r: 1, g: 1, b: 1, a: 1} - maxColor: {r: 1, g: 1, b: 1, a: 1} - maxGradient: - serializedVersion: 2 - key0: {r: 1, g: 1, b: 1, a: 1} - key1: {r: 1, g: 1, b: 1, a: 1} - key2: {r: 0, g: 0, b: 0, a: 0} - key3: {r: 0, g: 0, b: 0, a: 0} - key4: {r: 0, g: 0, b: 0, a: 0} - key5: {r: 0, g: 0, b: 0, a: 0} - key6: {r: 0, g: 0, b: 0, a: 0} - key7: {r: 0, g: 0, b: 0, a: 0} - ctime0: 0 - ctime1: 65535 - ctime2: 0 - ctime3: 0 - ctime4: 0 - ctime5: 0 - ctime6: 0 - ctime7: 0 - atime0: 0 - atime1: 65535 - atime2: 0 - atime3: 0 - atime4: 0 - atime5: 0 - atime6: 0 - atime7: 0 - m_Mode: 0 - m_NumColorKeys: 2 - m_NumAlphaKeys: 2 - minGradient: - serializedVersion: 2 - key0: {r: 1, g: 1, b: 1, a: 1} - key1: {r: 1, g: 1, b: 1, a: 1} - key2: {r: 0, g: 0, b: 0, a: 0} - key3: {r: 0, g: 0, b: 0, a: 0} - key4: {r: 0, g: 0, b: 0, a: 0} - key5: {r: 0, g: 0, b: 0, a: 0} - key6: {r: 0, g: 0, b: 0, a: 0} - key7: {r: 0, g: 0, b: 0, a: 0} - ctime0: 0 - ctime1: 65535 - ctime2: 0 - ctime3: 0 - ctime4: 0 - ctime5: 0 - ctime6: 0 - ctime7: 0 - atime0: 0 - atime1: 65535 - atime2: 0 - atime3: 0 - atime4: 0 - atime5: 0 - atime6: 0 - atime7: 0 - m_Mode: 0 - m_NumColorKeys: 2 - m_NumAlphaKeys: 2 - colorLabel0: Color - vector0_0: - serializedVersion: 2 - minMaxState: 3 - scalar: 1 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - vectorLabel0_0: X - vector0_1: - serializedVersion: 2 - minMaxState: 3 - scalar: 1 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - vectorLabel0_1: Y - vector0_2: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - vectorLabel0_2: Z - vector0_3: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - vectorLabel0_3: W - mode1: 0 - vectorComponentCount1: 4 - color1: - serializedVersion: 2 - minMaxState: 0 - minColor: {r: 1, g: 1, b: 1, a: 1} - maxColor: {r: 1, g: 1, b: 1, a: 1} - maxGradient: - serializedVersion: 2 - key0: {r: 1, g: 1, b: 1, a: 1} - key1: {r: 1, g: 1, b: 1, a: 1} - key2: {r: 0, g: 0, b: 0, a: 0} - key3: {r: 0, g: 0, b: 0, a: 0} - key4: {r: 0, g: 0, b: 0, a: 0} - key5: {r: 0, g: 0, b: 0, a: 0} - key6: {r: 0, g: 0, b: 0, a: 0} - key7: {r: 0, g: 0, b: 0, a: 0} - ctime0: 0 - ctime1: 65535 - ctime2: 0 - ctime3: 0 - ctime4: 0 - ctime5: 0 - ctime6: 0 - ctime7: 0 - atime0: 0 - atime1: 65535 - atime2: 0 - atime3: 0 - atime4: 0 - atime5: 0 - atime6: 0 - atime7: 0 - m_Mode: 0 - m_NumColorKeys: 2 - m_NumAlphaKeys: 2 - minGradient: - serializedVersion: 2 - key0: {r: 1, g: 1, b: 1, a: 1} - key1: {r: 1, g: 1, b: 1, a: 1} - key2: {r: 0, g: 0, b: 0, a: 0} - key3: {r: 0, g: 0, b: 0, a: 0} - key4: {r: 0, g: 0, b: 0, a: 0} - key5: {r: 0, g: 0, b: 0, a: 0} - key6: {r: 0, g: 0, b: 0, a: 0} - key7: {r: 0, g: 0, b: 0, a: 0} - ctime0: 0 - ctime1: 65535 - ctime2: 0 - ctime3: 0 - ctime4: 0 - ctime5: 0 - ctime6: 0 - ctime7: 0 - atime0: 0 - atime1: 65535 - atime2: 0 - atime3: 0 - atime4: 0 - atime5: 0 - atime6: 0 - atime7: 0 - m_Mode: 0 - m_NumColorKeys: 2 - m_NumAlphaKeys: 2 - colorLabel1: Color - vector1_0: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - vectorLabel1_0: X - vector1_1: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - vectorLabel1_1: Y - vector1_2: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - vectorLabel1_2: Z - vector1_3: - serializedVersion: 2 - minMaxState: 0 - scalar: 0 - minScalar: 0 - maxCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - minCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - vectorLabel1_3: W ---- !u!199 &8974554360790158463 -ParticleSystemRenderer: - serializedVersion: 6 - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 3270076511764283730} - m_Enabled: 1 - m_CastShadows: 0 - m_ReceiveShadows: 0 - m_DynamicOccludee: 1 - m_MotionVectors: 1 - m_LightProbeUsage: 0 - m_ReflectionProbeUsage: 0 - m_RayTracingMode: 0 - m_RayTraceProcedural: 0 - m_RenderingLayerMask: 1 - m_RendererPriority: 0 - m_Materials: - - {fileID: 2100000, guid: 6af0f423a2567be43b16d152d2b60e0c, type: 2} - - {fileID: 2100000, guid: 555ada90c0f01654a9387483bd95a688, type: 2} - m_StaticBatchInfo: - firstSubMesh: 0 - subMeshCount: 0 - m_StaticBatchRoot: {fileID: 0} - m_ProbeAnchor: {fileID: 0} - m_LightProbeVolumeOverride: {fileID: 0} - m_ScaleInLightmap: 1 - m_ReceiveGI: 1 - m_PreserveUVs: 0 - m_IgnoreNormalsForChartDetection: 0 - m_ImportantGI: 0 - m_StitchLightmapSeams: 1 - m_SelectedEditorRenderState: 3 - m_MinimumChartSize: 4 - m_AutoUVMaxDistance: 0.5 - m_AutoUVMaxAngle: 89 - m_LightmapParameters: {fileID: 0} - m_SortingLayerID: 0 - m_SortingLayer: 0 - m_SortingOrder: 0 - m_RenderMode: 5 - m_SortMode: 0 - m_MinParticleSize: 0 - m_MaxParticleSize: 0.5 - m_CameraVelocityScale: 0 - m_VelocityScale: 0 - m_LengthScale: 2 - m_SortingFudge: 0 - m_NormalDirection: 1 - m_ShadowBias: 0 - m_RenderAlignment: 0 - m_Pivot: {x: 0, y: 0, z: 0} - m_Flip: {x: 0, y: 0, z: 0} - m_UseCustomVertexStreams: 1 - m_EnableGPUInstancing: 1 - m_ApplyActiveColorSpace: 1 - m_AllowRoll: 1 - m_FreeformStretching: 0 - m_RotateWithStretchDirection: 1 - m_VertexStreams: 0001030420 - m_Mesh: {fileID: 0} - m_Mesh1: {fileID: 0} - m_Mesh2: {fileID: 0} - m_Mesh3: {fileID: 0} - m_MaskInteraction: 0 diff --git a/Assets/Content/Prefabs/Thrusters/Aether Drive.prefab.meta b/Assets/Content/Prefabs/Thrusters/Aether Drive.prefab.meta deleted file mode 100644 index e33810938..000000000 --- a/Assets/Content/Prefabs/Thrusters/Aether Drive.prefab.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 151cf0ff54a22274caf671ece149fa6e -PrefabImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Materials/Particles/Dithered Particle Trail AetherDrive.mat b/Assets/Materials/Particles/Dithered Particle Trail AetherDrive.mat deleted file mode 100644 index d4224d262..000000000 --- a/Assets/Materials/Particles/Dithered Particle Trail AetherDrive.mat +++ /dev/null @@ -1,78 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!21 &2100000 -Material: - serializedVersion: 6 - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_Name: Dithered Particle Trail AetherDrive - m_Shader: {fileID: 4800000, guid: 46776b9fa35a437448f683f31b04d68d, type: 3} - m_ShaderKeywords: - m_LightmapFlags: 4 - m_EnableInstancingVariants: 0 - m_DoubleSidedGI: 0 - m_CustomRenderQueue: -1 - stringTagMap: {} - disabledShaderPasses: [] - m_SavedProperties: - serializedVersion: 3 - m_TexEnvs: - - _BumpMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _DetailAlbedoMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _DetailMask: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _DetailNormalMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _EmissionMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _MainTex: - m_Texture: {fileID: 2800000, guid: 1d6eba2a167939a468c00fda35723dd0, type: 3} - m_Scale: {x: 0, y: 1} - m_Offset: {x: 0.5, y: 0} - - _MetallicGlossMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _OcclusionMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _ParallaxMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - m_Floats: - - _BumpScale: 1 - - _Cutoff: 0.5 - - _DetailNormalMapScale: 1 - - _DstBlend: 0 - - _GlossMapScale: 1 - - _Glossiness: 0 - - _GlossyReflections: 1 - - _Metallic: 0 - - _Mode: 0 - - _OcclusionStrength: 1 - - _Parallax: 0.02 - - _SmoothnessTextureChannel: 0 - - _SpecularHighlights: 1 - - _SrcBlend: 1 - - _UVSec: 0 - - _ZWrite: 1 - m_Colors: - - _Color: {r: 1.216786, g: 1.216786, b: 1.216786, a: 1} - - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} - m_BuildTextureStacks: [] diff --git a/Assets/Materials/Particles/Dithered Particle Trail AetherDrive.mat.meta b/Assets/Materials/Particles/Dithered Particle Trail AetherDrive.mat.meta deleted file mode 100644 index 292027b73..000000000 --- a/Assets/Materials/Particles/Dithered Particle Trail AetherDrive.mat.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 555ada90c0f01654a9387483bd95a688 -NativeFormatImporter: - externalObjects: {} - mainObjectFileID: 0 - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index 604864aa8..a90fdde53 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -948,7 +948,7 @@ public class ActionGameManager : MonoBehaviour Dialog.MoveToCursor(); // TODO: SFX: Fail } - else if (CurrentEntity.GetBehavior() == null && CurrentEntity.GetBehavior() == null) + else if (CurrentEntity.GetBehavior() == null) { Dialog.Clear(); Dialog.Title.text = "Can't undock. Missing thruster component!"; diff --git a/Assets/Scripts/Gameplay/ShipInstance.cs b/Assets/Scripts/Gameplay/ShipInstance.cs index cc110fc8f..287eb42aa 100644 --- a/Assets/Scripts/Gameplay/ShipInstance.cs +++ b/Assets/Scripts/Gameplay/ShipInstance.cs @@ -18,16 +18,7 @@ public class ShipInstance : EntityInstance public int MaxParticleCount; } - private class AetherDriveInstance - { - public AetherDrive Drive; - public ParticleSystem Particles; - public float BaseEmission; - public float BaseForce; - } - private ThrusterInstance[] _thrusters; - private AetherDriveInstance _aetherDrive; public Ship Ship { get; private set; } @@ -47,20 +38,6 @@ public class ShipInstance : EntityInstance return; } Ship = ship; - var drive = ship.GetBehavior(); - if (drive != null) - { - var particles = Instantiate(EngineAssets.Load(drive.DriveData.Particles), transform, false); - var main = particles.main; - main.customSimulationSpace = LocalSpace; - _aetherDrive = new AetherDriveInstance - { - Drive = drive, - BaseEmission = particles.emission.rateOverTimeMultiplier, - Particles = particles, - BaseForce = particles.forceOverLifetime.z.curveMultiplier - }; - } _thrusters = ship.GetBehaviors().Select(thruster => { var effectData = (ThrusterData) thruster.Data; @@ -115,19 +92,6 @@ public class ShipInstance : EntityInstance TractorBeam.Power = Entity.TractorPower; TractorBeam.Direction = Entity.Aim.ToUnity(); - if (_aetherDrive != null) - { - var thrust = length(_aetherDrive.Drive.ThrustDirection); - var forceOverLifetime = _aetherDrive.Particles.forceOverLifetime; - forceOverLifetime.xMultiplier = _aetherDrive.Drive.ThrustDirection.x * _aetherDrive.BaseForce; - forceOverLifetime.zMultiplier = _aetherDrive.Drive.ThrustDirection.y * _aetherDrive.BaseForce; - var emissionModule = _aetherDrive.Particles.emission; - // Cut 8 (operator ask 2026-09-19): intent (thrust) times condition -- presentation reads the - // simulation's own ratio rather than recomputing any part of it. AetherDrive.Condition is the - // equivalent of Thruster.Condition below (Torque in place of Thrust). - emissionModule.rateOverTimeMultiplier = _aetherDrive.BaseEmission * thrust * _aetherDrive.Drive.Condition; - } - foreach (var thrusterInstance in _thrusters) { var emissionModule = thrusterInstance.System.emission; diff --git a/Assets/Scripts/ServerShared/Behaviors/AetherDrive.cs b/Assets/Scripts/ServerShared/Behaviors/AetherDrive.cs deleted file mode 100644 index e8498ca7f..000000000 --- a/Assets/Scripts/ServerShared/Behaviors/AetherDrive.cs +++ /dev/null @@ -1,185 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - -using GameCult.Caching; -using MessagePack; -using Newtonsoft.Json; -using CultMath; -using static CultMath.math; - -[Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn), EntityTypeRestriction(HullType.Ship), RuntimeInspectable] -public class AetherDriveData : BehaviorData -{ - [Inspectable, JsonProperty("diameters"), Key(1)] - public float3 RotorDiameter; - - [Inspectable, JsonProperty("masses"), Key(2)] - public float3 RotorMass; - - [Inspectable, JsonProperty("rpm"), Key(3), RuntimeInspectable] - public PerformanceStat MaximumRpm; - - [Inspectable, JsonProperty("couplingLambdas"), Key(4)] - public float3 CouplingLambda; - - [Inspectable, JsonProperty("lambdaMultiplier"), Key(5)] - public PerformanceStat LambdaMultiplier; - - [Inspectable, JsonProperty("couplingEfficiency"), Key(6), RuntimeInspectable] - public PerformanceStat CouplingEfficiency; - - [Inspectable, JsonProperty("torque"), Key(7), RuntimeInspectable] - public PerformanceStat Torque; - - [Inspectable, JsonProperty("torqueProfile"), Key(8), RuntimeInspectable] - public BezierCurve TorqueProfile; - - [Inspectable, JsonProperty("draw"), Key(9), RuntimeInspectable] - public PerformanceStat EnergyDraw; - - [Inspectable, JsonProperty("passiveCoupling"), Key(10), RuntimeInspectable] - public PerformanceStat PassiveCoupling; - - [Inspectable, JsonProperty("rpmAudio"), Key(11), RuntimeInspectable] - public uint RpmAudioParameter; - - [Inspectable, JsonProperty("torqueAudio"), Key(12), RuntimeInspectable] - public uint TorqueRatioAudioParameter; - - [Inspectable, CultInspectorAssetGuid, JsonProperty("particles"), Key(13)] - public string Particles; - - public override Behavior CreateInstance(EquippedItem item) - { - return new AetherDrive(this, item); - } - - public override Behavior CreateInstance(ConsumableItemEffect item) - { - return new AetherDrive(this, item); - } -} - -public class AetherDrive : Behavior, IPowerConsumer -{ - private AetherDriveData _data; - private float3 _axis; - - public float3 Thrust { get; private set; } - public float3 Rpm { get; private set; } - public float MaximumRpm { get; private set; } - public float2 ThrustDirection { get; private set; } - - public AetherDriveData DriveData => _data; - - // Cut 8 (operator ask 2026-09-19): the equivalent quantity to Thruster.Condition -- Torque is the stat that - // actually governs this behaviour's rotor spin-up (PowerRequest/Execute above both read it), so it is what - // "broken" means for a drive. 1f for the item-less ConsumableItemEffect case, same reasoning as Thruster. - public float Condition => Item?.ConditionRatio(_data.Torque) ?? 1f; - - public float3 Axis - { - get => _axis; - set => _axis = clamp(value, -1, 1); - } - - public AetherDrive(AetherDriveData data, EquippedItem item) : base(data, item) - { - _data = data; - } - - public AetherDrive(AetherDriveData data, ConsumableItemEffect item) : base(data, item) - { - _data = data; - } - - // Cut 3 (docs/stats-and-power-cut.md): the request PowerBus needs before Execute runs. Only the rotor - // spin-up (torque accelerating Rpm toward MaximumRpm) costs power -- spending existing spin into thrust - // (Execute's decay-to-thrust arithmetic below) is free, same as before. Pure and side-effect-free: it reads - // Rpm but does not write it, so Execute's own identical arithmetic a few lines later -- which does perform - // the real decay -- produces the same numbers Rpm actually moves by. Accepts the recompute (Q3's ruling). - // - // Nominal-request ruling (docs/stats-and-power-cut.md, operator ruling 2026-09-19): Torque, LambdaMultiplier, - // MaximumRpm, PassiveCoupling and EnergyDraw are all registered request fields (StatValidation. - // PowerRequestFields) -- read nominally (what full power would produce) so Torque carrying its own - // PowerSupply term (Cut 7's brownout curve) no longer makes this tick's request depend on this tick's own - // grant. Execute below is unchanged: it calls the real, curved Evaluate, so the actual spin-up still - // degrades with whatever the bus actually grants. - public float PowerRequest(float dt) - { - var couplingLambda = _data.CouplingLambda * Item.EvaluateNominalPower(_data.LambdaMultiplier) * max(abs(_axis), EvaluateNominalPower(_data.PassiveCoupling)); - var rpmAfterDecay = decay(Rpm, couplingLambda, dt); - var maximumRpm = EvaluateNominalPower(_data.MaximumRpm); - var torqueProfile = float3( - _data.TorqueProfile.Evaluate(rpmAfterDecay.x / maximumRpm), - _data.TorqueProfile.Evaluate(rpmAfterDecay.y / maximumRpm), - _data.TorqueProfile.Evaluate(rpmAfterDecay.z / maximumRpm)); - var potentialTorque = EvaluateNominalPower(_data.Torque) * torqueProfile; - var potentialRpmDelta = potentialTorque / length(_data.RotorMass) * dt; - var actualRpmDelta = min(maximumRpm - rpmAfterDecay, potentialRpmDelta); - var torqueRatio = actualRpmDelta / potentialRpmDelta; - var draw = torqueRatio * EvaluateNominalPower(_data.EnergyDraw) / 3; - return (draw.x + draw.y + draw.z) * dt; - } - - // Cut 5 (docs/stats-and-power-cut.md §1.3, PowerTiers.cs): Medium -- mobility, same as Thruster. - public int DefaultPowerTier => PowerTiers.Medium; - - public override bool Execute(float dt) - { - var rotorSpeed = Rpm * _data.RotorDiameter / 100; - - var forward = normalize(Entity.Direction); - var right = forward.Rotate(ItemRotation.Clockwise); - - var speed = float2(dot(Entity.Velocity, forward), dot(Entity.Velocity, right)); - var couplingEfficiency = Evaluate(_data.CouplingEfficiency); - var efficiency = float3(saturate(1 - speed / max(rotorSpeed.xy, 1) * sign(_axis.xy)) * couplingEfficiency, 1); - - Thrust = (Rpm - decay(Rpm, _data.CouplingLambda, dt)) * _data.RotorMass * efficiency; - - var couplingLambda = _data.CouplingLambda * Item.Evaluate(_data.LambdaMultiplier) * max(abs(_axis), Evaluate(_data.PassiveCoupling)); - var previousRpm = Rpm; - Rpm = decay(Rpm, couplingLambda, dt); - var rpmLoss = previousRpm - Rpm; - var force = rpmLoss * _data.RotorMass * efficiency; - - var heat = rpmLoss * _data.RotorMass * (1 - couplingEfficiency); - AddHeat((heat.x + heat.y + heat.z)*ItemManager.GameplaySettings.AetherHeatMultiplier); - - ThrustDirection = forward * (_axis.x * force.x / Entity.Mass) + right * (_axis.y * force.y / Entity.Mass); - Entity.Velocity += ThrustDirection; - - Entity.Direction = mul(Entity.Direction, - CultMath.float2x2.Rotate(force.z * _axis.z * ItemManager.GameplaySettings.AetherTorqueMultiplier / Entity.Mass)); - - if(float.IsNaN(Entity.Velocity.x)) - ItemManager.Log("FUCK FUCK FUCK FUCK"); - - MaximumRpm = Evaluate(_data.MaximumRpm); - var torqueProfile = float3( - _data.TorqueProfile.Evaluate(Rpm.x / MaximumRpm), - _data.TorqueProfile.Evaluate(Rpm.y / MaximumRpm), - _data.TorqueProfile.Evaluate(Rpm.z / MaximumRpm)); - var potentialTorque = Evaluate(_data.Torque) * torqueProfile; - var potentialRpmDelta = potentialTorque / length(_data.RotorMass) * dt; - var actualRpmDelta = min(MaximumRpm - Rpm, potentialRpmDelta); - var torqueRatio = actualRpmDelta / potentialRpmDelta; - - Item.SetAudioParameter(SpecialAudioParameter.Intensity, max(max(abs(_axis.x), abs(_axis.y)), abs(_axis.z))); - Item.SetAudioParameter(_data.RpmAudioParameter, (Rpm.x + Rpm.y + Rpm.z) / 3 / MaximumRpm); - Item.SetAudioParameter(_data.TorqueRatioAudioParameter, max(max(torqueRatio.x, torqueRatio.y), torqueRatio.z)); - - // Cut 7 (docs/stats-and-power-target.md): actualRpmDelta already derives from Evaluate(_data.Torque) - // above, so a Torque stat carrying a PowerSupply term already shrinks the rotor's spin-up under a - // partial grant -- this no longer demands a full one, only that supply has not been cut to true zero. - if (Item.PowerSupply > 1e-4f) - { - Rpm += actualRpmDelta; - return true; - } - - return false; - } -} \ No newline at end of file diff --git a/Assets/Scripts/ServerShared/Behaviors/AetherDrive.cs.meta b/Assets/Scripts/ServerShared/Behaviors/AetherDrive.cs.meta deleted file mode 100644 index 96fd33b04..000000000 --- a/Assets/Scripts/ServerShared/Behaviors/AetherDrive.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 2022c838bbc64a0d86cfbd477e6a379c -timeCreated: 1618332827 \ No newline at end of file diff --git a/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs b/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs index 4bc5731a7..9f2e292a6 100644 --- a/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs +++ b/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs @@ -157,7 +157,7 @@ public interface IPowerConsumer Union(9, typeof(WearData)), Union(10, typeof(VelocityConversionData)), Union(11, typeof(VelocityLimitData)), - Union(12, typeof(AetherDriveData)), + // Union(12): retired, do not reuse -- was AetherDriveData (retire-longinusx; parked/aether-drive) // Union(14, typeof(TowingControllerData)), Union(15, typeof(CooldownData)), Union(16, typeof(HeatData)), diff --git a/Assets/Scripts/ServerShared/Entity.cs b/Assets/Scripts/ServerShared/Entity.cs index 60b84ce87..4ba9ac2f1 100644 --- a/Assets/Scripts/ServerShared/Entity.cs +++ b/Assets/Scripts/ServerShared/Entity.cs @@ -1818,7 +1818,7 @@ public class EquippedItem : IStatContext // RefreshInputCapacitor implementation now calls for a request-field stat, and it pins that stat's own // PowerSupplyFactor to 1 regardless of what Terms the stat declares -- so a direct PowerSupply term on a // request field can no longer make the request depend on its own answer, and the six shipped records this - // ruling exists to make legal again (RadiatorData.PumpedHeat, AetherDriveData.Torque) are not an authoring + // ruling exists to make legal again (RadiatorData.PumpedHeat) are not an authoring // error to refuse. What is still refused: a request field fed a PowerSupply-tainted value through a // *modifier chain* (StatModifier.ValidateNoPowerSupplyChain, StatModifier.cs) -- EvaluateNominalPower // forwards ScaleModifier/ConstantModifier to the item's real, non-nominal resolver entries (same as @@ -1834,8 +1834,8 @@ public class EquippedItem : IStatContext // Nominal-request ruling (docs/stats-and-power-cut.md, operator ruling 2026-09-19): what this item wants at // full power supply, not what it is currently managing -- the read every PowerRequest/RefreshReserve/ // RefreshInputCapacitor implementation uses for a stat named in StatValidation.PowerRequestFields, so a stat - // that is both what a request asks for and what it also produces (RadiatorData.PumpedHeat, AetherDriveData. - // Torque) stops being circular at the root instead of being forbidden outright. Only PowerSupply is pinned: + // that is both what a request asks for and what it also produces (RadiatorData.PumpedHeat) + // stops being circular at the root instead of being forbidden outright. Only PowerSupply is pinned: // Heat and Durability are NOT, because they are not the term a request would be circular through -- this // tick's PowerBus.Step (which calls PowerRequest) always runs before this tick's grant exists, so nothing // here could depend on an answer that has not been computed yet, and a hot or worn item honestly asking for diff --git a/Assets/Scripts/ServerShared/Enums.cs b/Assets/Scripts/ServerShared/Enums.cs index 2b5a9637b..cbd970c3f 100644 --- a/Assets/Scripts/ServerShared/Enums.cs +++ b/Assets/Scripts/ServerShared/Enums.cs @@ -49,8 +49,7 @@ public enum HardpointType Energy, Ballistic, Launcher, - ControlModule, - AetherDrive + ControlModule } public enum WeaponType diff --git a/Assets/Scripts/ServerShared/ItemData.cs b/Assets/Scripts/ServerShared/ItemData.cs index 40ec27532..ef1b3f46f 100644 --- a/Assets/Scripts/ServerShared/ItemData.cs +++ b/Assets/Scripts/ServerShared/ItemData.cs @@ -855,8 +855,7 @@ public static class StatValidation // // F6 (docs/stats-and-power-cut.md, Soul pass 2026-09-19) named every field a PowerRequest implementation // actually reads, not just each consumer's top-level request field: Radiator.PowerRequest also reads - // PumpedHeat and WasteHeat, AetherDrive.PowerRequest also reads Torque, LambdaMultiplier, MaximumRpm and - // PassiveCoupling, Shield.PowerRequest (via RefreshReserve) also reads RefillDuration and RestoreDuration, + // PumpedHeat and WasteHeat, Shield.PowerRequest (via RefreshReserve) also reads RefillDuration and RestoreDuration, // InstantWeapon.PowerRequest (via RefreshInputCapacitor) also reads Cooldown and Count, and Sensor. // PowerRequest (via RefreshInputCapacitor) also reads PingCooldown. Audited directly against each // PowerRequest method's own body (and RefreshReserve/RefreshInputCapacitor, which PowerRequest calls into) @@ -892,11 +891,6 @@ public static class StatValidation (typeof(RadiatorData), nameof(RadiatorData.EnergyUsage)), (typeof(RadiatorData), nameof(RadiatorData.PumpedHeat)), (typeof(RadiatorData), nameof(RadiatorData.WasteHeat)), - (typeof(AetherDriveData), nameof(AetherDriveData.EnergyDraw)), - (typeof(AetherDriveData), nameof(AetherDriveData.Torque)), - (typeof(AetherDriveData), nameof(AetherDriveData.LambdaMultiplier)), - (typeof(AetherDriveData), nameof(AetherDriveData.MaximumRpm)), - (typeof(AetherDriveData), nameof(AetherDriveData.PassiveCoupling)), }; // Whether `stat` is the request stat of some IPowerConsumer behaviour on `data` -- used by StatModifier's diff --git a/Assets/Scripts/ServerShared/PowerTiers.cs b/Assets/Scripts/ServerShared/PowerTiers.cs index e6bf90d89..c7c88233b 100644 --- a/Assets/Scripts/ServerShared/PowerTiers.cs +++ b/Assets/Scripts/ServerShared/PowerTiers.cs @@ -13,7 +13,7 @@ // every other item on the hull (EquippedItem.UpdatePerformance). Feed it first or lose the rest anyway. // - High: Shield. Hull survival the player is actively trading power for; not a life-support constant, so it // sits below Radiator rather than beside it. -// - Medium: Thruster, AetherDrive. Mobility -- needed to hold position, evade, or disengage, but a ship that +// - Medium: Thruster. Mobility -- needed to hold position, evade, or disengage, but a ship that // loses thrust for a tick under brownout is inconvenienced, not dead the way an overheating hull is. // - Low: ConstantWeapon, InstantWeapon. Offense. The ruling's own framing ("low-priority subsystems starve // first") names weapons as the default example of what a reactor throttle should sacrifice first. diff --git a/Assets/Scripts/ServerShared/Settings.cs b/Assets/Scripts/ServerShared/Settings.cs index 9ab2928fd..730ee3700 100644 --- a/Assets/Scripts/ServerShared/Settings.cs +++ b/Assets/Scripts/ServerShared/Settings.cs @@ -213,8 +213,6 @@ public class GameplaySettings public ExponentialLerp LockSpinSpeed; public float TorqueFloor; public float TorqueMultiplier; - public float AetherTorqueMultiplier; - public float AetherHeatMultiplier; public float VisibilityDecay; public float TargetInfoDecay; public float TargetDetectionInfoThreshold; @@ -240,7 +238,7 @@ public class GameplaySettings // UnaidedTracking above -- FireControl.Precision falls back to this the same way it already falls back to // UnaidedAccuracy/UnaidedTracking. Precision is now a grouping tightness (FireControl.Sigma = 1/Precision, // in hull-schematic cell units), not the old coin-flip probability, so the unaided floor has to be - // authored on that scale. .3 -> sigma 3.33 cells: against the live catalog's own hulls (LonginusX 6x17, + // authored on that scale. .3 -> sigma 3.33 cells: against the live catalog's own hulls (Longinus 6x17, // Zenith 12x12, Turret 8x8) that puts pOnHull at the hull's own centre of mass around .5-.84 -- broadly // sprayed across the whole silhouette (sigma is a large fraction of the hull's own width) while still // landing on the ship more often than not. A tighter floor (e.g. .1, sigma 10) was tried first and diff --git a/Assets/Scripts/ServerShared/Ship.cs b/Assets/Scripts/ServerShared/Ship.cs index 1ec9fff27..98dfa4fb3 100644 --- a/Assets/Scripts/ServerShared/Ship.cs +++ b/Assets/Scripts/ServerShared/Ship.cs @@ -38,8 +38,6 @@ public class Ship : Entity private HashSet _leftThrusters; private HashSet _clockwiseThrusters; private HashSet _counterClockwiseThrusters; - private HashSet _aetherDriveItems; - private HashSet _aetherDrives; private bool _exitingWormhole = false; private bool _enteringWormhole = false; @@ -96,9 +94,6 @@ public class Ship : Entity { base.Activate(); - _aetherDrives = new HashSet(GetBehaviors()); - _aetherDriveItems = new HashSet(_aetherDrives.Select(x => x.Item)); - _allThrusters = GetBehaviors().ToArray(); _thrusterItems = new HashSet(_allThrusters.Select(x=>x.Item)); @@ -124,13 +119,6 @@ public class Ship : Entity public Ship(ItemManager itemManager, Zone zone, EquippableItem hull, EntitySettings settings) : base(itemManager, zone, hull, settings) { ItemDestroyed.Where(item=>_thrusterItems.Contains(item)).Subscribe(RemoveThruster); - ItemDestroyed.Where(item=>_aetherDriveItems.Contains(item)).Subscribe(RemoveAetherDrive); - } - - private void RemoveAetherDrive(EquippedItem item) - { - _aetherDriveItems.Remove(item); - _aetherDrives.Remove(item.GetBehavior()); } private void RemoveThruster(EquippedItem item) @@ -176,10 +164,6 @@ public class Ship : Entity foreach (var thruster in _forwardThrusters) if (thruster.Item.Active.Value) ForwardThrust += thruster.Thrust; - - foreach (var drive in _aetherDrives) - if (drive.Item.Active.Value) - ForwardThrust += drive.Thrust.x; } private void RecalculateReverseThrust() @@ -188,10 +172,6 @@ public class Ship : Entity foreach (var thruster in _reverseThrusters) if (thruster.Item.Active.Value) ReverseThrust += thruster.Thrust; - - foreach (var drive in _aetherDrives) - if (drive.Item.Active.Value) - ReverseThrust += drive.Thrust.x; } private void RecalculateLeftStrafeThrust() @@ -210,10 +190,6 @@ public class Ship : Entity foreach(var thruster in _leftThrusters) if (abs(sign(thruster.Torque) - sign(LeftStrafeTotalTorque)) < .01f) LeftStrafeTorqueThrusters.Add(thruster); - - foreach (var drive in _aetherDrives) - if (drive.Item.Active.Value) - LeftStrafeThrust += drive.Thrust.y; } private void RecalculateRightStrafeThrust() @@ -232,10 +208,6 @@ public class Ship : Entity foreach(var thruster in _rightThrusters) if (abs(sign(thruster.Torque) - sign(RightStrafeTotalTorque)) < .01f) RightStrafeTorqueThrusters.Add(thruster); - - foreach (var drive in _aetherDrives) - if (drive.Item.Active.Value) - RightStrafeThrust += drive.Thrust.y; } private void RecalculateClockwiseTorque() @@ -244,10 +216,6 @@ public class Ship : Entity foreach (var thruster in _clockwiseThrusters) if (thruster.Item.Active.Value) ClockwiseTorque += thruster.Torque; - - foreach (var drive in _aetherDrives) - if (drive.Item.Active.Value) - ClockwiseTorque += drive.Thrust.z; } private void RecalculateCounterClockwiseTorque() @@ -256,10 +224,6 @@ public class Ship : Entity foreach (var thruster in _counterClockwiseThrusters) if (thruster.Item.Active.Value) CounterClockwiseTorque -= thruster.Torque; - - foreach (var drive in _aetherDrives) - if (drive.Item.Active.Value) - CounterClockwiseTorque += drive.Thrust.z; } #endregion @@ -293,9 +257,6 @@ public class Ship : Entity foreach (var thruster in _clockwiseThrusters) thruster.Axis += Turn; foreach (var thruster in _counterClockwiseThrusters) thruster.Axis += -Turn; - - foreach (var drive in _aetherDrives) - drive.Axis = float3(MovementDirection.y, MovementDirection.x, Turn); } var velocityMagnitude = length(Velocity); diff --git a/Assets/Scripts/UI/HUD/SchematicDisplay.cs b/Assets/Scripts/UI/HUD/SchematicDisplay.cs index e2eb764f7..d6402e3f6 100644 --- a/Assets/Scripts/UI/HUD/SchematicDisplay.cs +++ b/Assets/Scripts/UI/HUD/SchematicDisplay.cs @@ -22,8 +22,6 @@ public class SchematicDisplay : MonoBehaviour public GameObject ShieldIcon; public Image HeatsinkBackground; - public GameObject AetherDriveUi; - public TextMeshProUGUI EnergyLabel; public TextMeshProUGUI CockpitTemperatureLabel; public TextMeshProUGUI RadiatorTemperatureLabel; @@ -32,9 +30,6 @@ public class SchematicDisplay : MonoBehaviour public TextMeshProUGUI VisibilityLabel; public TextMeshProUGUI HullDurabilityLabel; public TextMeshProUGUI DistanceLabel; - public TextMeshProUGUI ForwardRPMLabel; - public TextMeshProUGUI StrafeRPMLabel; - public TextMeshProUGUI TurnRPMLabel; public RectTransform SensorCooldownFill; public RectTransform EnergyFill; @@ -42,9 +37,6 @@ public class SchematicDisplay : MonoBehaviour public RectTransform HeatstrokeMeterFill; public RectTransform HypothermiaMeterFill; public RectTransform HeatstrokeLimitFill; - public RectTransform ForwardRPMFill; - public RectTransform StrafeRPMFill; - public RectTransform TurnRPMFill; private Entity _entity; private EquippableItem _hull; @@ -55,7 +47,6 @@ public class SchematicDisplay : MonoBehaviour private HeatStorage[] _heatStorages; private EquippedCargoBay[] _cargoBays; private SchematicDisplayItem[] _schematicItems; - private AetherDrive _aetherDrive; private bool _enemy; private Entity _player; @@ -90,8 +81,6 @@ public class SchematicDisplay : MonoBehaviour _cockpit = entity.GetBehavior(); _reactor = entity.GetBehavior(); _capacitors = entity.GetBehaviors().ToArray(); - _aetherDrive = entity.GetBehavior(); - AetherDriveUi.SetActive(_aetherDrive != null); _radiators = entity.GetBehaviors().ToArray(); if (_radiators.Length == 0) @@ -186,18 +175,6 @@ public class SchematicDisplay : MonoBehaviour EnergyFill.anchorMax = new Vector2(charge / maxCharge, 1); EnergyLabel.text = $"{((int)charge).ToString()}/{((int)maxCharge).ToString()} + ({((int)_reactor.Draw).ToString()})"; } - - if (_aetherDrive != null) - { - ForwardRPMLabel.text = ActionGameManager.PlayerSettings.Format(_aetherDrive.Rpm.x); - ForwardRPMFill.anchorMax = new Vector2(_aetherDrive.Rpm.x / _aetherDrive.MaximumRpm, 1); - - StrafeRPMLabel.text = ActionGameManager.PlayerSettings.Format(_aetherDrive.Rpm.y); - StrafeRPMFill.anchorMax = new Vector2(_aetherDrive.Rpm.y / _aetherDrive.MaximumRpm, 1); - - TurnRPMLabel.text = ActionGameManager.PlayerSettings.Format(_aetherDrive.Rpm.z); - TurnRPMFill.anchorMax = new Vector2(_aetherDrive.Rpm.z / _aetherDrive.MaximumRpm, 1); - } } // else // { diff --git a/tests/Aetheria.Shared.Tests/FireControlCut12Tests.cs b/tests/Aetheria.Shared.Tests/FireControlCut12Tests.cs index c613bfa4b..5ab1d2862 100644 --- a/tests/Aetheria.Shared.Tests/FireControlCut12Tests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlCut12Tests.cs @@ -1324,7 +1324,7 @@ public sealed class FireControlCut12Tests : IDisposable } // S3 fix batch (Hands, 2026-09-25): Lane's own comment ("never allocates on its own") and Silhouette's - // pooled path are pinned directly, instead of trusting the comment. Measured (Release, LonginusX-shaped + // pooled path are pinned directly, instead of trusting the comment. Measured (Release, Longinus-shaped // fixture unavailable to this test file -- a 7x9 solid hull is used instead): Lane and Silhouette (given a // caller-supplied buffer) allocate exactly 0 bytes over 1000 calls, confirming the S3 change itself // (Array.Sort(T[], int, int) resolving Interval/LaneCell's own IComparable at JIT time, not the diff --git a/tests/Aetheria.Shared.Tests/FireControlCut9Tests.cs b/tests/Aetheria.Shared.Tests/FireControlCut9Tests.cs index c5aa51857..8a2d55038 100644 --- a/tests/Aetheria.Shared.Tests/FireControlCut9Tests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlCut9Tests.cs @@ -374,7 +374,7 @@ public sealed class FireControlCut9Tests : IDisposable // Even extents: CenterOfMass is the raw mean of integer cell coordinates (ItemData.cs), with no // +0.5 cell-centre offset -- an odd extent's mean lands exactly on an occupied cell (no cliff to // reproduce), while an even one lands on the half-cell boundary between two cells, the same - // off-integer shape every shipped hull's real (irregular) footprint produces (LonginusX 2.5/6.697, + // off-integer shape every shipped hull's real (irregular) footprint produces (Longinus 2.5/6.697, // Zenith 5.5/5.5, Turret 3.5/3.5). var hullShape = SolidShape(6, 12); diff --git a/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs b/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs index 229cb042f..802e73947 100644 --- a/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs +++ b/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs @@ -201,6 +201,23 @@ public sealed class RestoredHullsTests } } + // The dock intro dereferences the ship's VelocityLimit unguarded (ActionGameManager), so every ship hull the catalog + // ships carries exactly one, with a top speed that is a finite positive number. After the LonginusX's retirement + // (ruling retire-longinusx) the shipped ship hulls are the Longinus and the Djinni. + [Fact] + public void EveryShippedShipHullHasATopSpeed() + { + using var cache = OpenCatalog(); + var ships = cache.GetAll().Where(h => h.HullType == HullType.Ship).OrderBy(h => h.Name, StringComparer.Ordinal).ToArray(); + Assert.Equal(new[] { "Djinni", "Longinus" }, ships.Select(h => h.Name)); + foreach (var hull in ships) + { + Assert.Single(hull.Behaviors.OfType()); + var limit = BuildThrustedShip(cache, hull.Name).GetBehavior().Limit; + Assert.True(float.IsFinite(limit) && limit > 0f, $"{hull.Name}'s top speed is {limit}"); + } + } + // A restored hull with every thruster hardpoint (and its reactor) filled, activated and warmed up: the // rotation thrusters the helm tests need. The caller owns the cache. internal static Ship BuildThrustedShip(CultCache cache, string name) diff --git a/tools/AetherDb/Program.cs b/tools/AetherDb/Program.cs index 07e931f3e..0b8743e7d 100644 --- a/tools/AetherDb/Program.cs +++ b/tools/AetherDb/Program.cs @@ -203,12 +203,6 @@ public static class Program ["Heat"] = "injector", ["EnergyUsage"] = "injector", }; - private static readonly Dictionary AetherDriveRoles = new Dictionary - { - ["MaximumRpm"] = "rotor", ["Torque"] = "rotor", - ["CouplingEfficiency"] = "coupling", ["PassiveCoupling"] = "coupling", ["EnergyDraw"] = "coupling", - }; - private static readonly Dictionary HullShipRoles = new Dictionary { ["CrossSection"] = "plating", @@ -251,7 +245,6 @@ public static class Program "Reactor" => ReactorRoles, "Sensors" => SensorRoles, "Thruster" => ThrusterRoles, - "AetherDrive" => AetherDriveRoles, _ when kind == "Hull/Ship" => HullShipRoles, "Tool" => ToolRolesByDesign.TryGetValue(item.Name, out var byName) ? byName : null, _ => null, @@ -936,11 +929,11 @@ public static class Program // only has anything to curve if some shipped stat actually declares a PowerSupply term -- otherwise every // one of those stats keeps answering PowerSupplyFactor's identity (Entity.cs), and removing the gates makes // a partial grant read exactly like a full one instead of a reduced one. This authors the term, once, on the - // one performance stat each of the four curve-eligible behaviours (EnergyDraw has none -- see EnergyDraw.cs) - // actually reads for its continuous effect: ThrusterData.Thrust, AetherDriveData.Torque, - // RadiatorData.PumpedHeat, ConstantWeaponData.Damage. Two of the four (AetherDriveData.Torque, - // RadiatorData.PumpedHeat) are ALSO power-request fields (StatValidation.PowerRequestFields) -- at the time - // this tool authored the shipped catalog's curves that briefly made those two records illegal under F6's + // one performance stat each of the three curve-eligible behaviours (EnergyDraw has none -- see EnergyDraw.cs) + // actually reads for its continuous effect: ThrusterData.Thrust, + // RadiatorData.PumpedHeat, ConstantWeaponData.Damage. One of the three (RadiatorData.PumpedHeat) + // is ALSO a power-request field (StatValidation.PowerRequestFields) -- at the time + // this tool authored the shipped catalog's curves that briefly made that record illegal under F6's // now-deleted static check (StatValidation.ValidateNoPowerSupplyOnRequest); the nominal-request ruling // (docs/stats-and-power-cut.md, operator ruling 2026-09-19, see ItemData.cs's PowerRequestFields comment) // resolved that by having PowerRequest read request fields nominally instead of forbidding the term, so @@ -953,12 +946,11 @@ public static class Program var db = AetherDb.Open(catalogWritable: apply); const float gentleExponent = 1f; - // (behaviour type, target performance-stat field, short label) -- two of these are also request fields + // (behaviour type, target performance-stat field, short label) -- one of these is also a request field // (StatValidation.PowerRequestFields); that is legal under the nominal-request ruling above. var targets = new (Type BehaviorType, string Field, string Label)[] { (typeof(ThrusterData), nameof(ThrusterData.Thrust), "thrust"), - (typeof(AetherDriveData), nameof(AetherDriveData.Torque), "torque"), (typeof(RadiatorData), nameof(RadiatorData.PumpedHeat), "pumpedHeat"), (typeof(ConstantWeaponData), nameof(WeaponData.Damage), "damage"), }; @@ -1472,7 +1464,7 @@ public static class Program // and are meaningless as written under the new one -- re-authored here on the sigma-reciprocal scale // instead, same convention as 6c.1's Resolution fix. // - // Both ranges are chosen against real hull scale (GameData/Aetheria.cc's own hulls: LonginusX 6x17, + // Both ranges are chosen against real hull scale (GameData/Aetheria.cc's own hulls: Longinus 6x17, // Zenith 12x12, Turret 8x8) and the unaided floor this cut adds alongside them // (GameplaySettings.UnaidedPrecision, .3 -> sigma 3.33 cells): a design that did not clear the unaided // floor by a wide margin would make the required Tool item pointless to equip. diff --git a/tools/blender/aetheria_ships/ship_cc.py b/tools/blender/aetheria_ships/ship_cc.py index f16fe72a5..6018d6a96 100644 --- a/tools/blender/aetheria_ships/ship_cc.py +++ b/tools/blender/aetheria_ships/ship_cc.py @@ -33,7 +33,7 @@ HARDPOINT_MEMBERS = ("Type", "Position", "Shape", "Transform", "Rotation", "Armo ANCHOR_MEMBERS = ("Id", "Role", "ModelNodeId", "ParentId", "Order") # Enum member names in value order (HardpointType, ItemRotation). HARDPOINT_TYPE_NAMES = ("Hull", "Tool", "Thermal", "Thruster", "WarpDrive", "Reactor", "Radiator", - "Shield", "Sensors", "Energy", "Ballistic", "Launcher", "ControlModule", "AetherDrive") + "Shield", "Sensors", "Energy", "Ballistic", "Launcher", "ControlModule") ROTATION_NAMES = ("None", "CounterClockwise", "Reversed", "Clockwise") From 328f9513d73c2f8dd77ae8a6f896ad483d276a96 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 14:04:52 +0200 Subject: [PATCH 008/120] Delete the transient retire-longinusx command now that the catalog has landed Co-Authored-By: Claude Sonnet 5.5 --- tools/AetherDb/Program.cs | 66 --------------------------------------- 1 file changed, 66 deletions(-) diff --git a/tools/AetherDb/Program.cs b/tools/AetherDb/Program.cs index 0b8743e7d..3e974555b 100644 --- a/tools/AetherDb/Program.cs +++ b/tools/AetherDb/Program.cs @@ -10,7 +10,6 @@ using System.Linq; using System.Reflection; using CultMath; using GameCult.Caching; -using GameCult.Caching.MessagePack; using Random = CultMath.Random; // Commands over the game database, run with: dotnet run --project tools/AetherDb -- @@ -34,7 +33,6 @@ public static class Program case "brownout-migrate": return BrownoutMigrate(args.Contains("apply")); case "roles-migrate": return RolesMigrate(args.Contains("apply")); case "firing-arc-migrate": return FiringArcMigrate(args.Contains("apply")); - case "retire-longinusx": return RetireLonginusX(args.Contains("apply")); case "targeting-catalog": return TargetingCatalog(args.Contains("apply")); case "targeting-catalog-6c": return TargetingCatalog6c(args.Contains("apply")); case "targeting-catalog-6d": return TargetingCatalog6d(args.Contains("apply")); @@ -1079,70 +1077,6 @@ public static class Program return 0; } - // retire-longinusx (docs/aetheria-release-map.md, "Retiring the LonginusX and parking the aether drive"): deletes - // HullData LonginusX, GearData Traction and the two FactionProductData that sell them, in one Commit. Dry run unless - // passed "apply". It refuses unless exactly those four records are present, and refuses when any other record's - // serialized bytes name one of their ids or the names LonginusX or Traction. A second apply finds nothing and refuses. - private static int RetireLonginusX(bool apply) - { - var hullId = "445d911a-9951-40a3-874e-0b7ce8b35129"; - var gearId = "42d1ffaa-f764-4a65-ba03-43cc4e6fd2bc"; - var gearProductId = "007ed2b1-8fdf-473d-9ebf-e55fcd1a6903"; - var db = AetherDb.Open(catalogWritable: apply); - - var stored = db.Cache.AllStoredDocuments.ToArray(); - var hull = stored.FirstOrDefault(d => d.Document is HullData && string.Equals(d.Key.Value, hullId, StringComparison.OrdinalIgnoreCase)); - var gear = stored.FirstOrDefault(d => d.Document is GearData && string.Equals(d.Key.Value, gearId, StringComparison.OrdinalIgnoreCase)); - if (hull == null || gear == null) - { - Console.WriteLine("Nothing to retire: the LonginusX hull and the Traction gear are not both present."); - return 1; - } - - var doomed = new List { hull, gear }; - doomed.AddRange(stored.Where(d => d.Document is FactionProductData product && - (product.Design.Key.Value == hull.Key.Value || product.Design.Key.Value == gear.Key.Value))); - if (doomed.Count != 4 || doomed.Count(d => string.Equals(d.Key.Value, gearProductId, StringComparison.OrdinalIgnoreCase)) != 1) - { - Console.WriteLine($"Refused: expected the hull, the gear and their two products, found {doomed.Count} records."); - return 1; - } - - // A needle is the whole serialized string: a name inside a longer string (a name list entry) is not a reference. - var needles = doomed.Select(d => d.Key.Value).Concat(new[] { "LonginusX", "Traction" }) - .Select(text => MessagePack.MessagePackSerializer.Serialize(text)).ToArray(); - var doomedKeys = new HashSet(doomed.Select(d => d.Key.Value)); - var named = 0; - foreach (var other in stored.Where(d => !doomedKeys.Contains(d.Key.Value))) - { - var options = CultDocumentMessagePackSerialization.OptionsFor(other.Document.GetType().Assembly); - var bytes = MessagePack.MessagePackSerializer.Serialize(other.Document.GetType(), other.Document, options); - if (!needles.Any(needle => bytes.AsSpan().IndexOf(needle) >= 0)) continue; - Console.WriteLine($"Named by {other.Document.GetType().Name} {NameOf(other.Document) ?? other.Key.Value}"); - named++; - } - if (named > 0) - { - Console.WriteLine($"Refused: {named} other records name the retiring records."); - return 1; - } - - foreach (var d in doomed) - Console.WriteLine($"{(apply ? "Deleting" : "Would delete")} {d.Document.GetType().Name} {NameOf(d.Document)} {d.Key.Value}"); - if (!apply) - { - Console.WriteLine("Dry run. Pass \"apply\" to delete 4 records."); - return 0; - } - - db.Cache.Commit(batch => - { - foreach (var d in doomed) batch.Remove(d.Key); - }); - Console.WriteLine("Deleted 4 records from Aetheria.cc"); - return 0; - } - // Fire control Cut 6a (docs/fire-control-cut.md, Cut 2's "Catalog:" bullet). Cut 2 shipped the behaviour and // never shipped the data: no targeting-system design exists, so tools/AetherDb loadout throws // InvalidLoadoutException ("No compatible targeting system found for entity!") for every armed hull. This From 04d2947a807fa059ef19ad95135b22f7ed7603d1 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 14:10:58 +0200 Subject: [PATCH 009/120] Gear without presence: IsAvailable reaches by allegiance alone Presence in the galaxy no longer gates gear (ruling gear-without-presence). Delete the IsPrelude exception, Galaxy.ContainsFaction with its cache, and the AetherDb loadout exclude-sellers mode that existed to exercise the presence gate. An absent maker weighs as one jump beyond the farthest zone the generator's zone reaches. Not yet verified; may not build. Co-Authored-By: Claude Sonnet 5.5 --- Assets/Scripts/ServerShared/Galaxy.cs | 7 -- .../Scripts/ServerShared/LoadoutGenerator.cs | 28 +++--- .../FireControlCut6cTests.cs | 11 ++- .../RunStartGearTests.cs | 92 +++++++++++++++++++ tests/Aetheria.Shared.Tests/RunStartTests.cs | 4 +- tools/AetherDb/Program.cs | 54 +---------- 6 files changed, 119 insertions(+), 77 deletions(-) create mode 100644 tests/Aetheria.Shared.Tests/RunStartGearTests.cs diff --git a/Assets/Scripts/ServerShared/Galaxy.cs b/Assets/Scripts/ServerShared/Galaxy.cs index a5660f52e..be2973a27 100644 --- a/Assets/Scripts/ServerShared/Galaxy.cs +++ b/Assets/Scripts/ServerShared/Galaxy.cs @@ -33,7 +33,6 @@ public class Galaxy // restored from a save. public uint Seed { get; } - private HashSet _containedFactions; private GalaxyZone[] _exitPath; private Dictionary _nameGenerators = new Dictionary(); private readonly CultCache _cache; @@ -432,12 +431,6 @@ public class Galaxy } } - public bool ContainsFaction(CultRecordRef faction) - { - _containedFactions ??= new HashSet(Factions.Select(f => _cache.RefOf(f).Key)); - return _containedFactions.Contains(faction.Key); - } - class DijkstraVertex { public float Cost; diff --git a/Assets/Scripts/ServerShared/LoadoutGenerator.cs b/Assets/Scripts/ServerShared/LoadoutGenerator.cs index 2fb2a69f9..91af76a90 100644 --- a/Assets/Scripts/ServerShared/LoadoutGenerator.cs +++ b/Assets/Scripts/ServerShared/LoadoutGenerator.cs @@ -174,26 +174,30 @@ public class LoadoutGenerator design.HardpointType == HardpointType.Tool || design.HardpointType == HardpointType.Hull || hulls.Any(hull => hull.Hardpoints.Any(hardpoint => hardpoint.Takes(design))); - // No galaxy means no availability to filter by: every product is on offer. A fixture generates loadouts that - // way, so a test can exercise placement and products without standing up a whole galaxy; no game path does. - // Loadouts.Materialize takes availability as a predicate; RunStart, the game's one preset spawner, passes this, so - // presets and generation share one availability rule. In a galaxy, a faction always reaches its own manufacturer's - // gear, and otherwise gear made by a manufacturer in that galaxy that its allegiance names (operator, 2026-09-30: - // allegiance lists only other factions). + // No galaxy, or no faction, means no allegiance to filter by: every product is on offer. A fixture generates + // loadouts that way, so a test can exercise placement and products without standing up a whole galaxy; a scenario + // stage has no faction and reaches every maker. Loadouts.Materialize takes availability as a predicate; RunStart, + // the game's one preset spawner, passes this, so presets and generation share one availability rule. A faction + // always reaches its own manufacturer's gear, and otherwise gear made by a manufacturer its allegiance names + // (operator, 2026-09-30: allegiance lists only other factions). Whether that manufacturer is present in the galaxy + // does not matter (operator, 2026-10-06: a faction need not be present for its gear to be available). public bool IsAvailable(FactionProductData product) => - Galaxy == null || Galaxy.IsPrelude || - Faction != null && ItemManager.ItemData.Get(product.Manufacturer) == Faction || - Galaxy.ContainsFaction(product.Manufacturer) && (Faction == null || Faction.Allegiance.ContainsKey(product.Manufacturer)); + Galaxy == null || Faction == null || + ItemManager.ItemData.Get(product.Manufacturer) == Faction || Faction.Allegiance.ContainsKey(product.Manufacturer); - // Prioritize products from the zone faction and its allies, penalizing distance to the manufacturer's headquarters - private float ManufacturerPreference(CultRecordRef manufacturer) + // Prioritize products from the zone faction and its allies, penalizing distance to the manufacturer's headquarters. + // A manufacturer with no home zone in this galaxy weighs as if its headquarters lay one jump beyond the farthest + // zone this zone reaches: reachable through allegiance, least preferred. + public float ManufacturerPreference(CultRecordRef manufacturer) { if (Faction == null || Galaxy == null) return 1; var maker = ItemManager.ItemData.Get(manufacturer); var allegiance = maker == Faction ? 1 : Faction.Allegiance.TryGetValue(manufacturer, out var a) ? a : 0; var home = maker != null && Galaxy.HomeZones.TryGetValue(maker, out var h) ? h : null; - var distance = home != null && Zone?.Distance != null && Zone.Distance.TryGetValue(home, out var d) ? d : 0; + var distance = Zone?.Distance == null ? 0 : + home != null && Zone.Distance.TryGetValue(home, out var d) ? d : + home == null && Zone.Distance.Count > 0 ? Zone.Distance.Values.Max() + 1 : 0; return allegiance / (1 + distance); } diff --git a/tests/Aetheria.Shared.Tests/FireControlCut6cTests.cs b/tests/Aetheria.Shared.Tests/FireControlCut6cTests.cs index 844e4995b..d02d8f9b7 100644 --- a/tests/Aetheria.Shared.Tests/FireControlCut6cTests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlCut6cTests.cs @@ -231,9 +231,9 @@ public sealed class FireControlCut6cTests : IDisposable var cache = AetheriaStores.Open(Catalog, Run(), Player()); _openCaches.Add(cache); - // A minimal one-zone galaxy whose faction roster excludes Targeting Maker entirely, the same - // SavedGame/Galaxy round trip RunSaveTests uses to build a real Galaxy without standing up full - // procedural generation. + // A minimal one-zone galaxy the same SavedGame/Galaxy round trip RunSaveTests uses builds, without standing + // up full procedural generation. Its generator's faction is Maker, whose allegiance does not name + // Targeting Maker, so IsAvailable rejects Targeting Maker's products by allegiance. var game = new SavedGame { Factions = new[] { maker }, @@ -250,13 +250,14 @@ public sealed class FireControlCut6cTests : IDisposable }; RunSave.Commit(cache, game, zones, new ProvenanceLedger()); var galaxy = new Galaxy(cache, cache.GetGlobal(), _ => { }); - Assert.False(galaxy.ContainsFaction(targetingMaker)); + var makerFaction = cache.Get(maker); + Assert.False(makerFaction.Allegiance.ContainsKey(targetingMaker)); var random = new Random(1u); var settings = TestSettings(); var log = new List(); var items = new ItemManager(cache, new ProvenanceLedger(), settings, log.Add); - var generator = new LoadoutGenerator(ref random, items, galaxy, null, null, .5f); + var generator = new LoadoutGenerator(ref random, items, galaxy, null, makerFaction, .5f); var pack = generator.GenerateShipLoadout(candidate => candidate.Name == "Armed"); diff --git a/tests/Aetheria.Shared.Tests/RunStartGearTests.cs b/tests/Aetheria.Shared.Tests/RunStartGearTests.cs new file mode 100644 index 000000000..800081852 --- /dev/null +++ b/tests/Aetheria.Shared.Tests/RunStartGearTests.cs @@ -0,0 +1,92 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +using System.Collections.Generic; +using System.Linq; +using GameCult.Caching; +using Xunit; + +// Gear does not need its maker present: LoadoutGenerator.IsAvailable reaches by allegiance alone, and an absent +// maker only weighs less (operator, 2026-10-06: a faction need not be present in the galaxy for its gear to be +// available). Every galaxy reaches the same way, the prelude included. +public sealed partial class RunStartTests +{ + // A maker the galaxy does not carry, with one product made from a real design, and a generator for a fresh + // faction whose allegiance names exactly the makers given (the absent one among them, or not). + private (FactionProductData product, Faction absent) AbsentMaker(string name) + { + var absent = new Faction { Name = name, ShortName = name }; + _cache.Upsert(absent); + var design = _cache.GetAll().First(p => p.Manufacturer.IsSet()).Design; + var product = new FactionProductData { Name = name + " Product", Design = design, Manufacturer = _cache.RefOf(absent) }; + _cache.Upsert(product); + return (product, absent); + } + + private LoadoutGenerator GeneratorFor(Galaxy galaxy, string name, params Faction[] allies) + { + var faction = new Faction { Name = name, ShortName = name }; + _cache.Upsert(faction); + foreach (var ally in allies) faction.Allegiance[_cache.RefOf(ally)] = 1; + return new LoadoutGenerator(ref _items.Random, _items, galaxy, galaxy.Entrance, faction, .5f); + } + + [Fact] + public void AnAbsentMakersGearIsAvailableThroughAllegiance() + { + var galaxy = MainSectorGalaxy(); + var (product, absent) = AbsentMaker("AbsentAlly"); + Assert.DoesNotContain(galaxy.Factions, f => f == absent); + Assert.True(GeneratorFor(galaxy, "Friend", absent).IsAvailable(product)); + } + + [Fact] + public void AnAbsentMakerOutsideAllegianceIsNot() + { + var galaxy = MainSectorGalaxy(); + var (product, _) = AbsentMaker("AbsentStranger"); + Assert.False(GeneratorFor(galaxy, "Loner").IsAvailable(product)); + } + + [Fact] + public void AFactionlessGeneratorReachesEveryMaker() + { + var galaxy = MainSectorGalaxy(); + AbsentMaker("AbsentToAll"); + var generator = new LoadoutGenerator(ref _items.Random, _items, galaxy, galaxy.Entrance, null, .5f); + var products = _cache.GetAll().Where(p => p.Manufacturer.IsSet()).ToArray(); + Assert.NotEmpty(products); + Assert.All(products, product => Assert.True(generator.IsAvailable(product), product.Name)); + } + + [Fact] + public void AnAbsentMakerWeighsAsTheFarthest() + { + var galaxy = MainSectorGalaxy(); + var (product, absent) = AbsentMaker("AbsentFar"); + var zone = galaxy.Entrance; + var farthest = galaxy.Factions + .Where(f => galaxy.HomeZones.TryGetValue(f, out var home) && zone.Distance.ContainsKey(home)) + .OrderByDescending(f => zone.Distance[galaxy.HomeZones[f]]).First(); + var generator = GeneratorFor(galaxy, "Trader", absent, farthest); + var absentWeight = generator.ManufacturerPreference(product.Manufacturer); + var farthestWeight = generator.ManufacturerPreference(_cache.RefOf(farthest)); + Assert.True(absentWeight > 0, "an absent ally is reachable"); + Assert.True(absentWeight < farthestWeight, $"absent {absentWeight} must weigh below the farthest present maker {farthestWeight}"); + Assert.Equal(1f / (2 + zone.Distance.Values.Max()), absentWeight); + } + + [Fact] + public void APreludeNoLongerOffersEverything() + { + Assert.True(_galaxy.IsPrelude); + var (absentProduct, _) = AbsentMaker("AbsentToPrelude"); + var generator = GeneratorFor(_galaxy, "PreludeLoner"); + Assert.False(generator.IsAvailable(absentProduct)); + var present = _cache.GetAll().Where(p => p.Manufacturer.IsSet() && + _galaxy.Factions.Any(f => _cache.RefOf(f).Key.Equals(p.Manufacturer.Key))).ToArray(); + Assert.NotEmpty(present); + Assert.All(present, product => Assert.False(generator.IsAvailable(product), product.Name)); + } +} diff --git a/tests/Aetheria.Shared.Tests/RunStartTests.cs b/tests/Aetheria.Shared.Tests/RunStartTests.cs index 755dd91bb..284469741 100644 --- a/tests/Aetheria.Shared.Tests/RunStartTests.cs +++ b/tests/Aetheria.Shared.Tests/RunStartTests.cs @@ -119,8 +119,8 @@ public sealed partial class RunStartTests : IDisposable private const float Freezing = 273.15f; - // A main-sector galaxy (not the prelude, which offers every product): each station's gear must come from - // manufacturers its faction can reach, the galaxy's factions its allegiance names. + // A main-sector galaxy: each station's gear must come from manufacturers its faction can reach, the makers its + // allegiance names, present in the galaxy or not. private Galaxy MainSectorGalaxy() { var authored = AuthoredSettings.Load(FindRepoRoot()); diff --git a/tools/AetherDb/Program.cs b/tools/AetherDb/Program.cs index c65f4c3f2..3c28c23ff 100644 --- a/tools/AetherDb/Program.cs +++ b/tools/AetherDb/Program.cs @@ -743,19 +743,11 @@ public static class Program } // Runs the real LoadoutGenerator against every hull, so a generation failure reproduces here instead of on a - // flight to a populated sector. No galaxy by default, so this covers placement, roles and products but NOT + // flight to a populated sector. No galaxy, so this covers placement, roles and products but NOT // availability filtering or distance weighting, which need a real galaxy. Seeded: a failure repeats. - // - // Cut 6c, 6c.2 verification: "exclude-sellers Short1,Short2,..." builds a minimal one-zone Galaxy (the same - // SavedGame/Galaxy round trip FireControlCut6cTests uses) whose faction roster is every catalog faction - // EXCEPT the named ones, so IsAvailable actually rejects their products instead of the null-Galaxy "every - // product is on offer" shortcut this command otherwise takes. That is the one way to reproduce the single - // point of failure 6c.2 fixed: a null-galaxy run can never exercise Galaxy.ContainsFaction at all. private static int Loadout(string[] args) { - var seedArgument = args.FirstOrDefault(a => !a.StartsWith("exclude-sellers")); - var excludeArg = args.FirstOrDefault(a => a.StartsWith("exclude-sellers")); - var seed = uint.TryParse(seedArgument, out var parsed) ? parsed : 1u; + var seed = uint.TryParse(args.FirstOrDefault(), out var parsed) ? parsed : 1u; var db = AetherDb.Open(); var settings = new GameplaySettings { @@ -764,45 +756,6 @@ public static class Program QualityPriceModifier = new ExponentialLerp() }; - Galaxy galaxy = null; - CultCache galaxyCache = null; - if (excludeArg != null) - { - var excludedShortNames = excludeArg.Contains(':') - ? excludeArg.Substring(excludeArg.IndexOf(':') + 1).Split(',', StringSplitOptions.RemoveEmptyEntries) - : Array.Empty(); - var excluded = new HashSet(excludedShortNames); - - // A second, scratch-run-store cache over the SAME catalog file, read-only on the catalog side -- - // RunSave.Commit only ever touches run-store record types (SavedGame, SavedZone, ProvenanceLedger), - // but it needs a store that is "home" to them, which the plain read-only db.Cache above is not. - // The scratch run.cc lives in the OS temp directory, never under GameData, so this command cannot - // leave behind or corrupt a real save. Factions are looked up through THIS cache instance, not - // db.Cache -- CultCache.RefOf needs the document to be one this specific instance's identity map - // tracks, and two Open() calls over the same file do not share object identity. - var scratchRun = Path.Combine(Path.GetTempPath(), $"aetherdb-loadout-exclude-{Guid.NewGuid():N}.cc"); - galaxyCache = AetheriaStores.Open(Path.Combine(db.Root, "GameData", "Aetheria.cc"), runPath: scratchRun); - var includedFactions = galaxyCache.GetAll().Where(f => !excluded.Contains(f.ShortName)).ToArray(); - Console.WriteLine($"Excluding sellers [{string.Join(", ", excluded)}] -- galaxy carries {includedFactions.Length} of {galaxyCache.GetAll().Count()} factions.\n"); - - var game = new SavedGame - { - Factions = includedFactions.Select(f => galaxyCache.RefOf(f)).ToArray(), - Relationships = includedFactions.Select(_ => FactionRelationship.Neutral).ToArray(), - HomeZones = new Dictionary { { 0, 0 } }, - BossZones = new Dictionary(), - DiscoveredZones = new[] { 0 }, - ActionBarBindings = new SavedActionBarBinding[0], - Exit = -1 - }; - var zones = new[] - { - new SavedZone { Name = "Zone 0", Position = new float2(0, 0), AdjacentZones = Array.Empty(), Factions = new[] { 0 }, Owner = 0, Contents = null } - }; - RunSave.Commit(galaxyCache, game, zones, new ProvenanceLedger()); - galaxy = new Galaxy(galaxyCache, galaxyCache.GetGlobal(), _ => { }); - } - var log = new List(); var itemManager = new ItemManager(db.Cache, new ProvenanceLedger(), settings, log.Add); var failures = 0; @@ -811,7 +764,7 @@ public static class Program foreach (var hull in db.Cache.GetAll().OrderBy(h => h.HullType).ThenBy(h => h.Name)) { var random = new Random(seed); - var generator = new LoadoutGenerator(ref random, itemManager, galaxy, null, null, .5f); + var generator = new LoadoutGenerator(ref random, itemManager, null, null, null, .5f); log.Clear(); string outcome; try @@ -837,7 +790,6 @@ public static class Program } Console.WriteLine($"\n{failures} hulls failed to generate a loadout (seed {seed}), {targetingGaps} armed hulls generated with no targeting system (fired unaided)"); - galaxyCache?.Dispose(); return failures; } From 5e0c0d44f80b9e0c22042f3583e9129b30ebf137 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 14:14:43 +0200 Subject: [PATCH 010/120] Scenario fits: the Fire Control Array lies turned on interior cells (1,6)-(1,7) Tools need interior hull cells (the shrunk shape); (0,4) is an edge cell. The 2x1 array turned clockwise takes the free interior column beside the cockpit. Co-Authored-By: Claude Sonnet 5.5 --- Assets/Scripts/ServerShared/Scenarios/Arcs.cs | 2 +- Assets/Scripts/ServerShared/Scenarios/Duel.cs | 2 +- Assets/Scripts/ServerShared/Scenarios/LauncherAngles.cs | 2 +- Assets/Scripts/ServerShared/Scenarios/StarvedReactor.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Assets/Scripts/ServerShared/Scenarios/Arcs.cs b/Assets/Scripts/ServerShared/Scenarios/Arcs.cs index 585ebe64b..bc5a5b53e 100644 --- a/Assets/Scripts/ServerShared/Scenarios/Arcs.cs +++ b/Assets/Scripts/ServerShared/Scenarios/Arcs.cs @@ -30,7 +30,7 @@ public sealed class Arcs : Scenario ("Iapyx", int2(2, 2), CounterClockwise), ("Iapyx", int2(3, 2), Clockwise), ("not if i see you first", int2(3, 10), None), - ("Fire Control Array", int2(0, 4), None)); + ("Fire Control Array", int2(1, 6), Clockwise)); var turret = stage.Fit("Turret", ("ClearPath", int2(1, 2), None), ("ClearPath", int2(5, 2), None), diff --git a/Assets/Scripts/ServerShared/Scenarios/Duel.cs b/Assets/Scripts/ServerShared/Scenarios/Duel.cs index 5fafd63e4..af7193c50 100644 --- a/Assets/Scripts/ServerShared/Scenarios/Duel.cs +++ b/Assets/Scripts/ServerShared/Scenarios/Duel.cs @@ -28,7 +28,7 @@ public sealed class Duel : Scenario ("Iapyx", int2(3, 2), Clockwise), ("not if i see you first", int2(3, 10), None), ("PotaT+-", int2(1, 5), None), - ("Fire Control Array", int2(0, 4), None), + ("Fire Control Array", int2(1, 6), Clockwise), ("Store-All Plus", int2(2, 8), None)); stage.Player(fighter, float2(0, 0)); diff --git a/Assets/Scripts/ServerShared/Scenarios/LauncherAngles.cs b/Assets/Scripts/ServerShared/Scenarios/LauncherAngles.cs index 3c0bb2f01..d81d7344a 100644 --- a/Assets/Scripts/ServerShared/Scenarios/LauncherAngles.cs +++ b/Assets/Scripts/ServerShared/Scenarios/LauncherAngles.cs @@ -27,7 +27,7 @@ public sealed class LauncherAngles : Scenario ("Iapyx", int2(2, 2), CounterClockwise), ("Iapyx", int2(3, 2), Clockwise), ("not if i see you first", int2(3, 10), None), - ("Fire Control Array", int2(0, 4), None)); + ("Fire Control Array", int2(1, 6), Clockwise)); stage.Player(launcher, float2(0, 0)); stage.Place(stage.Bare("Longinus"), float2(0, 900), facing: float2(1, 0), stance: ScenarioStance.Hostile); diff --git a/Assets/Scripts/ServerShared/Scenarios/StarvedReactor.cs b/Assets/Scripts/ServerShared/Scenarios/StarvedReactor.cs index 4cd9d3daa..839e32c10 100644 --- a/Assets/Scripts/ServerShared/Scenarios/StarvedReactor.cs +++ b/Assets/Scripts/ServerShared/Scenarios/StarvedReactor.cs @@ -25,7 +25,7 @@ public sealed class StarvedReactor : Scenario ("Iapyx", int2(2, 2), CounterClockwise), ("Iapyx", int2(3, 2), Clockwise), ("not if i see you first", int2(3, 10), None), - ("Fire Control Array", int2(0, 4), None)); + ("Fire Control Array", int2(1, 6), Clockwise)); stage.Player(starved, float2(0, 0)); stage.Place(stage.Bare("Longinus"), float2(0, 400)); } From 1937ea04fe6671ac7a4c67092ebb69153cb71af6 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 14:15:49 +0200 Subject: [PATCH 011/120] ARPG scene: the RPM Display object and SchematicDisplay's stale drive references are gone The RPM Display subtree (55 serialized objects) and the seven removed SchematicDisplay fields in both instances (AetherDriveUi, the three RPM labels and fills). Removed by a scripted edit of the scene YAML, not an editor re-save, to keep the diff to the subtraction; Unity 6000.3.24f1 batchmode then opened the scene and compiled the project with no error CS and no missing reference on either SchematicDisplay. Co-Authored-By: Claude Sonnet 5.5 --- Assets/Scenes/ARPG.unity | 1259 -------------------------------------- 1 file changed, 1259 deletions(-) diff --git a/Assets/Scenes/ARPG.unity b/Assets/Scenes/ARPG.unity index d735258a9..aa8eb61e0 100644 --- a/Assets/Scenes/ARPG.unity +++ b/Assets/Scenes/ARPG.unity @@ -1648,81 +1648,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 125165634} m_CullTransparentMesh: 0 ---- !u!1 &126433269 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 126433270} - - component: {fileID: 126433272} - - component: {fileID: 126433271} - m_Layer: 5 - m_Name: Icon - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &126433270 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 126433269} - m_LocalRotation: {x: 0, y: 0, z: 0.7071068, w: 0.7071068} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 691045497} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 90} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0, y: 1} - m_AnchoredPosition: {x: 8, y: 0} - m_SizeDelta: {x: 14, y: -2} - m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &126433271 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 126433269} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 1} - m_RaycastTarget: 0 - m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} - m_Maskable: 0 - m_OnCullStateChanged: - m_PersistentCalls: - m_Calls: [] - m_Sprite: {fileID: 21300000, guid: 55bed7a5ff978bf4c8cbda9971585df0, type: 3} - m_Type: 0 - m_PreserveAspect: 1 - m_FillCenter: 1 - m_FillMethod: 4 - m_FillAmount: 1 - m_FillClockwise: 1 - m_FillOrigin: 0 - m_UseSpriteMesh: 0 - m_PixelsPerUnitMultiplier: 1 ---- !u!222 &126433272 -CanvasRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 126433269} - m_CullTransparentMesh: 0 --- !u!1 &134113270 GameObject: m_ObjectHideFlags: 0 @@ -5081,143 +5006,6 @@ RectTransform: m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 100, y: 100} m_Pivot: {x: 0.5, y: 0.5} ---- !u!1 &383790463 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 383790464} - - component: {fileID: 383790466} - - component: {fileID: 383790465} - m_Layer: 5 - m_Name: Strafe RPM - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &383790464 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 383790463} - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 691045497} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 1, y: 1} - m_AnchoredPosition: {x: 8, y: 0} - m_SizeDelta: {x: -16, y: -4} - m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &383790465 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 383790463} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 1} - m_RaycastTarget: 1 - m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} - m_Maskable: 0 - m_OnCullStateChanged: - m_PersistentCalls: - m_Calls: [] - m_text: 18,000 - m_isRightToLeft: 0 - m_fontAsset: {fileID: 11400000, guid: 3fb7d873bb5a7944bb9a7c73c430c44b, type: 2} - m_sharedMaterial: {fileID: 2100000, guid: b9d2f6c9804cec1489502dcc1c19ec54, type: 2} - m_fontSharedMaterials: [] - m_fontMaterial: {fileID: 0} - m_fontMaterials: [] - m_fontColor32: - serializedVersion: 2 - rgba: 4294967295 - m_fontColor: {r: 1, g: 1, b: 1, a: 1} - m_enableVertexGradient: 0 - m_colorMode: 3 - m_fontColorGradient: - topLeft: {r: 1, g: 1, b: 1, a: 1} - topRight: {r: 1, g: 1, b: 1, a: 1} - bottomLeft: {r: 1, g: 1, b: 1, a: 1} - bottomRight: {r: 1, g: 1, b: 1, a: 1} - m_fontColorGradientPreset: {fileID: 0} - m_spriteAsset: {fileID: 0} - m_tintAllSprites: 0 - m_StyleSheet: {fileID: 0} - m_TextStyleHashCode: -1183493901 - m_overrideHtmlColors: 0 - m_faceColor: - serializedVersion: 2 - rgba: 4294967295 - m_fontSize: 10 - m_fontSizeBase: 10 - m_fontWeight: 400 - m_enableAutoSizing: 1 - m_fontSizeMin: 6 - m_fontSizeMax: 16 - m_fontStyle: 0 - m_HorizontalAlignment: 1 - m_VerticalAlignment: 512 - m_textAlignment: 65535 - m_characterSpacing: 0 - m_characterHorizontalScale: 1 - m_wordSpacing: 0 - m_lineSpacing: 0 - m_lineSpacingMax: 0 - m_paragraphSpacing: 0 - m_charWidthMaxAdj: 0 - m_TextWrappingMode: 1 - m_wordWrappingRatios: 0.4 - m_overflowMode: 0 - m_linkedTextComponent: {fileID: 0} - parentLinkedComponent: {fileID: 0} - m_enableKerning: 1 - m_ActiveFontFeatures: 6e72656b - m_enableExtraPadding: 0 - checkPaddingRequired: 0 - m_isRichText: 1 - m_EmojiFallbackSupport: 1 - m_parseCtrlCharacters: 1 - m_isOrthographic: 1 - m_isCullingEnabled: 0 - m_horizontalMapping: 0 - m_verticalMapping: 0 - m_uvLineOffset: 0 - m_geometrySortingOrder: 0 - m_IsTextObjectScaleStatic: 0 - m_VertexBufferAutoSizeReduction: 1 - m_useMaxVisibleDescender: 1 - m_pageToDisplay: 1 - m_margin: {x: 0, y: 0, z: 0, w: 0} - m_isUsingLegacyAnimationComponent: 0 - m_isVolumetricText: 0 - m_hasFontAssetChanged: 0 - m_baseMaterial: {fileID: 0} - m_maskOffset: {x: 0, y: 0, z: 0, w: 0} ---- !u!222 &383790466 -CanvasRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 383790463} - m_CullTransparentMesh: 0 --- !u!1 &388927454 GameObject: m_ObjectHideFlags: 0 @@ -5979,7 +5767,6 @@ MonoBehaviour: OverrideIconBlinkSpeed: 0 ShieldIcon: {fileID: 0} HeatsinkBackground: {fileID: 0} - AetherDriveUi: {fileID: 0} EnergyLabel: {fileID: 0} CockpitTemperatureLabel: {fileID: 0} RadiatorTemperatureLabel: {fileID: 0} @@ -5988,18 +5775,12 @@ MonoBehaviour: VisibilityLabel: {fileID: 0} HullDurabilityLabel: {fileID: 0} DistanceLabel: {fileID: 0} - ForwardRPMLabel: {fileID: 0} - StrafeRPMLabel: {fileID: 0} - TurnRPMLabel: {fileID: 0} SensorCooldownFill: {fileID: 0} EnergyFill: {fileID: 0} HullDurabilityFill: {fileID: 0} HeatstrokeMeterFill: {fileID: 0} HypothermiaMeterFill: {fileID: 0} HeatstrokeLimitFill: {fileID: 0} - ForwardRPMFill: {fileID: 0} - StrafeRPMFill: {fileID: 0} - TurnRPMFill: {fileID: 0} --- !u!114 &410701130 MonoBehaviour: m_ObjectHideFlags: 0 @@ -11195,105 +10976,6 @@ MeshFilter: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 677960091} m_Mesh: {fileID: 10210, guid: 0000000000000000e000000000000000, type: 0} ---- !u!1 &691045496 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 691045497} - - component: {fileID: 691045500} - - component: {fileID: 691045499} - - component: {fileID: 691045498} - m_Layer: 5 - m_Name: Strafe - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &691045497 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 691045496} - m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: - - {fileID: 1542388328} - - {fileID: 126433270} - - {fileID: 383790464} - m_Father: {fileID: 1735948312} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0, y: 0} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 0, y: 0} - m_Pivot: {x: 0.5, y: 1} ---- !u!114 &691045498 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 691045496} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 0.011764706} - m_RaycastTarget: 1 - m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} - m_Maskable: 1 - m_OnCullStateChanged: - m_PersistentCalls: - m_Calls: [] - m_Sprite: {fileID: 0} - m_Type: 1 - m_PreserveAspect: 0 - m_FillCenter: 1 - m_FillMethod: 4 - m_FillAmount: 1 - m_FillClockwise: 1 - m_FillOrigin: 0 - m_UseSpriteMesh: 0 - m_PixelsPerUnitMultiplier: 1 ---- !u!222 &691045499 -CanvasRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 691045496} - m_CullTransparentMesh: 1 ---- !u!114 &691045500 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 691045496} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} - m_Name: - m_EditorClassIdentifier: - m_IgnoreLayout: 0 - m_MinWidth: -1 - m_MinHeight: -1 - m_PreferredWidth: -1 - m_PreferredHeight: 16 - m_FlexibleWidth: -1 - m_FlexibleHeight: -1 - m_LayoutPriority: 1 --- !u!1 &693745763 GameObject: m_ObjectHideFlags: 0 @@ -11877,105 +11559,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 719842150} m_CullTransparentMesh: 1 ---- !u!1 &723697980 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 723697981} - - component: {fileID: 723697984} - - component: {fileID: 723697983} - - component: {fileID: 723697982} - m_Layer: 5 - m_Name: Forward - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &723697981 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 723697980} - m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: - - {fileID: 1946192591} - - {fileID: 1463754850} - - {fileID: 1558236436} - m_Father: {fileID: 1735948312} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0, y: 0} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 0, y: 0} - m_Pivot: {x: 0.5, y: 1} ---- !u!114 &723697982 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 723697980} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 0.011764706} - m_RaycastTarget: 1 - m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} - m_Maskable: 1 - m_OnCullStateChanged: - m_PersistentCalls: - m_Calls: [] - m_Sprite: {fileID: 0} - m_Type: 1 - m_PreserveAspect: 0 - m_FillCenter: 1 - m_FillMethod: 4 - m_FillAmount: 1 - m_FillClockwise: 1 - m_FillOrigin: 0 - m_UseSpriteMesh: 0 - m_PixelsPerUnitMultiplier: 1 ---- !u!222 &723697983 -CanvasRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 723697980} - m_CullTransparentMesh: 1 ---- !u!114 &723697984 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 723697980} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} - m_Name: - m_EditorClassIdentifier: - m_IgnoreLayout: 0 - m_MinWidth: -1 - m_MinHeight: -1 - m_PreferredWidth: -1 - m_PreferredHeight: 16 - m_FlexibleWidth: -1 - m_FlexibleHeight: -1 - m_LayoutPriority: 1 --- !u!1 &732687889 GameObject: m_ObjectHideFlags: 0 @@ -12226,7 +11809,6 @@ RectTransform: m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: - - {fileID: 1735948312} - {fileID: 232470597} m_Father: {fileID: 60313920} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} @@ -13912,81 +13494,6 @@ MonoBehaviour: m_FlexibleWidth: -1 m_FlexibleHeight: -1 m_LayoutPriority: 1 ---- !u!1 &853400335 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 853400336} - - component: {fileID: 853400338} - - component: {fileID: 853400337} - m_Layer: 5 - m_Name: Icon - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &853400336 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 853400335} - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: -1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 1214607512} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0, y: 1} - m_AnchoredPosition: {x: 8, y: 0} - m_SizeDelta: {x: 10, y: -4} - m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &853400337 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 853400335} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 1} - m_RaycastTarget: 0 - m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} - m_Maskable: 0 - m_OnCullStateChanged: - m_PersistentCalls: - m_Calls: [] - m_Sprite: {fileID: 21300000, guid: 4b33557baae0da54a96c0aa97bf752ee, type: 3} - m_Type: 0 - m_PreserveAspect: 1 - m_FillCenter: 1 - m_FillMethod: 4 - m_FillAmount: 1 - m_FillClockwise: 1 - m_FillOrigin: 0 - m_UseSpriteMesh: 0 - m_PixelsPerUnitMultiplier: 1 ---- !u!222 &853400338 -CanvasRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 853400335} - m_CullTransparentMesh: 0 --- !u!1 &861158450 GameObject: m_ObjectHideFlags: 0 @@ -14952,143 +14459,6 @@ RectTransform: m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 56, y: 56} m_Pivot: {x: 0, y: 0} ---- !u!1 &932757481 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 932757482} - - component: {fileID: 932757484} - - component: {fileID: 932757483} - m_Layer: 5 - m_Name: Turn RPM - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &932757482 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 932757481} - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 1214607512} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 1, y: 1} - m_AnchoredPosition: {x: 8, y: 0} - m_SizeDelta: {x: -16, y: -4} - m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &932757483 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 932757481} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 1} - m_RaycastTarget: 1 - m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} - m_Maskable: 0 - m_OnCullStateChanged: - m_PersistentCalls: - m_Calls: [] - m_text: 18,000 - m_isRightToLeft: 0 - m_fontAsset: {fileID: 11400000, guid: 3fb7d873bb5a7944bb9a7c73c430c44b, type: 2} - m_sharedMaterial: {fileID: 2100000, guid: b9d2f6c9804cec1489502dcc1c19ec54, type: 2} - m_fontSharedMaterials: [] - m_fontMaterial: {fileID: 0} - m_fontMaterials: [] - m_fontColor32: - serializedVersion: 2 - rgba: 4294967295 - m_fontColor: {r: 1, g: 1, b: 1, a: 1} - m_enableVertexGradient: 0 - m_colorMode: 3 - m_fontColorGradient: - topLeft: {r: 1, g: 1, b: 1, a: 1} - topRight: {r: 1, g: 1, b: 1, a: 1} - bottomLeft: {r: 1, g: 1, b: 1, a: 1} - bottomRight: {r: 1, g: 1, b: 1, a: 1} - m_fontColorGradientPreset: {fileID: 0} - m_spriteAsset: {fileID: 0} - m_tintAllSprites: 0 - m_StyleSheet: {fileID: 0} - m_TextStyleHashCode: -1183493901 - m_overrideHtmlColors: 0 - m_faceColor: - serializedVersion: 2 - rgba: 4294967295 - m_fontSize: 10 - m_fontSizeBase: 10 - m_fontWeight: 400 - m_enableAutoSizing: 1 - m_fontSizeMin: 6 - m_fontSizeMax: 16 - m_fontStyle: 0 - m_HorizontalAlignment: 1 - m_VerticalAlignment: 512 - m_textAlignment: 65535 - m_characterSpacing: 0 - m_characterHorizontalScale: 1 - m_wordSpacing: 0 - m_lineSpacing: 0 - m_lineSpacingMax: 0 - m_paragraphSpacing: 0 - m_charWidthMaxAdj: 0 - m_TextWrappingMode: 1 - m_wordWrappingRatios: 0.4 - m_overflowMode: 0 - m_linkedTextComponent: {fileID: 0} - parentLinkedComponent: {fileID: 0} - m_enableKerning: 1 - m_ActiveFontFeatures: 6e72656b - m_enableExtraPadding: 0 - checkPaddingRequired: 0 - m_isRichText: 1 - m_EmojiFallbackSupport: 1 - m_parseCtrlCharacters: 1 - m_isOrthographic: 1 - m_isCullingEnabled: 0 - m_horizontalMapping: 0 - m_verticalMapping: 0 - m_uvLineOffset: 0 - m_geometrySortingOrder: 0 - m_IsTextObjectScaleStatic: 0 - m_VertexBufferAutoSizeReduction: 1 - m_useMaxVisibleDescender: 1 - m_pageToDisplay: 1 - m_margin: {x: 0, y: 0, z: 0, w: 0} - m_isUsingLegacyAnimationComponent: 0 - m_isVolumetricText: 0 - m_hasFontAssetChanged: 0 - m_baseMaterial: {fileID: 0} - m_maskOffset: {x: 0, y: 0, z: 0, w: 0} ---- !u!222 &932757484 -CanvasRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 932757481} - m_CullTransparentMesh: 0 --- !u!1 &933815652 GameObject: m_ObjectHideFlags: 0 @@ -17165,81 +16535,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1012981707} m_CullTransparentMesh: 0 ---- !u!1 &1013676407 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1013676408} - - component: {fileID: 1013676410} - - component: {fileID: 1013676409} - m_Layer: 5 - m_Name: Turn Fill - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &1013676408 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1013676407} - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 1214607512} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0.8, y: 1} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 0, y: 0} - m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &1013676409 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1013676407} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 0.101960786} - m_RaycastTarget: 1 - m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} - m_Maskable: 1 - m_OnCullStateChanged: - m_PersistentCalls: - m_Calls: [] - m_Sprite: {fileID: 0} - m_Type: 1 - m_PreserveAspect: 0 - m_FillCenter: 1 - m_FillMethod: 4 - m_FillAmount: 1 - m_FillClockwise: 1 - m_FillOrigin: 0 - m_UseSpriteMesh: 0 - m_PixelsPerUnitMultiplier: 1 ---- !u!222 &1013676410 -CanvasRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1013676407} - m_CullTransparentMesh: 0 --- !u!1 &1029396710 GameObject: m_ObjectHideFlags: 0 @@ -19818,105 +19113,6 @@ RectTransform: m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 0, y: 0} m_Pivot: {x: 0, y: 0} ---- !u!1 &1214607511 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1214607512} - - component: {fileID: 1214607515} - - component: {fileID: 1214607514} - - component: {fileID: 1214607513} - m_Layer: 5 - m_Name: Turn - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &1214607512 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1214607511} - m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: - - {fileID: 1013676408} - - {fileID: 853400336} - - {fileID: 932757482} - m_Father: {fileID: 1735948312} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0, y: 0} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 0, y: 0} - m_Pivot: {x: 0.5, y: 1} ---- !u!114 &1214607513 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1214607511} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 0.011764706} - m_RaycastTarget: 1 - m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} - m_Maskable: 1 - m_OnCullStateChanged: - m_PersistentCalls: - m_Calls: [] - m_Sprite: {fileID: 0} - m_Type: 1 - m_PreserveAspect: 0 - m_FillCenter: 1 - m_FillMethod: 4 - m_FillAmount: 1 - m_FillClockwise: 1 - m_FillOrigin: 0 - m_UseSpriteMesh: 0 - m_PixelsPerUnitMultiplier: 1 ---- !u!222 &1214607514 -CanvasRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1214607511} - m_CullTransparentMesh: 1 ---- !u!114 &1214607515 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1214607511} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} - m_Name: - m_EditorClassIdentifier: - m_IgnoreLayout: 0 - m_MinWidth: -1 - m_MinHeight: -1 - m_PreferredWidth: -1 - m_PreferredHeight: 16 - m_FlexibleWidth: -1 - m_FlexibleHeight: -1 - m_LayoutPriority: 1 --- !u!1 &1218822609 GameObject: m_ObjectHideFlags: 0 @@ -22922,81 +22118,6 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 1281fe44325f9b548aa42dc331b17266, type: 3} m_Name: m_EditorClassIdentifier: ---- !u!1 &1463754849 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1463754850} - - component: {fileID: 1463754852} - - component: {fileID: 1463754851} - m_Layer: 5 - m_Name: Icon - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &1463754850 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1463754849} - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 723697981} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0, y: 1} - m_AnchoredPosition: {x: 8, y: 0} - m_SizeDelta: {x: 14, y: -2} - m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &1463754851 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1463754849} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 1} - m_RaycastTarget: 0 - m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} - m_Maskable: 0 - m_OnCullStateChanged: - m_PersistentCalls: - m_Calls: [] - m_Sprite: {fileID: 21300000, guid: 55bed7a5ff978bf4c8cbda9971585df0, type: 3} - m_Type: 0 - m_PreserveAspect: 1 - m_FillCenter: 1 - m_FillMethod: 4 - m_FillAmount: 1 - m_FillClockwise: 1 - m_FillOrigin: 0 - m_UseSpriteMesh: 0 - m_PixelsPerUnitMultiplier: 1 ---- !u!222 &1463754852 -CanvasRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1463754849} - m_CullTransparentMesh: 0 --- !u!1 &1464157084 GameObject: m_ObjectHideFlags: 0 @@ -24418,81 +23539,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1541341820} m_CullTransparentMesh: 1 ---- !u!1 &1542388327 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1542388328} - - component: {fileID: 1542388330} - - component: {fileID: 1542388329} - m_Layer: 5 - m_Name: Strafe Fill - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &1542388328 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1542388327} - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 691045497} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0.8, y: 1} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 0, y: 0} - m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &1542388329 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1542388327} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 0.101960786} - m_RaycastTarget: 1 - m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} - m_Maskable: 1 - m_OnCullStateChanged: - m_PersistentCalls: - m_Calls: [] - m_Sprite: {fileID: 0} - m_Type: 1 - m_PreserveAspect: 0 - m_FillCenter: 1 - m_FillMethod: 4 - m_FillAmount: 1 - m_FillClockwise: 1 - m_FillOrigin: 0 - m_UseSpriteMesh: 0 - m_PixelsPerUnitMultiplier: 1 ---- !u!222 &1542388330 -CanvasRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1542388327} - m_CullTransparentMesh: 0 --- !u!1 &1547889398 GameObject: m_ObjectHideFlags: 0 @@ -24992,143 +24038,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1558182380} m_CullTransparentMesh: 1 ---- !u!1 &1558236435 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1558236436} - - component: {fileID: 1558236438} - - component: {fileID: 1558236437} - m_Layer: 5 - m_Name: Forward RPM - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &1558236436 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1558236435} - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 723697981} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 1, y: 1} - m_AnchoredPosition: {x: 8, y: 0} - m_SizeDelta: {x: -16, y: -4} - m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &1558236437 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1558236435} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 1} - m_RaycastTarget: 1 - m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} - m_Maskable: 0 - m_OnCullStateChanged: - m_PersistentCalls: - m_Calls: [] - m_text: 18,000 - m_isRightToLeft: 0 - m_fontAsset: {fileID: 11400000, guid: 3fb7d873bb5a7944bb9a7c73c430c44b, type: 2} - m_sharedMaterial: {fileID: 2100000, guid: b9d2f6c9804cec1489502dcc1c19ec54, type: 2} - m_fontSharedMaterials: [] - m_fontMaterial: {fileID: 0} - m_fontMaterials: [] - m_fontColor32: - serializedVersion: 2 - rgba: 4294967295 - m_fontColor: {r: 1, g: 1, b: 1, a: 1} - m_enableVertexGradient: 0 - m_colorMode: 3 - m_fontColorGradient: - topLeft: {r: 1, g: 1, b: 1, a: 1} - topRight: {r: 1, g: 1, b: 1, a: 1} - bottomLeft: {r: 1, g: 1, b: 1, a: 1} - bottomRight: {r: 1, g: 1, b: 1, a: 1} - m_fontColorGradientPreset: {fileID: 0} - m_spriteAsset: {fileID: 0} - m_tintAllSprites: 0 - m_StyleSheet: {fileID: 0} - m_TextStyleHashCode: -1183493901 - m_overrideHtmlColors: 0 - m_faceColor: - serializedVersion: 2 - rgba: 4294967295 - m_fontSize: 10 - m_fontSizeBase: 10 - m_fontWeight: 400 - m_enableAutoSizing: 1 - m_fontSizeMin: 6 - m_fontSizeMax: 16 - m_fontStyle: 0 - m_HorizontalAlignment: 1 - m_VerticalAlignment: 512 - m_textAlignment: 65535 - m_characterSpacing: 0 - m_characterHorizontalScale: 1 - m_wordSpacing: 0 - m_lineSpacing: 0 - m_lineSpacingMax: 0 - m_paragraphSpacing: 0 - m_charWidthMaxAdj: 0 - m_TextWrappingMode: 1 - m_wordWrappingRatios: 0.4 - m_overflowMode: 0 - m_linkedTextComponent: {fileID: 0} - parentLinkedComponent: {fileID: 0} - m_enableKerning: 1 - m_ActiveFontFeatures: 6e72656b - m_enableExtraPadding: 0 - checkPaddingRequired: 0 - m_isRichText: 1 - m_EmojiFallbackSupport: 1 - m_parseCtrlCharacters: 1 - m_isOrthographic: 1 - m_isCullingEnabled: 0 - m_horizontalMapping: 0 - m_verticalMapping: 0 - m_uvLineOffset: 0 - m_geometrySortingOrder: 0 - m_IsTextObjectScaleStatic: 0 - m_VertexBufferAutoSizeReduction: 1 - m_useMaxVisibleDescender: 1 - m_pageToDisplay: 1 - m_margin: {x: 0, y: 0, z: 0, w: 0} - m_isUsingLegacyAnimationComponent: 0 - m_isVolumetricText: 0 - m_hasFontAssetChanged: 0 - m_baseMaterial: {fileID: 0} - m_maskOffset: {x: 0, y: 0, z: 0, w: 0} ---- !u!222 &1558236438 -CanvasRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1558236435} - m_CullTransparentMesh: 0 --- !u!1 &1558520821 GameObject: m_ObjectHideFlags: 0 @@ -27929,92 +26838,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1733988258} m_CullTransparentMesh: 1 ---- !u!1 &1735948311 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1735948312} - - component: {fileID: 1735948314} - - component: {fileID: 1735948313} - m_Layer: 5 - m_Name: RPM Display - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &1735948312 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1735948311} - m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: - - {fileID: 723697981} - - {fileID: 691045497} - - {fileID: 1214607512} - m_Father: {fileID: 746825913} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0, y: 0} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 0, y: 0} - m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &1735948313 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1735948311} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Padding: - m_Left: 0 - m_Right: 0 - m_Top: 0 - m_Bottom: 0 - m_ChildAlignment: 0 - m_Spacing: 2 - m_ChildForceExpandWidth: 1 - m_ChildForceExpandHeight: 0 - m_ChildControlWidth: 1 - m_ChildControlHeight: 1 - m_ChildScaleWidth: 0 - m_ChildScaleHeight: 0 - m_ReverseArrangement: 0 ---- !u!114 &1735948314 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1735948311} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} - m_Name: - m_EditorClassIdentifier: - m_IgnoreLayout: 0 - m_MinWidth: -1 - m_MinHeight: -1 - m_PreferredWidth: -1 - m_PreferredHeight: 14 - m_FlexibleWidth: -1 - m_FlexibleHeight: -1 - m_LayoutPriority: 1 --- !u!1 &1741914909 GameObject: m_ObjectHideFlags: 0 @@ -30089,81 +28912,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1943948574} m_CullTransparentMesh: 0 ---- !u!1 &1946192590 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1946192591} - - component: {fileID: 1946192593} - - component: {fileID: 1946192592} - m_Layer: 5 - m_Name: Forward Fill - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &1946192591 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1946192590} - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 723697981} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0.8, y: 1} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 0, y: 0} - m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &1946192592 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1946192590} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 0.101960786} - m_RaycastTarget: 1 - m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} - m_Maskable: 1 - m_OnCullStateChanged: - m_PersistentCalls: - m_Calls: [] - m_Sprite: {fileID: 0} - m_Type: 1 - m_PreserveAspect: 0 - m_FillCenter: 1 - m_FillMethod: 4 - m_FillAmount: 1 - m_FillClockwise: 1 - m_FillOrigin: 0 - m_UseSpriteMesh: 0 - m_PixelsPerUnitMultiplier: 1 ---- !u!222 &1946192593 -CanvasRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1946192590} - m_CullTransparentMesh: 0 --- !u!1 &1952022815 GameObject: m_ObjectHideFlags: 0 @@ -39366,7 +38114,6 @@ MonoBehaviour: OverrideIconBlinkSpeed: 10 ShieldIcon: {fileID: 19222411} HeatsinkBackground: {fileID: 661332569} - AetherDriveUi: {fileID: 1735948311} EnergyLabel: {fileID: 804489113} CockpitTemperatureLabel: {fileID: 212527384} RadiatorTemperatureLabel: {fileID: 1752418078} @@ -39375,18 +38122,12 @@ MonoBehaviour: VisibilityLabel: {fileID: 2122035461} HullDurabilityLabel: {fileID: 697075017} DistanceLabel: {fileID: 0} - ForwardRPMLabel: {fileID: 1558236437} - StrafeRPMLabel: {fileID: 383790465} - TurnRPMLabel: {fileID: 932757483} SensorCooldownFill: {fileID: 1943948575} EnergyFill: {fileID: 271575215} HullDurabilityFill: {fileID: 753112995} HeatstrokeMeterFill: {fileID: 1029396711} HypothermiaMeterFill: {fileID: 79341832} HeatstrokeLimitFill: {fileID: 933815653} - ForwardRPMFill: {fileID: 1946192591} - StrafeRPMFill: {fileID: 1542388328} - TurnRPMFill: {fileID: 1013676408} --- !u!224 &7628189704657849088 RectTransform: m_ObjectHideFlags: 0 From 9e5c8de7d5f24b47574661e980577505b38fa649 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 14:27:37 +0200 Subject: [PATCH 012/120] Top-speed test comment: drop the retired hull's name so the negative grep for it stays empty Co-Authored-By: Claude Sonnet 5.5 --- tests/Aetheria.Shared.Tests/RestoredHullsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs b/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs index 802e73947..7447e8037 100644 --- a/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs +++ b/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs @@ -202,7 +202,7 @@ public sealed class RestoredHullsTests } // The dock intro dereferences the ship's VelocityLimit unguarded (ActionGameManager), so every ship hull the catalog - // ships carries exactly one, with a top speed that is a finite positive number. After the LonginusX's retirement + // ships carries exactly one, with a top speed that is a finite positive number. After the retirement // (ruling retire-longinusx) the shipped ship hulls are the Longinus and the Djinni. [Fact] public void EveryShippedShipHullHasATopSpeed() From 7e3c872f0f334ae59245cffb5343bc2ac2667b1d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 14:38:45 +0200 Subject: [PATCH 013/120] AetherDb: pd-gear command authors three generic PD designs and eleven branded products Dry run prints every record; apply commits (the field-kinds pattern). Designs are deep copies of ClearPath, Spectra and DeathCluster with only the cut-spec stats overridden; branding lives in the eleven products. Catalog not yet applied in this commit. Not yet compiled when committed; the next commit's job builds it. Co-Authored-By: Claude Sonnet 5.5 --- tools/AetherDb/Program.cs | 143 +++++++++++++++++++++++++++++++++++++- 1 file changed, 142 insertions(+), 1 deletion(-) diff --git a/tools/AetherDb/Program.cs b/tools/AetherDb/Program.cs index c65f4c3f2..a3c00510f 100644 --- a/tools/AetherDb/Program.cs +++ b/tools/AetherDb/Program.cs @@ -10,6 +10,8 @@ using System.Linq; using System.Reflection; using CultMath; using GameCult.Caching; +using GameCult.Caching.MessagePack; +using MessagePack; using Random = CultMath.Random; // Commands over the game database, run with: dotnet run --project tools/AetherDb -- @@ -38,8 +40,9 @@ public static class Program case "targeting-catalog-6d": return TargetingCatalog6d(args.Contains("apply")); case "ship-authoring": return ShipAuthoringCommands.Run(args.Skip(1).ToArray()); case "field-kinds": return FieldKindsCatalog(args.Contains("apply")); + case "pd-gear": return PdGearCatalog(args.Contains("apply")); default: - Console.WriteLine("commands: census, factions, station-fit, hardpoint-fit, loadout [seed], save, settings, settings-dump, dangling [clear ]... [apply], shield-migrate [apply], brownout-migrate [apply], roles-migrate [apply], firing-arc-migrate [apply], targeting-catalog [apply], targeting-catalog-6c [apply], targeting-catalog-6d [apply], ship-authoring create|inspect|validate|compose, field-kinds [apply]"); + Console.WriteLine("commands: census, factions, station-fit, hardpoint-fit, loadout [seed], save, settings, settings-dump, dangling [clear ]... [apply], shield-migrate [apply], brownout-migrate [apply], roles-migrate [apply], firing-arc-migrate [apply], targeting-catalog [apply], targeting-catalog-6c [apply], targeting-catalog-6d [apply], ship-authoring create|inspect|validate|compose, field-kinds [apply], pd-gear [apply]"); return 1; } } @@ -1505,4 +1508,142 @@ public static class Program Console.WriteLine("\nAuthored \"Asteroid\" in Aetheria.cc"); return 0; } + + // Aetheria release, cut pd-gear (ruling catalog-grows-generic-designs-branded-products): the catalog's first + // point-defence class guns. Nothing in data marks a gun as PD (ruling pd-who-engages); these are PD by their + // stats: a hose of light rounds, a quick light beam, and a small pellet volley. Each design is a deep copy of a + // template weapon (so roles, effect, sound and temperatures are inherited) with only the stats below overridden; + // each carries several products, and branding lives in the products alone. Dry run unless passed "apply". + private static int PdGearCatalog(bool apply) + { + var db = AetherDb.Open(catalogWritable: apply); + var weapons = db.Cache.GetAll().ToArray(); + + var designNames = new[] { "Point Defense Gun", "Point Defense Laser", "Flak Gun" }; + var already = designNames.Count(name => weapons.Any(w => w.Name == name)); + if (already == designNames.Length) + { + Console.WriteLine("Point-defence gear is already authored; nothing to do."); + return 0; + } + if (already != 0) throw new InvalidOperationException("Point-defence gear is partly authored; the catalog needs a human."); + + WeaponItemData Template(string name) => + weapons.FirstOrDefault(w => w.Name == name) ?? throw new InvalidOperationException($"No weapon design named \"{name}\"."); + Faction Maker(string shortName) => + db.Cache.GetAll().FirstOrDefault(f => f.ShortName == shortName) ?? throw new InvalidOperationException($"No faction with short name \"{shortName}\"."); + + var ammoRef = Template("Autocannon").Behaviors.OfType().Single().AmmoType; + var ammunition = db.Cache.Get(ammoRef.Key) ?? throw new InvalidOperationException("Autocannon's ammunition commodity is not in the catalog."); + + var options = CultDocumentMessagePackSerialization.OptionsFor(typeof(WeaponItemData).Assembly); + WeaponItemData Copy(WeaponItemData template, string name, string description, float mass, int price, float durability, int width, int height, WeaponModifiers modifiers) + { + var design = MessagePackSerializer.Deserialize(MessagePackSerializer.Serialize(template, options), options); + design.Name = name; + design.Description = description; + design.Mass = mass; + design.Price = price; + design.Durability = durability; + design.Shape = new Shape(width, height); + for (var x = 0; x < width; x++) + for (var y = 0; y < height; y++) design.Shape.Cells[x, y] = true; + design.WeaponModifiers = modifiers; + return design; + } + + // The stat keeps the template's terms (so quality still moves it as it moves the template); only Min and Max are authored. + static void Set(PerformanceStat stat, float min, float max) { stat.Min = min; stat.Max = max; } + static void Track(WeaponData data, float rate) => data.Tracking = new PerformanceStat { Min = rate, Max = rate, Terms = new List() }; + + var gun = Copy(Template("ClearPath"), designNames[0], "Close-in defence gun: a hose of light rounds.", 40f, 80000, 60f, 1, 1, WeaponModifiers.RapidFire); + var gunData = gun.Behaviors.OfType().Single(); + Set(gunData.Damage, 1.5f, 4f); Set(gunData.Range, 300f, 550f); Set(gunData.MinRange, 0f, 0f); + Set(gunData.Cooldown, .06f, .025f); Set(gunData.Count, 1f, 1f); Set(gunData.Spread, 1f, .3f); + Set(gunData.Velocity, 1100f, 1600f); Set(gunData.Energy, .4f, .2f); Set(gunData.Heat, 40f, 15f); Set(gunData.Visibility, 400f, 150f); + gunData.AmmoType = db.Cache.RefOf(ammunition); gunData.MagazineSize = 250; gunData.ReloadTime = 3f; + Track(gunData, 40f); + + var laser = Copy(Template("Spectra"), designNames[1], "Close-in defence beam: quick, light, instant.", 30f, 110000, 30f, 1, 1, Template("Spectra").WeaponModifiers); + var laserData = laser.Behaviors.OfType().Single(); + Set(laserData.Damage, 5f, 12f); Set(laserData.Range, 400f, 750f); Set(laserData.MinRange, 0f, 0f); + Set(laserData.Cooldown, .2f, .1f); Set(laserData.Count, 1f, 1f); Set(laserData.Spread, .05f, 0f); + Set(laserData.Velocity, 0f, 0f); Set(laserData.Energy, 6f, 10f); Set(laserData.Heat, 300f, 150f); Set(laserData.Visibility, 300f, 100f); + Track(laserData, 40f); + + var flak = Copy(Template("DeathCluster"), designNames[2], "Small flak gun: a short-range pellet volley.", 120f, 180000, 80f, 1, 2, WeaponModifiers.Cluster); + var flakData = flak.Behaviors.OfType().Single(); + Set(flakData.Damage, 30f, 70f); Set(flakData.Count, 6f, 8f); Set(flakData.Range, 350f, 800f); Set(flakData.MinRange, 0f, 0f); + Set(flakData.Cooldown, .6f, .35f); Set(flakData.Spread, 4f, 2f); Set(flakData.Velocity, 900f, 1300f); + Set(flakData.Energy, 1f, .5f); Set(flakData.Heat, 300f, 150f); Set(flakData.Visibility, 800f, 300f); + flakData.AmmoType = db.Cache.RefOf(ammunition); flakData.MagazineSize = 40; flakData.ReloadTime = 4f; + Track(flakData, 20f); + + var makers = new Dictionary + { + ["Zhestokost"] = (.50f, .08f), ["AU"] = (.50f, .12f), ["Lightsail"] = (.55f, .10f), ["R&D"] = (.60f, .15f), + ["Lucent"] = (.60f, .15f), ["NiteLife"] = (.55f, .12f), ["Finch"] = (.65f, .20f), ["Alakrita"] = (.60f, .14f), + ["DME"] = (.55f, .20f), ["Adrasteia"] = (.60f, .10f), + }; + var products = new (WeaponItemData Design, string Name, string Maker, string Description)[] + { + (gun, "Waykeeper", "Lightsail", "Freight first, questions later. Keeps a hauler's paint unscratched through a full salvo."), + (gun, "Rampart", "Zhestokost", "A column does not dodge. It does not need to."), + (gun, "Swatter", "AU", "Keeps the rocks and the rockets off your back while you work."), + (gun, "Catch-All", "R&D", "Guaranteed to stop the missile!* *Missile defined as whatever it stops."), + (laser, "Glare", "Lucent", "Every missile gets its moment in the spotlight. A short one."), + (laser, "Nightlight", "NiteLife", "Lights out for anything with a warhead. Shares the bus with your blasters, so mind the meter."), + (laser, "Wren", "Finch", "Small, quiet and quicker than it looks, most days."), + (laser, "Gnat", "Alakrita", "Weighs less than your conscience and keeps the fast pass fast."), + (flak, "MFer", "Zhestokost", "Our shrapnel turns your enemies into scrapnel."), + (flak, "Hailstorm", "AU", "A wall of steel for crews who would rather not count missiles."), + (flak, "sky full of teeth", "DME", "light it up/ shake it loose/ nothing gets through tonight"), + }; + foreach (var product in products) Maker(product.Maker); + + void Describe(WeaponItemData design) + { + var data = design.Behaviors.OfType().Single(); + Console.WriteLine($" {design.Name,-20} {design.HardpointType,-9} {design.Shape.Width}x{design.Shape.Height} mass {design.Mass:0.##} price {design.Price} durability {design.Durability:0.##} modifiers {design.WeaponModifiers}"); + Console.WriteLine($" damage {data.Damage.Min:0.##}-{data.Damage.Max:0.##} count {data.Count.Min:0.##}-{data.Count.Max:0.##} range {data.Range.Min:0.##}-{data.Range.Max:0.##} cooldown {data.Cooldown.Min:0.###}-{data.Cooldown.Max:0.###} spread {data.Spread.Min:0.##}-{data.Spread.Max:0.##} velocity {data.Velocity.Min:0.##}-{data.Velocity.Max:0.##}"); + Console.WriteLine($" energy {data.Energy.Min:0.##}-{data.Energy.Max:0.##} heat {data.Heat.Min:0.##}-{data.Heat.Max:0.##} visibility {data.Visibility.Min:0.##}-{data.Visibility.Max:0.##} ammo {(data.AmmoType.IsSet() ? ammunition.Name : "none")} magazine {data.MagazineSize} reload {data.ReloadTime:0.##} tracking {data.Tracking.Min:0.##} roles [{string.Join(", ", design.Roles.Select(r => r.Name))}]"); + Console.WriteLine($" \"{design.Description}\""); + } + + Console.WriteLine("New designs:"); + foreach (var design in new[] { gun, laser, flak }) Describe(design); + Console.WriteLine("\nNew products:"); + foreach (var (design, name, maker, description) in products) + { + var (mean, dev) = makers[maker]; + Console.WriteLine($" {design.Name} -> \"{name}\" by {maker}, every role mean {mean:0.##} dev {dev:0.##}: {description}"); + } + + if (!apply) + { + Console.WriteLine($"\nDry run. Pass \"apply\" to author 3 designs and {products.Length} products."); + return 0; + } + + foreach (var design in new[] { gun, laser, flak }) CultRecordRefs.Validate(design); + db.Cache.Commit(batch => + { + var keys = new Dictionary(); + foreach (var design in new[] { gun, laser, flak }) keys[design] = batch.Upsert(typeof(WeaponItemData), design); + foreach (var (design, name, maker, description) in products) + { + var (mean, dev) = makers[maker]; + batch.Upsert(typeof(FactionProductData), new FactionProductData + { + Name = name, + Description = description, + Design = new CultRecordRef(keys[design]), + Manufacturer = db.Cache.RefOf(Maker(maker)), + Roles = design.Roles.Select(r => new ProductRole { Role = r.Name, Mean = mean, StandardDeviation = dev }).ToList(), + }); + } + }); + Console.WriteLine($"\nAuthored 3 designs and {products.Length} products in Aetheria.cc"); + return 0; + } } From 7330058991ba1835e715dd6a3afc720a6b70711d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 14:40:08 +0200 Subject: [PATCH 014/120] AetherDb: pirates-faction command authors the Pirates and eight emoji-named rebrands Dry run unless given 'apply'. Not yet built when committed. Co-Authored-By: Claude Sonnet 5.5 --- tools/AetherDb/Program.cs | 110 +++++++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 1 deletion(-) diff --git a/tools/AetherDb/Program.cs b/tools/AetherDb/Program.cs index c65f4c3f2..d1b488c7a 100644 --- a/tools/AetherDb/Program.cs +++ b/tools/AetherDb/Program.cs @@ -38,8 +38,9 @@ public static class Program case "targeting-catalog-6d": return TargetingCatalog6d(args.Contains("apply")); case "ship-authoring": return ShipAuthoringCommands.Run(args.Skip(1).ToArray()); case "field-kinds": return FieldKindsCatalog(args.Contains("apply")); + case "pirates-faction": return PiratesFaction(args.Contains("apply")); default: - Console.WriteLine("commands: census, factions, station-fit, hardpoint-fit, loadout [seed], save, settings, settings-dump, dangling [clear ]... [apply], shield-migrate [apply], brownout-migrate [apply], roles-migrate [apply], firing-arc-migrate [apply], targeting-catalog [apply], targeting-catalog-6c [apply], targeting-catalog-6d [apply], ship-authoring create|inspect|validate|compose, field-kinds [apply]"); + Console.WriteLine("commands: census, factions, station-fit, hardpoint-fit, loadout [seed], save, settings, settings-dump, dangling [clear ]... [apply], shield-migrate [apply], brownout-migrate [apply], roles-migrate [apply], firing-arc-migrate [apply], targeting-catalog [apply], targeting-catalog-6c [apply], targeting-catalog-6d [apply], ship-authoring create|inspect|validate|compose, field-kinds [apply], pirates-faction [apply]"); return 1; } } @@ -1505,4 +1506,111 @@ public static class Program Console.WriteLine("\nAuthored \"Asteroid\" in Aetheria.cc"); return 0; } + + // Aetheria release, cut pirates-record (rulings pirates-record-content, catalog-grows-generic-designs-branded-products): + // the Pirates faction and eight rebrands of other makers' designs, named in emoji. The Pirates own no design: their range + // is what they took, so every product points at a design someone else also makes. Each product carries one ProductRole per + // role its design declares, middling and inconsistent (mean .45, sd .22, against makers' .15). Names are stored as the emoji + // only, written here as \U escapes so no editor or shell mangles them; the dry run prints them raw and as code points. + // + // Dry run unless passed "apply", same contract as the other *-migrate commands. + private static int PiratesFaction(bool apply) + { + Console.OutputEncoding = new System.Text.UTF8Encoding(false); + var db = AetherDb.Open(catalogWritable: apply); + + var existing = db.Cache.GetAll().FirstOrDefault(f => f.Name == "Pirates"); + if (existing != null) + { + Console.WriteLine("A faction named \"Pirates\" already exists; nothing to do."); + return 0; + } + + CraftedItemData DesignByName(string name) + { + var matches = db.Cache.GetAll().Where(i => i.Name == name).ToArray(); + if (matches.Length != 1) throw new InvalidOperationException($"Expected exactly one design named \"{name}\", found {matches.Length}."); + return matches[0]; + } + + string CodePoints(string text) => string.Join(" ", text.EnumerateRunes().Select(r => $"U+{r.Value:X4}")); + + var nameFile = db.Cache.GetAll().FirstOrDefault(n => n.Name == "pleiades"); + if (nameFile == null) throw new InvalidOperationException("No name file named \"pleiades\"."); + + var others = db.Cache.GetAll().ToArray(); + var pirates = new Faction + { + Name = "Pirates", + ShortName = "Pirates", + Description = "A loose coalition of crews and hidden docks moving people and goods outside official control. No command, no common law: possession has a maintenance schedule.", + PrimaryColor = new float3(.42f, .40f, .37f), + SecondaryColor = new float3(1f, .45f, .05f), + GeonameFile = db.Cache.RefOf(nameFile), + InfluenceDistance = 3, + }; + foreach (var other in others) pirates.Allegiance[db.Cache.RefOf(other)] = 1f; + + var rebrands = new (string Design, string Name, string Description)[] + { + ("Autocannon", "\U0001FAF3\U0001F52B", "Serial number filed off. Still shoots."), + ("Earp", "\U0001FAA6\U0001F920", "The previous owner had no further use for it."), + ("FastBlast+-", "\U0001F9FE\U0001F6AB", "Warranty void where prohibited, which is everywhere."), + ("6k Shooter", "\U0001F3A8\U0001F648", "The logo underneath is somebody else's problem."), + ("scorched void policy", "\U0001F525", "Turns out we did start the fire after all."), + ("Targeting Computer", "\U0001F440\U0001F3AF", "Somebody else's eyes. They still work."), + ("Small Drive", "\U0001F511\U0001F3C3\U0001F4A8", "Hotwired. Do not ask about the ignition."), + ("Store-All Plus", "\U0001F4E6\U0001F92B", "Contents not as declared."), + }; + + var products = rebrands.Select(r => + { + var design = DesignByName(r.Design); + return (Design: design, r.Name, r.Description, + Roles: (design.Roles ?? new List()).Select(role => new ProductRole { Role = role.Name, Mean = .45f, StandardDeviation = .22f }).ToList()); + }).ToArray(); + + Console.WriteLine($"New faction \"{pirates.Name}\": influence {pirates.InfluenceDistance}, names \"{nameFile.Name}\", allegiance 1.0 to {pirates.Allegiance.Count} factions."); + Console.WriteLine($" {pirates.Description}"); + Console.WriteLine($"\nNew products ({products.Length}):"); + foreach (var (design, name, description, roles) in products) + { + var makers = db.Cache.GetAll() + .Where(p => p.Design.Key.Equals(db.Cache.RefOf(design).Key)) + .Select(p => db.Cache.Get(p.Manufacturer)?.ShortName).OrderBy(n => n, StringComparer.Ordinal); + Console.WriteLine($" {name} [{CodePoints(name)}]"); + Console.WriteLine($" design {design.GetType().Name} \"{design.Name}\" (made by {string.Join(", ", makers)}), roles [{string.Join(", ", roles.Select(r => r.Role))}]"); + Console.WriteLine($" \"{description}\""); + } + + CultRecordRefs.Validate(pirates); + foreach (var (design, name, description, roles) in products) + CultRecordRefs.Validate(new FactionProductData + { + Name = name, Description = description, Roles = roles, + Design = new CultRecordRef(db.Cache.RefOf(design).Key), + Manufacturer = db.Cache.RefOf(others[0]), + }); + + if (!apply) + { + Console.WriteLine($"\nDry run. Pass \"apply\" to land the Pirates faction and {products.Length} products."); + return 0; + } + + db.Cache.Commit(batch => + { + var piratesKey = batch.Upsert(typeof(Faction), pirates); + foreach (var (design, name, description, roles) in products) + batch.Upsert(typeof(FactionProductData), new FactionProductData + { + Name = name, Description = description, Roles = roles, + Design = new CultRecordRef(db.Cache.RefOf(design).Key), + Manufacturer = new CultRecordRef(piratesKey), + }); + }); + + Console.WriteLine($"\nLanded the Pirates faction and {products.Length} products in Aetheria.cc"); + return 0; + } } From bb175f2bc487de38d5b61e24f507377e7d36125b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 14:45:09 +0200 Subject: [PATCH 015/120] Catalog: pd-gear apply output, 3 designs and 11 products Applied with: AetherDb pd-gear apply (dry-run text below), on Yggdrasil against a copy of the base catalog. New designs: Point Defense Gun Ballistic 1x1 mass 40 price 80000 durability 60 modifiers RapidFire damage 1.5-4 count 1-1 range 300-550 cooldown 0.06-0.025 spread 1-0.3 velocity 1100-1600 energy 0.4-0.2 heat 40-15 visibility 400-150 ammo Ammo magazine 250 reload 3 tracking 40 roles [barrel, feed mechanism] "Close-in defence gun: a hose of light rounds." Point Defense Laser Energy 1x1 mass 30 price 110000 durability 30 modifiers None damage 5-12 count 1-1 range 400-750 cooldown 0.2-0.1 spread 0.05-0 velocity 0-0 energy 6-10 heat 300-150 visibility 300-100 ammo none magazine 1 reload 1 tracking 40 roles [focusing array, power coupling] "Close-in defence beam: quick, light, instant." Flak Gun Ballistic 1x2 mass 120 price 180000 durability 80 modifiers Cluster damage 30-70 count 6-8 range 350-800 cooldown 0.6-0.35 spread 4-2 velocity 900-1300 energy 1-0.5 heat 300-150 visibility 800-300 ammo Ammo magazine 40 reload 4 tracking 20 roles [barrel, feed mechanism] "Small flak gun: a short-range pellet volley." New products: Point Defense Gun -> "Waykeeper" by Lightsail, every role mean 0.55 dev 0.1: Freight first, questions later. Keeps a hauler's paint unscratched through a full salvo. Point Defense Gun -> "Rampart" by Zhestokost, every role mean 0.5 dev 0.08: A column does not dodge. It does not need to. Point Defense Gun -> "Swatter" by AU, every role mean 0.5 dev 0.12: Keeps the rocks and the rockets off your back while you work. Point Defense Gun -> "Catch-All" by R&D, every role mean 0.6 dev 0.15: Guaranteed to stop the missile!* *Missile defined as whatever it stops. Point Defense Laser -> "Glare" by Lucent, every role mean 0.6 dev 0.15: Every missile gets its moment in the spotlight. A short one. Point Defense Laser -> "Nightlight" by NiteLife, every role mean 0.55 dev 0.12: Lights out for anything with a warhead. Shares the bus with your blasters, so mind the meter. Point Defense Laser -> "Wren" by Finch, every role mean 0.65 dev 0.2: Small, quiet and quicker than it looks, most days. Point Defense Laser -> "Gnat" by Alakrita, every role mean 0.6 dev 0.14: Weighs less than your conscience and keeps the fast pass fast. Flak Gun -> "MFer" by Zhestokost, every role mean 0.5 dev 0.08: Our shrapnel turns your enemies into scrapnel. Flak Gun -> "Hailstorm" by AU, every role mean 0.5 dev 0.12: A wall of steel for crews who would rather not count missiles. Flak Gun -> "sky full of teeth" by DME, every role mean 0.55 dev 0.2: light it up/ shake it loose/ nothing gets through tonight Dry run. Pass "apply" to author 3 designs and 11 products. Replay after apply: Point-defence gear is already authored; nothing to do. Tests (EvasionTrackingTests): PdGearIsAuthored, PdGunsHaveHomes, PdGunHoldsASpeedDemon. Not yet run when committed; the next job reports them. Co-Authored-By: Claude Sonnet 5.5 --- GameData/Aetheria.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GameData/Aetheria.cc b/GameData/Aetheria.cc index 96135d923..eec46823c 100644 --- a/GameData/Aetheria.cc +++ b/GameData/Aetheria.cc @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63250cf21ae9abeec657f0603336bb11b4d50b66560903ef45a8cceb01a7ae25 -size 12703489 +oid sha256:0da74f4d54d1f171c063eb0a29adc914f1b7d7220cd7310fa6b43af71f525b2f +size 12710153 From 13d26df2d5d2fd7668865bed30735d79204834bd Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 14:45:25 +0200 Subject: [PATCH 016/120] Tests: PdGearIsAuthored, PdGunsHaveHomes, PdGunHoldsASpeedDemon Pins the authored PD designs (hardpoint, shape, Tracking 40/40/20, at least three products each, no maker twice, none by Miss Terri's), that Djinni and Longinus hardpoints take them, and that the PD gun follows a 150 m/s crosser at 200 m where ClearPath does not. Built and run in the next job. Co-Authored-By: Claude Sonnet 5.5 --- .../EvasionTrackingTests.cs | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/Aetheria.Shared.Tests/EvasionTrackingTests.cs b/tests/Aetheria.Shared.Tests/EvasionTrackingTests.cs index 08d886c57..c945e19ac 100644 --- a/tests/Aetheria.Shared.Tests/EvasionTrackingTests.cs +++ b/tests/Aetheria.Shared.Tests/EvasionTrackingTests.cs @@ -312,6 +312,73 @@ public sealed partial class RunStartTests } } + private WeaponItemData PdDesign(string name) => _cache.GetAll().Single(w => w.Name == name); + + // Data check, not a behaviour test: cut pd-gear's authored catalog (ruling catalog-grows-generic-designs-branded-products): + // three generic designs, each with several products, no maker twice on one design, and no brand on a design. + [Fact] + public void PdGearIsAuthored() + { + var expected = new (string design, HardpointType hardpoint, int width, int height, float tracking)[] + { + ("Point Defense Gun", HardpointType.Ballistic, 1, 1, 40f), + ("Point Defense Laser", HardpointType.Energy, 1, 1, 40f), + ("Flak Gun", HardpointType.Ballistic, 1, 2, 20f), + }; + var products = _cache.GetAll().ToArray(); + foreach (var (name, hardpoint, width, height, tracking) in expected) + { + var design = PdDesign(name); + Assert.Equal(hardpoint, design.HardpointType); + Assert.Equal(width, design.Shape.Width); + Assert.Equal(height, design.Shape.Height); + Assert.True(design.Shape.Cells.Cast().All(cell => cell), $"{name} occupies every cell of its shape"); + var rate = design.Behaviors.OfType().Single().Tracking; + Assert.Equal(tracking, rate.Min); + Assert.Equal(tracking, rate.Max); + + var key = _cache.RefOf(design).Key; + var sold = products.Where(p => p.Design.Key == key).ToArray(); + Assert.True(sold.Length >= 3, $"{name} has at least three products, not {sold.Length}"); + var makers = sold.Select(p => _cache.Get(p.Manufacturer)).ToArray(); + Assert.DoesNotContain(null, makers); + Assert.Equal(makers.Length, makers.Select(m => m.ShortName).Distinct().Count()); + Assert.DoesNotContain(makers, m => m.ShortName == "Miss Terri's"); + Assert.All(sold, p => Assert.NotEqual(design.Name, p.Name)); + Assert.All(sold, p => Assert.Equal(design.Roles.Select(r => r.Name).OrderBy(n => n), p.Roles.Select(r => r.Role).OrderBy(n => n))); + } + } + + // Data check: a design is offered only if some hull has a hardpoint that takes it (LoadoutGenerator.HasHome), so these + // guns are stocked and fitted only while a catalog hull takes them (body fact CG6). + [Fact] + public void PdGunsHaveHomes() + { + var djinni = _cache.GetAll().Single(h => h.Name == "Djinni"); + var longinus = _cache.GetAll().Single(h => h.Name == "Longinus"); + foreach (var name in new[] { "Point Defense Gun", "Flak Gun" }) + Assert.Contains(djinni.Hardpoints, hardpoint => hardpoint.Type == HardpointType.Ballistic && hardpoint.Takes(PdDesign(name))); + Assert.Contains(longinus.Hardpoints, hardpoint => hardpoint.Type == HardpointType.Energy && hardpoint.Takes(PdDesign("Point Defense Laser"))); + } + + // Ruling weapon-tracking-authored, in play: a PD gun follows a speed demon crossing close in, and the machine gun does not. + [Fact] + public void PdGunHoldsASpeedDemon() + { + var range = EvMountRange(40f, 3f); + EvCross(range, jinking: false, float2(150, 0)); + var shooter = range.Gunner; + var omega = FireControl.AngularVelocity(shooter, range.Target); + Assert.InRange(omega, degrees(150f / 200f) * .999f, degrees(150f / 200f) * 1.001f); + var gear = FireControl.Tracking(shooter) / _items.GameplaySettings.UnaidedTracking; + Assert.True(gear > 1.2f, $"fixture: the shooter's targeting gear tracks better than unaided ({gear})"); + float RateOf(string design) => PdDesign(design).Behaviors.OfType().Single().Tracking.Max * gear; + var pd = FireControl.PMount(omega, RateOf("Point Defense Gun")); + var clearPath = FireControl.PMount(omega, RateOf("ClearPath")); + Assert.True(pd >= .75f, $"the PD gun follows a speed demon: {pd}"); + Assert.True(clearPath <= .25f, $"the machine gun does not: {clearPath}"); + } + // Data check, not a behaviour test: ruling plight-shape, plight is one wide, two high and fits a Longinus Energy hardpoint. [Fact] public void PlightHasItsRuledShape() From 1f83235919f268e273060798b6fc982388ae62a0 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 14:49:58 +0200 Subject: [PATCH 017/120] Tests: compare record keys with Equals (the previous commit does not build) CultRecordKey has no == operator. 13d26df2 fails to compile for that reason. Co-Authored-By: Claude Sonnet 5.5 --- tests/Aetheria.Shared.Tests/EvasionTrackingTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Aetheria.Shared.Tests/EvasionTrackingTests.cs b/tests/Aetheria.Shared.Tests/EvasionTrackingTests.cs index c945e19ac..f3411704f 100644 --- a/tests/Aetheria.Shared.Tests/EvasionTrackingTests.cs +++ b/tests/Aetheria.Shared.Tests/EvasionTrackingTests.cs @@ -338,7 +338,7 @@ public sealed partial class RunStartTests Assert.Equal(tracking, rate.Max); var key = _cache.RefOf(design).Key; - var sold = products.Where(p => p.Design.Key == key).ToArray(); + var sold = products.Where(p => p.Design.Key.Equals(key)).ToArray(); Assert.True(sold.Length >= 3, $"{name} has at least three products, not {sold.Length}"); var makers = sold.Select(p => _cache.Get(p.Manufacturer)).ToArray(); Assert.DoesNotContain(null, makers); From e16288bfcf88dc37a7b50f6170682d91f3398936 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 14:50:48 +0200 Subject: [PATCH 018/120] Pirates record: the demo tests read the shipped faction; tests pin its eight emoji-named products Deletes the AddPirates stand-in and its 21 calls. DemoTerminus_missing_cast_refused removes the shipped record from its scratch copy first. LoadoutGenerator.HasHome is public so the catalog test asks production's own home rule. These tests are red on a catalog without the Pirates: the catalog commit (pirates-faction apply) follows separately. Co-Authored-By: Claude Sonnet 5.5 --- .../Scripts/ServerShared/LoadoutGenerator.cs | 2 +- .../DemoTerminusTests.cs | 31 +++------ .../PiratesRecordTests.cs | 67 +++++++++++++++++++ tests/Aetheria.Shared.Tests/RunGoalTests.cs | 10 --- tests/Aetheria.Shared.Tests/ScenarioTests.cs | 2 - 5 files changed, 76 insertions(+), 36 deletions(-) create mode 100644 tests/Aetheria.Shared.Tests/PiratesRecordTests.cs diff --git a/Assets/Scripts/ServerShared/LoadoutGenerator.cs b/Assets/Scripts/ServerShared/LoadoutGenerator.cs index 42f43033e..f2fd1c955 100644 --- a/Assets/Scripts/ServerShared/LoadoutGenerator.cs +++ b/Assets/Scripts/ServerShared/LoadoutGenerator.cs @@ -198,7 +198,7 @@ public class LoadoutGenerator // Hardpoint gear that no hull in the catalog can mount has no home (operator, 2026-09-30: "we'll need to author a // bunch more hulls before all the gear variety in the game has a home"): generation never offers it, so no station // stocks it. It gains one the moment a hull with a hardpoint that takes it exists. Tool gear goes in any interior. - private static bool HasHome(EquippableItemData design, HullData[] hulls) => + public static bool HasHome(EquippableItemData design, HullData[] hulls) => design.HardpointType == HardpointType.Tool || design.HardpointType == HardpointType.Hull || hulls.Any(hull => hull.Hardpoints.Any(hardpoint => hardpoint.Takes(design))); diff --git a/tests/Aetheria.Shared.Tests/DemoTerminusTests.cs b/tests/Aetheria.Shared.Tests/DemoTerminusTests.cs index 577296ac3..1f282f48a 100644 --- a/tests/Aetheria.Shared.Tests/DemoTerminusTests.cs +++ b/tests/Aetheria.Shared.Tests/DemoTerminusTests.cs @@ -9,30 +9,16 @@ using CultMath; using Xunit; // The demo scenario (aetheria-release, cut demo-galaxy): a prelude galaxy of a fixed cast whose antagonist's home is the -// boss zone and the exit. The shipped catalog holds no Pirates faction yet, so each test that needs one adds it to its -// scratch copy of the catalog. +// boss zone and the exit. The cast, the Pirates included, is in the shipped catalog. public sealed partial class RunStartTests { private static readonly uint[] DemoSeeds = { 1, 2, 3, 4, 5 }; - // The catalog record the demo's protagonist needs, until the shipped catalog has one. Zone names need a name file, - // and the entrance's station needs a faction whose allegiance reaches some manufacturer, so it reaches every one. - private void AddPirates() - { - var pirates = new Faction - { - Name = "Pirates", ShortName = "Pirates", GeonameFile = _cache.RefOf(_cache.GetAll().First()) - }; - foreach (var faction in _cache.GetAll().ToArray()) pirates.Allegiance[_cache.RefOf(faction)] = 1; - _cache.Upsert(pirates); - } - private Galaxy DemoGalaxy(uint seed) => RunStart.Generate(new DemoTerminus(), Inputs(() => seed)); [Fact] public void DemoTerminus_fixed_cast() { - AddPirates(); foreach (var seed in DemoSeeds) Assert.Equal(new[] { "Pirates", "Zhestokost", "Lucent Media", "Aeronautics Unlimited" }, DemoGalaxy(seed).Factions.Select(faction => faction.Name)); @@ -41,7 +27,6 @@ public sealed partial class RunStartTests [Fact] public void DemoTerminus_gate_at_antagonist_home() { - AddPirates(); foreach (var seed in DemoSeeds) { var galaxy = DemoGalaxy(seed); @@ -60,7 +45,6 @@ public sealed partial class RunStartTests [Fact] public void DemoTerminus_standing_survives_continue() { - AddPirates(); var (galaxy, arena, staged, failures) = Launch(new DemoTerminus(), Inputs(() => 1)); Assert.True(failures.Count == 0, string.Join("; ", failures)); galaxy.Entrance.Contents = arena; @@ -83,11 +67,16 @@ public sealed partial class RunStartTests restored.BossZones.Select(boss => (boss.Key.Name, Array.IndexOf(restored.Zones, boss.Value)))); } - // On the shipped catalog, which has no Pirates, the run does not start: the galaxy refuses, naming the cast field, - // and the scenario never reaches staging. + // A catalog without the Pirates (the shipped record and its products removed from this scratch copy) does not start the + // run: the galaxy refuses, naming the cast field, and the scenario never reaches staging. [Fact] public void DemoTerminus_missing_cast_refused() { + var pirates = _cache.GetAll().Single(faction => faction.Name == "Pirates"); + var piratesRef = _cache.RefOf(pirates); + foreach (var product in _cache.GetAll().Where(product => product.Manufacturer.Equals(piratesRef)).ToArray()) + Assert.True(_cache.Remove(_cache.RefOf(product).Key)); + Assert.True(_cache.Remove(piratesRef.Key)); Assert.DoesNotContain(_cache.GetAll(), faction => faction.Name.StartsWith("Pirates", StringComparison.Ordinal)); var refusal = Assert.Throws(() => DemoGalaxy(1)); Assert.Contains("Pirates", refusal.Message); @@ -108,7 +97,6 @@ public sealed partial class RunStartTests [Fact] public void DemoTerminus_player_flies_a_non_pirates_brand_hull_the_longinus() { - AddPirates(); foreach (var seed in DemoSeeds) { var (galaxy, _, staged, failures) = Launch(new DemoTerminus(), Inputs(() => seed)); @@ -130,7 +118,6 @@ public sealed partial class RunStartTests [Fact] public void DemoTerminus_player_carries_the_longinus_default_fit() { - AddPirates(); string Fit(Entity ship) => string.Join(";", ship.Equipment .Select(item => $"{item.Data.Name}@{item.Position}/{item.EquippableItem.Rotation}") .OrderBy(entry => entry, StringComparer.Ordinal)); @@ -151,7 +138,6 @@ public sealed partial class RunStartTests [Fact] public void DemoTerminus_region_is_sixty_four_zones_linked_at_half_density() { - AddPirates(); string Links(Galaxy g) => string.Join(";", g.Zones.Select(zone => string.Join(",", zone.AdjacentZones.Select(adjacent => Array.IndexOf(g.Zones, adjacent)).OrderBy(index => index)))); Galaxy Reference(uint seed, float density) @@ -177,7 +163,6 @@ public sealed partial class RunStartTests [Fact] public void ACastNamingNoQuestFactionPlacesNoQuestZone() { - AddPirates(); var galaxy = DemoGalaxy(1); Assert.Equal(4, galaxy.HomeZones.Count); Assert.Equal(4, galaxy.Factions.Length); diff --git a/tests/Aetheria.Shared.Tests/PiratesRecordTests.cs b/tests/Aetheria.Shared.Tests/PiratesRecordTests.cs new file mode 100644 index 000000000..7a865d1e4 --- /dev/null +++ b/tests/Aetheria.Shared.Tests/PiratesRecordTests.cs @@ -0,0 +1,67 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +using System; +using System.Linq; +using Xunit; + +// The Pirates record (aetheria-release, cut pirates-record): the shipped catalog's faction of rebrands, named in emoji. +public sealed partial class RunStartTests +{ + private Faction ShippedPirates() => _cache.GetAll().Single(faction => faction.Name == "Pirates"); + + private FactionProductData[] PiratesProducts() + { + var pirates = _cache.RefOf(ShippedPirates()); + return _cache.GetAll().Where(product => product.Manufacturer.Equals(pirates)).ToArray(); + } + + // The demo's allied faction exists as catalog data with eight generatable rebrands of other makers' designs. + [Fact] + public void PiratesAreInTheShippedCatalog() + { + var pirates = ShippedPirates(); + var others = _cache.GetAll().Where(faction => faction != pirates).Select(faction => _cache.RefOf(faction)).ToArray(); + Assert.NotEmpty(others); + Assert.All(others, other => Assert.True(pirates.Allegiance.ContainsKey(other))); + Assert.NotNull(_cache.Get(pirates.GeonameFile)); + + var products = PiratesProducts(); + Assert.Equal(8, products.Length); + var hulls = _cache.GetAll().ToArray(); + foreach (var product in products) + { + var design = _cache.Get(product.Design) as EquippableItemData; + Assert.NotNull(design); + Assert.True(LoadoutGenerator.HasHome(design, hulls), $"{design.Name} has no home"); + Assert.True(design.Price > 0, $"{design.Name} has no price"); + Assert.Contains(_cache.GetAll(), other => + other.Design.Key.Equals(product.Design.Key) && !other.Manufacturer.Equals(product.Manufacturer)); + } + } + + // The Pirates' products are named in emoji and nothing else: no ASCII letter or digit survives in a name. + [Fact] + public void PiratesProductsAreNamedInEmoji() + { + foreach (var product in PiratesProducts()) + { + Assert.False(string.IsNullOrEmpty(product.Name)); + Assert.DoesNotContain(product.Name, character => character < 128 && char.IsLetterOrDigit(character)); + Assert.Contains(product.Name.EnumerateRunes(), rune => rune.Value >= 0x1F000); + } + } + + // Brand finds a lot's product by (maker, design), so a maker may sell one product per design. + [Fact] + public void NoMakerSellsTwoProductsOfOneDesign() + { + var twice = _cache.GetAll() + .GroupBy(product => (Maker: product.Manufacturer.Key.Value, Design: product.Design.Key.Value)) + .Where(group => group.Count() > 1) + .Select(group => string.Join(", ", group.Select(product => product.Name))) + .ToArray(); + Assert.Empty(twice); + } +} diff --git a/tests/Aetheria.Shared.Tests/RunGoalTests.cs b/tests/Aetheria.Shared.Tests/RunGoalTests.cs index da7ff39ba..afdaa658a 100644 --- a/tests/Aetheria.Shared.Tests/RunGoalTests.cs +++ b/tests/Aetheria.Shared.Tests/RunGoalTests.cs @@ -41,7 +41,6 @@ public sealed partial class RunStartTests [Fact] public void Boss_spawns_once_in_boss_zone() { - AddPirates(); foreach (var seed in DemoSeeds.Take(3)) { var galaxy = DemoGalaxy(seed); @@ -65,7 +64,6 @@ public sealed partial class RunStartTests [Fact] public void Boss_spawns_in_a_zone_that_is_not_ambient() { - AddPirates(); var galaxy = DemoGalaxy(1); var pack = BossZonePack(galaxy, _items, galaxy.Exit, ambient: false); Assert.Single(pack.Entities, entity => entity.Boss); @@ -75,7 +73,6 @@ public sealed partial class RunStartTests [Fact] public void Boss_hull_from_BossHull_else_fallback() { - AddPirates(); var galaxy = DemoGalaxy(1); var antagonist = Zhestokost(galaxy); var hulls = SoldShipHulls().OrderByDescending(hull => hull.Price).ThenBy(hull => hull.Name, StringComparer.Ordinal).ToArray(); @@ -103,7 +100,6 @@ public sealed partial class RunStartTests [Fact] public void Boss_hull_nobody_sells_falls_back() { - AddPirates(); var galaxy = DemoGalaxy(1); var antagonist = Zhestokost(galaxy); var sold = SoldShipHulls(); @@ -121,7 +117,6 @@ public sealed partial class RunStartTests [Fact] public void Exit_sealed_until_boss_dies() { - AddPirates(); var galaxy = DemoGalaxy(1); var exit = ZoneOf(galaxy, galaxy.Exit); var boss = Assert.Single(exit.Entities, entity => entity.IsBoss); @@ -143,7 +138,6 @@ public sealed partial class RunStartTests [Fact] public void Boss_mark_survives_continue() { - AddPirates(); var (galaxy, arena, staged, failures) = Launch(new DemoTerminus(), Inputs(() => 1)); Assert.True(failures.Count == 0, string.Join("; ", failures)); galaxy.Entrance.Contents = arena; @@ -185,7 +179,6 @@ public sealed partial class RunStartTests [Fact] public void Exit_sealed_and_logged_when_the_boss_cannot_be_generated() { - AddPirates(); var galaxy = DemoGalaxy(1); galaxy.BossZones.Clear(); var log = new List(); @@ -201,7 +194,6 @@ public sealed partial class RunStartTests [Fact] public void Exit_open_after_save_of_a_slain_boss() { - AddPirates(); var galaxy = DemoGalaxy(1); var exit = ZoneOf(galaxy, galaxy.Exit); Assert.Single(exit.Entities, entity => entity.IsBoss).HeatstrokeDeath.OnNext(Unit.Default); @@ -215,7 +207,6 @@ public sealed partial class RunStartTests [Fact] public void Exit_gate_shares_no_point_with_a_wormhole() { - AddPirates(); var ratio = float.Parse(File.ReadLines(Path.Combine(FindRepoRoot(), "Assets", "Resources", "Settings.asset")) .Select(line => Regex.Match(line, @"^ WormholeDistanceRatio: (.+)$")).First(match => match.Success).Groups[1].Value.Trim(), System.Globalization.CultureInfo.InvariantCulture); @@ -244,7 +235,6 @@ public sealed partial class RunStartTests [InlineData(new[] { 0.0, 45.0, 90.0, 135.0, 180.0, -135.0, -90.0, -45.0 })] public void Exit_gate_sits_in_the_widest_gap_at_the_wormhole_radius(double[] neighbourDegrees) { - AddPirates(); var galaxy = DemoGalaxy(1); var zone = ZoneOf(galaxy, galaxy.Exit); const float ratio = 0.75f; diff --git a/tests/Aetheria.Shared.Tests/ScenarioTests.cs b/tests/Aetheria.Shared.Tests/ScenarioTests.cs index 51554904e..a9e3b5678 100644 --- a/tests/Aetheria.Shared.Tests/ScenarioTests.cs +++ b/tests/Aetheria.Shared.Tests/ScenarioTests.cs @@ -96,7 +96,6 @@ public sealed partial class RunStartTests [Fact] public void EveryScenarioStages() { - AddPirates(); Assert.Equal(EveryScenario.Count(), EveryScenario.Select(scenario => scenario.Name).Distinct().Count()); foreach (var scenario in EveryScenario) { @@ -523,7 +522,6 @@ public sealed partial class RunStartTests [Fact] public void EveryUnsoldDesignIsAScenarioTestDesign() { - AddPirates(); var runs = EveryScenario.Select(scenario => Launch(scenario, Inputs(() => GalaxySeed)).staged).ToList(); Assert.Empty(UnsoldAndUnplaced(runs)); From 2c7c20607f9661b00747197d196b1c4117ef76e1 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 16:01:01 +0200 Subject: [PATCH 019/120] Tests: pin the PD designs' authored stats, the no-maker-name rule, and TrackingRate through the catalog design PdGearStatsAreAuthored reads the shipped catalog and pins every authored value of the three designs against the spec derivation (ammo draw, velocity, visibility, range, pellet count, heat, descriptions and the rest). PdDesignsCarryNoMakerName enumerates the makers from the catalog and rejects any in a PD design's name or description. PdGunHoldsASpeedDemon now sets the catalog design's authored Tracking on the fixture weapon and asks FireControl.TrackingRate, instead of recomputing it. Co-Authored-By: Claude Opus 5.5 --- .../EvasionTrackingTests.cs | 76 ++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/tests/Aetheria.Shared.Tests/EvasionTrackingTests.cs b/tests/Aetheria.Shared.Tests/EvasionTrackingTests.cs index f3411704f..00b0d9073 100644 --- a/tests/Aetheria.Shared.Tests/EvasionTrackingTests.cs +++ b/tests/Aetheria.Shared.Tests/EvasionTrackingTests.cs @@ -349,6 +349,75 @@ public sealed partial class RunStartTests } } + private static void PdStat(string design, string stat, PerformanceStat actual, float min, float max) => + Assert.True(actual.Min == min && actual.Max == max, $"{design} {stat} is {min}-{max}, not {actual.Min}-{actual.Max}"); + + // Data check: the authored numbers of the three designs, read from the shipped catalog through the production load path + // (addenda "Catalog growth: generic designs, branded products", the Derivation of the PD stats). Each is a lever the + // operator tunes, so each is pinned; the ammunition is the commodity Autocannon's rounds are. + [Fact] + public void PdGearStatsAreAuthored() + { + var ammunition = PdDesign("Autocannon").Behaviors.OfType().Single().AmmoType.Key; + var expected = new (string name, float mass, int price, float durability, WeaponModifiers modifiers, (float min, float max) damage, + (float min, float max) count, (float min, float max) range, (float min, float max) cooldown, (float min, float max) spread, + (float min, float max) velocity, (float min, float max) energy, (float min, float max) heat, (float min, float max) visibility, + bool ammo, int magazine, float reload)[] + { + ("Point Defense Gun", 40f, 80000, 60f, WeaponModifiers.RapidFire, (1.5f, 4f), (1f, 1f), (300f, 550f), (.06f, .025f), (1f, .3f), + (1100f, 1600f), (.4f, .2f), (40f, 15f), (400f, 150f), true, 250, 3f), + ("Point Defense Laser", 30f, 110000, 30f, PdDesign("Spectra").WeaponModifiers, (5f, 12f), (1f, 1f), (400f, 750f), (.2f, .1f), (.05f, 0f), + (0f, 0f), (6f, 10f), (300f, 150f), (300f, 100f), false, PdDesign("Spectra").Behaviors.OfType().Single().MagazineSize, PdDesign("Spectra").Behaviors.OfType().Single().ReloadTime), + ("Flak Gun", 120f, 180000, 80f, WeaponModifiers.Cluster, (30f, 70f), (6f, 8f), (350f, 800f), (.6f, .35f), (4f, 2f), + (900f, 1300f), (1f, .5f), (300f, 150f), (800f, 300f), true, 40, 4f), + }; + foreach (var e in expected) + { + var design = PdDesign(e.name); + var data = design.Behaviors.OfType().Single(); + Assert.Equal(e.mass, design.Mass); + Assert.Equal(e.price, design.Price); + Assert.Equal(e.durability, design.Durability); + Assert.Equal(e.modifiers, design.WeaponModifiers); + PdStat(e.name, "damage", data.Damage, e.damage.min, e.damage.max); + PdStat(e.name, "count", data.Count, e.count.min, e.count.max); + PdStat(e.name, "range", data.Range, e.range.min, e.range.max); + PdStat(e.name, "min range", data.MinRange, 0f, 0f); + PdStat(e.name, "cooldown", data.Cooldown, e.cooldown.min, e.cooldown.max); + PdStat(e.name, "spread", data.Spread, e.spread.min, e.spread.max); + PdStat(e.name, "velocity", data.Velocity, e.velocity.min, e.velocity.max); + PdStat(e.name, "energy", data.Energy, e.energy.min, e.energy.max); + PdStat(e.name, "heat", data.Heat, e.heat.min, e.heat.max); + PdStat(e.name, "visibility", data.Visibility, e.visibility.min, e.visibility.max); + Assert.Equal(e.magazine, data.MagazineSize); + Assert.Equal(e.reload, data.ReloadTime); + if (e.ammo) Assert.True(data.AmmoType.Key.Equals(ammunition), $"{e.name} draws the ammunition commodity"); + else Assert.False(data.AmmoType.IsSet(), $"{e.name} draws no ammunition"); + } + Assert.Equal("Close-in defence gun: a hose of light rounds.", PdDesign("Point Defense Gun").Description); + Assert.Equal("Close-in defence beam: quick, light, instant.", PdDesign("Point Defense Laser").Description); + Assert.Equal("Small flak gun: a short-range pellet volley.", PdDesign("Flak Gun").Description); + } + + // Rule test (ruling catalog-grows-generic-designs-branded-products): branding lives in the products, so no PD design's + // name or description carries any maker's name or short name. The makers are enumerated from the catalog. + [Fact] + public void PdDesignsCarryNoMakerName() + { + var makers = _cache.GetAll().SelectMany(f => new[] { f.Name, f.ShortName }).Where(n => !string.IsNullOrWhiteSpace(n)).Distinct().ToArray(); + Assert.True(makers.Length > 10, $"fixture: the catalog lists its makers ({makers.Length})"); + foreach (var name in new[] { "Point Defense Gun", "Point Defense Laser", "Flak Gun" }) + { + var design = PdDesign(name); + foreach (var maker in makers) + { + var word = new System.Text.RegularExpressions.Regex($@"(? 1.2f, $"fixture: the shooter's targeting gear tracks better than unaided ({gear})"); - float RateOf(string design) => PdDesign(design).Behaviors.OfType().Single().Tracking.Max * gear; + // The rate the production path reads: the catalog design's authored Tracking on the weapon, times the shooter's gear. + float RateOf(string design) + { + EvSetRate(range.Gun, range.Gunner, PdDesign(design).Behaviors.OfType().Single().Tracking.Max); + return FireControl.TrackingRate(range.Gun, range.Gunner); + } var pd = FireControl.PMount(omega, RateOf("Point Defense Gun")); var clearPath = FireControl.PMount(omega, RateOf("ClearPath")); Assert.True(pd >= .75f, $"the PD gun follows a speed demon: {pd}"); From 32b14b966b0d0fc3cdcbfdc14078551d5db8683e Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 16:12:48 +0200 Subject: [PATCH 020/120] Pin ManufacturerPreference: neutral without galaxy or faction, zero outside allegiance, table-bound distance Kills the five Stryker survivors of the first pass on LoadoutGenerator.cs availability and preference. Tests only; builds against the prior commit. Co-Authored-By: Claude Sonnet 5.5 --- .../RunStartGearTests.cs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/Aetheria.Shared.Tests/RunStartGearTests.cs b/tests/Aetheria.Shared.Tests/RunStartGearTests.cs index 800081852..3b5766dcd 100644 --- a/tests/Aetheria.Shared.Tests/RunStartGearTests.cs +++ b/tests/Aetheria.Shared.Tests/RunStartGearTests.cs @@ -89,4 +89,46 @@ public sealed partial class RunStartTests Assert.NotEmpty(present); Assert.All(present, product => Assert.False(generator.IsAvailable(product), product.Name)); } + + // Preference only weighs by distance when there is a galaxy and a faction to weigh for. + [Fact] + public void PreferenceIsNeutralWithoutAGalaxyOrAFaction() + { + var galaxy = MainSectorGalaxy(); + var (product, absent) = AbsentMaker("AbsentNeutral"); + var faction = galaxy.Factions.First(); + var noGalaxy = new LoadoutGenerator(ref _items.Random, _items, null, null, faction, .5f); + var noFaction = new LoadoutGenerator(ref _items.Random, _items, galaxy, galaxy.Entrance, null, .5f); + Assert.Equal(1f, noGalaxy.ManufacturerPreference(product.Manufacturer)); + Assert.Equal(1f, noFaction.ManufacturerPreference(product.Manufacturer)); + } + + // A maker the generator's faction does not name weighs nothing, present or not. + [Fact] + public void AMakerOutsideAllegianceWeighsNothing() + { + var galaxy = MainSectorGalaxy(); + var (product, _) = AbsentMaker("AbsentUnnamed"); + var generator = GeneratorFor(galaxy, "Hermit"); + var stranger = galaxy.Factions.First(); + Assert.Equal(0f, generator.ManufacturerPreference(product.Manufacturer)); + Assert.Equal(0f, generator.ManufacturerPreference(_cache.RefOf(stranger))); + } + + // The distance table decides: a present maker whose home the table does not reach adds no distance, an absent + // maker is one jump past the table's farthest zone, and an empty table adds none either way. + [Fact] + public void ADistanceTableOnlyDistancesTheMakersItNames() + { + var galaxy = MainSectorGalaxy(); + var (product, absent) = AbsentMaker("AbsentOffTable"); + var present = galaxy.Factions.First(f => galaxy.HomeZones.TryGetValue(f, out var home) && home != galaxy.Entrance); + var friend = GeneratorFor(galaxy, "Ally", absent, present); + var table = new GalaxyZone { Distance = new Dictionary { { galaxy.Entrance, 0 } } }; + var generator = new LoadoutGenerator(ref _items.Random, _items, galaxy, table, friend.Faction, .5f); + Assert.Equal(1f, generator.ManufacturerPreference(_cache.RefOf(present))); // home not in the table: distance 0 + Assert.Equal(.5f, generator.ManufacturerPreference(product.Manufacturer)); // absent: 0 + 1 + var empty = new LoadoutGenerator(ref _items.Random, _items, galaxy, new GalaxyZone { Distance = new Dictionary() }, friend.Faction, .5f); + Assert.Equal(1f, empty.ManufacturerPreference(product.Manufacturer)); + } } From 978632bc26545af3daf5e418fc95dab0bba4c60b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 16:45:56 +0200 Subject: [PATCH 021/120] A lot records its product: Lot.Product owns an item's brand, Brand reads it Brand no longer re-derives the product from maker and design (key-first pick); CreateLot(product) is the one writer of Lot.Product. The census duplicate-brand block and the key-order tie-break test go with the derivation they policed. Unverified at this commit; build and tests follow. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/ItemManager.cs | 15 ++++--- Assets/Scripts/ServerShared/Provenance.cs | 8 +++- tests/Aetheria.Shared.Tests/ItemTitleTests.cs | 34 +++++++++++++-- tests/Aetheria.Shared.Tests/LoadoutTests.cs | 28 +------------ tests/Aetheria.Shared.Tests/RunSaveTests.cs | 42 +++++++++++++++++++ tools/AetherDb/Program.cs | 18 +------- 6 files changed, 89 insertions(+), 56 deletions(-) diff --git a/Assets/Scripts/ServerShared/ItemManager.cs b/Assets/Scripts/ServerShared/ItemManager.cs index bebfb5431..d47c6aaf8 100644 --- a/Assets/Scripts/ServerShared/ItemManager.cs +++ b/Assets/Scripts/ServerShared/ItemManager.cs @@ -140,6 +140,7 @@ public class ItemManager { Design = ItemData.RefOf(design), Origin = new Attributed { Faction = product.Manufacturer }, + Product = ItemData.RefOf(product), Quality = RollQuality(), Roles = new List() }; @@ -200,9 +201,10 @@ public class ItemManager return CreateInstance(CreateLot(product)); } - // Who made an item and what it is branded as, derived from its lot's provenance. Null maker (an unset faction, - // or an Extracted lot with none) means no brand at all; a set maker with no matching product still shows the - // maker, with no product name or flavour text. + // Who made an item and what it is branded as. The maker derives from its lot's provenance and the product is the + // one its lot records; nothing re-derives it. Null maker (an unset faction, or an Extracted lot with none) means + // no brand at all; a set maker whose lot records no product (or one the catalog no longer holds) still shows + // the maker, with no product name or flavour text. public (Faction Maker, FactionProductData Product) Brand(CraftedItemInstance item) { var lot = GetLot(item); @@ -214,14 +216,11 @@ public class ItemManager }; if (!maker.IsSet()) return (null, null); - var product = ItemData.GetAll() - .Where(p => p.Manufacturer.Key.Equals(maker.Key) && p.Design.Key.Equals(item.Data.Key)) - .OrderBy(p => ItemData.RefOf(p).Key.Value, StringComparer.Ordinal) - .FirstOrDefault(); + var product = lot.Product.IsSet() ? ItemData.Get(lot.Product) : null; return (ItemData.Get(maker), product); } - // What the player reads an item as: its product's name when its lot's maker has a product of this design, + // What the player reads an item as: its lot's product name when it records one, // otherwise the design's own name (unbranded, console-given and extracted lots, and commodities). public string Title(ItemInstance item) => (item is CraftedItemInstance crafted ? Brand(crafted).Product?.Name : null) ?? GetData(item).Name; diff --git a/Assets/Scripts/ServerShared/Provenance.cs b/Assets/Scripts/ServerShared/Provenance.cs index b04a4aeeb..80e8e31f0 100644 --- a/Assets/Scripts/ServerShared/Provenance.cs +++ b/Assets/Scripts/ServerShared/Provenance.cs @@ -58,8 +58,8 @@ public class ProvenanceLedger } } -// One lot: the design it built, who made it and from what, its rolled workmanship, and how well each design role -// was filled. Immutable once minted; instances carry a LotId, never a copy. +// One lot: the design it built, who made it and from what, its rolled workmanship, how well each design role +// was filled, and the branded product it was made as. Immutable once minted; instances carry a LotId, never a copy. [MessagePackObject] public class Lot { @@ -68,6 +68,10 @@ public class Lot [Key(2)] public float Quality; [Key(3)] public List Roles; + // The product the lot was made as: the one owner of which product names its units. Unset for unbranded lots + // (console give, presets, extracted), which brand their maker only. + [Key(4)] public CultRecordRef Product; + // The quality a stat reads for one of the design's roles. An unnamed role, a design without roles, and a lot // minted before roles existed all fall back to the lot's own workmanship. public float QualityForRole(string role) diff --git a/tests/Aetheria.Shared.Tests/ItemTitleTests.cs b/tests/Aetheria.Shared.Tests/ItemTitleTests.cs index 3df7484cc..d6c57ae2d 100644 --- a/tests/Aetheria.Shared.Tests/ItemTitleTests.cs +++ b/tests/Aetheria.Shared.Tests/ItemTitleTests.cs @@ -6,13 +6,15 @@ using GameCult.Caching; using Xunit; // Items are titled by their product (ruling catalog-grows-generic-designs-branded-products): ItemManager.Title reads -// Brand, so the lot's maker picks which of a design's products names the unit, and nothing else carries a name. +// Brand, so the lot's own product names the unit (a maker may sell several products of one design), and nothing +// else carries a name. public sealed class ItemTitleTests : IDisposable { private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-itemtitle-" + Guid.NewGuid().ToString("N")); private readonly CultCache _cache; private readonly ItemManager _items; - private readonly FactionProductData _panopticon, _clapBack, _spiceBlend; + private readonly FactionProductData _panopticon, _zulu, _clapBack, _spiceBlend; + private readonly CultRecordRef _finch; private readonly CultRecordRef _stranger; private readonly CompoundCommodityData _spice; private readonly GearData _array; @@ -25,13 +27,16 @@ public sealed class ItemTitleTests : IDisposable _array = new GearData { Name = "Array", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Price = 10, Durability = 5 }; _cache.Upsert(_array); _cache.Upsert(new SimpleCommodityData { Name = "Ore", Shape = new Shape(), Price = 7, MaxStack = 100 }); - var finch = _cache.Upsert(new Faction { Name = "Finch", ShortName = "FIN" }); + var finch = _finch = _cache.Upsert(new Faction { Name = "Finch", ShortName = "FIN" }); var lucent = _cache.Upsert(new Faction { Name = "Lucent", ShortName = "LUC" }); _stranger = _cache.Upsert(new Faction { Name = "Stranger", ShortName = "STR" }); _spice = new CompoundCommodityData { Name = "Spice", Shape = new Shape(), Price = 3 }; _cache.Upsert(_spice); var design = _cache.RefOf(_cache.GetByName("Array")); _panopticon = new FactionProductData { Name = "Panopticon Prime", Design = design, Manufacturer = finch }; + // Finch's second Array product, keyed to sort after the first: a key-order lookup would hand its units the first. + _zulu = new FactionProductData { Name = "Zulu Prime", Design = design, Manufacturer = finch }; + _cache.Commit(batch => batch.Upsert(typeof(FactionProductData), _zulu, new CultRecordKey("array-finch-zulu"))); _clapBack = new FactionProductData { Name = "ClapBack Ultra", Design = design, Manufacturer = lucent }; _cache.Commit(batch => batch.Upsert(typeof(FactionProductData), _panopticon, new CultRecordKey("array-finch"))); _cache.Commit(batch => batch.Upsert(typeof(FactionProductData), _clapBack, new CultRecordKey("array-lucent"))); @@ -64,6 +69,29 @@ public sealed class ItemTitleTests : IDisposable Assert.Equal("ClapBack Ultra", _items.Title(lucent)); } + [Fact] + public void A_makers_second_product_titles_and_brands_as_itself() + { + var zulu = (CraftedItemInstance) _items.CreateInstance(_items.CreateLot(_zulu)); + Assert.Equal("Zulu Prime", _items.Title(zulu)); + var (maker, product) = _items.Brand(zulu); + Assert.Equal("Finch", maker.Name); + Assert.Equal("Zulu Prime", product.Name); + + var panopticon = _items.CreateInstance(_items.CreateLot(_panopticon)); + Assert.Equal("Panopticon Prime", _items.Title(panopticon)); + } + + [Fact] + public void A_lot_without_a_product_brands_its_maker_only() + { + var unit = (CraftedItemInstance) _items.CreateInstance(_items.CreateLot(_array, _finch, .5f)); + var (maker, product) = _items.Brand(unit); + Assert.Equal("Finch", maker.Name); + Assert.Null(product); + Assert.Equal("Array", _items.Title(unit)); + } + [Fact] public void Unbranded_units_are_titled_by_design() { diff --git a/tests/Aetheria.Shared.Tests/LoadoutTests.cs b/tests/Aetheria.Shared.Tests/LoadoutTests.cs index 0807dd234..1f2c9c842 100644 --- a/tests/Aetheria.Shared.Tests/LoadoutTests.cs +++ b/tests/Aetheria.Shared.Tests/LoadoutTests.cs @@ -313,8 +313,8 @@ public sealed class LoadoutTests : IDisposable } // Two new makers, each with one Lamp product, upserted into the live cache so its enumeration order (z before a) - // disagrees with record-key order; a reopened snapshot could already hand them back sorted. Brand() requires at - // most one product per (maker, design), so each gets its own maker rather than sharing "Maker". + // disagrees with record-key order; a reopened snapshot could already hand them back sorted. Each gets its own + // maker rather than sharing "Maker". [Fact] public void FirstAvailableProductInKeyOrderBuildsTheSlot() { @@ -372,30 +372,6 @@ public sealed class LoadoutTests : IDisposable Assert.Equal("slot 0,0: no available product of Lamp", Assert.Single(failures)); } - // Census's duplicate (maker, design) detector exists precisely because this should never happen in an - // authored catalog; Brand()'s tie-break is what a runtime item falls back on if it ever does, so it must stay - // deterministic rather than however the cache happens to enumerate. - [Fact] - public void BrandPicksFirstProductInKeyOrderForSameMakerAndDesign() - { - using var cache = AetheriaStores.Open(Catalog, catalogWritable: true); - var maker = cache.Upsert(new Faction { Name = "TieBreaker", ShortName = "TIE" }); - var lamp = new CultRecordRef(cache.RefOf(cache.GetByName("Lamp")).Key); - cache.Commit(batch => batch.Upsert(typeof(FactionProductData), new FactionProductData { Name = "lamp-b", Design = lamp, Manufacturer = maker }, new CultRecordKey("lamp-b"))); - cache.Commit(batch => batch.Upsert(typeof(FactionProductData), new FactionProductData { Name = "lamp-a", Design = lamp, Manufacturer = maker }, new CultRecordKey("lamp-a"))); - - var items = new ItemManager(cache, new ProvenanceLedger(), RunSaveTests.TestSettings(), _ => { }); - var lotId = items.Lots.Add(new Lot - { - Design = cache.RefOf(cache.GetByName("Lamp")), - Origin = new Produced { Faction = maker, Station = 1, Facility = 2, Inputs = Array.Empty() }, - Quality = .5f, Roles = new List() - }); - var instance = (EquippableItem) items.CreateInstance(lotId); - var (_, product) = items.Brand(instance); - Assert.Equal("lamp-a", product.Name); - } - // F2: this test must not enumerate reloaded instances through EntitySerializer.Items, the GC root walk under // test. It hand-mints one real lot per EntityPack root (Hull, Equipment, CargoBays, DockingBays, CargoContents, // Children, DockingBayContents) so the expected lot ids are known independently of any enumerator, and reads diff --git a/tests/Aetheria.Shared.Tests/RunSaveTests.cs b/tests/Aetheria.Shared.Tests/RunSaveTests.cs index 1f60fb4fe..20635e344 100644 --- a/tests/Aetheria.Shared.Tests/RunSaveTests.cs +++ b/tests/Aetheria.Shared.Tests/RunSaveTests.cs @@ -360,6 +360,48 @@ public sealed class RunSaveTests : IDisposable } } + // A lot's product is what names its units, so it must come back from the run file as the same product key; a lot + // minted without one reads unset. + [Fact] + public void ALotsProductSurvivesTheRunSave() + { + CultRecordRef product; + using (var cache = AetheriaStores.Open(Catalog, catalogWritable: true)) + { + var lamp = new CultRecordRef(cache.Upsert(new GearData { Name = "Lamp" }).Key); + var maker = cache.Upsert(new Faction { Name = "Maker", ShortName = "MAK" }); + product = new CultRecordRef(cache.Upsert( + new FactionProductData { Name = "Lamp Prime", Design = lamp, Manufacturer = maker }).Key); + cache.FlushAsync().Wait(); + } + using (var cache = Open()) + { + var design = cache.RefOf(cache.GetByName("Lamp")); + var lots = new ProvenanceLedger { NextLot = 3 }; + lots.Lots[1] = new Lot { Design = design, Origin = new Attributed(), Product = product }; + lots.Lots[2] = new Lot { Design = design, Origin = new Attributed() }; + var zone = new SavedZone + { + Name = "Zone 0", AdjacentZones = Array.Empty(), Factions = Array.Empty(), Owner = -1, + Contents = new ZonePack + { + Entities = new List + { + BarePack(hull: new EquippableItem { Data = design, Lot = 1 }), + BarePack(hull: new EquippableItem { Data = design, Lot = 2 }) + } + } + }; + RunSave.Commit(cache, Game(cache), new[] { zone }, lots); + } + using (var cache = Open()) + { + var stored = RunSave.Lots(cache); + Assert.Equal(product.Key, stored[1].Product.Key); + Assert.False(stored[2].Product.IsSet()); + } + } + // MQ4: Continue refuses a run that names a design the catalog no longer holds, naming the missing mod ships, and // never edits the save. A design can be named by an item in a zone or only by a minted lot, so each is covered. [Theory] diff --git a/tools/AetherDb/Program.cs b/tools/AetherDb/Program.cs index c65f4c3f2..d09410958 100644 --- a/tools/AetherDb/Program.cs +++ b/tools/AetherDb/Program.cs @@ -52,7 +52,7 @@ public static class Program var products = db.Cache.GetAll() .OrderBy(p => db.Cache.RefOf(p).Key.Value, StringComparer.Ordinal).ToArray(); - // The manufacturer no longer lives on the design; a runtime item picks one via Brand()'s tie-break, but + // The manufacturer no longer lives on the design; a runtime item's brand is the product its lot records, but // census must not invent that same attribution for a design with several sellers. Instead it lists every // distinct maker that sells the design (via products), in record-key order. A design no product sells, // or that only unset-manufacturer products sell, shows "(none)". @@ -120,22 +120,6 @@ public static class Program } } Console.WriteLine($"\n{roleGaps} (product, role) pairs where a sold, role-bearing design's product authors no matching spread"); - - // Brand() picks the first (maker, design) product in record-key order; more than one means the choice is - // arbitrary rather than authored, which the rule until segment bands exist forbids. - var duplicateBrands = products - .Where(p => p.Manufacturer.IsSet()) - .GroupBy(p => (Maker: p.Manufacturer.Key, Design: p.Design.Key)) - .Where(g => g.Count() > 1) - .ToArray(); - Console.WriteLine($"\n{duplicateBrands.Length} (maker, design) pairs with more than one product:"); - foreach (var group in duplicateBrands) - { - var maker = db.Cache.Get(group.Key.Maker); - var design = db.Cache.Get(group.Key.Design); - Console.WriteLine($" {maker?.ShortName ?? "(unknown)"} / {design?.Name ?? group.Key.Design.Value}: " + - string.Join(", ", group.Select(p => p.Name))); - } return 0; } From 3d081f0d3b8381a7cd36b645f3e61972f44180e8 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 16:58:55 +0200 Subject: [PATCH 022/120] RunSaveTests: the product round-trip uses the fixture's one faction The first commit's new fact built a second Faction and Game() requires exactly one; that commit's ALotsProductSurvivesTheRunSave failed (1004 of 1005 passed). Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/RunSaveTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Aetheria.Shared.Tests/RunSaveTests.cs b/tests/Aetheria.Shared.Tests/RunSaveTests.cs index 20635e344..b0eb4dd71 100644 --- a/tests/Aetheria.Shared.Tests/RunSaveTests.cs +++ b/tests/Aetheria.Shared.Tests/RunSaveTests.cs @@ -369,7 +369,7 @@ public sealed class RunSaveTests : IDisposable using (var cache = AetheriaStores.Open(Catalog, catalogWritable: true)) { var lamp = new CultRecordRef(cache.Upsert(new GearData { Name = "Lamp" }).Key); - var maker = cache.Upsert(new Faction { Name = "Maker", ShortName = "MAK" }); + var maker = cache.RefOf(cache.GetAll().Single()); product = new CultRecordRef(cache.Upsert( new FactionProductData { Name = "Lamp Prime", Design = lamp, Manufacturer = maker }).Key); cache.FlushAsync().Wait(); From 78755fd176dbc482eb48db1cb80e13ff7428c032 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 18:16:10 +0200 Subject: [PATCH 023/120] pirates-faction: callable on a root, prints its colours; tests pin dry run, idempotence, conflict and missing design Not yet built when committed. Co-Authored-By: Claude Sonnet 5.5 --- .../PiratesFactionCommandTests.cs | 89 +++++++++++++++++++ tools/AetherDb/Program.cs | 10 ++- 2 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 tests/Aetheria.Shared.Tests/PiratesFactionCommandTests.cs diff --git a/tests/Aetheria.Shared.Tests/PiratesFactionCommandTests.cs b/tests/Aetheria.Shared.Tests/PiratesFactionCommandTests.cs new file mode 100644 index 000000000..f5b0d5851 --- /dev/null +++ b/tests/Aetheria.Shared.Tests/PiratesFactionCommandTests.cs @@ -0,0 +1,89 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +using System; +using System.IO; +using System.Linq; +using Xunit; + +// AetherDb's pirates-faction command (aetheria-release, cut pirates-record), run on a scratch copy of the shipped catalog +// with the Pirates removed from it. +public sealed class PiratesFactionCommandTests : IDisposable +{ + private readonly string _root = Path.Combine(Path.GetTempPath(), "pirates-command-" + Guid.NewGuid().ToString("N")); + private string CatalogPath => Path.Combine(_root, "GameData", "Aetheria.cc"); + + public PiratesFactionCommandTests() + { + Directory.CreateDirectory(Path.Combine(_root, "GameData")); + File.Copy(Path.Combine(AetherDb.FindRoot(), "GameData", "Aetheria.cc"), CatalogPath); + var db = AetherDb.Open(catalogWritable: true, root: _root); + var pirates = db.Cache.GetAll().Single(faction => faction.Name == "Pirates"); + var ours = db.Cache.RefOf(pirates); + foreach (var product in db.Cache.GetAll().Where(product => product.Manufacturer.Equals(ours)).ToArray()) + Assert.True(db.Cache.Remove(db.Cache.RefOf(product).Key)); + Assert.True(db.Cache.Remove(ours.Key)); + db.Cache.Dispose(); + } + + public void Dispose() => Directory.Delete(_root, true); + + private (Faction[] Pirates, FactionProductData[] Products) Read() + { + var db = AetherDb.Open(root: _root); + try + { + var pirates = db.Cache.GetAll().Where(faction => faction.Name == "Pirates").ToArray(); + var products = pirates.Length == 0 ? new FactionProductData[0] + : db.Cache.GetAll().Where(product => product.Manufacturer.Equals(db.Cache.RefOf(pirates[0]))).ToArray(); + return (pirates, products); + } + finally { db.Cache.Dispose(); } + } + + [Fact] + public void A_dry_run_writes_nothing_and_apply_lands_the_record_once() + { + var before = File.ReadAllBytes(CatalogPath); + Assert.Equal(0, Program.PiratesFaction(apply: false, root: _root)); + Assert.Equal(before, File.ReadAllBytes(CatalogPath)); + Assert.Empty(Read().Pirates); + + Assert.Equal(0, Program.PiratesFaction(apply: true, root: _root)); + var landed = File.ReadAllBytes(CatalogPath); + var (pirates, products) = Read(); + Assert.Single(pirates); + Assert.Equal(8, products.Length); + + // A second apply finds the faction and changes nothing. + Assert.Equal(0, Program.PiratesFaction(apply: true, root: _root)); + Assert.Equal(landed, File.ReadAllBytes(CatalogPath)); + } + + [Fact] + public void An_existing_Pirates_faction_is_left_as_it_is() + { + var db = AetherDb.Open(catalogWritable: true, root: _root); + db.Cache.Commit(batch => batch.Upsert(typeof(Faction), new Faction { Name = "Pirates", ShortName = "Mine", Description = "authored by hand" })); + db.Cache.Dispose(); + + Assert.Equal(0, Program.PiratesFaction(apply: true, root: _root)); + var (pirates, products) = Read(); + Assert.Equal("authored by hand", Assert.Single(pirates).Description); + Assert.Empty(products); + } + + [Fact] + public void A_missing_design_refuses_and_lands_nothing() + { + var db = AetherDb.Open(catalogWritable: true, root: _root); + var autocannon = db.Cache.GetAll().Single(weapon => weapon.Name == "Autocannon"); + autocannon.Name = "Autocannon (renamed)"; + db.Cache.Commit(batch => batch.Upsert(typeof(WeaponItemData), autocannon, db.Cache.RefOf(autocannon).Key)); + db.Cache.Dispose(); + + Assert.Throws(() => Program.PiratesFaction(apply: true, root: _root)); + Assert.Empty(Read().Pirates); + } +} diff --git a/tools/AetherDb/Program.cs b/tools/AetherDb/Program.cs index d1b488c7a..de1319acf 100644 --- a/tools/AetherDb/Program.cs +++ b/tools/AetherDb/Program.cs @@ -38,7 +38,9 @@ public static class Program case "targeting-catalog-6d": return TargetingCatalog6d(args.Contains("apply")); case "ship-authoring": return ShipAuthoringCommands.Run(args.Skip(1).ToArray()); case "field-kinds": return FieldKindsCatalog(args.Contains("apply")); - case "pirates-faction": return PiratesFaction(args.Contains("apply")); + case "pirates-faction": + Console.OutputEncoding = new System.Text.UTF8Encoding(false); + return PiratesFaction(args.Contains("apply")); default: Console.WriteLine("commands: census, factions, station-fit, hardpoint-fit, loadout [seed], save, settings, settings-dump, dangling [clear ]... [apply], shield-migrate [apply], brownout-migrate [apply], roles-migrate [apply], firing-arc-migrate [apply], targeting-catalog [apply], targeting-catalog-6c [apply], targeting-catalog-6d [apply], ship-authoring create|inspect|validate|compose, field-kinds [apply], pirates-faction [apply]"); return 1; @@ -1514,10 +1516,9 @@ public static class Program // only, written here as \U escapes so no editor or shell mangles them; the dry run prints them raw and as code points. // // Dry run unless passed "apply", same contract as the other *-migrate commands. - private static int PiratesFaction(bool apply) + public static int PiratesFaction(bool apply, string root = null) { - Console.OutputEncoding = new System.Text.UTF8Encoding(false); - var db = AetherDb.Open(catalogWritable: apply); + var db = AetherDb.Open(catalogWritable: apply, root: root); var existing = db.Cache.GetAll().FirstOrDefault(f => f.Name == "Pirates"); if (existing != null) @@ -1571,6 +1572,7 @@ public static class Program }).ToArray(); Console.WriteLine($"New faction \"{pirates.Name}\": influence {pirates.InfluenceDistance}, names \"{nameFile.Name}\", allegiance 1.0 to {pirates.Allegiance.Count} factions."); + Console.WriteLine($" colours: primary ({pirates.PrimaryColor.x:0.00}, {pirates.PrimaryColor.y:0.00}, {pirates.PrimaryColor.z:0.00}) primer grey, secondary ({pirates.SecondaryColor.x:0.00}, {pirates.SecondaryColor.y:0.00}, {pirates.SecondaryColor.z:0.00}) hazard orange"); Console.WriteLine($" {pirates.Description}"); Console.WriteLine($"\nNew products ({products.Length}):"); foreach (var (design, name, description, roles) in products) From 59bc0e556c05d094e1b2824c3db53d9a720d2403 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 18:21:50 +0200 Subject: [PATCH 024/120] Delete the EmojiOne sample sprite asset, its sheet and attribution Sixteen faces nothing references but TMP Settings (E3); EmojiOne art is not openly licensed. TMP Settings still names the asset until EmojiFont.Build rewrites its emoji routing, so this commit leaves a dangling reference and the next commits land the font that replaces it. Co-Authored-By: Claude Opus 5.5 --- .../TextMesh Pro/Resources/Sprite Assets.meta | 9 - .../Resources/Sprite Assets/EmojiOne.asset | 3 - .../Sprite Assets/EmojiOne.asset.meta | 8 - Assets/TextMesh Pro/Sprites.meta | 8 - .../Sprites/EmojiOne Attribution.txt | 3 - .../Sprites/EmojiOne Attribution.txt.meta | 7 - Assets/TextMesh Pro/Sprites/EmojiOne.json | 156 ------- .../TextMesh Pro/Sprites/EmojiOne.json.meta | 8 - Assets/TextMesh Pro/Sprites/EmojiOne.png | 3 - Assets/TextMesh Pro/Sprites/EmojiOne.png.meta | 431 ------------------ 10 files changed, 636 deletions(-) delete mode 100644 Assets/TextMesh Pro/Resources/Sprite Assets.meta delete mode 100644 Assets/TextMesh Pro/Resources/Sprite Assets/EmojiOne.asset delete mode 100644 Assets/TextMesh Pro/Resources/Sprite Assets/EmojiOne.asset.meta delete mode 100644 Assets/TextMesh Pro/Sprites.meta delete mode 100644 Assets/TextMesh Pro/Sprites/EmojiOne Attribution.txt delete mode 100644 Assets/TextMesh Pro/Sprites/EmojiOne Attribution.txt.meta delete mode 100644 Assets/TextMesh Pro/Sprites/EmojiOne.json delete mode 100644 Assets/TextMesh Pro/Sprites/EmojiOne.json.meta delete mode 100644 Assets/TextMesh Pro/Sprites/EmojiOne.png delete mode 100644 Assets/TextMesh Pro/Sprites/EmojiOne.png.meta diff --git a/Assets/TextMesh Pro/Resources/Sprite Assets.meta b/Assets/TextMesh Pro/Resources/Sprite Assets.meta deleted file mode 100644 index 5171f1b68..000000000 --- a/Assets/TextMesh Pro/Resources/Sprite Assets.meta +++ /dev/null @@ -1,9 +0,0 @@ -fileFormatVersion: 2 -guid: 512a49d95c0c4332bdd98131869c23c9 -folderAsset: yes -timeCreated: 1441876896 -licenseType: Pro -DefaultImporter: - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/TextMesh Pro/Resources/Sprite Assets/EmojiOne.asset b/Assets/TextMesh Pro/Resources/Sprite Assets/EmojiOne.asset deleted file mode 100644 index 8fd3185ec..000000000 --- a/Assets/TextMesh Pro/Resources/Sprite Assets/EmojiOne.asset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:155fff99f467baa93dd2d0886df466bd242cd5e3c8c60c9d43e9cdad269f7f54 -size 13441 diff --git a/Assets/TextMesh Pro/Resources/Sprite Assets/EmojiOne.asset.meta b/Assets/TextMesh Pro/Resources/Sprite Assets/EmojiOne.asset.meta deleted file mode 100644 index c7ac83f4d..000000000 --- a/Assets/TextMesh Pro/Resources/Sprite Assets/EmojiOne.asset.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: c41005c129ba4d66911b75229fd70b45 -timeCreated: 1480316912 -licenseType: Pro -NativeFormatImporter: - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/TextMesh Pro/Sprites.meta b/Assets/TextMesh Pro/Sprites.meta deleted file mode 100644 index 8b699e5fc..000000000 --- a/Assets/TextMesh Pro/Sprites.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: d0603b6d5186471b96c778c3949c7ce2 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/TextMesh Pro/Sprites/EmojiOne Attribution.txt b/Assets/TextMesh Pro/Sprites/EmojiOne Attribution.txt deleted file mode 100644 index 384180a92..000000000 --- a/Assets/TextMesh Pro/Sprites/EmojiOne Attribution.txt +++ /dev/null @@ -1,3 +0,0 @@ -This sample of beautiful emojis are provided by EmojiOne https://www.emojione.com/ - -Please visit their website to view the complete set of their emojis and review their licensing terms. \ No newline at end of file diff --git a/Assets/TextMesh Pro/Sprites/EmojiOne Attribution.txt.meta b/Assets/TextMesh Pro/Sprites/EmojiOne Attribution.txt.meta deleted file mode 100644 index 0d30e6532..000000000 --- a/Assets/TextMesh Pro/Sprites/EmojiOne Attribution.txt.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 381dcb09d5029d14897e55f98031fca5 -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/TextMesh Pro/Sprites/EmojiOne.json b/Assets/TextMesh Pro/Sprites/EmojiOne.json deleted file mode 100644 index 6c4e50bc8..000000000 --- a/Assets/TextMesh Pro/Sprites/EmojiOne.json +++ /dev/null @@ -1,156 +0,0 @@ -{"frames": [ - -{ - "filename": "1f60a.png", - "frame": {"x":0,"y":0,"w":128,"h":128}, - "rotated": false, - "trimmed": false, - "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, - "sourceSize": {"w":128,"h":128}, - "pivot": {"x":0.5,"y":0.5} -}, -{ - "filename": "1f60b.png", - "frame": {"x":128,"y":0,"w":128,"h":128}, - "rotated": false, - "trimmed": false, - "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, - "sourceSize": {"w":128,"h":128}, - "pivot": {"x":0.5,"y":0.5} -}, -{ - "filename": "1f60d.png", - "frame": {"x":256,"y":0,"w":128,"h":128}, - "rotated": false, - "trimmed": false, - "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, - "sourceSize": {"w":128,"h":128}, - "pivot": {"x":0.5,"y":0.5} -}, -{ - "filename": "1f60e.png", - "frame": {"x":384,"y":0,"w":128,"h":128}, - "rotated": false, - "trimmed": false, - "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, - "sourceSize": {"w":128,"h":128}, - "pivot": {"x":0.5,"y":0.5} -}, -{ - "filename": "1f600.png", - "frame": {"x":0,"y":128,"w":128,"h":128}, - "rotated": false, - "trimmed": false, - "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, - "sourceSize": {"w":128,"h":128}, - "pivot": {"x":0.5,"y":0.5} -}, -{ - "filename": "1f601.png", - "frame": {"x":128,"y":128,"w":128,"h":128}, - "rotated": false, - "trimmed": false, - "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, - "sourceSize": {"w":128,"h":128}, - "pivot": {"x":0.5,"y":0.5} -}, -{ - "filename": "1f602.png", - "frame": {"x":256,"y":128,"w":128,"h":128}, - "rotated": false, - "trimmed": false, - "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, - "sourceSize": {"w":128,"h":128}, - "pivot": {"x":0.5,"y":0.5} -}, -{ - "filename": "1f603.png", - "frame": {"x":384,"y":128,"w":128,"h":128}, - "rotated": false, - "trimmed": false, - "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, - "sourceSize": {"w":128,"h":128}, - "pivot": {"x":0.5,"y":0.5} -}, -{ - "filename": "1f604.png", - "frame": {"x":0,"y":256,"w":128,"h":128}, - "rotated": false, - "trimmed": false, - "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, - "sourceSize": {"w":128,"h":128}, - "pivot": {"x":0.5,"y":0.5} -}, -{ - "filename": "1f605.png", - "frame": {"x":128,"y":256,"w":128,"h":128}, - "rotated": false, - "trimmed": false, - "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, - "sourceSize": {"w":128,"h":128}, - "pivot": {"x":0.5,"y":0.5} -}, -{ - "filename": "1f606.png", - "frame": {"x":256,"y":256,"w":128,"h":128}, - "rotated": false, - "trimmed": false, - "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, - "sourceSize": {"w":128,"h":128}, - "pivot": {"x":0.5,"y":0.5} -}, -{ - "filename": "1f609.png", - "frame": {"x":384,"y":256,"w":128,"h":128}, - "rotated": false, - "trimmed": false, - "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, - "sourceSize": {"w":128,"h":128}, - "pivot": {"x":0.5,"y":0.5} -}, -{ - "filename": "1f618.png", - "frame": {"x":0,"y":384,"w":128,"h":128}, - "rotated": false, - "trimmed": false, - "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, - "sourceSize": {"w":128,"h":128}, - "pivot": {"x":0.5,"y":0.5} -}, -{ - "filename": "1f923.png", - "frame": {"x":128,"y":384,"w":128,"h":128}, - "rotated": false, - "trimmed": false, - "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, - "sourceSize": {"w":128,"h":128}, - "pivot": {"x":0.5,"y":0.5} -}, -{ - "filename": "263a.png", - "frame": {"x":256,"y":384,"w":128,"h":128}, - "rotated": false, - "trimmed": false, - "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, - "sourceSize": {"w":128,"h":128}, - "pivot": {"x":0.5,"y":0.5} -}, -{ - "filename": "2639.png", - "frame": {"x":384,"y":384,"w":128,"h":128}, - "rotated": false, - "trimmed": false, - "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, - "sourceSize": {"w":128,"h":128}, - "pivot": {"x":0.5,"y":0.5} -}], -"meta": { - "app": "http://www.codeandweb.com/texturepacker", - "version": "1.0", - "image": "EmojiOne.png", - "format": "RGBA8888", - "size": {"w":512,"h":512}, - "scale": "1", - "smartupdate": "$TexturePacker:SmartUpdate:196a26a2e149d875b91ffc8fa3581e76:fc928c7e275404b7e0649307410475cb:424723c3774975ddb2053fd5c4b85f6e$" -} -} diff --git a/Assets/TextMesh Pro/Sprites/EmojiOne.json.meta b/Assets/TextMesh Pro/Sprites/EmojiOne.json.meta deleted file mode 100644 index 762cf15c9..000000000 --- a/Assets/TextMesh Pro/Sprites/EmojiOne.json.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 8f05276190cf498a8153f6cbe761d4e6 -timeCreated: 1480316860 -licenseType: Pro -TextScriptImporter: - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/TextMesh Pro/Sprites/EmojiOne.png b/Assets/TextMesh Pro/Sprites/EmojiOne.png deleted file mode 100644 index 63e49d095..000000000 --- a/Assets/TextMesh Pro/Sprites/EmojiOne.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a87b965a701de3dc31d7401e542b1fc946587f912c51d7a26e46ade9f098bd1a -size 112319 diff --git a/Assets/TextMesh Pro/Sprites/EmojiOne.png.meta b/Assets/TextMesh Pro/Sprites/EmojiOne.png.meta deleted file mode 100644 index c9fa1a7df..000000000 --- a/Assets/TextMesh Pro/Sprites/EmojiOne.png.meta +++ /dev/null @@ -1,431 +0,0 @@ -fileFormatVersion: 2 -guid: dffef66376be4fa480fb02b19edbe903 -TextureImporter: - fileIDToRecycleName: - 21300000: EmojiOne_0 - 21300002: EmojiOne_1 - 21300004: EmojiOne_2 - 21300006: EmojiOne_3 - 21300008: EmojiOne_4 - 21300010: EmojiOne_6 - 21300012: EmojiOne_7 - 21300014: EmojiOne_8 - 21300016: EmojiOne_9 - 21300018: EmojiOne_10 - 21300020: EmojiOne_11 - 21300022: EmojiOne_12 - 21300024: EmojiOne_13 - 21300026: EmojiOne_5 - 21300028: EmojiOne_14 - externalObjects: {} - serializedVersion: 5 - mipmaps: - mipMapMode: 0 - enableMipMap: 1 - sRGBTexture: 1 - linearTexture: 0 - fadeOut: 0 - borderMipMap: 0 - mipMapsPreserveCoverage: 0 - alphaTestReferenceValue: 0.5 - mipMapFadeDistanceStart: 1 - mipMapFadeDistanceEnd: 3 - bumpmap: - convertToNormalMap: 0 - externalNormalMap: 0 - heightScale: 0.25 - normalMapFilter: 0 - isReadable: 0 - grayScaleToAlpha: 0 - generateCubemap: 6 - cubemapConvolution: 0 - seamlessCubemap: 0 - textureFormat: -1 - maxTextureSize: 2048 - textureSettings: - serializedVersion: 2 - filterMode: -1 - aniso: -1 - mipBias: -1 - wrapU: 1 - wrapV: 1 - wrapW: 1 - nPOTScale: 0 - lightmap: 0 - compressionQuality: 50 - spriteMode: 2 - spriteExtrude: 1 - spriteMeshType: 1 - alignment: 0 - spritePivot: {x: 0.5, y: 0.5} - spriteBorder: {x: 0, y: 0, z: 0, w: 0} - spritePixelsToUnits: 100 - alphaUsage: 1 - alphaIsTransparency: 1 - spriteTessellationDetail: -1 - textureType: 0 - textureShape: 1 - singleChannelComponent: 0 - maxTextureSizeSet: 0 - compressionQualitySet: 0 - textureFormatSet: 0 - platformSettings: - - serializedVersion: 2 - buildTarget: DefaultTexturePlatform - maxTextureSize: 512 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - androidETC2FallbackOverride: 0 - - serializedVersion: 2 - buildTarget: Standalone - maxTextureSize: 512 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - androidETC2FallbackOverride: 0 - - serializedVersion: 2 - buildTarget: iPhone - maxTextureSize: 512 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - androidETC2FallbackOverride: 0 - - serializedVersion: 2 - buildTarget: Android - maxTextureSize: 512 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - androidETC2FallbackOverride: 0 - spriteSheet: - serializedVersion: 2 - sprites: - - serializedVersion: 2 - name: EmojiOne_0 - rect: - serializedVersion: 2 - x: 0 - y: 384 - width: 128 - height: 128 - alignment: 0 - pivot: {x: 0.5, y: 0.5} - border: {x: 0, y: 0, z: 0, w: 0} - outline: [] - physicsShape: [] - tessellationDetail: 0 - bones: [] - spriteID: 4bcc36da2108f2c4ba3de5c921d25c3c - vertices: [] - indices: - edges: [] - weights: [] - - serializedVersion: 2 - name: EmojiOne_1 - rect: - serializedVersion: 2 - x: 128 - y: 384 - width: 128 - height: 128 - alignment: 0 - pivot: {x: 0.5, y: 0.5} - border: {x: 0, y: 0, z: 0, w: 0} - outline: [] - physicsShape: [] - tessellationDetail: 0 - bones: [] - spriteID: e9eea8093eaeaee4d901c4553f572c22 - vertices: [] - indices: - edges: [] - weights: [] - - serializedVersion: 2 - name: EmojiOne_2 - rect: - serializedVersion: 2 - x: 256 - y: 384 - width: 128 - height: 128 - alignment: 0 - pivot: {x: 0.5, y: 0.5} - border: {x: 0, y: 0, z: 0, w: 0} - outline: [] - physicsShape: [] - tessellationDetail: 0 - bones: [] - spriteID: 49451da35411dcc42a3692e39b0fde70 - vertices: [] - indices: - edges: [] - weights: [] - - serializedVersion: 2 - name: EmojiOne_3 - rect: - serializedVersion: 2 - x: 384 - y: 384 - width: 128 - height: 128 - alignment: 0 - pivot: {x: 0.5, y: 0.5} - border: {x: 0, y: 0, z: 0, w: 0} - outline: [] - physicsShape: [] - tessellationDetail: 0 - bones: [] - spriteID: f65709664b924904790c850a50ca82bc - vertices: [] - indices: - edges: [] - weights: [] - - serializedVersion: 2 - name: EmojiOne_4 - rect: - serializedVersion: 2 - x: 0 - y: 256 - width: 128 - height: 128 - alignment: 0 - pivot: {x: 0.5, y: 0.5} - border: {x: 0, y: 0, z: 0, w: 0} - outline: [] - physicsShape: [] - tessellationDetail: 0 - bones: [] - spriteID: 5b92c568a5ec9ad4b9ed90e271f1c9a8 - vertices: [] - indices: - edges: [] - weights: [] - - serializedVersion: 2 - name: EmojiOne_6 - rect: - serializedVersion: 2 - x: 256 - y: 256 - width: 128 - height: 128 - alignment: 0 - pivot: {x: 0.5, y: 0.5} - border: {x: 0, y: 0, z: 0, w: 0} - outline: [] - physicsShape: [] - tessellationDetail: 0 - bones: [] - spriteID: b10f2b48b7281594bb8a24a6511a35af - vertices: [] - indices: - edges: [] - weights: [] - - serializedVersion: 2 - name: EmojiOne_7 - rect: - serializedVersion: 2 - x: 384 - y: 256 - width: 128 - height: 128 - alignment: 0 - pivot: {x: 0.5, y: 0.5} - border: {x: 0, y: 0, z: 0, w: 0} - outline: [] - physicsShape: [] - tessellationDetail: 0 - bones: [] - spriteID: 10a600f9329dc2246a897e89f4d283cd - vertices: [] - indices: - edges: [] - weights: [] - - serializedVersion: 2 - name: EmojiOne_8 - rect: - serializedVersion: 2 - x: 0 - y: 128 - width: 128 - height: 128 - alignment: 0 - pivot: {x: 0.5, y: 0.5} - border: {x: 0, y: 0, z: 0, w: 0} - outline: [] - physicsShape: [] - tessellationDetail: 0 - bones: [] - spriteID: 66cffa363b90ab14787d8a5b90cf4502 - vertices: [] - indices: - edges: [] - weights: [] - - serializedVersion: 2 - name: EmojiOne_9 - rect: - serializedVersion: 2 - x: 128 - y: 128 - width: 128 - height: 128 - alignment: 0 - pivot: {x: 0.5, y: 0.5} - border: {x: 0, y: 0, z: 0, w: 0} - outline: [] - physicsShape: [] - tessellationDetail: 0 - bones: [] - spriteID: 55cf3d409c9b89349b1e1bdc1cc224ad - vertices: [] - indices: - edges: [] - weights: [] - - serializedVersion: 2 - name: EmojiOne_10 - rect: - serializedVersion: 2 - x: 256 - y: 128 - width: 128 - height: 128 - alignment: 0 - pivot: {x: 0.5, y: 0.5} - border: {x: 0, y: 0, z: 0, w: 0} - outline: [] - physicsShape: [] - tessellationDetail: 0 - bones: [] - spriteID: 2a9e58eaf96feef42bcefa1cf257193f - vertices: [] - indices: - edges: [] - weights: [] - - serializedVersion: 2 - name: EmojiOne_11 - rect: - serializedVersion: 2 - x: 384 - y: 128 - width: 128 - height: 128 - alignment: 0 - pivot: {x: 0.5, y: 0.5} - border: {x: 0, y: 0, z: 0, w: 0} - outline: [] - physicsShape: [] - tessellationDetail: 0 - bones: [] - spriteID: 2489120affc155840ae6a7be2e93ce19 - vertices: [] - indices: - edges: [] - weights: [] - - serializedVersion: 2 - name: EmojiOne_12 - rect: - serializedVersion: 2 - x: 0 - y: 0 - width: 128 - height: 128 - alignment: 0 - pivot: {x: 0.5, y: 0.5} - border: {x: 0, y: 0, z: 0, w: 0} - outline: [] - physicsShape: [] - tessellationDetail: 0 - bones: [] - spriteID: 412349a150598d14da4d7140df5c0286 - vertices: [] - indices: - edges: [] - weights: [] - - serializedVersion: 2 - name: EmojiOne_13 - rect: - serializedVersion: 2 - x: 128 - y: 0 - width: 128 - height: 128 - alignment: 0 - pivot: {x: 0.5, y: 0.5} - border: {x: 0, y: 0, z: 0, w: 0} - outline: [] - physicsShape: [] - tessellationDetail: 0 - bones: [] - spriteID: a937464b42bb3634782dea34c6becb6c - vertices: [] - indices: - edges: [] - weights: [] - - serializedVersion: 2 - name: EmojiOne_5 - rect: - serializedVersion: 2 - x: 256 - y: 0 - width: 128 - height: 128 - alignment: 0 - pivot: {x: 0, y: 0} - border: {x: 0, y: 0, z: 0, w: 0} - outline: [] - physicsShape: [] - tessellationDetail: 0 - bones: [] - spriteID: b0f933b217682124dbfc5e6b89abe3d0 - vertices: [] - indices: - edges: [] - weights: [] - - serializedVersion: 2 - name: EmojiOne_14 - rect: - serializedVersion: 2 - x: 128 - y: 256 - width: 128 - height: 128 - alignment: 0 - pivot: {x: 0, y: 0} - border: {x: 0, y: 0, z: 0, w: 0} - outline: [] - physicsShape: [] - tessellationDetail: 0 - bones: [] - spriteID: f7235c763afe4434e8bb666750a41096 - vertices: [] - indices: - edges: [] - weights: [] - outline: [] - physicsShape: [] - bones: [] - spriteID: 3e32d8f5477abfc43b19066e8ad5032e - vertices: [] - indices: - edges: [] - weights: [] - spritePackingTag: - userData: - assetBundleName: - assetBundleVariant: From c4bd6e153e809c3232fec5be479eabd2f49aac2f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 18:26:02 +0200 Subject: [PATCH 025/120] Sim speed: one SimClock steps the zone; presenters draw ahead by Lead SimClock (engine-free) turns real frame seconds into whole fixed steps; ActionGameManager.Update feeds it outside the pause gate and Zone.Update(Time.deltaTime) is gone, with IntroCutscene (dead, Unity-driven pose writes). Entity.TurnRate is the sum of the rotation actuators' applied turn per sim second, zeroed each Update; DrawAhead reads it so EntityInstance and ShipInstance draw the latest sim state ahead by Lead. Unverified at this commit. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/Gameplay/ActionGameManager.cs | 37 +-- Assets/Scripts/Gameplay/EntityInstance.cs | 5 +- Assets/Scripts/Gameplay/ShipInstance.cs | 2 +- .../ServerShared/Behaviors/AetherDrive.cs | 5 +- .../ServerShared/Behaviors/Thruster.cs | 5 +- Assets/Scripts/ServerShared/DrawAhead.cs | 36 +++ Assets/Scripts/ServerShared/DrawAhead.cs.meta | 11 + Assets/Scripts/ServerShared/Entity.cs | 5 + Assets/Scripts/ServerShared/SimClock.cs | 54 ++++ Assets/Scripts/ServerShared/SimClock.cs.meta | 11 + tests/Aetheria.Shared.Tests/SimClockTests.cs | 262 ++++++++++++++++++ 11 files changed, 393 insertions(+), 40 deletions(-) create mode 100644 Assets/Scripts/ServerShared/DrawAhead.cs create mode 100644 Assets/Scripts/ServerShared/DrawAhead.cs.meta create mode 100644 Assets/Scripts/ServerShared/SimClock.cs create mode 100644 Assets/Scripts/ServerShared/SimClock.cs.meta create mode 100644 tests/Aetheria.Shared.Tests/SimClockTests.cs diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index f920522ae..0476acde4 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -177,8 +177,6 @@ public class ActionGameManager : MonoBehaviour public Sprite ShieldIcon; public Sprite NoShieldIcon; - public float IntroDuration; - //public PlayerInput Input; // private CinemachineFramingTransposer _transposer; @@ -231,6 +229,7 @@ public class ActionGameManager : MonoBehaviour public ItemManager ItemManager { get; private set; } public Zone Zone { get; private set; } + public static SimClock Clock { get; private set; } private readonly (float2 direction, string name)[] _directions = { (float2(0, 1), "Front"), @@ -728,6 +727,7 @@ public class ActionGameManager : MonoBehaviour galaxyZone.Contents = new Zone(ItemManager, Settings.PlanetSettings, galaxyZone.PackedContents, galaxyZone, CurrentGalaxy); } Zone = galaxyZone.Contents; + Clock = new SimClock(1f / 60f); PlayMusic(MusicType.Overworld); Zone.Log = s => Debug.Log($"Zone: {s}"); @@ -818,7 +818,6 @@ public class ActionGameManager : MonoBehaviour } else { - //StartCoroutine(IntroCutscene(targetEntity as Ship)); BindToEntity(targetEntity); } @@ -866,35 +865,6 @@ public class ActionGameManager : MonoBehaviour BindToEntity(staged.Player); } - private IEnumerator IntroCutscene(Ship ship) - { - ZoneRenderer.PerspectiveEntity = ship; - var entityPosition = ship.Position.xz; - var followOrbit = Zone.Orbits.Keys.MinBy(o => lengthsq(Zone.GetOrbitPosition(o) - entityPosition)); - var followPlanet = ZoneRenderer.Planets[Zone.Planets.FirstOrDefault(p => p.Value.Orbit.Key.Equals(followOrbit)).Key]; - DockCamera.Follow = followPlanet.Body.transform; - var rootOrbit = followOrbit; - while (Zone.Orbits[rootOrbit].Data.Parent.IsSet()) - rootOrbit = Zone.Orbits[rootOrbit].Data.Parent.Key; - var rootPlanet = ZoneRenderer.Planets[Zone.Planets.FirstOrDefault(p => p.Value.Orbit.Key.Equals(rootOrbit)).Key]; - DockCamera.LookAt = rootPlanet.Body.transform; - - var shipVelocity = ship.GetBehavior().Limit; - var followOrbitPosition = Zone.GetOrbitPosition(followOrbit); - var shipDirection = normalize(Zone.GetOrbitPosition(rootOrbit) - followOrbitPosition); - ship.Position.xz = followOrbitPosition - shipDirection * shipVelocity * IntroDuration; - - var startTime = Time.time; - while (Time.time - startTime < IntroDuration) - { - ship.Direction = shipDirection; - ship.Velocity = shipDirection * shipVelocity; - yield return null; - } - - BindToEntity(ship); - } - public void Dock() { if (CurrentEntity.Parent != null) return; @@ -1356,8 +1326,9 @@ public class ActionGameManager : MonoBehaviour CurrentEntity.TractorPower = saturate(CurrentEntity.TractorPower + sign(tractorPower - CurrentEntity.TractorPower) * Time.deltaTime * 2); } - Zone.Update(Time.deltaTime); } + // One clock steps the zone, paused or not: a paused frame feeds it no real time. + Clock.Advance(_paused ? 0f : Time.deltaTime, Zone.Update); } private static string ResultLabel(ShotResult result) => result switch diff --git a/Assets/Scripts/Gameplay/EntityInstance.cs b/Assets/Scripts/Gameplay/EntityInstance.cs index 61146034e..318a3007e 100644 --- a/Assets/Scripts/Gameplay/EntityInstance.cs +++ b/Assets/Scripts/Gameplay/EntityInstance.cs @@ -416,9 +416,10 @@ public class EntityInstance : MonoBehaviour var direction = FireControl.Solution(weapon, Entity, Entity.Target.Value).Direction; group.Value.position = barrel + new Vector3(direction.x, 0, direction.y) * aimDistance; } - LocalSpace.localPosition = transform.position = Entity.Position.ToUnity(); + var drawn = DrawAhead.Position(Entity, ActionGameManager.Clock.Lead); + LocalSpace.localPosition = transform.position = drawn.ToUnity(); if (_influenceInstance) - _influenceInstance.position = new Vector3(Entity.Position.x, 0, Entity.Position.z); + _influenceInstance.position = new Vector3(drawn.x, 0, drawn.z); } public virtual void OnDestroy() diff --git a/Assets/Scripts/Gameplay/ShipInstance.cs b/Assets/Scripts/Gameplay/ShipInstance.cs index cc110fc8f..18902a422 100644 --- a/Assets/Scripts/Gameplay/ShipInstance.cs +++ b/Assets/Scripts/Gameplay/ShipInstance.cs @@ -138,6 +138,6 @@ public class ShipInstance : EntityInstance emissionModule.rateOverTimeMultiplier = thrusterInstance.BaseEmission * thrusterInstance.Thruster.Axis * thrusterInstance.Thruster.Condition; } - transform.rotation = Ship.Rotation.ToUnity(); + transform.rotation = DrawAhead.Rotation(Ship, ActionGameManager.Clock.Lead).ToUnity(); } } \ No newline at end of file diff --git a/Assets/Scripts/ServerShared/Behaviors/AetherDrive.cs b/Assets/Scripts/ServerShared/Behaviors/AetherDrive.cs index e8498ca7f..fcb6de134 100644 --- a/Assets/Scripts/ServerShared/Behaviors/AetherDrive.cs +++ b/Assets/Scripts/ServerShared/Behaviors/AetherDrive.cs @@ -151,8 +151,9 @@ public class AetherDrive : Behavior, IPowerConsumer ThrustDirection = forward * (_axis.x * force.x / Entity.Mass) + right * (_axis.y * force.y / Entity.Mass); Entity.Velocity += ThrustDirection; - Entity.Direction = mul(Entity.Direction, - CultMath.float2x2.Rotate(force.z * _axis.z * ItemManager.GameplaySettings.AetherTorqueMultiplier / Entity.Mass)); + var turn = force.z * _axis.z * ItemManager.GameplaySettings.AetherTorqueMultiplier / Entity.Mass; + Entity.Direction = mul(Entity.Direction, CultMath.float2x2.Rotate(turn)); + if (dt > 0) Entity.TurnRate += turn / dt; if(float.IsNaN(Entity.Velocity.x)) ItemManager.Log("FUCK FUCK FUCK FUCK"); diff --git a/Assets/Scripts/ServerShared/Behaviors/Thruster.cs b/Assets/Scripts/ServerShared/Behaviors/Thruster.cs index be0d79900..007bf66bc 100644 --- a/Assets/Scripts/ServerShared/Behaviors/Thruster.cs +++ b/Assets/Scripts/ServerShared/Behaviors/Thruster.cs @@ -125,8 +125,9 @@ public class Thruster : Behavior, IAnalogBehavior, IPowerConsumer { var thrust = Thrust; Entity.Velocity -= Direction.xz * _input * thrust / Entity.Mass * dt; - Entity.Direction = mul(Entity.Direction, - CultMath.float2x2.Rotate(_input * Torque * thrust * ItemManager.GameplaySettings.TorqueMultiplier / Entity.Mass * dt)); + var turnRate = _input * Torque * thrust * ItemManager.GameplaySettings.TorqueMultiplier / Entity.Mass; + Entity.Direction = mul(Entity.Direction, CultMath.float2x2.Rotate(turnRate * dt)); + Entity.TurnRate += turnRate; AddHeat(_input * Evaluate(_data.Heat) * dt); var vis = _input * Evaluate(_data.Visibility); if (!Entity.VisibilitySources.ContainsKey(this) || vis > Entity.VisibilitySources[this]) diff --git a/Assets/Scripts/ServerShared/DrawAhead.cs b/Assets/Scripts/ServerShared/DrawAhead.cs new file mode 100644 index 000000000..da4e7d94a --- /dev/null +++ b/Assets/Scripts/ServerShared/DrawAhead.cs @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +using CultMath; +using static CultMath.math; + +// Where a presenter draws the latest sim state when the frame falls Lead sim seconds after the last step. Pure reads +// of the simulation for presenters (never called by ServerShared): the sim owns every pose and no presenter stores +// one, a previous state or a velocity of its own. +public static class DrawAhead +{ + public static float3 Position(Entity entity, float lead) => + entity.Position + float3(entity.Velocity.x, 0, entity.Velocity.y) * lead; + + // The ship's rotation turned about its own up axis by the rate its last step turned, for lead seconds. + public static quaternion Rotation(Ship ship, float lead) + { + var q = ship.Rotation; + var u = float3(q.x, q.y, q.z); + var up = float3(0, 1, 0); + up += 2 * (q.w * cross(u, up) + cross(u, cross(u, up))); + var half = -ship.TurnRate * lead * .5f; + var s = sin(half); + var t = new quaternion(up.x * s, up.y * s, up.z * s, cos(half)); + // Hamilton product t * q: CultMath carries no quaternion product (a gap in CultMath, not filled here). + return normalize(new quaternion( + t.w * q.x + t.x * q.w + t.y * q.z - t.z * q.y, + t.w * q.y - t.x * q.z + t.y * q.w + t.z * q.x, + t.w * q.z + t.x * q.y - t.y * q.x + t.z * q.w, + t.w * q.w - t.x * q.x - t.y * q.y - t.z * q.z)); + } + + // Zone time for a presenter that evaluates a sim function of time (FireControl.RoundAt). + public static float Time(Zone zone, float lead) => zone.Time + lead; +} diff --git a/Assets/Scripts/ServerShared/DrawAhead.cs.meta b/Assets/Scripts/ServerShared/DrawAhead.cs.meta new file mode 100644 index 000000000..628d71b5d --- /dev/null +++ b/Assets/Scripts/ServerShared/DrawAhead.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 007fe4255cd84c26a010151a399f45c2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/Entity.cs b/Assets/Scripts/ServerShared/Entity.cs index b83fc141e..a1eb14c49 100644 --- a/Assets/Scripts/ServerShared/Entity.cs +++ b/Assets/Scripts/ServerShared/Entity.cs @@ -27,6 +27,10 @@ public abstract class Entity public float3 Position; public float2 Direction = float2(0,1); public float2 Velocity; + // Rad per sim second, signed as the step turns Direction: the sum of this step's rotation actuators' applied + // angle over the step. Runtime only, zeroed at the start of Update, so a staged, loaded or wormhole write to + // Direction is never a turn. Read only by DrawAhead. + public float TurnRate; // Runtime only, never saved; Entity.Update is the one writer of both. Acceleration is the planar velocity change // across the base update over its delta (thrust, not the drag or gravity a subclass applies before calling it), @@ -1359,6 +1363,7 @@ public abstract class Entity public virtual void Update(float delta) { + TurnRate = 0; if (!_active) return; var velocityBefore = Velocity; diff --git a/Assets/Scripts/ServerShared/SimClock.cs b/Assets/Scripts/ServerShared/SimClock.cs new file mode 100644 index 000000000..15e132ac0 --- /dev/null +++ b/Assets/Scripts/ServerShared/SimClock.cs @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +using System; + +// The one relation between simulation time and real time. Real seconds go in; whole fixed steps come out, each +// handed to the caller's step action with the same Step. Nothing else in the game converts frame time to sim time, +// and nothing integrates a sim quantity on this clock: presenters read Lead to draw ahead of the latest step +// (DrawAhead) and decide no sim fact. +public sealed class SimClock +{ + // The longest real time one frame may feed. A hitch (a load, a breakpoint) is a slow frame, not a burst of + // catch-up steps. + public const float MaxFrameSeconds = .1f; + + private readonly double _period; + private double _accumulator; + + public SimClock(float step, float stepsPerRealSecond = 60) + { + if (!(step > 0)) throw new ArgumentOutOfRangeException(nameof(step)); + if (!(stepsPerRealSecond > 0)) throw new ArgumentOutOfRangeException(nameof(stepsPerRealSecond)); + Step = step; + StepsPerRealSecond = stepsPerRealSecond; + _period = 1.0 / stepsPerRealSecond; + } + + // Sim seconds per step, fixed for the clock's life. + public float Step { get; } + + public float StepsPerRealSecond { get; } + + public long Steps { get; private set; } + + // Sim seconds since the last step, 0 <= Lead < Step. + public float Lead => (float) (_accumulator * StepsPerRealSecond * Step); + + // Feeds real seconds (negative counts as none, one frame counts at most MaxFrameSeconds) and runs the whole + // steps they pay for. Returns how many ran. + public int Advance(float realSeconds, Action step) + { + _accumulator += Math.Min(Math.Max(realSeconds, 0f), MaxFrameSeconds); + var ran = 0; + while (_accumulator >= _period) + { + _accumulator -= _period; + step(Step); + Steps++; + ran++; + } + return ran; + } +} diff --git a/Assets/Scripts/ServerShared/SimClock.cs.meta b/Assets/Scripts/ServerShared/SimClock.cs.meta new file mode 100644 index 000000000..30e06c489 --- /dev/null +++ b/Assets/Scripts/ServerShared/SimClock.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7a9e3edac65f4a8e8313bdbfe69ccebd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/tests/Aetheria.Shared.Tests/SimClockTests.cs b/tests/Aetheria.Shared.Tests/SimClockTests.cs new file mode 100644 index 000000000..0fdc55d81 --- /dev/null +++ b/tests/Aetheria.Shared.Tests/SimClockTests.cs @@ -0,0 +1,262 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using CultMath; +using UniRx; +using Xunit; +using static CultMath.math; +using float2 = CultMath.float2; +using float3 = CultMath.float3; + +// SimClock owns sim time's relation to real time: frames of real seconds in, whole fixed steps out. Nothing here hands +// a clock to a presenter; DrawAhead is pure reads of the latest state. +public sealed class SimClockTests +{ + private static readonly float[] Irregular = { 1f / 144, 1f / 30, 1f / 90 }; + + // Runs frames cycling through the lengths until the clock has run the steps; returns the step values it passed. + private static List Drive(SimClock clock, float[] frames, long steps) + { + var passed = new List(); + for (var frame = 0; clock.Steps < steps; frame++) + clock.Advance(frames[frame % frames.Length], dt => passed.Add(dt)); + return passed; + } + + [Fact] + public void TheStepNeverChanges() + { + foreach (var frames in new[] { new[] { 1f / 60 }, Irregular, new[] { .09f, .004f, 0f, 3f } }) + { + var clock = new SimClock(1f / 60f); + var passed = Drive(clock, frames, 500); + Assert.True(passed.Count >= 500); + Assert.All(passed, dt => Assert.Equal(clock.Step, dt)); + } + var slow = new SimClock(.1f, 6); + Assert.All(Drive(slow, Irregular, 50), dt => Assert.Equal(.1f, dt)); + } + + [Fact] + public void StepsFollowRealTime() + { + // Ten real seconds of irregular frames. + var clock = new SimClock(1f / 60f); + var elapsed = 0.0; + for (var frame = 0; elapsed < 10.0; frame++) + { + var seconds = Irregular[frame % Irregular.Length]; + elapsed += seconds; + clock.Advance(seconds, _ => { }); + } + Assert.InRange(elapsed, 10.0, 10.1); + Assert.InRange(clock.Steps, Math.Round(elapsed * 60) - 1, Math.Round(elapsed * 60) + 1); + + var slow = new SimClock(1f / 60f, 30); + for (var frame = 0; frame < 600; frame++) slow.Advance(1f / 60, _ => { }); + Assert.InRange(slow.Steps, 299, 301); + } + + [Fact] + public void AHitchIsClamped() + { + var clock = new SimClock(1f / 60f); + var ran = clock.Advance(5f, _ => { }); + Assert.Equal(6, ran); + Assert.Equal(6, clock.Steps); + } + + [Fact] + public void PauseFeedsNothing() + { + var clock = new SimClock(1f / 60f); + clock.Advance(.025f, _ => { }); + var lead = clock.Lead; + var steps = clock.Steps; + Assert.True(lead > 0); + foreach (var seconds in new[] { 0f, -1f }) + { + Assert.Equal(0, clock.Advance(seconds, _ => throw new InvalidOperationException())); + Assert.Equal(lead, clock.Lead); + Assert.Equal(steps, clock.Steps); + } + } + + [Fact] + public void LeadIsHonest() + { + var clock = new SimClock(.1f, 6); + double sinceLastStep = 0; + for (var frame = 0; frame < 400; frame++) + { + // The real time since the last step: what was left over plus this frame, less the steps it paid for. + sinceLastStep += Irregular[frame % Irregular.Length]; + var seconds = Irregular[frame % Irregular.Length]; + clock.Advance(seconds, _ => sinceLastStep -= 1.0 / clock.StepsPerRealSecond); + Assert.InRange(clock.Lead, 0f, clock.Step * .99999f); + Assert.Equal(sinceLastStep * clock.StepsPerRealSecond * clock.Step, clock.Lead, 4); + } + } + + [Fact] + public void ABadClockIsRefusedByFieldName() + { + Assert.Equal("step", Assert.Throws(() => new SimClock(0f)).ParamName); + Assert.Equal("stepsPerRealSecond", Assert.Throws(() => new SimClock(1f, 0f)).ParamName); + } +} + +public sealed partial class RunStartTests +{ + private const float ClockFrame = 1f / 60f; + private const int HistorySteps = 1800; + + // The Duel launched as the game launches it, its item random reseeded so play draws the same numbers every time. + private (Zone arena, Ship player) LaunchDuel() + { + var launched = Launch(new Duel()); + Assert.Empty(launched.failures); + _items.Random = new CultMath.Random(0xC0FFEE); + launched.staged.Player.MovementDirection = float2(0, 1); + launched.staged.Player.Turn = .5f; + return (launched.arena, launched.staged.Player); + } + + private static string R(float value) => value.ToString("R", CultureInfo.InvariantCulture); + + // Everything the step decides, as text: the zone clock, each entity's pose and condition, and each shot resolved. + private static string Snapshot(Zone zone, List shots) + { + var text = new StringBuilder(R(zone.Time)); + foreach (var entity in zone.Entities) + { + text.Append($"|{entity.Name} {R(entity.Position.x)},{R(entity.Position.z)} v{R(entity.Velocity.x)},{R(entity.Velocity.y)} " + + $"d{R(entity.Direction.x)},{R(entity.Direction.y)} w{R(entity.TurnRate)} h{R(entity.Hull.Durability)}"); + if (entity.Shield != null) text.Append($" s{R(entity.Shield.Progress)}"); + if (entity.Temperature != null) + { + var heat = 0f; + foreach (var cell in entity.Temperature) heat += cell; + text.Append($" t{R(heat)}"); + } + } + text.Append($"|shots {string.Join(",", shots)}"); + shots.Clear(); + return text.ToString(); + } + + // Feeds the zone through one SimClock with the frames cycled, snapshotting every step until HistorySteps have run. + private static List History(Zone zone, float[] frames) + { + var clock = new SimClock(1f / 60f); + var shots = new List(); + using var _ = zone.ShotResolved.Subscribe(outcome => shots.Add($"{outcome.ShotId}:{outcome.Result}")); + var history = new List(); + for (var frame = 0; history.Count < HistorySteps; frame++) + clock.Advance(frames[frame % frames.Length], dt => + { + zone.Update(dt); + history.Add(Snapshot(zone, shots)); + }); + return history.Take(HistorySteps).ToList(); + } + + // Control: the same launch driven twice the same way is the same history. If this fails the simulation is + // nondeterministic before the clock is involved, and FrameTimesDoNotChangeHistory proves nothing. + [Fact] + public void OneHistoryTwice() + { + var first = History(LaunchDuel().arena, new[] { ClockFrame }); + var second = History(LaunchDuel().arena, new[] { ClockFrame }); + Assert.Equal(first, second); + Assert.Contains(first, line => !line.EndsWith("shots ")); + } + + [Fact] + public void FrameTimesDoNotChangeHistory() + { + var steady = History(LaunchDuel().arena, new[] { ClockFrame }); + var irregular = History(LaunchDuel().arena, new[] { 1f / 144, 1f / 30, 1f / 90 }); + Assert.Equal(steady.Count, irregular.Count); + for (var step = 0; step < steady.Count; step++) + Assert.True(steady[step] == irregular[step], $"step {step}: {steady[step]} != {irregular[step]}"); + } + + private static float SignedTurn(float2 before, float2 after) => + atan2(before.x * after.y - before.y * after.x, dot(before, after)); + + [Fact] + public void TurnRateIsTheActuatorsTurn() + { + var (arena, player) = LaunchDuel(); + Assert.Equal(0f, player.TurnRate); // staged: nothing has turned it + var turned = 0; + for (var step = 0; step < 240; step++) + { + var before = player.Direction; + arena.Update(ClockFrame); + var angle = SignedTurn(before, player.Direction); + Assert.Equal(abs(angle), abs(player.TurnRate * ClockFrame), 5); + // A positive turn demand turns clockwise, and TurnRate is signed the way the step turned Direction. + if (abs(player.TurnRate) > 1e-4f) + { + turned++; + Assert.True(player.TurnRate > 0 == dot(player.Direction, before.Rotate(ItemRotation.Clockwise)) > 0); + } + } + Assert.True(turned > 60, "the held turn turned the ship"); + + // A write to Direction between steps (staging, a load, a wormhole exit) is not a turn. + player.Turn = 0; + for (var step = 0; step < 600 && abs(player.TurnRate) > 0; step++) arena.Update(ClockFrame); + player.Direction = normalize(float2(3, -4)); + arena.Update(ClockFrame); + Assert.Equal(0f, player.TurnRate); + } + + [Fact] + public void DrawAheadIsTheStateAtZeroLead() + { + var (arena, player) = LaunchDuel(); + for (var step = 0; step < 120; step++) arena.Update(ClockFrame); + var before = Snapshot(arena, new List()); + + var zero = DrawAhead.Position(player, 0); + Assert.Equal(player.Position.x, zero.x); + Assert.Equal(player.Position.y, zero.y); + Assert.Equal(player.Position.z, zero.z); + var rotation = DrawAhead.Rotation(player, 0); + Assert.Equal(1f, abs(rotation.x * player.Rotation.x + rotation.y * player.Rotation.y + rotation.z * player.Rotation.z + rotation.w * player.Rotation.w), 5); + + const float lead = .007f; + var ahead = DrawAhead.Position(player, lead); + Assert.Equal(player.Position.x + player.Velocity.x * lead, ahead.x, 6); + Assert.Equal(player.Position.y, ahead.y); + Assert.Equal(player.Position.z + player.Velocity.y * lead, ahead.z, 6); + Assert.Equal(arena.Time + lead, DrawAhead.Time(arena, lead)); + Assert.Equal(before, Snapshot(arena, new List())); + } + + // The turn a presenter draws ahead is the turn the next step makes: a ship held in a steady turn, drawn one whole + // step ahead of the pose it was last given, is where the sim puts that pose next. + [Fact] + public void DrawnRotationFollowsTheTurn() + { + var (arena, player) = LaunchDuel(); + player.Turn = 1f; + for (var step = 0; step < 300; step++) arena.Update(ClockFrame); + var rate = player.TurnRate; + Assert.True(abs(rate) > .05f, "the ship is turning"); + var drawn = DrawAhead.Rotation(player, ClockFrame); + arena.Update(ClockFrame); + Assert.True(abs(player.TurnRate - rate) < 1e-3f * abs(rate), "the turn is steady"); + var next = player.Rotation; + Assert.Equal(1f, abs(drawn.x * next.x + drawn.y * next.y + drawn.z * next.z + drawn.w * next.w), 4); + } +} From 0a57e417bd99c9e26b862c002c801c4763c5c616 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 18:29:23 +0200 Subject: [PATCH 026/120] Catalog: pirates-faction apply lands the Pirates and eight emoji-named products MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator approved the dry run as shown (ruling pirates-record-apply). Dry run output: New faction "Pirates": influence 3, names "pleiades", allegiance 1.0 to 12 factions. colours: primary (0.42, 0.40, 0.37) primer grey, secondary (1.00, 0.45, 0.05) hazard orange A loose coalition of crews and hidden docks moving people and goods outside official control. No command, no common law: possession has a maintenance schedule. New products (8): 🫳🔫 [U+1FAF3 U+1F52B] design WeaponItemData "Autocannon" (made by Zhestokost), roles [] "Serial number filed off. Still shoots." 🪦🤠 [U+1FAA6 U+1F920] design WeaponItemData "Earp" (made by AU), roles [barrel, feed mechanism] "The previous owner had no further use for it." 🧾🚫 [U+1F9FE U+1F6AB] design WeaponItemData "FastBlast+-" (made by NiteLife), roles [focusing array, power coupling] "Warranty void where prohibited, which is everywhere." 🎨🙈 [U+1F3A8 U+1F648] design WeaponItemData "6k Shooter" (made by Zhestokost), roles [barrel, feed mechanism] "The logo underneath is somebody else's problem." 🔥 [U+1F525] design WeaponItemData "scorched void policy" (made by DME), roles [guidance system, thruster, warhead] "Turns out we did start the fire after all." 👀🎯 [U+1F440 U+1F3AF] design GearData "Targeting Computer" (made by Finch, Lucent, NiteLife), roles [processor, array] "Somebody else's eyes. They still work." 🔑🏃💨 [U+1F511 U+1F3C3 U+1F4A8] design GearData "Small Drive" (made by Lightsail), roles [injector, nozzle] "Hotwired. Do not ask about the ignition." 📦🤫 [U+1F4E6 U+1F92B] design CargoBayData "Store-All Plus" (made by Lightsail), roles [] "Contents not as declared." Dry run. Pass "apply" to land the Pirates faction and 8 products. Co-Authored-By: Claude Sonnet 5.5 --- GameData/Aetheria.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GameData/Aetheria.cc b/GameData/Aetheria.cc index 96135d923..a68ed245a 100644 --- a/GameData/Aetheria.cc +++ b/GameData/Aetheria.cc @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63250cf21ae9abeec657f0603336bb11b4d50b66560903ef45a8cceb01a7ae25 -size 12703489 +oid sha256:ea087d34398654c2373437de273eb113dc2f60374486561abf9e4f51df91e46d +size 12706863 From 4d64966d0499ce4e9204315c5a86190c4e974d3f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 18:41:29 +0200 Subject: [PATCH 027/120] Sim speed: the drawn turn goes the way the step turned; the history test fires both ships The first verification showed the rotation sense wrong and no shot resolving in 30 s of Duel; both tests now pin what they claim. Unverified at this commit. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/DrawAhead.cs | 2 +- tests/Aetheria.Shared.Tests/SimClockTests.cs | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Assets/Scripts/ServerShared/DrawAhead.cs b/Assets/Scripts/ServerShared/DrawAhead.cs index da4e7d94a..3cd1a4ea1 100644 --- a/Assets/Scripts/ServerShared/DrawAhead.cs +++ b/Assets/Scripts/ServerShared/DrawAhead.cs @@ -20,7 +20,7 @@ public static class DrawAhead var u = float3(q.x, q.y, q.z); var up = float3(0, 1, 0); up += 2 * (q.w * cross(u, up) + cross(u, cross(u, up))); - var half = -ship.TurnRate * lead * .5f; + var half = ship.TurnRate * lead * .5f; var s = sin(half); var t = new quaternion(up.x * s, up.y * s, up.z * s, cos(half)); // Hamilton product t * q: CultMath carries no quaternion product (a gap in CultMath, not filled here). diff --git a/tests/Aetheria.Shared.Tests/SimClockTests.cs b/tests/Aetheria.Shared.Tests/SimClockTests.cs index 0fdc55d81..11cbc7a30 100644 --- a/tests/Aetheria.Shared.Tests/SimClockTests.cs +++ b/tests/Aetheria.Shared.Tests/SimClockTests.cs @@ -125,6 +125,8 @@ public sealed partial class RunStartTests _items.Random = new CultMath.Random(0xC0FFEE); launched.staged.Player.MovementDirection = float2(0, 1); launched.staged.Player.Turn = .5f; + foreach (var ship in launched.arena.Entities.OfType()) + ship.SetTarget(launched.arena.Entities.OfType().First(other => other != ship)); return (launched.arena, launched.staged.Player); } @@ -161,6 +163,12 @@ public sealed partial class RunStartTests for (var frame = 0; history.Count < HistorySteps; frame++) clock.Advance(frames[frame % frames.Length], dt => { + // Both ships open fire every 90th step, on the step count, so the fight is the same fight whatever + // the frames were. + if (history.Count % 90 == 0) + foreach (var ship in zone.Entities.OfType()) + foreach (var weapon in ship.GetBehaviors()) + FireControl.Fire(weapon, weapon.Item, ship); zone.Update(dt); history.Add(Snapshot(zone, shots)); }); @@ -175,7 +183,7 @@ public sealed partial class RunStartTests var first = History(LaunchDuel().arena, new[] { ClockFrame }); var second = History(LaunchDuel().arena, new[] { ClockFrame }); Assert.Equal(first, second); - Assert.Contains(first, line => !line.EndsWith("shots ")); + Assert.Contains(first, line => !line.EndsWith("|shots ")); // shots were fired and resolved } [Fact] @@ -202,7 +210,7 @@ public sealed partial class RunStartTests var before = player.Direction; arena.Update(ClockFrame); var angle = SignedTurn(before, player.Direction); - Assert.Equal(abs(angle), abs(player.TurnRate * ClockFrame), 5); + Assert.InRange(abs(abs(angle) - abs(player.TurnRate * ClockFrame)), 0f, 1e-6f); // A positive turn demand turns clockwise, and TurnRate is signed the way the step turned Direction. if (abs(player.TurnRate) > 1e-4f) { @@ -257,6 +265,9 @@ public sealed partial class RunStartTests arena.Update(ClockFrame); Assert.True(abs(player.TurnRate - rate) < 1e-3f * abs(rate), "the turn is steady"); var next = player.Rotation; - Assert.Equal(1f, abs(drawn.x * next.x + drawn.y * next.y + drawn.z * next.z + drawn.w * next.w), 4); + var error = 2 * acos(min(1f, abs(drawn.x * next.x + drawn.y * next.y + drawn.z * next.z + drawn.w * next.w))); + var turn = abs(rate * ClockFrame); + Assert.True(turn > .005f, "the step turns the ship enough to tell its sense"); + Assert.True(error < .2f * turn, $"drawn one step ahead is where the sim puts the pose next (off by {error} of a {turn} turn)"); } } From 4e1b4882d470d3989aa1ea28e2887fe17e8310f8 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 18:43:39 +0200 Subject: [PATCH 028/120] Add the emoji font pipeline: pinned Twemoji fetch, font builder, rendering check tools/emoji/fetch_emoji_font.py builds Assets/Fonts/Emoji/Twemoji.ttf from the pinned jdecked/twemoji release with nanoemoji (glyf_colr_0); EmojiFont.Build is the one writer of Emoji.asset and TMP Settings' emoji routing; EmojiCheck.Run renders every single-code-point emoji and the Pirates names through TMP. The generated font, lock and asset land in the next commit. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/Editor/EmojiCheck.cs | 118 ++++++++++++++++++++++ Assets/Scripts/Editor/EmojiCheck.cs.meta | 2 + Assets/Scripts/Editor/EmojiFont.cs | 74 ++++++++++++++ Assets/Scripts/Editor/EmojiFont.cs.meta | 2 + tools/emoji/fetch_emoji_font.py | 122 +++++++++++++++++++++++ tools/emoji/requirements.txt | 23 +++++ 6 files changed, 341 insertions(+) create mode 100644 Assets/Scripts/Editor/EmojiCheck.cs create mode 100644 Assets/Scripts/Editor/EmojiCheck.cs.meta create mode 100644 Assets/Scripts/Editor/EmojiFont.cs create mode 100644 Assets/Scripts/Editor/EmojiFont.cs.meta create mode 100644 tools/emoji/fetch_emoji_font.py create mode 100644 tools/emoji/requirements.txt diff --git a/Assets/Scripts/Editor/EmojiCheck.cs b/Assets/Scripts/Editor/EmojiCheck.cs new file mode 100644 index 000000000..3623587ce --- /dev/null +++ b/Assets/Scripts/Editor/EmojiCheck.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using TMPro; +using UnityEditor; +using UnityEngine; + +// Every single-code-point emoji the pinned font set draws, and the Pirates product names, render through an +// ordinary TMP text on the game's body font with no missing code point and with ink in the emoji atlas. +// Run as -executeMethod EmojiCheck.Run (exit 1 on failure in batchmode, as EngineAssetCheck). Corpus: +// tools/emoji/emoji-singles.txt; -emojiLimit N spreads N entries evenly across it for the spike. +public static class EmojiCheck +{ + const string BodyFontPath = "Assets/Fonts/Ubuntu/Ubuntu-L Small SDF.asset"; + const int Chunk = 64; + + // The eight Pirates products (addenda "The Pirates record r2"), written as code points. + static readonly string[] PiratesNames = + { + "\U0001FAF3\U0001F52B", "\U0001FAA6\U0001F920", "\U0001F9FE\U0001F6AB", "\U0001F3A8\U0001F648", + "\U0001F525", "\U0001F440\U0001F3AF", "\U0001F511\U0001F3C3\U0001F4A8", "\U0001F4E6\U0001F92B", + }; + + [MenuItem("Aetheria/Text/Check Emoji Rendering")] + public static void Run() + { + var failures = new List(); + var summary = RunCheck(failures); + foreach (var failure in failures.Take(20)) Debug.LogError(failure); + Debug.Log($"EmojiCheck: {summary}; {failures.Count} failure(s)."); + if (Application.isBatchMode) EditorApplication.Exit(failures.Count == 0 ? 0 : 1); + } + + static string RunCheck(List failures) + { + var emoji = AssetDatabase.LoadAssetAtPath(EmojiFont.AssetPath); + var body = AssetDatabase.LoadAssetAtPath(BodyFontPath); + if (emoji == null || body == null) + { + failures.Add("EmojiCheck: Emoji.asset or the body font is missing; run EmojiFont.Build first."); + return "nothing checked"; + } + + var corpus = LoadSingles(); + var missing = new HashSet(); + TMP_Text.MissingCharacterEventCallback onMissing = (unicode, index, source, asset, component) => missing.Add(unicode); + TMP_Text.OnMissingCharacter += onMissing; + + var canvas = new GameObject("EmojiCheck", typeof(Canvas)); + var text = new GameObject("Text", typeof(RectTransform)).AddComponent(); + text.transform.SetParent(canvas.transform, false); + text.font = body; + text.textWrappingMode = TextWrappingModes.NoWrap; + text.rectTransform.sizeDelta = new Vector2(100000, 100000); + + int viaEmoji = 0, viaOther = 0, blank = 0; + try + { + void Render(string content, string label) + { + text.text = content; + text.ForceMeshUpdate(); + for (var i = 0; i < text.textInfo.characterCount; i++) + { + var info = text.textInfo.characterInfo[i]; + if (info.character == ' ' || info.elementType != TMP_TextElementType.Character) continue; + if (info.fontAsset == emoji) viaEmoji++; else viaOther++; + if (info.fontAsset == emoji && info.textElement.glyph.glyphRect.width <= 0) + { + blank++; + failures.Add($"blank glyph U+{(int)info.character:X4} in {label}"); + } + } + } + + for (var i = 0; i < corpus.Count; i += Chunk) + { + var slice = corpus.Skip(i).Take(Chunk).ToList(); + Render(string.Join(" ", slice.Select(char.ConvertFromUtf32)), $"chunk {i}"); + } + foreach (var name in PiratesNames) Render(name, "Pirates name"); + } + finally + { + TMP_Text.OnMissingCharacter -= onMissing; + UnityEngine.Object.DestroyImmediate(canvas); + } + + foreach (var cp in missing.OrderBy(c => c)) failures.Add($"missing code point U+{cp:X4}"); + + long ink = 0, colour = 0; + foreach (var page in emoji.atlasTextures) + { + var pixels = page.GetPixels32(); + ink += pixels.LongCount(p => p.a != 0); + colour += pixels.LongCount(p => p.a != 0 && (p.r != p.g || p.g != p.b)); + } + if (ink == 0) failures.Add("the emoji atlas holds no ink after rendering"); + else if (colour == 0) failures.Add("the emoji atlas holds no coloured pixel: the glyphs rendered monochrome"); + + // Dynamic atlas content is cache: drop it so this check leaves the asset as Build wrote it. + emoji.ClearFontAssetData(true); + return $"checked {corpus.Count} single code points + {PiratesNames.Length} Pirates names, {missing.Count} missing, " + + $"{viaEmoji} glyphs via the emoji asset, {viaOther} via other fonts, {blank} blank, {ink} atlas ink pixels of which {colour} coloured"; + } + + static List LoadSingles() + { + var path = Path.Combine(Directory.GetParent(Application.dataPath).FullName, "tools", "emoji", "emoji-singles.txt"); + var all = File.ReadAllLines(path).Where(l => l.Length > 0).Select(l => Convert.ToInt32(l, 16)).ToList(); + var args = Environment.GetCommandLineArgs(); + var at = Array.IndexOf(args, "-emojiLimit"); + if (at < 0 || at + 1 >= args.Length) return all; + var limit = int.Parse(args[at + 1]); + return Enumerable.Range(0, limit).Select(i => all[i * all.Count / limit]).ToList(); + } +} diff --git a/Assets/Scripts/Editor/EmojiCheck.cs.meta b/Assets/Scripts/Editor/EmojiCheck.cs.meta new file mode 100644 index 000000000..945280e85 --- /dev/null +++ b/Assets/Scripts/Editor/EmojiCheck.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c6f940add256a754daac8460fc79480d \ No newline at end of file diff --git a/Assets/Scripts/Editor/EmojiFont.cs b/Assets/Scripts/Editor/EmojiFont.cs new file mode 100644 index 000000000..855bece3a --- /dev/null +++ b/Assets/Scripts/Editor/EmojiFont.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using TMPro; +using UnityEditor; +using UnityEngine; +using UnityEngine.TextCore.LowLevel; + +// The one writer of the emoji font asset and of TMP Settings' emoji routing. The font file is the +// output of tools/emoji/fetch_emoji_font.py; the asset is a dynamic COLOR font asset whose atlas pages +// are cache, filled on first use at runtime and cleared here. Every TMP text reaches emoji through +// TMP Settings' emoji fallback and global fallback list; no component names an emoji font. +public static class EmojiFont +{ + public const string FontPath = "Assets/Fonts/Emoji/Twemoji.ttf"; + public const string AssetPath = "Assets/Fonts/Emoji/Emoji.asset"; + + // Colour outlines (COLRv0) are rasterised at one size; 109 is the strike size a CBDT set needs. + const int SamplingPointSize = 64; + const int AtlasPadding = 4; + const int AtlasSize = 2048; + + // Menu entry for interactive use; -executeMethod EmojiFont.Build for batchmode, exit 1 on failure. + [MenuItem("Aetheria/Text/Build Emoji Font")] + public static void Build() + { + var ok = TryBuild(out var message); + if (ok) Debug.Log(message); else Debug.LogError(message); + if (Application.isBatchMode) EditorApplication.Exit(ok ? 0 : 1); + } + + static bool TryBuild(out string message) + { + var font = AssetDatabase.LoadAssetAtPath(FontPath); + if (font == null) + { + message = $"EmojiFont: {FontPath} is missing; run tools/emoji/fetch_emoji_font.py."; + return false; + } + + // An existing asset keeps its GUID, so a second Build changes nothing the settings point at. + var asset = AssetDatabase.LoadAssetAtPath(AssetPath); + if (asset == null) + { + asset = TMP_FontAsset.CreateFontAsset(font, SamplingPointSize, AtlasPadding, GlyphRenderMode.COLOR, + AtlasSize, AtlasSize, AtlasPopulationMode.Dynamic, enableMultiAtlasSupport: true); + if (asset == null) + { + message = $"EmojiFont: TMP could not create a font asset from {FontPath}."; + return false; + } + asset.name = "Emoji"; + AssetDatabase.CreateAsset(asset, AssetPath); + foreach (var page in asset.atlasTextures) + { + page.name = "Emoji Atlas"; + AssetDatabase.AddObjectToAsset(page, asset); + } + asset.material.name = "Emoji Atlas Material"; + AssetDatabase.AddObjectToAsset(asset.material, asset); + } + else asset.ClearFontAssetData(true); + + // Text-presentation emoji (U+263A without FE0F) are not routed through the emoji list, so the + // asset is also the global fallback. + TMP_Settings.emojiFallbackTextAssets = new List { asset }; + TMP_Settings.fallbackFontAssets = new List { asset }; + TMP_Settings.defaultSpriteAsset = null; + + EditorUtility.SetDirty(asset); + EditorUtility.SetDirty(TMP_Settings.instance); + AssetDatabase.SaveAssets(); + message = "EmojiFont: built " + AssetPath + " and routed TMP Settings' emoji fallback to it."; + return true; + } +} diff --git a/Assets/Scripts/Editor/EmojiFont.cs.meta b/Assets/Scripts/Editor/EmojiFont.cs.meta new file mode 100644 index 000000000..5888665c0 --- /dev/null +++ b/Assets/Scripts/Editor/EmojiFont.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: bd3a902defe23f14fbe821ca9fb26a08 \ No newline at end of file diff --git a/tools/emoji/fetch_emoji_font.py b/tools/emoji/fetch_emoji_font.py new file mode 100644 index 000000000..77e03e6f5 --- /dev/null +++ b/tools/emoji/fetch_emoji_font.py @@ -0,0 +1,122 @@ +"""Build Assets/Fonts/Emoji/Twemoji.ttf from the pinned Twemoji release. + +Run with the venv that has tools/emoji/requirements.txt installed: + py -3 tools/emoji/fetch_emoji_font.py [--cache DIR] [--jobs N] + +Downloads the jdecked/twemoji release source archive (sha256 pinned below), runs nanoemoji +(glyf_colr_0) over its SVGs, and writes the font, the licence text, the single-code-point corpus +the editor smoke reads, and emoji-font.lock. The font is the only artifact Unity consumes; nothing +here is hand-made art. A second run with the same pins must reproduce the font's sha256. +""" +import argparse, hashlib, os, re, shutil, subprocess, sys, tarfile, tempfile, urllib.request +from importlib import metadata +from pathlib import Path + +TWEMOJI_VERSION = "v17.0.3" +TWEMOJI_URL = "https://github.com/jdecked/twemoji/archive/refs/tags/v17.0.3.tar.gz" +TWEMOJI_SHA256 = "a0855654b633045ae2337537e77f1bb4361162f7fcd910e613eaab1d6d9c5fca" +# 2026-06-01T22:21:37Z, the release's publication time: fixes head.created/modified for reproducibility. +SOURCE_DATE_EPOCH = "1780352497" +FAMILY = "Twemoji" + +ROOT = Path(__file__).resolve().parents[2] +FONT_DIR = ROOT / "Assets" / "Fonts" / "Emoji" +SINGLES = ROOT / "tools" / "emoji" / "emoji-singles.txt" + + +def sha256(path): + h = hashlib.sha256() + with open(path, "rb") as f: + for block in iter(lambda: f.read(1 << 20), b""): + h.update(block) + return h.hexdigest() + + +def download(cache): + archive = cache / f"twemoji-{TWEMOJI_VERSION}.tar.gz" + if not archive.exists(): + cache.mkdir(parents=True, exist_ok=True) + urllib.request.urlretrieve(TWEMOJI_URL, archive) + got = sha256(archive) + if got != TWEMOJI_SHA256: + archive.unlink() + sys.exit(f"archive sha256 {got} != pinned {TWEMOJI_SHA256}") + return archive + + +def licence_text(root): + readme = (root / "README.md").read_text(encoding="utf-8") + attribution = re.search(r"## Attribution Requirements\n\n(.*?)\n\n## ", readme, re.S).group(1) + graphics = re.search(r"Graphics licensed under CC-BY 4.0: \S+", readme).group(0) + copyright_lines = [l for l in (root / "LICENSE").read_text(encoding="utf-8").splitlines() if l.startswith("Copyright")] + return ( + f"Twemoji {TWEMOJI_VERSION}: {TWEMOJI_URL}\n" + + "\n".join(copyright_lines) + "\n" + graphics + "\n\n" + "Change notice (CC BY 4.0 section 3(a)(1)(B)): the SVG graphics are converted to a COLRv0 colour\n" + "font (Twemoji.ttf) by tools/emoji/fetch_emoji_font.py using nanoemoji. No artwork is edited.\n\n" + "Attribution Requirements (upstream README)\n\n" + attribution + "\n\n" + "LICENSE-GRAPHICS (upstream, verbatim)\n" + "=======================================================================\n\n" + + (root / "LICENSE-GRAPHICS").read_text(encoding="utf-8") + ) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--cache", type=Path, default=Path(tempfile.gettempdir()) / "aetheria-emoji-cache") + ap.add_argument("--jobs", type=int, default=max(1, min(4, (os.cpu_count() or 2) // 2))) + args = ap.parse_args() + + archive = download(args.cache) + work = args.cache / f"build-{TWEMOJI_VERSION}" + if work.exists(): + shutil.rmtree(work) + work.mkdir(parents=True) + with tarfile.open(archive) as tar: + tar.extractall(work / "src", filter="data") + src = next((work / "src").iterdir()) + svgs = sorted((src / "assets" / "svg").glob("*.svg")) + + env = dict(os.environ, SOURCE_DATE_EPOCH=SOURCE_DATE_EPOCH) + # nanoemoji shells out to ninja and picosvg by name; they live beside this interpreter. + env["PATH"] = str(Path(sys.executable).parent) + os.pathsep + env["PATH"] + build = work / "build" + # 4000 paths overflow the Windows command line, so the inputs travel in an absl flagfile. + flagfile = work / "inputs.flags" + flagfile.write_text("".join(f"{p.as_posix()}\n" for p in svgs), encoding="utf-8") + run = [sys.executable, "-c", "from nanoemoji.nanoemoji import main; main()", "--color_format", "glyf_colr_0", + "--family", FAMILY, "--output_file", f"{FAMILY}.ttf", "--build_dir", str(build), "--noexec_ninja", + f"--flagfile={flagfile}"] + subprocess.run(run, check=True, env=env, cwd=work) + subprocess.run(["ninja", "-j", str(args.jobs), "-C", str(build)], check=True, env=env) + + FONT_DIR.mkdir(parents=True, exist_ok=True) + out = FONT_DIR / f"{FAMILY}.ttf" + shutil.copyfile(build / f"{FAMILY}.ttf", out) + (FONT_DIR / "LICENSE.txt").write_text(licence_text(src), encoding="utf-8", newline="\n") + + # The smoke's corpus: every single-code-point glyph the pinned release draws. + singles = sorted(p.stem for p in svgs if re.fullmatch(r"[0-9a-f]{4,6}", p.stem)) + SINGLES.write_text("".join(f"{s}\n" for s in singles), encoding="utf-8", newline="\n") + + tools = {n: metadata.version(n) for n in ("nanoemoji", "fonttools", "picosvg", "ninja")} + lock = [ + f"source_url={TWEMOJI_URL}", f"source_version={TWEMOJI_VERSION}", f"source_sha256={TWEMOJI_SHA256}", + f"svg_count={len(svgs)}", f"single_codepoint_count={len(singles)}", + *(f"tool_{n}={v}" for n, v in tools.items()), + f"source_date_epoch={SOURCE_DATE_EPOCH}", "color_format=glyf_colr_0", + f"font_file={out.name}", f"font_sha256={sha256(out)}", f"singles_sha256={sha256(SINGLES)}", + ] + previous = (FONT_DIR / "emoji-font.lock") + if previous.exists(): + old = dict(l.split("=", 1) for l in previous.read_text().splitlines() if "=" in l) + if old.get("font_sha256") != sha256(out): + print(f"NOT REPRODUCED: font_sha256 {sha256(out)} != lock {old.get('font_sha256')}") + else: + print("reproduced: font_sha256 equals the lock") + previous.write_text("\n".join(lock) + "\n", encoding="utf-8", newline="\n") + print("\n".join(lock)) + + +if __name__ == "__main__": + main() diff --git a/tools/emoji/requirements.txt b/tools/emoji/requirements.txt new file mode 100644 index 000000000..955d3e31b --- /dev/null +++ b/tools/emoji/requirements.txt @@ -0,0 +1,23 @@ +# Pinned toolchain for fetch_emoji_font.py (py -3 -m venv, then pip install -r this file). +# nanoemoji is the spec's tool; the rest is its resolved dependency set at 0.16.0. +absl-py==2.5.0 +attrs==26.1.0 +booleanOperations==0.10.0 +cffsubr==0.4.0 +fontMath==0.10.0 +fonttools==4.66.1 +lxml==6.1.3 +nanoemoji==0.16.0 +ninja==1.13.2 +picosvg==0.23.0 +pillow==12.3.0 +pngquant-cli==3.0.3 +pyclipper==1.4.0 +regex==2026.9.29 +resvg-cli==0.44.0 +skia-pathops==0.9.2 +toml==0.10.2 +typing_extensions==4.16.0 +ufo2ft==3.9.1 +ufoLib2==0.19.0 +zopfli==0.4.3 From 8cb53049aa81c2ba724bec4c44754ff455dc61f9 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 18:53:18 +0200 Subject: [PATCH 029/120] PiratesFactionCommandTests: strip the copy through a shipped-assembly registry, then seed the test global AetherDb.Open demands Co-Authored-By: Claude Sonnet 5.5 --- .../PiratesFactionCommandTests.cs | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/PiratesFactionCommandTests.cs b/tests/Aetheria.Shared.Tests/PiratesFactionCommandTests.cs index f5b0d5851..feed84228 100644 --- a/tests/Aetheria.Shared.Tests/PiratesFactionCommandTests.cs +++ b/tests/Aetheria.Shared.Tests/PiratesFactionCommandTests.cs @@ -5,6 +5,9 @@ using System; using System.IO; using System.Linq; +using System.Reflection; +using GameCult.Caching; +using GameCult.Caching.MessagePack; using Xunit; // AetherDb's pirates-faction command (aetheria-release, cut pirates-record), run on a scratch copy of the shipped catalog @@ -18,13 +21,28 @@ public sealed class PiratesFactionCommandTests : IDisposable { Directory.CreateDirectory(Path.Combine(_root, "GameData")); File.Copy(Path.Combine(AetherDb.FindRoot(), "GameData", "Aetheria.cc"), CatalogPath); - var db = AetherDb.Open(catalogWritable: true, root: _root); - var pirates = db.Cache.GetAll().Single(faction => faction.Name == "Pirates"); - var ours = db.Cache.RefOf(pirates); - foreach (var product in db.Cache.GetAll().Where(product => product.Manufacturer.Equals(ours)).ToArray()) - Assert.True(db.Cache.Remove(db.Cache.RefOf(product).Key)); - Assert.True(db.Cache.Remove(ours.Key)); - db.Cache.Dispose(); + // AetherDb.Open composes the process-wide registry, which in this test host also sees this assembly's TestCatalogGlobal + // (see FireControlCut7Tests). So the copy is stripped through a registry scoped to the shipped assembly and then given + // the record that registry's shared sibling demands, as AetheriaStoresTests does. + var registry = CultDocumentRegistry.ForTypes(typeof(ItemData).Assembly.GetTypes() + .Where(t => t is { IsAbstract: false, IsInterface: false }) + .Where(t => t.GetCustomAttribute() != null)); + using (var cache = new CultCache(registry)) + { + cache.AddBackingStore(new SingleFileMessagePackBackingStore(CatalogPath), AetheriaStores.CatalogTypes); + var pirates = cache.GetAll().Single(faction => faction.Name == "Pirates"); + var ours = cache.RefOf(pirates); + foreach (var product in cache.GetAll().Where(product => product.Manufacturer.Equals(ours)).ToArray()) + Assert.True(cache.Remove(cache.RefOf(product).Key)); + Assert.True(cache.Remove(ours.Key)); + cache.FlushAsync().Wait(); + } + using (var cache = new CultCache()) + { + cache.AddBackingStore(new SingleFileMessagePackBackingStore(CatalogPath), AetheriaStores.CatalogTypes); + cache.Upsert(new TestCatalogGlobal { Name = "Temperament" }); + cache.FlushAsync().Wait(); + } } public void Dispose() => Directory.Delete(_root, true); From ec081b9b6d2cd1deef343c8620b25e244d15b141 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 19:04:09 +0200 Subject: [PATCH 030/120] Brand: ItemData.Get already refuses an unset ref; drop the duplicate IsSet guard Stryker's one survivor in the first sweep was the guard's `true ?` mutant: the cache's Get(ref) extension already returns null for an unset reference, so the guard was a second path. Adds the removed-product fact the spec names. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/ItemManager.cs | 2 +- tests/Aetheria.Shared.Tests/ItemTitleTests.cs | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Assets/Scripts/ServerShared/ItemManager.cs b/Assets/Scripts/ServerShared/ItemManager.cs index d47c6aaf8..d9c42d02a 100644 --- a/Assets/Scripts/ServerShared/ItemManager.cs +++ b/Assets/Scripts/ServerShared/ItemManager.cs @@ -216,7 +216,7 @@ public class ItemManager }; if (!maker.IsSet()) return (null, null); - var product = lot.Product.IsSet() ? ItemData.Get(lot.Product) : null; + var product = ItemData.Get(lot.Product); return (ItemData.Get(maker), product); } diff --git a/tests/Aetheria.Shared.Tests/ItemTitleTests.cs b/tests/Aetheria.Shared.Tests/ItemTitleTests.cs index d6c57ae2d..b54a90b0a 100644 --- a/tests/Aetheria.Shared.Tests/ItemTitleTests.cs +++ b/tests/Aetheria.Shared.Tests/ItemTitleTests.cs @@ -92,6 +92,18 @@ public sealed class ItemTitleTests : IDisposable Assert.Equal("Array", _items.Title(unit)); } + [Fact] + public void A_lot_whose_product_left_the_catalog_brands_its_maker_only() + { + var lot = _items.CreateLot(_panopticon); + _items.Lots[lot].Product = new CultRecordRef(new CultRecordKey("removed-product")); + var unit = (CraftedItemInstance) _items.CreateInstance(lot); + var (maker, product) = _items.Brand(unit); + Assert.Equal("Finch", maker.Name); + Assert.Null(product); + Assert.Equal("Array", _items.Title(unit)); + } + [Fact] public void Unbranded_units_are_titled_by_design() { From b385c65194395fc837f9d02fcfe0d539d87c25e3 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 19:15:37 +0200 Subject: [PATCH 031/120] PiratesFactionCommandTests: pin the faction's fields, the eight products' designs, descriptions and roles, and the missing name file Co-Authored-By: Claude Sonnet 5.5 --- .../PiratesFactionCommandTests.cs | 50 ++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/PiratesFactionCommandTests.cs b/tests/Aetheria.Shared.Tests/PiratesFactionCommandTests.cs index feed84228..e9d321e20 100644 --- a/tests/Aetheria.Shared.Tests/PiratesFactionCommandTests.cs +++ b/tests/Aetheria.Shared.Tests/PiratesFactionCommandTests.cs @@ -71,8 +71,42 @@ public sealed class PiratesFactionCommandTests : IDisposable Assert.Equal(0, Program.PiratesFaction(apply: true, root: _root)); var landed = File.ReadAllBytes(CatalogPath); var (pirates, products) = Read(); - Assert.Single(pirates); - Assert.Equal(8, products.Length); + var faction = Assert.Single(pirates); + Assert.Equal("Pirates", faction.ShortName); + Assert.StartsWith("A loose coalition of crews and hidden docks", faction.Description); + Assert.Equal(3, faction.InfluenceDistance); + Assert.Equal(new CultMath.float3(.42f, .40f, .37f), faction.PrimaryColor); + Assert.Equal(new CultMath.float3(1f, .45f, .05f), faction.SecondaryColor); + Assert.Equal(12, faction.Allegiance.Count); + Assert.All(faction.Allegiance.Values, value => Assert.Equal(1f, value)); + + var db = AetherDb.Open(root: _root); + try + { + Assert.Equal("pleiades", db.Cache.Get(faction.GeonameFile).Name); + var expected = new (string Design, string Name, string Description)[] + { + ("Autocannon", "🫳🔫", "Serial number filed off. Still shoots."), + ("Earp", "🪦🤠", "The previous owner had no further use for it."), + ("FastBlast+-", "🧾🚫", "Warranty void where prohibited, which is everywhere."), + ("6k Shooter", "🎨🙈", "The logo underneath is somebody else's problem."), + ("scorched void policy", "🔥", "Turns out we did start the fire after all."), + ("Targeting Computer", "👀🎯", "Somebody else's eyes. They still work."), + ("Small Drive", "🔑🏃💨", "Hotwired. Do not ask about the ignition."), + ("Store-All Plus", "📦🤫", "Contents not as declared."), + }; + Assert.Equal(expected.Select(e => e.Name).OrderBy(n => n, StringComparer.Ordinal), products.Select(p => p.Name).OrderBy(n => n, StringComparer.Ordinal)); + foreach (var (designName, name, description) in expected) + { + var product = products.Single(p => p.Name == name); + var design = (CraftedItemData) db.Cache.Get(product.Design); + Assert.Equal(designName, design.Name); + Assert.Equal(description, product.Description); + Assert.Equal(design.Roles.Select(role => role.Name), product.Roles.Select(role => role.Role)); + Assert.All(product.Roles, role => { Assert.Equal(.45f, role.Mean); Assert.Equal(.22f, role.StandardDeviation); }); + } + } + finally { db.Cache.Dispose(); } // A second apply finds the faction and changes nothing. Assert.Equal(0, Program.PiratesFaction(apply: true, root: _root)); @@ -104,4 +138,16 @@ public sealed class PiratesFactionCommandTests : IDisposable Assert.Throws(() => Program.PiratesFaction(apply: true, root: _root)); Assert.Empty(Read().Pirates); } + + [Fact] + public void A_missing_name_file_refuses_and_lands_nothing() + { + var db = AetherDb.Open(catalogWritable: true, root: _root); + var pleiades = db.Cache.GetAll().Single(file => file.Name == "pleiades"); + Assert.True(db.Cache.Remove(db.Cache.RefOf(pleiades).Key)); + db.Cache.Dispose(); + + Assert.Throws(() => Program.PiratesFaction(apply: true, root: _root)); + Assert.Empty(Read().Pirates); + } } From 38f7d73823843eca414a34d514269c2bde96db0b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 19:16:10 +0200 Subject: [PATCH 032/120] Sim speed: Entity.TurnRate's comment names no presenter Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/Entity.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Assets/Scripts/ServerShared/Entity.cs b/Assets/Scripts/ServerShared/Entity.cs index a1eb14c49..da315d873 100644 --- a/Assets/Scripts/ServerShared/Entity.cs +++ b/Assets/Scripts/ServerShared/Entity.cs @@ -29,7 +29,7 @@ public abstract class Entity public float2 Velocity; // Rad per sim second, signed as the step turns Direction: the sum of this step's rotation actuators' applied // angle over the step. Runtime only, zeroed at the start of Update, so a staged, loaded or wormhole write to - // Direction is never a turn. Read only by DrawAhead. + // Direction is never a turn. Read only by presenters that draw ahead of the step. public float TurnRate; // Runtime only, never saved; Entity.Update is the one writer of both. Acceleration is the planar velocity change From 1c549325f938d1ad7bdae9f9a0c122754723dbad Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 19:34:49 +0200 Subject: [PATCH 033/120] Generate the Twemoji colour font, build Emoji.asset and route TMP's emoji fallback to it Assets/Fonts/Emoji/Twemoji.ttf is fetch_emoji_font.py's output for jdecked/twemoji v17.0.3 (nanoemoji 0.16.0, glyf_colr_0), reproduced bit-identically by two independent runs on Yggdrasil; the lock pins source and font sha256. EmojiFont.Build wrote Emoji.asset (dynamic, COLOR, multi-atlas) and TMP Settings' emoji fallback list, global fallback entry and empty default sprite asset. EmojiCheck.Run: 1425 single code points and the eight Pirates names render with no missing code point. CC BY 4.0 attribution ships in Assets/Fonts/Emoji/LICENSE.txt. Builds. Co-Authored-By: Claude Opus 5.5 --- Assets/Fonts/Emoji.meta | 8 + Assets/Fonts/Emoji/Emoji.asset | 3 + Assets/Fonts/Emoji/Emoji.asset.meta | 8 + Assets/Fonts/Emoji/LICENSE.txt | 410 +++++ Assets/Fonts/Emoji/LICENSE.txt.meta | 7 + Assets/Fonts/Emoji/Twemoji.ttf | 3 + Assets/Fonts/Emoji/Twemoji.ttf.meta | 21 + Assets/Fonts/Emoji/emoji-font.lock | 14 + Assets/Fonts/Emoji/emoji-font.lock.meta | 7 + Assets/Scripts/Editor/EmojiCheck.cs | 5 +- Assets/Scripts/Editor/EmojiFont.cs | 4 +- .../TextMesh Pro/Resources/TMP Settings.asset | 4 +- tools/emoji/emoji-singles.txt | 1425 +++++++++++++++++ 13 files changed, 1915 insertions(+), 4 deletions(-) create mode 100644 Assets/Fonts/Emoji.meta create mode 100644 Assets/Fonts/Emoji/Emoji.asset create mode 100644 Assets/Fonts/Emoji/Emoji.asset.meta create mode 100644 Assets/Fonts/Emoji/LICENSE.txt create mode 100644 Assets/Fonts/Emoji/LICENSE.txt.meta create mode 100644 Assets/Fonts/Emoji/Twemoji.ttf create mode 100644 Assets/Fonts/Emoji/Twemoji.ttf.meta create mode 100644 Assets/Fonts/Emoji/emoji-font.lock create mode 100644 Assets/Fonts/Emoji/emoji-font.lock.meta create mode 100644 tools/emoji/emoji-singles.txt diff --git a/Assets/Fonts/Emoji.meta b/Assets/Fonts/Emoji.meta new file mode 100644 index 000000000..442fb19cd --- /dev/null +++ b/Assets/Fonts/Emoji.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5e0ef88f85924e146855ca7bb57bbdb6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Fonts/Emoji/Emoji.asset b/Assets/Fonts/Emoji/Emoji.asset new file mode 100644 index 000000000..b37478ebd --- /dev/null +++ b/Assets/Fonts/Emoji/Emoji.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0da0ff045c78def703de64f1c79a0f6c0842985e21c261b165ada948dc726b1e +size 5791 diff --git a/Assets/Fonts/Emoji/Emoji.asset.meta b/Assets/Fonts/Emoji/Emoji.asset.meta new file mode 100644 index 000000000..f89bc04be --- /dev/null +++ b/Assets/Fonts/Emoji/Emoji.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4cbb1005dc560d54ba85c3d091dbe30b +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Fonts/Emoji/LICENSE.txt b/Assets/Fonts/Emoji/LICENSE.txt new file mode 100644 index 000000000..db961e007 --- /dev/null +++ b/Assets/Fonts/Emoji/LICENSE.txt @@ -0,0 +1,410 @@ +Twemoji v17.0.3: https://github.com/jdecked/twemoji/archive/refs/tags/v17.0.3.tar.gz +Copyright (c) 2022–present Jason Sofonia & Justine De Caires +Copyright (c) 2014–2021 Twitter +Graphics licensed under CC-BY 4.0: + +Change notice (CC BY 4.0 section 3(a)(1)(B)): the SVG graphics are converted to a COLRv0 colour +font (Twemoji.ttf) by tools/emoji/fetch_emoji_font.py using nanoemoji. No artwork is edited. + +Attribution Requirements (upstream README) + +As an open source project, attribution is critical from a legal, practical and motivational perspective in our opinion. The graphics are licensed under the CC-BY 4.0 which has a pretty good guide on [best practices for attribution](https://wiki.creativecommons.org/Best_practices_for_attribution). + +However, we consider the guide a bit onerous and as a project, will accept a mention in a project README or an 'About' section or footer on a website. In mobile applications, a common place would be in the Settings/About section (for example, see the mobile Twitter application Settings->About->Legal section). We would consider a mention in the HTML/JS source sufficient also. + +LICENSE-GRAPHICS (upstream, verbatim) +======================================================================= + +Attribution 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More_considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution 4.0 International Public License ("Public License"). To the +extent this Public License may be interpreted as a contract, You are +granted the Licensed Rights in consideration of Your acceptance of +these terms and conditions, and the Licensor grants You such rights in +consideration of benefits the Licensor receives from making the +Licensed Material available under these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public licenses. +Notwithstanding, Creative Commons may elect to apply one of its public +licenses to material it publishes and in those instances will be +considered the "Licensor." Except for the limited purpose of indicating +that material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the public +licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/Assets/Fonts/Emoji/LICENSE.txt.meta b/Assets/Fonts/Emoji/LICENSE.txt.meta new file mode 100644 index 000000000..8dabd4444 --- /dev/null +++ b/Assets/Fonts/Emoji/LICENSE.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: db23d2e6bc295794888af02d32cbae60 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Fonts/Emoji/Twemoji.ttf b/Assets/Fonts/Emoji/Twemoji.ttf new file mode 100644 index 000000000..a9e0584f1 --- /dev/null +++ b/Assets/Fonts/Emoji/Twemoji.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de66290bfa98226e40d8999a3ab024c2242e8d2938f939d9a85c1494844f6aa0 +size 1805808 diff --git a/Assets/Fonts/Emoji/Twemoji.ttf.meta b/Assets/Fonts/Emoji/Twemoji.ttf.meta new file mode 100644 index 000000000..5cc7ab440 --- /dev/null +++ b/Assets/Fonts/Emoji/Twemoji.ttf.meta @@ -0,0 +1,21 @@ +fileFormatVersion: 2 +guid: bde541f3fd3b82b4aa6aa068e4a205a7 +TrueTypeFontImporter: + externalObjects: {} + serializedVersion: 4 + fontSize: 16 + forceTextureCase: -2 + characterSpacing: 0 + characterPadding: 1 + includeFontData: 1 + fontNames: + - Aetheria Emoji + fallbackFontReferences: [] + customCharacters: + fontRenderingMode: 0 + ascentCalculationMode: 1 + useLegacyBoundsCalculation: 0 + shouldRoundAdvanceValue: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Fonts/Emoji/emoji-font.lock b/Assets/Fonts/Emoji/emoji-font.lock new file mode 100644 index 000000000..de5012a8b --- /dev/null +++ b/Assets/Fonts/Emoji/emoji-font.lock @@ -0,0 +1,14 @@ +source_url=https://github.com/jdecked/twemoji/archive/refs/tags/v17.0.3.tar.gz +source_version=v17.0.3 +source_sha256=a0855654b633045ae2337537e77f1bb4361162f7fcd910e613eaab1d6d9c5fca +svg_count=4009 +single_codepoint_count=1425 +tool_nanoemoji=0.16.0 +tool_fonttools=4.66.1 +tool_picosvg=0.23.0 +tool_ninja=1.13.2 +source_date_epoch=1780352497 +color_format=glyf_colr_0 +font_file=Twemoji.ttf +font_sha256=de66290bfa98226e40d8999a3ab024c2242e8d2938f939d9a85c1494844f6aa0 +singles_sha256=d07a0693cef0530acf540cdeb60d341912f639370e7e30271e90452a8aecdc0d diff --git a/Assets/Fonts/Emoji/emoji-font.lock.meta b/Assets/Fonts/Emoji/emoji-font.lock.meta new file mode 100644 index 000000000..d59e8a3eb --- /dev/null +++ b/Assets/Fonts/Emoji/emoji-font.lock.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6290b5397f6ae1d47a19c6886ebd87e2 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Editor/EmojiCheck.cs b/Assets/Scripts/Editor/EmojiCheck.cs index 3623587ce..48afc0018 100644 --- a/Assets/Scripts/Editor/EmojiCheck.cs +++ b/Assets/Scripts/Editor/EmojiCheck.cs @@ -90,8 +90,11 @@ public static class EmojiCheck foreach (var cp in missing.OrderBy(c => c)) failures.Add($"missing code point U+{cp:X4}"); long ink = 0, colour = 0; + int pages = 0, nullPages = 0; foreach (var page in emoji.atlasTextures) { + if (page == null) { nullPages++; continue; } + pages++; var pixels = page.GetPixels32(); ink += pixels.LongCount(p => p.a != 0); colour += pixels.LongCount(p => p.a != 0 && (p.r != p.g || p.g != p.b)); @@ -102,7 +105,7 @@ public static class EmojiCheck // Dynamic atlas content is cache: drop it so this check leaves the asset as Build wrote it. emoji.ClearFontAssetData(true); return $"checked {corpus.Count} single code points + {PiratesNames.Length} Pirates names, {missing.Count} missing, " + - $"{viaEmoji} glyphs via the emoji asset, {viaOther} via other fonts, {blank} blank, {ink} atlas ink pixels of which {colour} coloured"; + $"{viaEmoji} glyphs via the emoji asset, {viaOther} via other fonts, {blank} blank, {ink} atlas ink pixels of which {colour} coloured, {pages} atlas pages ({nullPages} null)"; } static List LoadSingles() diff --git a/Assets/Scripts/Editor/EmojiFont.cs b/Assets/Scripts/Editor/EmojiFont.cs index 855bece3a..c7ce97aaf 100644 --- a/Assets/Scripts/Editor/EmojiFont.cs +++ b/Assets/Scripts/Editor/EmojiFont.cs @@ -57,7 +57,9 @@ public static class EmojiFont asset.material.name = "Emoji Atlas Material"; AssetDatabase.AddObjectToAsset(asset.material, asset); } - else asset.ClearFontAssetData(true); + + // The atlas pages are cache: Build leaves them empty, the same state EmojiCheck restores. + asset.ClearFontAssetData(true); // Text-presentation emoji (U+263A without FE0F) are not routed through the emoji list, so the // asset is also the global fallback. diff --git a/Assets/TextMesh Pro/Resources/TMP Settings.asset b/Assets/TextMesh Pro/Resources/TMP Settings.asset index 769423630..0371c3b56 100644 --- a/Assets/TextMesh Pro/Resources/TMP Settings.asset +++ b/Assets/TextMesh Pro/Resources/TMP Settings.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76affcb169f370013c97b08f1d6d3e092a054a2abf970e164cd40400799552a7 -size 1852 +oid sha256:f00aad0de57d69c610a4a421bf4652e93f45c2a39bacc76dec8a7921bf699ecb +size 1930 diff --git a/tools/emoji/emoji-singles.txt b/tools/emoji/emoji-singles.txt new file mode 100644 index 000000000..09fcc972e --- /dev/null +++ b/tools/emoji/emoji-singles.txt @@ -0,0 +1,1425 @@ +1f004 +1f0cf +1f170 +1f171 +1f17e +1f17f +1f18e +1f191 +1f192 +1f193 +1f194 +1f195 +1f196 +1f197 +1f198 +1f199 +1f19a +1f1e6 +1f1e7 +1f1e8 +1f1e9 +1f1ea +1f1eb +1f1ec +1f1ed +1f1ee +1f1ef +1f1f0 +1f1f1 +1f1f2 +1f1f3 +1f1f4 +1f1f5 +1f1f6 +1f1f7 +1f1f8 +1f1f9 +1f1fa +1f1fb +1f1fc +1f1fd +1f1fe +1f1ff +1f201 +1f202 +1f21a +1f22f +1f232 +1f233 +1f234 +1f235 +1f236 +1f237 +1f238 +1f239 +1f23a +1f250 +1f251 +1f300 +1f301 +1f302 +1f303 +1f304 +1f305 +1f306 +1f307 +1f308 +1f309 +1f30a +1f30b +1f30c +1f30d +1f30e +1f30f +1f310 +1f311 +1f312 +1f313 +1f314 +1f315 +1f316 +1f317 +1f318 +1f319 +1f31a +1f31b +1f31c +1f31d +1f31e +1f31f +1f320 +1f321 +1f324 +1f325 +1f326 +1f327 +1f328 +1f329 +1f32a +1f32b +1f32c +1f32d +1f32e +1f32f +1f330 +1f331 +1f332 +1f333 +1f334 +1f335 +1f336 +1f337 +1f338 +1f339 +1f33a +1f33b +1f33c +1f33d +1f33e +1f33f +1f340 +1f341 +1f342 +1f343 +1f344 +1f345 +1f346 +1f347 +1f348 +1f349 +1f34a +1f34b +1f34c +1f34d +1f34e +1f34f +1f350 +1f351 +1f352 +1f353 +1f354 +1f355 +1f356 +1f357 +1f358 +1f359 +1f35a +1f35b +1f35c +1f35d +1f35e +1f35f +1f360 +1f361 +1f362 +1f363 +1f364 +1f365 +1f366 +1f367 +1f368 +1f369 +1f36a +1f36b +1f36c +1f36d +1f36e +1f36f +1f370 +1f371 +1f372 +1f373 +1f374 +1f375 +1f376 +1f377 +1f378 +1f379 +1f37a +1f37b +1f37c +1f37d +1f37e +1f37f +1f380 +1f381 +1f382 +1f383 +1f384 +1f385 +1f386 +1f387 +1f388 +1f389 +1f38a +1f38b +1f38c +1f38d +1f38e +1f38f +1f390 +1f391 +1f392 +1f393 +1f396 +1f397 +1f399 +1f39a +1f39b +1f39e +1f39f +1f3a0 +1f3a1 +1f3a2 +1f3a3 +1f3a4 +1f3a5 +1f3a6 +1f3a7 +1f3a8 +1f3a9 +1f3aa +1f3ab +1f3ac +1f3ad +1f3ae +1f3af +1f3b0 +1f3b1 +1f3b2 +1f3b3 +1f3b4 +1f3b5 +1f3b6 +1f3b7 +1f3b8 +1f3b9 +1f3ba +1f3bb +1f3bc +1f3bd +1f3be +1f3bf +1f3c0 +1f3c1 +1f3c2 +1f3c3 +1f3c4 +1f3c5 +1f3c6 +1f3c7 +1f3c8 +1f3c9 +1f3ca +1f3cb +1f3cc +1f3cd +1f3ce +1f3cf +1f3d0 +1f3d1 +1f3d2 +1f3d3 +1f3d4 +1f3d5 +1f3d6 +1f3d7 +1f3d8 +1f3d9 +1f3da +1f3db +1f3dc +1f3dd +1f3de +1f3df +1f3e0 +1f3e1 +1f3e2 +1f3e3 +1f3e4 +1f3e5 +1f3e6 +1f3e7 +1f3e8 +1f3e9 +1f3ea +1f3eb +1f3ec +1f3ed +1f3ee +1f3ef +1f3f0 +1f3f3 +1f3f4 +1f3f5 +1f3f7 +1f3f8 +1f3f9 +1f3fa +1f3fb +1f3fc +1f3fd +1f3fe +1f3ff +1f400 +1f401 +1f402 +1f403 +1f404 +1f405 +1f406 +1f407 +1f408 +1f409 +1f40a +1f40b +1f40c +1f40d +1f40e +1f40f +1f410 +1f411 +1f412 +1f413 +1f414 +1f415 +1f416 +1f417 +1f418 +1f419 +1f41a +1f41b +1f41c +1f41d +1f41e +1f41f +1f420 +1f421 +1f422 +1f423 +1f424 +1f425 +1f426 +1f427 +1f428 +1f429 +1f42a +1f42b +1f42c +1f42d +1f42e +1f42f +1f430 +1f431 +1f432 +1f433 +1f434 +1f435 +1f436 +1f437 +1f438 +1f439 +1f43a +1f43b +1f43c +1f43d +1f43e +1f43f +1f440 +1f441 +1f442 +1f443 +1f444 +1f445 +1f446 +1f447 +1f448 +1f449 +1f44a +1f44b +1f44c +1f44d +1f44e +1f44f +1f450 +1f451 +1f452 +1f453 +1f454 +1f455 +1f456 +1f457 +1f458 +1f459 +1f45a +1f45b +1f45c +1f45d +1f45e +1f45f +1f460 +1f461 +1f462 +1f463 +1f464 +1f465 +1f466 +1f467 +1f468 +1f469 +1f46a +1f46b +1f46c +1f46d +1f46e +1f46f +1f470 +1f471 +1f472 +1f473 +1f474 +1f475 +1f476 +1f477 +1f478 +1f479 +1f47a +1f47b +1f47c +1f47d +1f47e +1f47f +1f480 +1f481 +1f482 +1f483 +1f484 +1f485 +1f486 +1f487 +1f488 +1f489 +1f48a +1f48b +1f48c +1f48d +1f48e +1f48f +1f490 +1f491 +1f492 +1f493 +1f494 +1f495 +1f496 +1f497 +1f498 +1f499 +1f49a +1f49b +1f49c +1f49d +1f49e +1f49f +1f4a0 +1f4a1 +1f4a2 +1f4a3 +1f4a4 +1f4a5 +1f4a6 +1f4a7 +1f4a8 +1f4a9 +1f4aa +1f4ab +1f4ac +1f4ad +1f4ae +1f4af +1f4b0 +1f4b1 +1f4b2 +1f4b3 +1f4b4 +1f4b5 +1f4b6 +1f4b7 +1f4b8 +1f4b9 +1f4ba +1f4bb +1f4bc +1f4bd +1f4be +1f4bf +1f4c0 +1f4c1 +1f4c2 +1f4c3 +1f4c4 +1f4c5 +1f4c6 +1f4c7 +1f4c8 +1f4c9 +1f4ca +1f4cb +1f4cc +1f4cd +1f4ce +1f4cf +1f4d0 +1f4d1 +1f4d2 +1f4d3 +1f4d4 +1f4d5 +1f4d6 +1f4d7 +1f4d8 +1f4d9 +1f4da +1f4db +1f4dc +1f4dd +1f4de +1f4df +1f4e0 +1f4e1 +1f4e2 +1f4e3 +1f4e4 +1f4e5 +1f4e6 +1f4e7 +1f4e8 +1f4e9 +1f4ea +1f4eb +1f4ec +1f4ed +1f4ee +1f4ef +1f4f0 +1f4f1 +1f4f2 +1f4f3 +1f4f4 +1f4f5 +1f4f6 +1f4f7 +1f4f8 +1f4f9 +1f4fa +1f4fb +1f4fc +1f4fd +1f4ff +1f500 +1f501 +1f502 +1f503 +1f504 +1f505 +1f506 +1f507 +1f508 +1f509 +1f50a +1f50b +1f50c +1f50d +1f50e +1f50f +1f510 +1f511 +1f512 +1f513 +1f514 +1f515 +1f516 +1f517 +1f518 +1f519 +1f51a +1f51b +1f51c +1f51d +1f51e +1f51f +1f520 +1f521 +1f522 +1f523 +1f524 +1f525 +1f526 +1f527 +1f528 +1f529 +1f52a +1f52b +1f52c +1f52d +1f52e +1f52f +1f530 +1f531 +1f532 +1f533 +1f534 +1f535 +1f536 +1f537 +1f538 +1f539 +1f53a +1f53b +1f53c +1f53d +1f549 +1f54a +1f54b +1f54c +1f54d +1f54e +1f550 +1f551 +1f552 +1f553 +1f554 +1f555 +1f556 +1f557 +1f558 +1f559 +1f55a +1f55b +1f55c +1f55d +1f55e +1f55f +1f560 +1f561 +1f562 +1f563 +1f564 +1f565 +1f566 +1f567 +1f56f +1f570 +1f573 +1f574 +1f575 +1f576 +1f577 +1f578 +1f579 +1f57a +1f587 +1f58a +1f58b +1f58c +1f58d +1f590 +1f595 +1f596 +1f5a4 +1f5a5 +1f5a8 +1f5b1 +1f5b2 +1f5bc +1f5c2 +1f5c3 +1f5c4 +1f5d1 +1f5d2 +1f5d3 +1f5dc +1f5dd +1f5de +1f5e1 +1f5e3 +1f5e8 +1f5ef +1f5f3 +1f5fa +1f5fb +1f5fc +1f5fd +1f5fe +1f5ff +1f600 +1f601 +1f602 +1f603 +1f604 +1f605 +1f606 +1f607 +1f608 +1f609 +1f60a +1f60b +1f60c +1f60d +1f60e +1f60f +1f610 +1f611 +1f612 +1f613 +1f614 +1f615 +1f616 +1f617 +1f618 +1f619 +1f61a +1f61b +1f61c +1f61d +1f61e +1f61f +1f620 +1f621 +1f622 +1f623 +1f624 +1f625 +1f626 +1f627 +1f628 +1f629 +1f62a +1f62b +1f62c +1f62d +1f62e +1f62f +1f630 +1f631 +1f632 +1f633 +1f634 +1f635 +1f636 +1f637 +1f638 +1f639 +1f63a +1f63b +1f63c +1f63d +1f63e +1f63f +1f640 +1f641 +1f642 +1f643 +1f644 +1f645 +1f646 +1f647 +1f648 +1f649 +1f64a +1f64b +1f64c +1f64d +1f64e +1f64f +1f680 +1f681 +1f682 +1f683 +1f684 +1f685 +1f686 +1f687 +1f688 +1f689 +1f68a +1f68b +1f68c +1f68d +1f68e +1f68f +1f690 +1f691 +1f692 +1f693 +1f694 +1f695 +1f696 +1f697 +1f698 +1f699 +1f69a +1f69b +1f69c +1f69d +1f69e +1f69f +1f6a0 +1f6a1 +1f6a2 +1f6a3 +1f6a4 +1f6a5 +1f6a6 +1f6a7 +1f6a8 +1f6a9 +1f6aa +1f6ab +1f6ac +1f6ad +1f6ae +1f6af +1f6b0 +1f6b1 +1f6b2 +1f6b3 +1f6b4 +1f6b5 +1f6b6 +1f6b7 +1f6b8 +1f6b9 +1f6ba +1f6bb +1f6bc +1f6bd +1f6be +1f6bf +1f6c0 +1f6c1 +1f6c2 +1f6c3 +1f6c4 +1f6c5 +1f6cb +1f6cc +1f6cd +1f6ce +1f6cf +1f6d0 +1f6d1 +1f6d2 +1f6d5 +1f6d6 +1f6d7 +1f6d8 +1f6dc +1f6dd +1f6de +1f6df +1f6e0 +1f6e1 +1f6e2 +1f6e3 +1f6e4 +1f6e5 +1f6e9 +1f6eb +1f6ec +1f6f0 +1f6f3 +1f6f4 +1f6f5 +1f6f6 +1f6f7 +1f6f8 +1f6f9 +1f6fa +1f6fb +1f6fc +1f7e0 +1f7e1 +1f7e2 +1f7e3 +1f7e4 +1f7e5 +1f7e6 +1f7e7 +1f7e8 +1f7e9 +1f7ea +1f7eb +1f7f0 +1f90c +1f90d +1f90e +1f90f +1f910 +1f911 +1f912 +1f913 +1f914 +1f915 +1f916 +1f917 +1f918 +1f919 +1f91a +1f91b +1f91c +1f91d +1f91e +1f91f +1f920 +1f921 +1f922 +1f923 +1f924 +1f925 +1f926 +1f927 +1f928 +1f929 +1f92a +1f92b +1f92c +1f92d +1f92e +1f92f +1f930 +1f931 +1f932 +1f933 +1f934 +1f935 +1f936 +1f937 +1f938 +1f939 +1f93a +1f93c +1f93d +1f93e +1f93f +1f940 +1f941 +1f942 +1f943 +1f944 +1f945 +1f947 +1f948 +1f949 +1f94a +1f94b +1f94c +1f94d +1f94e +1f94f +1f950 +1f951 +1f952 +1f953 +1f954 +1f955 +1f956 +1f957 +1f958 +1f959 +1f95a +1f95b +1f95c +1f95d +1f95e +1f95f +1f960 +1f961 +1f962 +1f963 +1f964 +1f965 +1f966 +1f967 +1f968 +1f969 +1f96a +1f96b +1f96c +1f96d +1f96e +1f96f +1f970 +1f971 +1f972 +1f973 +1f974 +1f975 +1f976 +1f977 +1f978 +1f979 +1f97a +1f97b +1f97c +1f97d +1f97e +1f97f +1f980 +1f981 +1f982 +1f983 +1f984 +1f985 +1f986 +1f987 +1f988 +1f989 +1f98a +1f98b +1f98c +1f98d +1f98e +1f98f +1f990 +1f991 +1f992 +1f993 +1f994 +1f995 +1f996 +1f997 +1f998 +1f999 +1f99a +1f99b +1f99c +1f99d +1f99e +1f99f +1f9a0 +1f9a1 +1f9a2 +1f9a3 +1f9a4 +1f9a5 +1f9a6 +1f9a7 +1f9a8 +1f9a9 +1f9aa +1f9ab +1f9ac +1f9ad +1f9ae +1f9af +1f9b0 +1f9b1 +1f9b2 +1f9b3 +1f9b4 +1f9b5 +1f9b6 +1f9b7 +1f9b8 +1f9b9 +1f9ba +1f9bb +1f9bc +1f9bd +1f9be +1f9bf +1f9c0 +1f9c1 +1f9c2 +1f9c3 +1f9c4 +1f9c5 +1f9c6 +1f9c7 +1f9c8 +1f9c9 +1f9ca +1f9cb +1f9cc +1f9cd +1f9ce +1f9cf +1f9d0 +1f9d1 +1f9d2 +1f9d3 +1f9d4 +1f9d5 +1f9d6 +1f9d7 +1f9d8 +1f9d9 +1f9da +1f9db +1f9dc +1f9dd +1f9de +1f9df +1f9e0 +1f9e1 +1f9e2 +1f9e3 +1f9e4 +1f9e5 +1f9e6 +1f9e7 +1f9e8 +1f9e9 +1f9ea +1f9eb +1f9ec +1f9ed +1f9ee +1f9ef +1f9f0 +1f9f1 +1f9f2 +1f9f3 +1f9f4 +1f9f5 +1f9f6 +1f9f7 +1f9f8 +1f9f9 +1f9fa +1f9fb +1f9fc +1f9fd +1f9fe +1f9ff +1fa70 +1fa71 +1fa72 +1fa73 +1fa74 +1fa75 +1fa76 +1fa77 +1fa78 +1fa79 +1fa7a +1fa7b +1fa7c +1fa80 +1fa81 +1fa82 +1fa83 +1fa84 +1fa85 +1fa86 +1fa87 +1fa88 +1fa89 +1fa8a +1fa8e +1fa8f +1fa90 +1fa91 +1fa92 +1fa93 +1fa94 +1fa95 +1fa96 +1fa97 +1fa98 +1fa99 +1fa9a +1fa9b +1fa9c +1fa9d +1fa9e +1fa9f +1faa0 +1faa1 +1faa2 +1faa3 +1faa4 +1faa5 +1faa6 +1faa7 +1faa8 +1faa9 +1faaa +1faab +1faac +1faad +1faae +1faaf +1fab0 +1fab1 +1fab2 +1fab3 +1fab4 +1fab5 +1fab6 +1fab7 +1fab8 +1fab9 +1faba +1fabb +1fabc +1fabd +1fabe +1fabf +1fac0 +1fac1 +1fac2 +1fac3 +1fac4 +1fac5 +1fac6 +1fac8 +1facd +1face +1facf +1fad0 +1fad1 +1fad2 +1fad3 +1fad4 +1fad5 +1fad6 +1fad7 +1fad8 +1fad9 +1fada +1fadb +1fadc +1fadf +1fae0 +1fae1 +1fae2 +1fae3 +1fae4 +1fae5 +1fae6 +1fae7 +1fae8 +1fae9 +1faea +1faef +1faf0 +1faf1 +1faf2 +1faf3 +1faf4 +1faf5 +1faf6 +1faf7 +1faf8 +203c +2049 +2122 +2139 +2194 +2195 +2196 +2197 +2198 +2199 +21a9 +21aa +231a +231b +2328 +23cf +23e9 +23ea +23eb +23ec +23ed +23ee +23ef +23f0 +23f1 +23f2 +23f3 +23f8 +23f9 +23fa +24c2 +25aa +25ab +25b6 +25c0 +25fb +25fc +25fd +25fe +2600 +2601 +2602 +2603 +2604 +260e +2611 +2614 +2615 +2618 +261d +2620 +2622 +2623 +2626 +262a +262e +262f +2638 +2639 +263a +2640 +2642 +2648 +2649 +264a +264b +264c +264d +264e +264f +2650 +2651 +2652 +2653 +265f +2660 +2663 +2665 +2666 +2668 +267b +267e +267f +2692 +2693 +2694 +2695 +2696 +2697 +2699 +269b +269c +26a0 +26a1 +26a7 +26aa +26ab +26b0 +26b1 +26bd +26be +26c4 +26c5 +26c8 +26ce +26cf +26d1 +26d3 +26d4 +26e9 +26ea +26f0 +26f1 +26f2 +26f3 +26f4 +26f5 +26f7 +26f8 +26f9 +26fa +26fd +2702 +2705 +2708 +2709 +270a +270b +270c +270d +270f +2712 +2714 +2716 +271d +2721 +2728 +2733 +2734 +2744 +2747 +274c +274e +2753 +2754 +2755 +2757 +2763 +2764 +2795 +2796 +2797 +27a1 +27b0 +27bf +2934 +2935 +2b05 +2b06 +2b07 +2b1b +2b1c +2b50 +2b55 +3030 +303d +3297 +3299 +e50a From 13a6b4f5b855bcec2bc92edd8b9b71790f1bc903 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 19:38:33 +0200 Subject: [PATCH 034/120] Catalog: replay retire-longinusx apply on master's catalog Master's catalog (LFS 9e75d9a4, carrying the Mine Launcher and the gear-without-presence merge base) differed from this branch's base, so the branch's catalog was not picked. AetherDb retire-longinusx apply was rerun on master's catalog (tool as of 8038b46a plus origin/master): the dry run listed the same four records (HullData LonginusX, GearData Traction, their two FactionProductData), a second apply is refused, dangling reports 0. Co-Authored-By: Claude Opus 5.5 --- GameData/Aetheria.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GameData/Aetheria.cc b/GameData/Aetheria.cc index 33a924176..5a30a9f87 100644 --- a/GameData/Aetheria.cc +++ b/GameData/Aetheria.cc @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e75d9a4926bbbe05f3e09480a4020b0d1844b4f32a6aeeee25b03a5ca79c19e -size 12704336 +oid sha256:adba03892bc391c33b7a73694e8ed66dbbf35a76f1f1e1f7210403ade473b77c +size 12702360 From 8e51016f23ebc5ea2337732b18aa1d5968e30505 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 19:39:54 +0200 Subject: [PATCH 035/120] PiratesFactionCommandTests: flush the name-file removal; tolerate a null empty role list Co-Authored-By: Claude Sonnet 5.5 --- tests/Aetheria.Shared.Tests/PiratesFactionCommandTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Aetheria.Shared.Tests/PiratesFactionCommandTests.cs b/tests/Aetheria.Shared.Tests/PiratesFactionCommandTests.cs index e9d321e20..6a1c245b0 100644 --- a/tests/Aetheria.Shared.Tests/PiratesFactionCommandTests.cs +++ b/tests/Aetheria.Shared.Tests/PiratesFactionCommandTests.cs @@ -3,6 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; @@ -102,7 +103,7 @@ public sealed class PiratesFactionCommandTests : IDisposable var design = (CraftedItemData) db.Cache.Get(product.Design); Assert.Equal(designName, design.Name); Assert.Equal(description, product.Description); - Assert.Equal(design.Roles.Select(role => role.Name), product.Roles.Select(role => role.Role)); + Assert.Equal((design.Roles ?? new List()).Select(role => role.Name), (product.Roles ?? new List()).Select(role => role.Role)); Assert.All(product.Roles, role => { Assert.Equal(.45f, role.Mean); Assert.Equal(.22f, role.StandardDeviation); }); } } @@ -145,6 +146,7 @@ public sealed class PiratesFactionCommandTests : IDisposable var db = AetherDb.Open(catalogWritable: true, root: _root); var pleiades = db.Cache.GetAll().Single(file => file.Name == "pleiades"); Assert.True(db.Cache.Remove(db.Cache.RefOf(pleiades).Key)); + db.Cache.FlushAsync().Wait(); db.Cache.Dispose(); Assert.Throws(() => Program.PiratesFaction(apply: true, root: _root)); From a8f71d463bf1160de0b5f23caa56dc7957252345 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 19:56:56 +0200 Subject: [PATCH 036/120] Catalog: replay pd-gear apply on master's catalog Master's catalog (LFS adba0389: Mine Launcher present, LonginusX and Traction retired) differs from this branch's base, so the branch's catalog was not picked. AetherDb pd-gear apply was rerun on master's catalog (merged tool): 3 designs and 11 products authored, a second run reports it already authored, dangling reports 0. Co-Authored-By: Claude Opus 5.5 --- GameData/Aetheria.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GameData/Aetheria.cc b/GameData/Aetheria.cc index 5a30a9f87..a96c28f15 100644 --- a/GameData/Aetheria.cc +++ b/GameData/Aetheria.cc @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:adba03892bc391c33b7a73694e8ed66dbbf35a76f1f1e1f7210403ade473b77c -size 12702360 +oid sha256:5ecf375ffcddbd92ea3afabc539cea01954bbc0faa7f650f7c1905fdb953a011 +size 12709024 From 090b6d18dc9b8af4c092ee701365cce0f0bcbbad Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 21:00:16 +0200 Subject: [PATCH 037/120] Sim speed: tests for the survivors Stryker found (thruster turn, tilted draw, exact step) DrawAhead.Rotation delegates to Turned(pose, angle) so the turn is testable on a tilted pose, where the flat Duel poses make the quaternion product's off-axis terms vanish. Unverified at this commit. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/DrawAhead.cs | 8 +-- tests/Aetheria.Shared.Tests/SimClockTests.cs | 51 ++++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/Assets/Scripts/ServerShared/DrawAhead.cs b/Assets/Scripts/ServerShared/DrawAhead.cs index 3cd1a4ea1..52c92fee8 100644 --- a/Assets/Scripts/ServerShared/DrawAhead.cs +++ b/Assets/Scripts/ServerShared/DrawAhead.cs @@ -14,13 +14,15 @@ public static class DrawAhead entity.Position + float3(entity.Velocity.x, 0, entity.Velocity.y) * lead; // The ship's rotation turned about its own up axis by the rate its last step turned, for lead seconds. - public static quaternion Rotation(Ship ship, float lead) + public static quaternion Rotation(Ship ship, float lead) => Turned(ship.Rotation, ship.TurnRate * lead); + + // q turned about its own up axis by the angle, in the sense Direction turns for a positive TurnRate. + public static quaternion Turned(quaternion q, float angle) { - var q = ship.Rotation; var u = float3(q.x, q.y, q.z); var up = float3(0, 1, 0); up += 2 * (q.w * cross(u, up) + cross(u, cross(u, up))); - var half = ship.TurnRate * lead * .5f; + var half = angle * .5f; var s = sin(half); var t = new quaternion(up.x * s, up.y * s, up.z * s, cos(half)); // Hamilton product t * q: CultMath carries no quaternion product (a gap in CultMath, not filled here). diff --git a/tests/Aetheria.Shared.Tests/SimClockTests.cs b/tests/Aetheria.Shared.Tests/SimClockTests.cs index 11cbc7a30..bc5bdd539 100644 --- a/tests/Aetheria.Shared.Tests/SimClockTests.cs +++ b/tests/Aetheria.Shared.Tests/SimClockTests.cs @@ -58,6 +58,11 @@ public sealed class SimClockTests Assert.InRange(elapsed, 10.0, 10.1); Assert.InRange(clock.Steps, Math.Round(elapsed * 60) - 1, Math.Round(elapsed * 60) + 1); + // A frame that pays for exactly one step runs it (binary-exact seconds, so nothing rounds). + var exact = new SimClock(.25f, 4); + Assert.Equal(1, exact.Advance(.25f, _ => { })); + Assert.Equal(0f, exact.Lead); + var slow = new SimClock(1f / 60f, 30); for (var frame = 0; frame < 600; frame++) slow.Advance(1f / 60, _ => { }); Assert.InRange(slow.Steps, 299, 301); @@ -270,4 +275,50 @@ public sealed partial class RunStartTests Assert.True(turn > .005f, "the step turns the ship enough to tell its sense"); Assert.True(error < .2f * turn, $"drawn one step ahead is where the sim puts the pose next (off by {error} of a {turn} turn)"); } + + // Thrusters turn the same way: a Longinus held in a turn, settled, turns each step by exactly its TurnRate. + [Fact] + public void ThrustersReportTheirTurn() + { + var (_, longinus, _) = EvFleet(); + EvSettle(longinus); + longinus.Turn = 1f; + var turned = 0; + for (var step = 0; step < 120; step++) + { + var before = longinus.Direction; + longinus.Update(ClockFrame); + Assert.InRange(abs(abs(SignedTurn(before, longinus.Direction)) - abs(longinus.TurnRate * ClockFrame)), 0f, 1e-6f); + if (abs(longinus.TurnRate) > 1e-4f) + { + turned++; + Assert.True(longinus.TurnRate > 0 == dot(longinus.Direction, before.Rotate(ItemRotation.Clockwise)) > 0); + } + } + Assert.True(turned > 30, "the thrusters turned the ship"); + } + + // The draw-ahead turn on a pose that is not flat: the up axis stays the pose's up, and the forward axis swings about + // it by the angle (Rodrigues' rotation, written out apart from DrawAhead's quaternion product). + [Fact] + public void TheDrawnTurnIsAboutThePosesOwnUp() + { + var up = normalize(float3(.3f, .8f, -.2f)); + var pose = quaternion.LookRotation(normalize(cross(float3(1, 0, .4f), up)), up); + const float angle = .6f; + var turned = DrawAhead.Turned(pose, angle); + + float3 Axis(quaternion q, float3 v) + { + var u = float3(q.x, q.y, q.z); + return v + 2 * (q.w * cross(u, v) + cross(u, cross(u, v))); + } + float3 Close(float3 a, float3 b) => float3(abs(a.x - b.x), abs(a.y - b.y), abs(a.z - b.z)); + var forward = Axis(pose, float3(0, 0, 1)); + var rodrigues = forward * cos(angle) + cross(Axis(pose, float3(0, 1, 0)), forward) * sin(angle); + var upBefore = Axis(pose, float3(0, 1, 0)); + var tolerance = float3(1e-5f, 1e-5f, 1e-5f); + Assert.True(all(Close(Axis(turned, float3(0, 1, 0)), upBefore) < tolerance), "the pose's up does not move"); + Assert.True(all(Close(Axis(turned, float3(0, 0, 1)), rodrigues) < tolerance), "the forward axis swings about it by the angle"); + } } From cff4e0ffa7599e98d691586e1f5d871b4d2dfcac Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 21:01:27 +0200 Subject: [PATCH 038/120] Sim speed: the exact-step frame stays under the hitch clamp Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/SimClockTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/SimClockTests.cs b/tests/Aetheria.Shared.Tests/SimClockTests.cs index bc5bdd539..3a8f72fbd 100644 --- a/tests/Aetheria.Shared.Tests/SimClockTests.cs +++ b/tests/Aetheria.Shared.Tests/SimClockTests.cs @@ -59,8 +59,8 @@ public sealed class SimClockTests Assert.InRange(clock.Steps, Math.Round(elapsed * 60) - 1, Math.Round(elapsed * 60) + 1); // A frame that pays for exactly one step runs it (binary-exact seconds, so nothing rounds). - var exact = new SimClock(.25f, 4); - Assert.Equal(1, exact.Advance(.25f, _ => { })); + var exact = new SimClock(.25f, 16); + Assert.Equal(1, exact.Advance(.0625f, _ => { })); Assert.Equal(0f, exact.Lead); var slow = new SimClock(1f / 60f, 30); From 325ef4bc1aac750e1192fba9ca3efd0a95651118 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 21:41:45 +0200 Subject: [PATCH 039/120] Catalog: replay pirates-faction apply on master's catalog Master's catalog (LFS 5ecf375f: PD gear, Mine Launcher, retirements) differs from this branch's base, so the branch's blob was not taken. AetherDb pirates-faction apply was rerun on master's catalog (merged tool, Yggdrasil container): the Pirates faction and eight emoji-named products authored; catalog 5ecf375f -> 32a2e401 (12712398 bytes); a second run reports the faction already exists and the blob is byte-identical. Co-Authored-By: Claude Sonnet 5.5 --- GameData/Aetheria.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GameData/Aetheria.cc b/GameData/Aetheria.cc index a96c28f15..42a50cad4 100644 --- a/GameData/Aetheria.cc +++ b/GameData/Aetheria.cc @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ecf375ffcddbd92ea3afabc539cea01954bbc0faa7f650f7c1905fdb953a011 -size 12709024 +oid sha256:32a2e401a03b00bae994bf1a26a3be58db7226c7c6c9a4e12fa2c449429d80e8 +size 12712398 From e389e3972cc493846d4ffbb82c12179a13d5cfad Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 21:57:21 +0200 Subject: [PATCH 040/120] Map: catalog breadth (weapon matrix, roles backfill, stat-boosting gear) Body facts CB1-CB12, rules CB-R1..CB-R5 and rationale from the imagination-catalog-breadth pass; specs and questions are in the mind. Co-Authored-By: Claude Opus 5.5 --- docs/aetheria-release-map.md | 297 +++++++++++++++++++++++++++++++++++ 1 file changed, 297 insertions(+) diff --git a/docs/aetheria-release-map.md b/docs/aetheria-release-map.md index 9d83f1d89..65f85ef3b 100644 --- a/docs/aetheria-release-map.md +++ b/docs/aetheria-release-map.md @@ -6191,3 +6191,300 @@ reverse. One coverage test makes "triggered by" and "harmed by" the same disc. **Why no targeting provider or point defense against mines.** The old mine was neither detectable nor shootable, and ruling `pd-who-engages` prices munitions, which a mine is not. Detection of floating bodies stays with follow-up `bodies-detection`. + +## Catalog breadth: the weapon matrix, roles backfill and stat-boosting gear + +Pass: Imagination `imagination-catalog-breadth`, session `self-2026-10-08b`. Self merges this into +`docs/aetheria-release-map.md`; nothing here is committed. Body: `GameCult/Aetheria` origin/master +`a8f71d463bf1160de0b5f23caa56dc7957252345` (fetched 2026-10-08), detached worktree in the session +scratchpad, removed at the end. Rulings applied: `catalog-breadth-weapons-gear-consumables`, +`catalog-grows-generic-designs-branded-products`, `tech-lineage-per-component-role`, +`weapon-tracking-authored`, `plight-not-one-cell`, `plight-shape`. Operator, 2026-10-08, relayed by +Self: "All this comes with roles and components too, right?" (not yet admitted as a ruling; Self +should admit it so specs can cite it by id). + +Part (c), consumables, is not mapped here. The component-role rule (CB-R1) and the stacking rule +(question `boost-stacking`) are written so that pass reuses them unchanged. + +### Body facts + +Decode method: `cultcache_py` `SingleFileMessagePackBackingStore.pull_all()` over +`GameData/Aetheria.cc` at a8f71d46 (LFS pulled into the worktree), payloads `msgpack.unpackb`ed and +read by MessagePack key. Scratch scripts `cat.py`, `wm.py`, `stats.py`, not committed. + +- **CB1. Census at a8f71d46.** 74 FactionProductData, 51 CompoundCommodityData, 31 GearData, + 22 WeaponItemData, 13 SimpleCommodityData, 12 Faction, 12 NameFile, 4 CargoBayData, 4 HullData, + 3 PersonalityAttribute, 1 FieldKindData, 1 DockingBayData, 1 InputLayout. pd-gear and the Mine + Launcher are on master; the Pirates (pirates-record 8e51016f), lot-product (ec081b9b), + generic-design-names, product-lines-weapons, maker-only-faction (5dce222b), mods-in-tree + (cc83f5ad) and faction-play-1 (b3d1e929) are not. Design names are still the branded ones. +- **CB2. The catalog already has a category axis and a size axis on weapons** (source read + `ItemData.cs:477-487`, `Enums.cs:55-83`). `WeaponItemData.WeaponType` (key 26: ElectromagneticallyPropelled, + ExplosivelyPropelled, Laser, Electrostatic, ParticleProjection, Missile, MicroMissile, SplitMissile, + Mine, Jet) and `WeaponCaliber` (key 25: Small, Medium, Large, ExtraLarge). The operator's vault note + `AetheriaLore Aetheria/Worldbuilding/Post-Elysium/Reference/Weapon Category Codes.md` (33cd06a) + defines the same taxonomy: caliber S/M/L/XL, weapon types EMPS, XPS, L, PL, ESD, PPC, launchers + M, MM, MIRV with guidance D/G/S. The enum lacks PL (pulse laser) and adds Mine and Jet. The only + code readers are the action-bar icon (`Gameplay/ActionBarSlot.cs:154`, `GameSettings.cs:52-54`). +- **CB3. The matrix at a8f71d46** (type x authored caliber, with cell footprint): + + | Type | Small | Medium | Large | ExtraLarge | + |---|---|---|---|---| + | XPS (Ballistic) | ClearPath 2, Earp 2, 6k Shooter 2, PD Gun 1 | Autocannon 4, DeathCluster 4, pretty pretty bang bang 4, Flak Gun 2 | - | - | + | EMPS | - | - | - | - | + | Laser (Energy) | Spectra 2, ColdFire 2, PD Laser 1, CShot RainbowLite Lazer 9 | - | - | - | + | ESD (Energy) | - | - | plight 2 | - | + | PPC (Energy) | FastBlast+- 2 | ChargeBlast+- 4, ChargeBlast SG 4 | - | - | + | Missile (Launcher) | - | GT 3K 3 | - | - | + | MicroMissile (Launcher) | - | scorched void policy 3, pswarm 1 | LRMM72 6 | SRMM72 6 | + | SplitMissile | - | - | - | - | + | Mine (Launcher) | - | Mine Launcher 1 | - | - | + | Jet | - | - | - | - | + + Caliber and footprint disagree in six designs: CShot (Small, 9 cells), plight (Large, 2), + pswarm (Medium, 1), SRMM72 (ExtraLarge, 6 like LRMM72's Large), Mine Launcher (Medium, 1), + Flak Gun (Medium, 2). Map M30 already recorded CShot and plight. +- **CB4. Weapon homes on master** (`HullData.Hardpoints`, key 23). Longinus: Energy 1x2 x2, + Launcher 1x3 x2. Djinni: Ballistic 1x2 x2, Launcher 1x3 x2, Launcher 3x2 x2. Turret (station): + Ballistic 2x4 x2. Zenith: none. `LoadoutGenerator.HasHome` (`:201`) offers a design only if some + hull's hardpoint `Takes` it, so a Large or ExtraLarge Energy gun, an ExtraLarge launcher and a + 2x2 Energy gun have no home until a hull carries such a mount. No mod package is in the tree yet. +- **CB5. Roles and components today** (source read `ItemData.cs:315-331`, `FactionProduct.cs:14-40`, + `Provenance.cs:73`, `ItemData.cs:640-720`). A design declares `Roles` (`ItemRole`, a name only; + its comment: "the part itself earns a record when crafting needs one to exist"). A product + authors `ProductRole{Role, Mean, StandardDeviation}` per role; `ItemManager.CreateLot` + (`ItemManager.cs:149`) rolls each role's quality into the lot; a stat reads it through a + `StatTerm{Source: Quality, Role}`. No record type for a component item exists; the 51 + CompoundCommodityData (Circuit Board, Reaction Chamber, Heat Sink Tile, Trigger ...) carry no + roles and nothing links a role to them. Lineage (`tech-lineage-per-component-role`) has no field. +- **CB6. Roles are live only where a stat reads them.** `AetherDb roles-migrate` + (`tools/AetherDb/Program.cs:255-378`) assigned roles per kind from fixed field maps + (`:156-216`) and skipped flat stats (`Min == Max`) and kinds with no quality-reading behaviour. + The census prints, but no test fails on, products that author no spread for a role + (`Program.cs:110-127`). +- **CB7. The audit of in-flight content for roles** (decode plus pirates-record diff read): + - pd-gear (merged): all three designs declare their kind's roles, each read by non-flat stats; + every product authors ProductRole for each. No gap. + - Pirates (pirates-record 8e51016f, `Program.cs` diff lines 66-82): every rebrand mirrors its + design's roles at apply time (mean .45, sd .22). It inherits the design's gaps: its Autocannon + rebrand carries no roles because Autocannon declares none. + - Mine Launcher (merged): no roles, every MineLayerData stat flat with no terms at all; its DME + product authors none. + - Also role-less with flat stats: Autocannon, LRMM72, SRMM72 (every stat Min == Max, Quality term + with no role). Large Drive reads Quality with no role and declares none. Cargo bays, docking + bay, Cockpit 2x2, Turret Control Module and Tractor Beam carry no quality-reading stat. +- **CB8. Stat modifiers exist and work for passive gear** (source read + `Behaviors/StatModifier.cs`, `StatResolver.cs:108-145`, `Entity.cs:1960-1980`). + `StatModifierData` (behaviour union 4) names a target `StatReference{Target type name, Stat + field}`, a magnitude `PerformanceStat` (so per-role quality works), `Constant` or `Multiplier`, + and an optional `RequireBehavior`. While the gear is Active its behaviour groups execute in order + and stop at the first that fails (`Entity.cs:1969-1976`), so an `EnergyDraw` before the modifier + gates it on power; the modifier attaches to every matching stat on the same entity and detaches + when it stops executing. `PerformanceStat.Evaluate` applies `lerp * Scale + Constant`, then the + power factor (`ItemData.cs:700-722`). The sim reads every boosted stat through `Evaluate` + (Thruster.cs:43, Sensor.cs:168, Radiator.cs:118, Weapon.cs:133-143, VelocityLimit.cs:35, + TargetingSystem.cs:80). The catalog's one modifier: MoveOnPro (both sizes) scales + `CapacitorData.Capacity` by 1.1-1.25. +- **CB9. Gap: a modifier cannot target a base behaviour type.** `TargetsOf` + (`StatModifier.cs:91-114`) matches `GetType() == targetType` at `:98`, `:102` and `:104`, while + `ResolveStatField` resolves any `BehaviorData` subtype by name, abstract ones included + (`ItemData.cs:785-799`, `Extensions.cs:36-44`). A modifier on `WeaponData.Damage` validates and + then attaches to nothing, because every gun's behaviour is a concrete subtype (AutoWeaponData, + InstantWeaponData, ChargedWeaponData, ConstantWeaponData, LauncherData, MineLayerData). +- **CB10. Stacking today is unbounded.** `StatResolver.ScaleModifier` multiplies every attached + scale and `ConstantModifier` sums every constant (`StatResolver.cs:108-122`). Ten 1.15 + multipliers compound to 4.0x. Consumable effects attach through the same resolver + (`Entity.cs:857-881`, `StatModifier.cs:63`), so the stacking rule is shared with part (c). +- **CB11. Where stat gear lives.** Tool gear has no hardpoint and fits any free interior cell + (`Entity.cs:1076-1090`); Targeting Computer (Tool 1x1: TargetingSystem, EnergyDraw 2, Heat 15) is + the closest template for a powered booster. +- **CB12. Consumables already have a sim mechanism** (for part (c), not mapped here): + `ConsumableItemData` (`ItemData.cs`, keys 10-14: Behaviors, Stackable, Duration, Icon, + Effectiveness) is a CraftedItemData, so it declares roles; `Entity.TryActivateConsumable` + (`:872`) and `ConsumableItemEffect` (`:1582`) run its behaviours, including StatModifier, with a + `ConsumableProgress` stat source. The catalog holds no consumable record. + +### Model page row changes + +- **WeaponItemData (catalog)**: who decides gains "the matrix cell": `WeaponType` and + `WeaponCaliber` are the authored category and size (question `weapon-matrix-axes`); footprint + lies in its caliber's band (Small 1-2 cells, Medium 3-4, Large 5-6, ExtraLarge 7-9). +- **CraftedItemData.Roles / FactionProductData.Roles**: a design declares a role only if a non-flat + stat reads it; every product authors one ProductRole per role (rule CB-R1). +- **StatModifierData**: targets a behaviour type and its subtypes (after modifier-subtypes); the + resolver's stacking rule is the one for gear and consumables alike (question `boost-stacking`). + +### Rules (cited by the specs) + +- **CB-R1, roles and components (the whole ruling, consumables included).** A crafted design + (weapon, gear, consumable) declares roles iff it has a stat that varies with quality. Each + declared role is read by at least one non-flat stat's `Quality` term; each non-flat stat reads + exactly one declared role; every product that sells the design authors one ProductRole per + role, from its maker's profile (catalog-growth addenda table; Pirates .45/.22 per pirates-record). + Roles come from the kind's field map in `Program.cs:156-216`, extended for new kinds as named in + each spec. A consumable's roles govern its effect magnitude stats the same way. Under the + recommended option of question `component-items`, the component is the (design role, product + ProductRole) pair: no component item record until crafting reads one, and the lineage mark of + `tech-lineage-per-component-role` lands on ProductRole when the first aethertech product does. +- **CB-R2, authoring a new design.** Copy a template by record key (never by name, so it holds + across generic-design-names), set the shape, caliber, type and name, set every authored stat by + the ladder below as a Min-Max range of value x (0.8, 1.2) (benefit stats rise with quality, cost + stats fall), keep the template's non-quality terms, point each Quality term at its role, author + Tracking per weapon with no terms (`weapon-tracking-authored`), carry no BlastRadius or Fuse + (`FireControlCut124Tests.cs:2589`), no AmmoType (the ballistic-ammo revision owns rounds), and a + description with no maker name. Two or more products from distinct makers, never Miss Terri's or + the Pirates, product name never the design name. +- **CB-R3, caliber ladder** (per step from the template's caliber): Damage per shot x2.0, Range + x1.2, Mass x2.0, Price x2.0, Energy and Heat x1.8, Cooldown x1.25, Spread x0.85, Visibility x1.5. + Tracking (authored, steep): 2-cell Small 10-12, Medium 5-6, Large 3, ExtraLarge 1.5; flame guns + double those (the cone covers what tracking misses). +- **CB-R4, footprints for new designs.** Small 1x2, Medium 2x2 (launchers 1x3), Large 2x3 + (launchers 3x2), ExtraLarge 2x4 (fits the station Turret's Ballistic 2x4). +- **CB-R5, catalog blob.** Each content cut lands through one idempotent AetherDb command; under + follow-up `catalog-blob-merge-order` its merge takes master's catalog, reruns the command's + `apply`, commits the regenerated blob in its own LFS commit with the dry-run text and reruns the + suite. No cut picks a side's blob. + +### Design tables + +Product names are drafts in each maker's voice (profiles: catalog-growth addenda); Hands writes the +one-line descriptions in that voice. + +**matrix-coilguns** (EMPS, Ballistic hardpoint, template ClearPath `0271bb97` for Small and +Autocannon `13116166` above; EMPS vs XPS at one caliber: Velocity x2.5, Penetration authored high, +Energy x3 per `ballistics-cycle-power`, Damage x1.2, Cooldown x1.6; roles barrel, feed mechanism): + +| Design | Caliber | Shape | Products (maker) | +|---|---|---|---| +| Light Coilgun | Small | 1x2 | RC-1 Needle (R&D), Pin (Finch) | +| Coilgun | Medium | 2x2 | RC-4 Spindle (R&D), Plumb Line (Adrasteia) | +| Railgun | Large | 2x3 | True Meridian (Adrasteia), RX-9 Lancer (R&D), Gavel (Zhestokost) | +| Heavy Railgun | ExtraLarge | 2x4 | Axis (Adrasteia), Long Reach (AU) | + +**matrix-cannons** (XPS, template Autocannon, AutoWeaponData; barrel, feed mechanism): + +| Heavy Autocannon | Large | 2x3 | Bulwark (Zhestokost), Foreman (AU) | +|---|---|---|---| +| Siege Cannon | ExtraLarge | 2x4 | Last Word (Zhestokost), big red problem (DME) | + +**matrix-beams** (Energy; Laser from ColdFire `2933c4ce`, PPC from ChargeBlast+- `499f7891`; +focusing array, power coupling): + +| Medium Beam Laser | Medium | 2x2 | Filament (Alakrita), Limelight (Lucent) | +|---|---|---|---| +| Large Beam Laser | Large | 2x3 | Marquee (Lucent), Clear Sight (Adrasteia) | +| Heavy Ion Cannon | Large | 2x3 | Blackout (NiteLife), short circuit (DME) | +| Ion Lance | ExtraLarge | 2x4 | Lights Out (NiteLife), IX-12 Breaker (R&D) | + +**matrix-lightning** (ESD, ChargedWeaponData from plight `632420a6`; focusing array, power coupling): + +| Arc Thrower | Small | 1x2 | Sparkler (Lucent), Live Wire (NiteLife) | +|---|---|---|---| +| Storm Projector | Medium | 2x2 | Thunderhead (NiteLife), everybody dance (DME) | +| Tempest Projector | ExtraLarge | 2x4 | Grand Finale (Lucent), Mains (NiteLife) | + +**matrix-missiles** (Launcher; Missile from GT 3K `5ee2ff03`, MicroMissile from LRMM72 `32157982`; +guidance system, thruster, warhead): + +| Light Missile Launcher | Small | 1x2 | Fledgling (Finch), Courier (AU) | +|---|---|---|---| +| Heavy Missile Launcher | Large | 3x2 | GT 6K (R&D), Freight Forward (AU) | +| Torpedo Launcher | ExtraLarge | 2x4 | Battering Ram (Zhestokost), GT 12K (R&D) | +| Micromissile Battery | ExtraLarge | 2x4 | the whole show (DME), Fireworks (Lucent) | + +**matrix-mines** (Launcher, MineLayerData from Mine Launcher `1b4a2bb3`; new role map: warhead +reads Damage, Penetration, DamageSpread; dispenser reads Range, Cooldown, Spread, Velocity, Energy, +Heat, Visibility; ArmingDelay, FuseDelay and Lifetime stay authored per design, not ladder-scaled): + +| Mine Rack | Medium | 1x3 | party favours (DME), Caltrop (Zhestokost) | +|---|---|---|---| +| Heavy Mine Layer | Large | 3x2 | surprise party (DME), Wake Warden (Lightsail) | +| Minefield Projector | ExtraLarge | 2x4 | Exclusion Zone (Zhestokost), do not enter (DME) | + +**matrix-flamers** (Jet, Ballistic hardpoint, InstantWeaponData pellet gun from Flak Gun +`6af6446e`: DamageType Thermal, Count 8-12, Spread 25-35 deg, Range 150-300 at Small, Velocity +300; roles nozzle (Range, Spread, Velocity) and fuel pump (Damage, Cooldown, Energy, Heat, +Visibility)). Absorbs follow-up `pirates-flamethrower`'s design; the Pirates' own 🔥 product moves +in their own record, not here. + +| Light Flamethrower | Small | 1x2 | hot take (DME), Scorch (Zhestokost) | +|---|---|---|---| +| Flamethrower | Medium | 2x2 | keep it lit (DME), Brushfire (AU) | +| Heavy Flamethrower | Large | 2x3 | Purifier (Zhestokost), burn the boats (DME) | +| Inferno Projector | ExtraLarge | 2x4 | Ashfall (Zhestokost), Afterparty (NiteLife) | + +After these, every cell of CB3 is filled except the SplitMissile row (follow-up +`mirv-split-munition`): 24 new designs, 49 new products. + +**Stat boosters** (Tool 1x1, template Targeting Computer `c07ea205`'s EnergyDraw and Heat, then a +StatModifierData Multiplier in the same group so power gates it; roles amplifier (the magnitude) +and regulator (EnergyDraw, Heat); magnitude per question `boost-strength`): + +| Design | Targets | Products (maker) | Cut | +|---|---|---|---| +| Thrust Amplifier | ThrusterData.Thrust | Tailwind (Finch), Second Wind (Lightsail) | boost-gear-systems | +| Signal Amplifier | SensorData.Sensitivity | Earful (Finch), Lookout (AU) | boost-gear-systems | +| Emissivity Booster | RadiatorData.Emissivity | Cold Read (Adrasteia), Cool Head (Lightsail) | boost-gear-systems | +| Reactor Overclock | ReactorData.Charge | Afterhours (NiteLife), redline (DME) | boost-gear-systems | +| Damage Amplifier | WeaponData.Damage | Showstopper (Lucent), Heavy Hand (Zhestokost) | boost-gear-combat | +| Range Extender | WeaponData.Range | Far Sight (Adrasteia), Long Look (Finch) | boost-gear-combat | +| Shield Booster | ShieldData.Capacity | Thick Skin (AU), SB-2 Bastion (R&D) | boost-gear-combat | + +### Rationale + +**Why the category is WeaponType and the size is WeaponCaliber.** The operator's own taxonomy +(CB2) names exactly these two axes, and the catalog already carries both fields. Footprint is mount +space, which the brief's "sizes (cell footprints)" reads as the size; the two disagree in six +records (CB3). Recommended: caliber is the one authored size, and a catalog test holds every +footprint inside its caliber's band, so neither can drift; the six are fixed as content. plight is +the exception the operator holds open (ruling `plight-shape`, Defaulted, revisit after play): it +stays Large, and the band test names that ruling as its one exemption. Prior art: Starsector mounts +are type x size (small/medium/large; ballistic/energy/missile) and every type exists at every size; +Elite's hardpoints are class 1-4 with weapon families not filling every class. + +**Why MIRV waits.** A split missile with no split is a mislabelled missile. Missiles become stepped +munition records only after missile-records (r3, no report); splitting belongs on that record. + +**Why components are the role and its ProductRole.** No system reads a part record (crafting is not +in the demo; the target's not-in-scope line excludes new mechanics), so a component item now would +be inert data. X4 and EVE give components item records because their economies produce and consume +them; the role-quality pair already carries what the operator's examples need (a laser better at +what its focusing role governs). Question `component-items` asks the operator. + +**Why the stacking question recommends diminishing returns.** Interior cells are cheap compared with +hardpoints, so unbounded multiplication (CB10) makes a hold full of amplifiers the dominant fit. +EVE University, Stacking penalties (fetched 2026-10-08): the n-th percentage modifier on one +attribute is multiplied by S(n-1), S(u) = e^-(u/2.67)^2, strongest first, so 100%, 86.9%, 57.1%, +28.3%, 10.6%, 3.0%; absolute effects are never penalized; positive and negative effects are +penalized separately. Starsector hullmods are on or off per ship and priced in ordnance points +(starsector.wiki.gg/wiki/Hullmods, fetched 2026-10-08: Unstable Injector raises speed and cuts +weapon range 25%); the exact bonus values were not on that page. Nebulous jammers stack on a similar +diminishing curve (consumables prior-art file, "Stacking and exclusivity"). Because consumables +attach through the same resolver, whichever rule the operator picks covers part (c) too. + +**Cut order.** roles-backfill and modifier-subtypes are independent of everything here. The seven +matrix cuts are independent of one another and of generic-design-names (templates by key, new +designs born generic); they wait only on the axis ruling. caliber-bands lands last: it recalibers +six records and adds the band and coverage tests that would fail before the matrix fills. +product-lines-weapons r2 adds roles-backfill to r1's dependencies so its new products of Autocannon, +LRMM72, SRMM72 and Mine Launcher author ProductRole for real roles. product-lines-gear turns +follow-up `gear-product-lines` into a spec (Self withdraws the follow-up as absorbed when it +admits nothing else over it). boost-gear-combat waits on modifier-subtypes; modifier-stacking and +both boost cuts wait on the operator. + +**Record keys.** No cut here adds a MessagePack key to any type, so none touches the Faction key +collision (maker-only-faction 19, faction-play-1 Doctrine, livery 18). + +### Admitted (mind receipts 2026-10-08 19:48-19:50 UTC) + +Questions: `weapon-matrix-axes` (raised in matrix-coilguns, blocks every matrix cut and +caliber-bands), `component-items` (raised in nothing: the specs follow its recommended option and +stay valid under the others), `boost-strength` (raised in boost-gear-systems), `boost-stacking` +(raised in modifier-stacking). Cut specs r1: roles-backfill, modifier-subtypes, modifier-stacking, +boost-gear-systems, boost-gear-combat, matrix-coilguns, matrix-cannons, matrix-beams, +matrix-lightning, matrix-missiles, matrix-mines, matrix-flamers, caliber-bands, product-lines-gear; +product-lines-weapons r2 with resolution `cut_spec.cut-product-lines-weapons.r1.n1` (Superseded). +Follow-ups: `mirv-split-munition`, `homeless-calibers`, `matrix-rounds`. Left for Self: +follow-up `gear-product-lines` is absorbed by product-lines-gear (close it when that merges); +follow-up `pirates-flamethrower`'s design half is absorbed by matrix-flamers (its product move +stays the Pirates' record's). From 7ee40255cabbd3374787ceb5d987ad4374b7c943 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 22:01:47 +0200 Subject: [PATCH 041/120] Modifiers reach a behaviour type and its subtypes: TargetsOf matches assignable types A StatReference naming WeaponData.Damage validated and attached to nothing, because TargetsOf compared exact types. All three filters now use targetType.IsAssignableFrom; no exact-match path remains. Co-Authored-By: Claude Sonnet 5.5 --- .../ServerShared/Behaviors/StatModifier.cs | 6 +- .../StatResolverTests.cs | 61 ++++++++++++++++++- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/Assets/Scripts/ServerShared/Behaviors/StatModifier.cs b/Assets/Scripts/ServerShared/Behaviors/StatModifier.cs index 5e56e4c42..430536591 100644 --- a/Assets/Scripts/ServerShared/Behaviors/StatModifier.cs +++ b/Assets/Scripts/ServerShared/Behaviors/StatModifier.cs @@ -95,13 +95,13 @@ public class StatModifier : Behavior, IInitializableBehavior, IDisposable, IAlwa .Where(g => string.IsNullOrEmpty(data.RequireBehavior) || entity.ItemManager.GetData(g.EquippableItem).Behaviors.Any(b => b.GetType().Name == data.RequireBehavior)); return typeof(EquippableItemData).IsAssignableFrom(targetType) - ? gear.Where(g => entity.ItemManager.GetData(g.EquippableItem).GetType() == targetType) + ? gear.Where(g => targetType.IsAssignableFrom(entity.ItemManager.GetData(g.EquippableItem).GetType())) .Select(g => new Target(g, statField.GetValue(entity.ItemManager.GetData(g.EquippableItem)) as PerformanceStat)) .Where(t => t.Stat != null) .ToArray() - : gear.Where(g => entity.ItemManager.GetData(g.EquippableItem).Behaviors.Any(bd => bd.GetType() == targetType)) + : gear.Where(g => entity.ItemManager.GetData(g.EquippableItem).Behaviors.Any(bd => targetType.IsAssignableFrom(bd.GetType()))) .SelectMany(g => entity.ItemManager.GetData(g.EquippableItem).Behaviors - .Where(bd => bd.GetType() == targetType) + .Where(bd => targetType.IsAssignableFrom(bd.GetType())) .Select(bd => new Target(g, statField.GetValue(bd) as PerformanceStat))) .Where(t => t.Stat != null) .ToArray(); diff --git a/tests/Aetheria.Shared.Tests/StatResolverTests.cs b/tests/Aetheria.Shared.Tests/StatResolverTests.cs index 6c0677a2a..5b731215d 100644 --- a/tests/Aetheria.Shared.Tests/StatResolverTests.cs +++ b/tests/Aetheria.Shared.Tests/StatResolverTests.cs @@ -17,6 +17,8 @@ public sealed class StatResolverTests : IDisposable private string Catalog => Path.Combine(_root, "Aetheria.cc"); private static readonly int2 HardpointCell = new int2(0, 0); private static readonly int2 SecondHardpointCell = new int2(1, 0); + private static readonly int2 FirstGunCell = new int2(2, 0); + private static readonly int2 SecondGunCell = new int2(0, 1); public StatResolverTests() => Directory.CreateDirectory(_root); public void Dispose() => Directory.Delete(_root, true); @@ -40,7 +42,9 @@ public sealed class StatResolverTests : IDisposable Hardpoints = { new HardpointData { Type = HardpointType.Sensors, Position = HardpointCell, Shape = new Shape() }, - new HardpointData { Type = HardpointType.Sensors, Position = SecondHardpointCell, Shape = new Shape() } + new HardpointData { Type = HardpointType.Sensors, Position = SecondHardpointCell, Shape = new Shape() }, + new HardpointData { Type = HardpointType.Sensors, Position = FirstGunCell, Shape = new Shape() }, + new HardpointData { Type = HardpointType.Sensors, Position = SecondGunCell, Shape = new Shape() } } }); cache.Upsert(new GearData { Name = "Battery", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Durability = 10, Behaviors = { capacitor } }); @@ -72,6 +76,61 @@ public sealed class StatResolverTests : IDisposable return ship; } + // A modifier's StatReference names a behaviour type, and WeaponData.Damage is declared once on the abstract + // base. A ship carrying an AutoWeapon gun and a ChargedWeapon gun and a Booster whose modifier aims at + // `target` is the real case; the two tests below read each gun's resolved Damage through EquippedItem.Evaluate. + private (CultCache Cache, EquippedItem Auto, EquippedItem Charged, PerformanceStat AutoDamage, PerformanceStat ChargedDamage) + BuildGunShipWithBooster(string target) + { + var autoDamage = new PerformanceStat { Min = 10, Max = 10 }; + var chargedDamage = new PerformanceStat { Min = 10, Max = 10 }; + var modifier = new StatModifierData + { + Stat = new StatReference { Target = target, Stat = nameof(WeaponData.Damage) }, + Modifier = new PerformanceStat { Min = 3, Max = 3 }, + Type = StatModifierType.Multiplier + }; + var cache = OpenCatalog(new CapacitorData { Capacity = new PerformanceStat { Min = 1, Max = 1 } }, modifier); + cache.Upsert(new GearData { Name = "AutoGun", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Durability = 10, Behaviors = { new AutoWeaponData { Damage = autoDamage } } }); + cache.Upsert(new GearData { Name = "ChargedGun", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Durability = 10, Behaviors = { new ChargedWeaponData { Damage = chargedDamage } } }); + cache.FlushAsync().Wait(); + var items = new ItemManager(cache, new ProvenanceLedger(), Settings(), _ => { }); + var ship = BuildActivatedShip(cache, items, withBooster: true); + Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName("AutoGun")), FirstGunCell)); + Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName("ChargedGun")), SecondGunCell)); + // Re-activate so the booster's Initialize sees the guns it did not see at the first activation. + ship.Deactivate(); + ship.Activate(); + var booster = ship.Equipment.Single(e => e.Data.Name == "Booster"); + var behavior = booster.GetBehavior(); + behavior.Execute(0f); + behavior.Update(0f); + return (cache, ship.Equipment.Single(e => e.Data.Name == "AutoGun"), ship.Equipment.Single(e => e.Data.Name == "ChargedGun"), autoDamage, chargedDamage); + } + + // A modifier aimed at the abstract base reaches every subtype carried. Mutation: restore exact-type matching + // (`bd.GetType() == targetType`) in either behaviour filter of StatModifier.TargetsOf and both guns read 10. + [Fact] + public void AModifierOnABaseTypeReachesEverySubtype() + { + var (cache, auto, charged, autoDamage, chargedDamage) = BuildGunShipWithBooster(nameof(WeaponData)); + using var _ = cache; + + Assert.Equal(30f, auto.Evaluate(autoDamage), 3); + Assert.Equal(30f, charged.Evaluate(chargedDamage), 3); + } + + // A modifier aimed at one concrete subtype leaves its sibling alone: assignability is downward only. + [Fact] + public void AModifierOnAConcreteTypeReachesOnlyIt() + { + var (cache, auto, charged, autoDamage, chargedDamage) = BuildGunShipWithBooster(nameof(AutoWeaponData)); + using var _ = cache; + + Assert.Equal(30f, auto.Evaluate(autoDamage), 3); + Assert.Equal(10f, charged.Evaluate(chargedDamage), 3); + } + // §1.1's authority claim, structurally: "StatResolver, one per Entity, constructed with it." Mutation: make // Entity.Resolver a static field -- every entity then shares one resolver, and NotSame goes red. [Fact] From 851476bc23778093b279132e69bd1a2c2e2e39e4 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 22:05:09 +0200 Subject: [PATCH 042/120] Emoji font script: pin Unicode emoji-test.txt, refuse outside the pinned container The corpus now derives from emoji-test.txt (sha256 pinned), keeps single code points of any digit count, and lists the emoji Twemoji lacks. The script runs only on linux, Python 3.13, image python:3.13-slim and records those in the lock; identical pins with a different font exit 1 and leave the committed font and lock untouched. Co-Authored-By: Claude Sonnet 5.5 --- tools/emoji/fetch_emoji_font.py | 120 +++++++++++++++++++++++--------- 1 file changed, 87 insertions(+), 33 deletions(-) diff --git a/tools/emoji/fetch_emoji_font.py b/tools/emoji/fetch_emoji_font.py index 77e03e6f5..901709c4f 100644 --- a/tools/emoji/fetch_emoji_font.py +++ b/tools/emoji/fetch_emoji_font.py @@ -1,14 +1,19 @@ """Build Assets/Fonts/Emoji/Twemoji.ttf from the pinned Twemoji release. -Run with the venv that has tools/emoji/requirements.txt installed: - py -3 tools/emoji/fetch_emoji_font.py [--cache DIR] [--jobs N] +The font reproduces ONLY inside the pinned Linux container (PINNED_IMAGE, Python PINNED_PYTHON); on any +other platform the script refuses to run and writes nothing. It does not run under `py -3` on Windows. +Run it on Yggdrasil (or any docker host) from the repository root: + docker run --rm -e EMOJI_FONT_IMAGE=python:3.13-slim -v "$PWD":/src -w /src python:3.13-slim sh -c \\ + "pip install -r tools/emoji/requirements.txt && python tools/emoji/fetch_emoji_font.py [--jobs N]" -Downloads the jdecked/twemoji release source archive (sha256 pinned below), runs nanoemoji -(glyf_colr_0) over its SVGs, and writes the font, the licence text, the single-code-point corpus -the editor smoke reads, and emoji-font.lock. The font is the only artifact Unity consumes; nothing -here is hand-made art. A second run with the same pins must reproduce the font's sha256. +Downloads the jdecked/twemoji release source archive and Unicode's emoji-test.txt (both sha256 pinned +below), runs nanoemoji (glyf_colr_0) over the SVGs, and writes the font, the licence text, the smoke's +corpus (emoji-singles.txt, from emoji-test.txt), the list of emoji Twemoji lacks (emoji-unsupported.txt) +and emoji-font.lock. The font is the only artifact Unity consumes; nothing here is hand-made art. +A run whose font-determining pins match the lock must reproduce the font's sha256, or the script exits 1 +and leaves the committed font and lock untouched; the lock never follows the machine. """ -import argparse, hashlib, os, re, shutil, subprocess, sys, tarfile, tempfile, urllib.request +import argparse, hashlib, os, platform, re, shutil, subprocess, sys, tarfile, tempfile, urllib.request from importlib import metadata from pathlib import Path @@ -18,10 +23,20 @@ TWEMOJI_SHA256 = "a0855654b633045ae2337537e77f1bb4361162f7fcd910e613eaab1d6d9c5f # 2026-06-01T22:21:37Z, the release's publication time: fixes head.created/modified for reproducibility. SOURCE_DATE_EPOCH = "1780352497" FAMILY = "Twemoji" +# Unicode's emoji-test.txt for the Emoji version Twemoji v17.0.3 targets: the smoke's corpus. +EMOJI_TEST_VERSION = "17.0" +EMOJI_TEST_URL = "https://unicode.org/Public/17.0.0/emoji/emoji-test.txt" +EMOJI_TEST_SHA256 = "1d8a944f88d7952f7ef7c5167fef3c67995bcae24543949710231b03a201acda" +# The one environment the font reproduces in. The docker command passes the image tag in; a container +# cannot read its own image name. +PINNED_PLATFORM = "linux" +PINNED_PYTHON = "3.13" +PINNED_IMAGE = "python:3.13-slim" ROOT = Path(__file__).resolve().parents[2] FONT_DIR = ROOT / "Assets" / "Fonts" / "Emoji" SINGLES = ROOT / "tools" / "emoji" / "emoji-singles.txt" +UNSUPPORTED = ROOT / "tools" / "emoji" / "emoji-unsupported.txt" def sha256(path): @@ -32,16 +47,36 @@ def sha256(path): return h.hexdigest() -def download(cache): - archive = cache / f"twemoji-{TWEMOJI_VERSION}.tar.gz" - if not archive.exists(): +def download(cache, url, name, pinned): + path = cache / name + if not path.exists(): cache.mkdir(parents=True, exist_ok=True) - urllib.request.urlretrieve(TWEMOJI_URL, archive) - got = sha256(archive) - if got != TWEMOJI_SHA256: - archive.unlink() - sys.exit(f"archive sha256 {got} != pinned {TWEMOJI_SHA256}") - return archive + urllib.request.urlretrieve(url, path) + got = sha256(path) + if got != pinned: + path.unlink() + sys.exit(f"{name} sha256 {got} != pinned {pinned}") + return path + + +def single_code_points(emoji_test): + """Every emoji of the Emoji version that is one code point, bare or with its FE0F presentation + selector (so (c) and (r) are in), of any qualification status including components.""" + found = set() + for line in emoji_test.read_text(encoding="utf-8").splitlines(): + if not line or line.startswith("#"): + continue + points = [int(p, 16) for p in line.split(";", 1)[0].split() if int(p, 16) != 0xFE0F] + if len(points) == 1: + found.add(points[0]) + return sorted(found) + + +def refuse_unless_pinned_environment(): + here = (sys.platform, f"{sys.version_info.major}.{sys.version_info.minor}", os.environ.get("EMOJI_FONT_IMAGE")) + if here != (PINNED_PLATFORM, PINNED_PYTHON, PINNED_IMAGE): + sys.exit(f"refusing to run: the font reproduces only on {PINNED_PLATFORM}, Python {PINNED_PYTHON}, image " + f"{PINNED_IMAGE} (EMOJI_FONT_IMAGE); this is {here}. Nothing was written.") def licence_text(root): @@ -66,8 +101,10 @@ def main(): ap.add_argument("--cache", type=Path, default=Path(tempfile.gettempdir()) / "aetheria-emoji-cache") ap.add_argument("--jobs", type=int, default=max(1, min(4, (os.cpu_count() or 2) // 2))) args = ap.parse_args() + refuse_unless_pinned_environment() - archive = download(args.cache) + archive = download(args.cache, TWEMOJI_URL, f"twemoji-{TWEMOJI_VERSION}.tar.gz", TWEMOJI_SHA256) + emoji_test = download(args.cache, EMOJI_TEST_URL, f"emoji-test-{EMOJI_TEST_VERSION}.txt", EMOJI_TEST_SHA256) work = args.cache / f"build-{TWEMOJI_VERSION}" if work.exists(): shutil.rmtree(work) @@ -90,31 +127,48 @@ def main(): subprocess.run(run, check=True, env=env, cwd=work) subprocess.run(["ninja", "-j", str(args.jobs), "-C", str(build)], check=True, env=env) + built = build / f"{FAMILY}.ttf" + font_sha = sha256(built) + tools = {n: metadata.version(n) for n in ("nanoemoji", "fonttools", "picosvg", "ninja")} + font_pins = { + "source_sha256": TWEMOJI_SHA256, **{f"tool_{n}": v for n, v in tools.items()}, + "source_date_epoch": SOURCE_DATE_EPOCH, "color_format": "glyf_colr_0", + "python_version": platform.python_version(), "platform": PINNED_PLATFORM, "container_image": PINNED_IMAGE, + } + lock_path = FONT_DIR / "emoji-font.lock" + old = dict(l.split("=", 1) for l in lock_path.read_text().splitlines() if "=" in l) if lock_path.exists() else {} + if old and all(old.get(k) == v for k, v in font_pins.items()): + if old.get("font_sha256") != font_sha: + sys.exit(f"NOT REPRODUCED: font_sha256 {font_sha} != lock {old.get('font_sha256')} with identical pins. " + "Nothing was written.") + print("reproduced: font_sha256 equals the lock") + + # The smoke's corpus is Unicode's list of single-code-point emoji, not the supplier's file names; + # what Twemoji does not draw is reported, not hidden. + drawn = {int(p.stem, 16) for p in svgs if re.fullmatch(r"[0-9a-f]+", p.stem)} + singles = single_code_points(emoji_test) + unsupported = [cp for cp in singles if cp not in drawn] + FONT_DIR.mkdir(parents=True, exist_ok=True) out = FONT_DIR / f"{FAMILY}.ttf" - shutil.copyfile(build / f"{FAMILY}.ttf", out) + shutil.copyfile(built, out) (FONT_DIR / "LICENSE.txt").write_text(licence_text(src), encoding="utf-8", newline="\n") + SINGLES.write_text("".join(f"{cp:X}\n" for cp in singles), encoding="utf-8", newline="\n") + UNSUPPORTED.write_text("".join(f"{cp:X}\n" for cp in unsupported), encoding="utf-8", newline="\n") + print(f"Twemoji lacks {len(unsupported)} of {len(singles)} single-code-point emoji: " + + " ".join(f"U+{cp:04X}" for cp in unsupported)) - # The smoke's corpus: every single-code-point glyph the pinned release draws. - singles = sorted(p.stem for p in svgs if re.fullmatch(r"[0-9a-f]{4,6}", p.stem)) - SINGLES.write_text("".join(f"{s}\n" for s in singles), encoding="utf-8", newline="\n") - - tools = {n: metadata.version(n) for n in ("nanoemoji", "fonttools", "picosvg", "ninja")} lock = [ f"source_url={TWEMOJI_URL}", f"source_version={TWEMOJI_VERSION}", f"source_sha256={TWEMOJI_SHA256}", - f"svg_count={len(svgs)}", f"single_codepoint_count={len(singles)}", + f"emoji_test_version={EMOJI_TEST_VERSION}", f"emoji_test_url={EMOJI_TEST_URL}", f"emoji_test_sha256={EMOJI_TEST_SHA256}", + f"svg_count={len(svgs)}", f"single_codepoint_count={len(singles)}", f"unsupported_count={len(unsupported)}", *(f"tool_{n}={v}" for n, v in tools.items()), f"source_date_epoch={SOURCE_DATE_EPOCH}", "color_format=glyf_colr_0", - f"font_file={out.name}", f"font_sha256={sha256(out)}", f"singles_sha256={sha256(SINGLES)}", + f"python_version={font_pins['python_version']}", f"platform={PINNED_PLATFORM}", f"container_image={PINNED_IMAGE}", + f"font_file={out.name}", f"font_sha256={font_sha}", + f"singles_sha256={sha256(SINGLES)}", f"unsupported_sha256={sha256(UNSUPPORTED)}", ] - previous = (FONT_DIR / "emoji-font.lock") - if previous.exists(): - old = dict(l.split("=", 1) for l in previous.read_text().splitlines() if "=" in l) - if old.get("font_sha256") != sha256(out): - print(f"NOT REPRODUCED: font_sha256 {sha256(out)} != lock {old.get('font_sha256')}") - else: - print("reproduced: font_sha256 equals the lock") - previous.write_text("\n".join(lock) + "\n", encoding="utf-8", newline="\n") + lock_path.write_text("\n".join(lock) + "\n", encoding="utf-8", newline="\n") print("\n".join(lock)) From e7375d54a17c6be32a57f7ab3a3d37ddfa5a2114 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 22:06:21 +0200 Subject: [PATCH 043/120] Map: consumables (host fix, supply, throttle lock, vapour cloud) Body facts CC1-CC12, rules CS-R1..CS-R4 and rationale from the imagination-consumables pass; specs and questions are in the mind. Co-Authored-By: Claude Opus 5.5 --- docs/aetheria-release-map.md | 226 +++++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) diff --git a/docs/aetheria-release-map.md b/docs/aetheria-release-map.md index 65f85ef3b..5528cfd51 100644 --- a/docs/aetheria-release-map.md +++ b/docs/aetheria-release-map.md @@ -6488,3 +6488,229 @@ Follow-ups: `mirv-split-munition`, `homeless-calibers`, `matrix-rounds`. Left fo follow-up `gear-product-lines` is absorbed by product-lines-gear (close it when that merges); follow-up `pirates-flamethrower`'s design half is absorbed by matrix-flamers (its product move stays the Pirates' record's). + +## Consumables: the host fix, supply, throttle lock and vapour cloud + +Pass: Imagination `imagination-consumables`, session `self-2026-10-08b`. Self merges this into +`docs/aetheria-release-map.md`; nothing here is committed. Body: `GameCult/Aetheria` origin/master +`a8f71d463bf1160de0b5f23caa56dc7957252345` (fetched 2026-10-08 ~20:00 UTC; the pirates-record merge had +not landed), detached worktree in the session scratchpad, removed at the end. Catalog: LFS blob +`GameData/Aetheria.cc` sha256 `5ecf375f...a011`, decoded with `cultcache_py` +`SingleFileMessagePackBackingStore.pull_all()` and `msgpack` (payloads are arrays indexed by MessagePack +key). Scratch probes `q1_thrust_speed.py`, `q2_templates.py`, not committed. + +Rulings applied: `catalog-breadth-weapons-gear-consumables` (operator: "consumables (goofy stuff like +juicing your thrusters for a few seconds but locking the throttle at max, or a consumable coolant that +turns into a cloud of vapor to break locks)"), `catalog-breadth-roles-and-components` ("All this comes +with roles and components too, right?"), `catalog-grows-generic-designs-branded-products`, +`sensor-stat-set`. Reused unchanged from parts (a) and (b) +(`F:\Projects\aetheria-map-addenda-catalog-breadth-2026-10-08.md`): CB-R1 (roles), CB-R2 (authoring), +CB-R5 (catalog blob), and the stacking rule of question `boost-stacking` through cut `modifier-stacking`. +Prior art: `F:\Projects\aetheria-consumables-prior-art-2026-10-08.md` (cited below as PA section n). + +### Body facts + +- **CC1. The consumable mechanism exists end to end in code, and no record uses it.** Census: no + `aetheria.consumableitemdata` record in the catalog (decode). `ConsumableItemData : CraftedItemData` + (`ItemData.cs:342-358`, keys 10 Behaviors, 11 Stackable, 12 Duration, 13 Icon, 14 Effectiveness; + Roles at key 9 from CraftedItemData). Instance `ConsumableItem` (`ItemInstance.cs:65`, union 3 and 2). + Activation: `Entity.TryActivateConsumable` (`Entity.cs:872-884`) refuses a second active effect of a + non-Stackable design (`CanActivateConsumable`, `:867-870`), takes the first instance of that design + from cargo, builds a `ConsumableItemEffect` and removes the instance. Each tick + (`Entity.cs:1443-1454`) the effect updates its behaviours in order, stopping at the first that fails + (`ConsumableItemEffect.Update`, `:1604-1619`), and at `RemainingDuration < 0` is dropped after + `Resolver.Forget(effect)`. Catalog load validates a consumable's modifiers and role usage + (`AetheriaStores.cs:77-80`). +- **CC2. Player input exists; AI input does not.** The player drags a consumable from cargo onto an + action-bar slot (`ActionGameManager.cs:484-485`), the slot calls `TryActivateConsumable` + (`ActionBarSlot.cs:118-121`), shows the count in cargo and the remaining-duration fill + (`:128-134`), and the binding is saved (`SavedGame.cs:221,230`). Nothing in `Agents/` calls it + (grep: the only caller is `ActionBarSlot.cs:120`). +- **CC3. Defect: a consumable's stat modifier never reaches anything, and would throw.** + `StatModifier.Initialize` (`StatModifier.cs:82-89`) is what computes `_targets`; the only caller of + `IInitializableBehavior.Initialize` is `Entity.Activate` over `Equipment` (`Entity.cs:202-207`). + `ActivateConsumable` (`Entity.cs:857-860`) never calls it, so `_targets` stays null. The effect's + `Update` runs `IAlwaysUpdatedBehavior.Update` before `Execute`; on the second tick `_executed && !_applied` + calls `ApplyModifier` (`:224-230`), which iterates the null `_targets`: a NullReferenceException. + Source read; no test covers it (the expiry test `StatResolverTests.cs:363` uses a consumable with no + behaviours). Body fact CB10's claim that consumable effects attach through the resolver holds for the + resolver's API, not for this host path. +- **CC4. Defect: an expired consumable's modifiers would outlive it.** Attachments are keyed by the + target item as owner and the `StatModifier` instance as modifier key (`StatModifier.cs:224-237`). + Expiry calls `Resolver.Forget(effect)` (`Entity.cs:1451`), which removes only entries whose owner is + the effect (`StatResolver.cs:154-170`); nothing calls `StatModifier.Dispose` (`:245-249`). Once CC3 is + fixed, a boost would stay attached to the thrusters forever. +- **CC5. Defect: a bought or generated consumable is not a ConsumableItem.** + `ItemManager.CreateInstance(int lot)` (`ItemManager.cs:173-189`) returns `EquippableItem` or else + `CompoundCommodity`; `TryActivateConsumable` then casts the cargo instance to `ConsumableItem` + (`Entity.cs:880`), an InvalidCastException. The tests build `ConsumableItem` by hand for this reason + (`LoadoutTests.cs:787-791`). +- **CC6. Nobody sells consumables, and nothing restocks.** Station stock is + `RandomProducts(16, 1, ...)` into the station's cargo at generation + (`LoadoutGenerator.cs:110-119`); `RandomProducts` and `AvailableProducts` are constrained to + `EquippableItemData` (`:142`, `:154`) because the availability rule includes `HasHome` (`:164`). + Stations are generated once per zone (`ZoneGenerator.cs:303,324`); no code restocks a station + (grep `Restock`, `GenerateStationLoadout`). The trade menu sells out of station cargo + (`StationServices.TryBuy`, `StationServices.cs:73`; `BuyPrice` prices any CraftedItemInstance, `:13-24`). +- **CC7. Cargo is the inventory.** A crafted instance occupies its design's `Shape` cells in a cargo bay + (`Entity.cs:2187-2207`); there is no item stack for crafted items. A consumable design therefore needs + a Shape (ItemData key 5); CompoundCommodity records have none (decode: Shape None, Price 0), so they are + not templates. Targeting Computer `c07ea205` is the 1x1 template for mass, specific heat and + conductivity (CB11). +- **CC8. How locks are decided.** Per observer, `EntityInfoGathered[target]` rises by `Sensor.Gain` + (`Sensor.cs:187-189`: passive `visibility x sensitivity x curve(bearing) x dt / distance`, ping + `visibility x sensitivity x PingBoost x distance` once) and decays by `TargetInfoDecay` each tick + (`Sensor.cs:155-178`). Crossing `TargetDetectionInfoThreshold` adds or removes the target from + `VisibleEnemies` (`Entity.cs:261-277`); leaving `VisibleEnemies` clears the observer's target + (`Entity.cs:248-250`), which zeroes every `LockWeapon` lock (target changed, `LockWeapon.cs:85-90`). + Lock rate is `LockSpeed x info^SensorImpact` inside `LockAngle` (`:98-105`); launchers are + LockWeapons (`Launcher.cs:7`, `CreateInstance` returns `LockWeapon`). FireControl's sensor factor + also reads info (`FireControl.cs:439-442`). Minions retarget when an enemy re-enters + `VisibleEnemies` (`Minion.cs:13`). A missile already in flight is a Unity `GuidedProjectile` that + follows its `Target` transform (`GuidedProjectile.cs:44,142`); the simulation does not re-decide it. + The same `Gain` serves chunk detection through `PassiveRate` (`Sensor.cs:194-199`, `Entity.cs:384`), + and `MiningCut3Tests.cs:254` pins the entity info trace bit for bit. +- **CC9. How thrust and the throttle work.** `Ship.MovementDirection` (`Ship.cs:27`) has two writers, the + player's input (`ActionGameManager.cs:1339`) and the agent (`Agent.cs:70-80`); `Ship.Update` + (`Ship.cs:231-257`) turns it into each thruster's `Axis` (strafe at `:241-252`, forward and reverse at + `:255-256`) and `Turn` into rotation. A thruster pushes by `Thrust` and adds heat and visibility + proportional to its input, not to thrust (`Thruster.cs:116-137`), so a Thrust multiplier alone costs no + heat. Top speed is a hull's `VelocityLimitData.TopSpeed` (decode: Djinni 50, Longinus 100), clamped in + `VelocityLimit.Execute`; the hull is in `Equipment` (`Entity.cs:886`, MapEntity), so a modifier on + `VelocityLimitData.TopSpeed` reaches it. Catalog thrusters: Small Drive 75k, Talaria 75k-250k, Medium + Drive 100k-300k, deep space burnout and RevvITup 250k-1M, Victoire 200k-1.5M (Thrust Min-Max). +- **CC10. Zone bodies.** A mine is a runtime-only `KinematicBody` in `Zone.Mines` (`Mines.cs`, + `Zone.cs:22`), added only by `Zone.Lay` (`:235-239`), stepped and removed only by `StepMines` + (`:245-260`) after the entity loop, never saved, and drawn by one `ZoneRenderer` subscription + (`ZoneRenderer.cs:225-264`, `MinePrefab` `:47`). This is the pattern a vapour cloud follows. +- **CC11. A consumable's own heat does nothing.** `Behavior.AddHeat` is a no-op for a consumable host + (`Behaviors.cs:60`, "TODO: Heat for Consumables"). The first set takes its heat cost through the + thrusters' own Heat stat instead. +- **CC12. Record keys.** `BehaviorData` union ids run to 40 (`MineLayerData`, `Behaviors.cs:189`); 12 and + 26 are retired. No remote branch (71 refs, `git show :.../Behaviors.cs`) claims 41 or above. This + pass claims **41 `ThrottleLockData`** and **42 `VapourDumpData`**. No key is added to Faction, + ConsumableItemData or any saved type; the Faction 16-19 collision is untouched. `VapourCloud` is + runtime only, like `Mine`. + +### Model page row changes + +- **ConsumableItemData (catalog)**: new live row. Named by record key; a design with roles (CB-R1), + Shape 1x1, a Price, Stackable false, a Duration, and behaviours. Decided by its AetherDb command. +- **ConsumableItem (run save, cargo)**: created by `ItemManager.CreateInstance` for a consumable design; + lives in a cargo bay; consumed by `TryActivateConsumable`. +- **ConsumableItemEffect (runtime)**: one per activation; owns its behaviours' lifetime: initialised at + activation, disposed at expiry, then forgotten by the resolver. Not saved. +- **Entity.ThrottleLocked (derived)**: true while any active effect carries a `ThrottleLock`. Nobody + writes it. `Ship.Update` is the one place that turns intent (`MovementDirection`) into thruster axes, + and it reads the lock there. +- **VapourCloud (runtime, Zone.Clouds)**: added only by `Zone.Vent`, stepped and removed only by + `Zone.StepClouds`. `Zone.Obscuration(a, b)` is derived from the live clouds; `Sensor.Gain` is its one + reader. + +### Rules (cited by the specs) + +- **CS-R1, the consumable mechanic.** A consumable is a cargo item, single use, activated from the action + bar (player) or by an agent rule (AI). Its effect is timed (`Duration`) and carries its own drawback in + the same record. A non-Stackable design allows one active effect at a time, so its Duration is also its + cooldown; no second cooldown mechanism. Boosts from a consumable stack with gear under the one resolver + rule (`modifier-stacking`). +- **CS-R2, roles.** Per CB-R1, every non-flat effect stat reads one declared role and every product + authors one ProductRole per role. Benefit magnitudes rise with quality, cost magnitudes fall. +- **CS-R3, the throttle lock.** While locked, the effective movement intent is full forward, no strafe, + no reverse; turning stays with the pilot (question `overdrive-steering`). Intent writers keep writing; + they are not owners of the thruster axes. +- **CS-R4, obscuration.** A cloud is a disc with an opacity that fades linearly to 0 over its lifetime. + Every sensor gain (passive, ping and chunk) is multiplied by the product of `(1 - opacity)` over the + clouds the sight line crosses, endpoints inside counting as crossing; with no cloud crossed no + multiplication happens, so the info trace is bit-identical. Symmetric: the venting ship sees out no + better than others see in. Lock loss is the existing rule (CC8): info decays below the detection + threshold and the target is dropped. A sensor strong or close enough still burns through. + +### Design tables + +**Thruster Overdrive** (cut `consumables-first-set`; ConsumableItemData; Shape 1x1; Mass 10; Price +6,000; Stackable false; Duration 5 s; roles **propellant**, **regulator**; behaviours in order: +ThrottleLock, then four StatModifierData Multipliers): + +| Target stat | Magnitude Min-Max | Role | +|---|---|---| +| ThrusterData.Thrust | 2.0-3.0 | propellant | +| VelocityLimitData.TopSpeed | 1.5-2.0 | propellant | +| ThrusterData.Heat | 4.0-2.5 | regulator | +| ThrusterData.Visibility | 4.0-2.5 | regulator | + +Products: **send it** (Death Monkey Explosives), **Updraft** (Finch Cybernetics). The heat and plume cost +is real because thrusters heat by input, not thrust (CC9): a locked throttle runs every forward thruster +at full input for 5 s at up to 4x heat and plume. Prior art: Starsector Burn Drive (+200 speed for 5 s, +no steering, PA 1), Plasma Jets (+375% acceleration, 3 s, PA 1), EVE overheating (+50% propulsion, heat +damage, PA 5). + +**Coolant Vent** (same cut; ConsumableItemData; Shape 1x1; Mass 15; Price 4,000; Stackable false; +Duration 12 s; roles **nozzle**, **coolant**; behaviours in order: VapourDump, then one StatModifierData +Multiplier): + +| Stat | Min-Max | Role | +|---|---|---| +| VapourDumpData.Radius | authored so the cloud encloses the largest ship hull (Djinni, 14x17 cells) at Min; Max 1.5x Min | nozzle | +| VapourDumpData.Opacity | 0.80-0.95 | nozzle | +| VapourDumpData.Lifetime | 10-14 s | coolant | +| RadiatorData.Emissivity (penalty, the drawback: the coolant is gone) | 0.4-0.6 | coolant | + +Products: **Smoke Machine** (NiteLife Energy), **Morning Fog** (Lightsail Express). Prior art: Nebulous +jamming against seeker return (PA 8), Elite heat sink and silent running (PA 3), Star Citizen chaff as a +field effect (PA 9), EVE ECM chance as strength against sensor strength (PA 5). + +### Rationale + +**Fix the host before adding content.** The mechanism the brief names (CB12) exists but is broken in +three places (CC3, CC4, CC5): a consumable with a stat modifier throws on its second tick, would leak its +boost if it did not, and can never be bought. `consumable-host` is the smallest cut that makes the +existing path true; no new owner. Cut `modifier-stacking`'s test `AConsumableBoostStacksWithGear` cannot +pass before it, so its r2 adds the dependency and nothing else. + +**Why the throttle lock is derived, not latched.** Two writers already set `MovementDirection` (CC9). A +third writer (the consumable) would race them; a latch set by a behaviour and cleared each tick is a +second owner of time. `Entity.ThrottleLocked` derived from the active effects, read once in +`Ship.Update`, cannot outlive its effect and needs no reset path. + +**Why obscuration and not a flat break.** The sim already decides locks by information against a +threshold (CC8), and lock loss already clears targets and zeroes locks. Scaling gain along the sight line +makes the cloud a contest of signal against obscurant, the Nebulous shape (PA 8: a seeker's return must +exceed the jamming it feels; burnthrough exists), and EVE's lesson that a guaranteed area break is +unreliable or oppressive is avoided (PA 5). A flat break would add a second rule that drops targets, +beside the one that exists. The cloud rides `Sensor.Gain`, which survives the `sensor-stat-set` rebuild +(every emitter still produces a gain). In-flight missiles are not re-decided by the sim (CC8): the cloud +denies the lock before launch; seekers in flight are follow-up `cloud-vs-missiles-in-flight`. + +**Why the cloud is a zone body that inherits velocity.** Vented gas keeps the ship's velocity +(KinematicBody, CC10), so a coasting ship stays inside it and a ship that burns leaves it: the two +consumables combine (vent, then overdrive out of the cloud) and their drawbacks bite each other (the +overdrive plume is bright; leaving the cloud ends the cover). The symmetric rule makes hiding blind. + +**Hoarding.** The elixir literature (PA 10) names scarcity, unlimited inventory and unclear purpose as +causes, and caps, abundance and situational need as cures. Here: cargo cells and mass are the cap (CC7), +both designs are cheap (6,000 and 4,000 against 25,000-250,000 for a drive) and stocked at every station +under question `consumable-supply`, AI pilots use them (cut `consumable-ai`) so the player sees what +they do, and each solves a moment (being locked, needing distance) rather than adding a stat the player +could save for a boss. Neither is a permanent advantage: each carries its drawback inside the effect. +X4's lesson (PA 9: flares strong in AI hands, worthless in the player's) is answered by giving the player +and the AI the same rule and the same information (a painted target always knows, `sensor-stat-set`). + +**Magnitudes** are authored ranges grounded in the prior art above; follow-up `combat-pace-pass` +measures time-to-kill and engagement distance and tunes them with everything else. No question is +raised on them. + +**Cut order.** `consumable-host`, `throttle-lock` and `vapour-cloud` are independent. `consumable-supply` +needs the host (instances). `vapour-cloud-presenter` needs the cloud. `consumables-first-set` needs the +host, the lock and the cloud. `consumable-ai` needs all of those and supply. `modifier-stacking` r2 now +waits on the host. + +### Admitted + +Receipt `mind-commit-06b3cb76...` (2026-10-08 20:05 UTC). Questions: `consumable-supply` (raised in +consumable-supply), `vapour-cloud-model` (raised in vapour-cloud), `overdrive-steering` (raised in +throttle-lock). Cut specs r1: consumable-host, consumable-supply, throttle-lock, vapour-cloud, +vapour-cloud-presenter, consumables-first-set, consumable-ai; modifier-stacking r2 (adds depends_on +consumable-host) with resolution `cut_spec.cut-modifier-stacking.r1.n1` (Superseded). Follow-ups: +`cloud-vs-missiles-in-flight`, `modifier-magnitude-frozen-at-attach`, `consumable-heat-noop`, +`station-restock`. Claimed keys: BehaviorData union 41 ThrottleLockData, 42 VapourDumpData (keys 1-3). From 54999a642b1645383551ede3e98df6e65d7b0d0d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 22:09:18 +0200 Subject: [PATCH 044/120] Roles backfill: AetherDb command and catalog role tests (CB-R1) AetherDb roles-backfill gives Autocannon, LRMM72, SRMM72, Mine Launcher, Large Drive (and Small Drive, which declared roles no stat read) live roles: each mapped flat stat becomes a 0.8x..1.2x range read through a Quality term naming its role, and every seller authors a ProductRole per role from its maker profile. MineLayerRoles is new; the maker profile table is hoisted so pd-gear and this command share it. ShippedCatalogRolesAreLive and RolesBackfillIsAuthored read the shipped catalog. The catalog blob lands in its own commit. Co-Authored-By: Claude Sonnet 5.5 --- .../Aetheria.Shared.Tests/CatalogRoleTests.cs | 126 +++++++++++++++ tools/AetherDb/Program.cs | 149 ++++++++++++++++-- 2 files changed, 265 insertions(+), 10 deletions(-) create mode 100644 tests/Aetheria.Shared.Tests/CatalogRoleTests.cs diff --git a/tests/Aetheria.Shared.Tests/CatalogRoleTests.cs b/tests/Aetheria.Shared.Tests/CatalogRoleTests.cs new file mode 100644 index 000000000..2bab3218d --- /dev/null +++ b/tests/Aetheria.Shared.Tests/CatalogRoleTests.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +// Aetheria release, cut roles-backfill, rule CB-R1: a crafted design declares roles iff a stat varies with quality, +// every declared role is read by a non-flat stat, and every seller authors one ProductRole per role. Both tests read +// the shipped catalog through the real reader. +public sealed class CatalogRoleTests +{ + // Every weapon and every gear of a kind that carries per-part quality stats (the kinds roles-migrate keeps a role + // map for). Cargo bays, docking bays, control modules, tools and hulls carry no quality-reading stat. + private static readonly HardpointType[] QualityGearKinds = + { HardpointType.Radiator, HardpointType.Reactor, HardpointType.Sensors, HardpointType.Thruster }; + + private static IEnumerable StatsOf(EquippableItemData design) => + design.Behaviors.Where(b => b != null) + .SelectMany(b => b.GetType().GetFields().Where(f => f.FieldType == typeof(PerformanceStat)).Select(f => (PerformanceStat)f.GetValue(b))) + .Where(s => s?.Terms != null); + + private static PerformanceStat Stat(EquippableItemData design, string field) => + Assert.Single(design.Behaviors.SelectMany(b => b.GetType().GetFields().Where(f => f.Name == field && f.FieldType == typeof(PerformanceStat)).Select(f => (PerformanceStat)f.GetValue(b)))); + + private static string[] NamedQualityRoles(PerformanceStat stat) => + stat.Terms.Where(t => t.Source == StatSource.Quality && !string.IsNullOrEmpty(t.Role)).Select(t => t.Role).ToArray(); + + [Fact] + public void ShippedCatalogRolesAreLive() + { + var cache = RestoredHullsTests.OpenCatalog(); + var products = cache.GetAll().ToArray(); + var broken = new List(); + foreach (var design in cache.GetAll() + .Where(i => i is WeaponItemData || (i is GearData && QualityGearKinds.Contains(i.HardpointType))) + .OrderBy(i => i.Name, StringComparer.Ordinal)) + { + var varying = StatsOf(design).Where(s => s.Min != s.Max).ToArray(); + var read = varying.SelectMany(NamedQualityRoles).Distinct().OrderBy(r => r, StringComparer.Ordinal).ToArray(); + var declared = (design.Roles ?? new List()).Select(r => r.Name).OrderBy(r => r, StringComparer.Ordinal).ToArray(); + if (varying.Length == 0) broken.Add($"{design.Name}: every stat is flat"); + if (!read.SequenceEqual(declared)) + broken.Add($"{design.Name}: declares [{string.Join(", ", declared)}] but non-flat stats read [{string.Join(", ", read)}]"); + foreach (var product in products.Where(p => p.Design.Key.Equals(cache.RefOf(design).Key))) + { + var authored = (product.Roles ?? new List()).Select(r => r.Role).OrderBy(r => r, StringComparer.Ordinal).ToArray(); + if (!authored.SequenceEqual(declared)) + broken.Add($"{design.Name}: product \"{product.Name}\" authors [{string.Join(", ", authored)}], not each of [{string.Join(", ", declared)}] once"); + } + } + Assert.True(broken.Count == 0, string.Join("\n", broken)); + } + + // value, then the stat's expected Min and Max: a benefit rises with quality (0.8x to 1.2x), a cost falls (1.2x to 0.8x). + private static void AssertRange(EquippableItemData design, string field, float value, bool cost, string role) + { + var stat = Stat(design, field); + Assert.Equal(value * (cost ? 1.2f : .8f), stat.Min, 3); + Assert.Equal(value * (cost ? .8f : 1.2f), stat.Max, 3); + Assert.Contains(role, NamedQualityRoles(stat)); + } + + [Fact] + public void RolesBackfillIsAuthored() + { + var cache = RestoredHullsTests.OpenCatalog(); + EquippableItemData Design(string name) => Assert.Single(cache.GetAll(), i => i.Name == name); + string[] Roles(EquippableItemData d) => d.Roles.Select(r => r.Name).OrderBy(r => r, StringComparer.Ordinal).ToArray(); + + var autocannon = Design("Autocannon"); + Assert.Equal(new[] { "barrel", "feed mechanism" }, Roles(autocannon)); + AssertRange(autocannon, "Damage", 3f, false, "barrel"); + AssertRange(autocannon, "Range", 1000f, false, "barrel"); + + var lrm = Design("LRMM72"); + Assert.Equal(new[] { "guidance system", "thruster", "warhead" }, Roles(lrm)); + AssertRange(lrm, "Damage", 256f, false, "warhead"); + AssertRange(lrm, "Cooldown", 10f, true, "guidance system"); + + var srm = Design("SRMM72"); + Assert.Equal(new[] { "guidance system", "thruster", "warhead" }, Roles(srm)); + AssertRange(srm, "Damage", 128f, false, "warhead"); + AssertRange(srm, "Cooldown", 5f, true, "guidance system"); + + var mine = Design("Mine Launcher"); + Assert.Equal(new[] { "dispenser", "warhead" }, Roles(mine)); + AssertRange(mine, "Damage", 50f, false, "warhead"); + AssertRange(mine, "Cooldown", 2f, true, "dispenser"); + AssertRange(mine, "Spread", 5f, true, "dispenser"); + var layer = Assert.Single(mine.Behaviors.OfType()); + Assert.Equal(2f, layer.ArmingDelay); + Assert.Equal(2f, layer.FuseDelay); + Assert.Equal(30f, layer.Lifetime); + + var drive = Design("Large Drive"); + Assert.Equal(new[] { "injector", "nozzle" }, Roles(drive)); + AssertRange(drive, "Thrust", 250000f, false, "nozzle"); + AssertRange(drive, "Visibility", 100f, true, "nozzle"); + AssertRange(drive, "Heat", 2500f, true, "injector"); + + var smallDrive = Design("Small Drive"); + AssertRange(smallDrive, "Thrust", 75000f, false, "nozzle"); + + // Each seller's means come from its maker's profile; the Pirates are middling and inconsistent. + var profiles = new Dictionary + { + ["Zhestokost"] = (.50f, .08f), ["R&D"] = (.60f, .15f), ["DME"] = (.55f, .20f), ["Lightsail"] = (.55f, .10f), ["Pirates"] = (.45f, .22f), + }; + foreach (var design in new[] { autocannon, lrm, srm, mine, drive }) + { + var sellers = cache.GetAll().Where(p => p.Design.Key.Equals(cache.RefOf(design).Key)).ToArray(); + Assert.NotEmpty(sellers); + foreach (var product in sellers) + { + var maker = cache.Get(product.Manufacturer); + var (mean, dev) = profiles[maker.ShortName]; + Assert.Equal(Roles(design), product.Roles.Select(r => r.Role).OrderBy(r => r, StringComparer.Ordinal).ToArray()); + Assert.All(product.Roles, r => + { + Assert.Equal(mean, r.Mean, 3); + Assert.Equal(dev, r.StandardDeviation, 3); + }); + } + } + Assert.Contains(cache.GetAll(), p => p.Design.Key.Equals(cache.RefOf(autocannon).Key) && cache.Get(p.Manufacturer).ShortName == "Pirates"); + } +} diff --git a/tools/AetherDb/Program.cs b/tools/AetherDb/Program.cs index 6caed9ac5..2900ac4f1 100644 --- a/tools/AetherDb/Program.cs +++ b/tools/AetherDb/Program.cs @@ -42,11 +42,12 @@ public static class Program case "field-kinds": return FieldKindsCatalog(args.Contains("apply")); case "pd-gear": return PdGearCatalog(args.Contains("apply")); case "mine-launcher": return MineLauncherCatalog(args.Contains("apply")); + case "roles-backfill": return RolesBackfill(args.Contains("apply")); case "pirates-faction": Console.OutputEncoding = new System.Text.UTF8Encoding(false); return PiratesFaction(args.Contains("apply")); default: - Console.WriteLine("commands: census, factions, station-fit, hardpoint-fit, loadout [seed], save, settings, settings-dump, dangling [clear ]... [apply], shield-migrate [apply], brownout-migrate [apply], roles-migrate [apply], firing-arc-migrate [apply], targeting-catalog [apply], targeting-catalog-6c [apply], targeting-catalog-6d [apply], ship-authoring create|inspect|validate|compose, field-kinds [apply], mine-launcher [apply], pd-gear [apply], pirates-faction [apply]"); + Console.WriteLine("commands: census, factions, station-fit, hardpoint-fit, loadout [seed], save, settings, settings-dump, dangling [clear ]... [apply], shield-migrate [apply], brownout-migrate [apply], roles-migrate [apply], firing-arc-migrate [apply], targeting-catalog [apply], targeting-catalog-6c [apply], targeting-catalog-6d [apply], ship-authoring create|inspect|validate|compose, field-kinds [apply], mine-launcher [apply], roles-backfill [apply], pd-gear [apply], pirates-faction [apply]"); return 1; } } @@ -182,6 +183,15 @@ public static class Program ["Thrust"] = "thruster", ["MissileVelocity"] = "thruster", ["Velocity"] = "thruster", }; + // Warhead and dispenser: a mine layer is a launcher without guidance or thrust, so its stats split between the + // charge it lays and the mechanism that lays it. ArmingDelay, FuseDelay and Lifetime are timings, not stats. + private static readonly Dictionary MineLayerRoles = new Dictionary + { + ["Damage"] = "warhead", ["Penetration"] = "warhead", ["DamageSpread"] = "warhead", + ["Range"] = "dispenser", ["Cooldown"] = "dispenser", ["Spread"] = "dispenser", ["Velocity"] = "dispenser", + ["Energy"] = "dispenser", ["Heat"] = "dispenser", ["Visibility"] = "dispenser", + }; + private static readonly Dictionary RadiatorRoles = new Dictionary { ["Emissivity"] = "fins", ["WasteHeat"] = "fins", @@ -231,6 +241,17 @@ public static class Program }, }; + // One Mean / StandardDeviation per maker, applied to every role of a design the maker sells (catalog-growth + // addenda, "Maker profiles"); the Pirates are middling and inconsistent (pirates-record). + private static readonly Dictionary MakerProfiles = new Dictionary + { + ["Zhestokost"] = (.50f, .08f), ["AU"] = (.50f, .12f), ["Lightsail"] = (.55f, .10f), ["R&D"] = (.60f, .15f), + ["Lucent"] = (.60f, .15f), ["NiteLife"] = (.55f, .12f), ["Finch"] = (.65f, .20f), ["Alakrita"] = (.60f, .14f), + ["DME"] = (.55f, .20f), ["Adrasteia"] = (.60f, .10f), + }; + + private static readonly (float Mean, float Dev) PirateProfile = (.45f, .22f); + private static string KindOf(EquippableItemData item) => item is HullData hull ? $"Hull/{hull.HullType}" : item is DockingBayData ? "DockingBay" @@ -1527,12 +1548,6 @@ public static class Program flakData.AmmoType = db.Cache.RefOf(ammunition); flakData.MagazineSize = 40; flakData.ReloadTime = 4f; Track(flakData, 20f); - var makers = new Dictionary - { - ["Zhestokost"] = (.50f, .08f), ["AU"] = (.50f, .12f), ["Lightsail"] = (.55f, .10f), ["R&D"] = (.60f, .15f), - ["Lucent"] = (.60f, .15f), ["NiteLife"] = (.55f, .12f), ["Finch"] = (.65f, .20f), ["Alakrita"] = (.60f, .14f), - ["DME"] = (.55f, .20f), ["Adrasteia"] = (.60f, .10f), - }; var products = new (WeaponItemData Design, string Name, string Maker, string Description)[] { (gun, "Waykeeper", "Lightsail", "Freight first, questions later. Keeps a hauler's paint unscratched through a full salvo."), @@ -1563,7 +1578,7 @@ public static class Program Console.WriteLine("\nNew products:"); foreach (var (design, name, maker, description) in products) { - var (mean, dev) = makers[maker]; + var (mean, dev) = MakerProfiles[maker]; Console.WriteLine($" {design.Name} -> \"{name}\" by {maker}, every role mean {mean:0.##} dev {dev:0.##}: {description}"); } @@ -1580,7 +1595,7 @@ public static class Program foreach (var design in new[] { gun, laser, flak }) keys[design] = batch.Upsert(typeof(WeaponItemData), design); foreach (var (design, name, maker, description) in products) { - var (mean, dev) = makers[maker]; + var (mean, dev) = MakerProfiles[maker]; batch.Upsert(typeof(FactionProductData), new FactionProductData { Name = name, @@ -1706,6 +1721,120 @@ public static class Program return 0; } + // Aetheria release, cut roles-backfill (rule CB-R1): designs that shipped with flat stats and no roles get the + // roles their kind's map names, and every seller authors a ProductRole per role. Each mapped stat becomes a range + // of value x (0.8, 1.2) that rises with quality for a benefit and falls for a cost, read through one Quality term + // naming its role (exponent: the mode of the kind's existing designs on that field, 1 when there is none; an + // existing Quality term keeps its exponent). Zero stats stay flat. Small Drive joined the set because it declares + // roles that no non-flat stat reads. Idempotent: a design with no flat mapped stat is left alone, a product with a + // role is left alone. Dry run unless passed "apply". + private static readonly string[] BackfillDesigns = { "Autocannon", "LRMM72", "SRMM72", "Mine Launcher", "Large Drive", "Small Drive" }; + + private static readonly HashSet CostStats = new HashSet(StringComparer.Ordinal) + { + "Cooldown", "Spread", "Heat", "Visibility", "Energy", "EnergyUsage", "ChargeTime", "ChargeEnergy", "ChargeHeat", + "SensorImpact", "DirectionImpact", "Decay", + }; + + private static Dictionary BackfillMapFor(EquippableItemData item) => + item.Behaviors.OfType().Any() ? MineLayerRoles : RoleMapFor(item, KindOf(item)); + + private static IEnumerable StatsOf(EquippableItemData item) => + (item.Behaviors ?? new List()).Where(b => b != null) + .SelectMany(b => b.GetType().GetFields().Where(f => f.FieldType == typeof(PerformanceStat)).Select(f => f.GetValue(b) as PerformanceStat)) + .Where(s => s?.Terms != null); + + private static int RolesBackfill(bool apply) + { + var db = AetherDb.Open(catalogWritable: apply); + var items = db.Cache.GetAll().ToArray(); + var products = db.Cache.GetAll().ToArray(); + var targets = BackfillDesigns.Select(name => items.Where(i => i.Name == name).ToArray()) + .Select((found, i) => found.Length == 1 ? found[0] : throw new InvalidOperationException( + $"Expected exactly one design named \"{BackfillDesigns[i]}\", found {found.Length}.")).ToArray(); + + float ExponentFor(string kind, string field) + { + var precedent = items.Except(targets).Where(i => KindOf(i) == kind && i.Roles != null && i.Roles.Count > 0) + .SelectMany(i => i.Behaviors ?? new List()).Where(b => b != null) + .SelectMany(b => b.GetType().GetFields().Where(f => f.Name == field && f.FieldType == typeof(PerformanceStat)).Select(f => f.GetValue(b) as PerformanceStat)) + .Where(s => s?.Terms != null && s.Min != s.Max) + .SelectMany(s => s.Terms.Where(t => t.Source == StatSource.Quality && !string.IsNullOrEmpty(t.Role)).Select(t => t.Exponent)) + .GroupBy(e => e).OrderByDescending(g => g.Count()).ThenBy(g => g.Key).Select(g => g.Key).ToArray(); + return precedent.Length > 0 ? precedent[0] : 1f; + } + + var changedDesigns = new List<(EquippableItemData Document, CultRecordKey Key)>(); + foreach (var design in targets) + { + var kind = KindOf(design); + var map = BackfillMapFor(design) ?? throw new InvalidOperationException($"\"{design.Name}\" has no role map."); + var lines = new List(); + foreach (var behavior in design.Behaviors.Where(b => b != null)) + foreach (var field in behavior.GetType().GetFields().Where(f => f.FieldType == typeof(PerformanceStat))) + { + if (!map.TryGetValue(field.Name, out var role) || !(field.GetValue(behavior) is PerformanceStat stat)) continue; + if (stat.Min != stat.Max || stat.Min == 0f || float.IsInfinity(stat.Min)) continue; + var value = stat.Min; + (stat.Min, stat.Max) = CostStats.Contains(field.Name) ? (value * 1.2f, value * .8f) : (value * .8f, value * 1.2f); + stat.Terms ??= new List(); + var quality = stat.Terms.FirstOrDefault(t => t.Source == StatSource.Quality); + if (quality == null) + { + quality = new StatTerm { Source = StatSource.Quality, Exponent = ExponentFor(kind, field.Name) }; + stat.Terms.Add(quality); + } + quality.Role = role; + lines.Add($" {behavior.GetType().Name}.{field.Name,-16} {value:0.####} -> {stat.Min:0.####}..{stat.Max:0.####} {role} ^{quality.Exponent:0.##}"); + } + if (lines.Count == 0) continue; + design.Roles = StatsOf(design).Where(s => s.Min != s.Max) + .SelectMany(s => s.Terms.Where(t => t.Source == StatSource.Quality && !string.IsNullOrEmpty(t.Role)).Select(t => t.Role)) + .Distinct().OrderBy(r => r, StringComparer.Ordinal).Select(r => new ItemRole { Name = r }).ToList(); + Console.WriteLine($" {design.Name} ({kind}) roles [{string.Join(", ", design.Roles.Select(r => r.Name))}]"); + foreach (var line in lines) Console.WriteLine(line); + changedDesigns.Add((design, db.Cache.RefOf(design).Key)); + } + + var changedProducts = new List<(FactionProductData Document, CultRecordKey Key)>(); + foreach (var design in targets) + foreach (var product in products.Where(p => p.Design.Key.Equals(db.Cache.RefOf(design).Key)).OrderBy(p => p.Name, StringComparer.Ordinal)) + { + var maker = db.Cache.Get(product.Manufacturer); + var profile = maker?.Name == "Pirates" ? PirateProfile + : maker != null && MakerProfiles.TryGetValue(maker.ShortName, out var found) ? found + : throw new InvalidOperationException($"No maker profile for the seller of \"{design.Name}\" ({maker?.ShortName ?? "no maker"})."); + product.Roles ??= new List(); + var missing = design.Roles.Select(r => r.Name).Where(r => product.Roles.All(p => p.Role != r)).ToArray(); + if (missing.Length == 0) continue; + foreach (var role in missing) + product.Roles.Add(new ProductRole { Role = role, Mean = profile.Mean, StandardDeviation = profile.Dev }); + Console.WriteLine($" {product.Name,-28} ({design.Name}, {maker.ShortName}) {string.Join(", ", missing)} at {profile.Mean:0.00}/{profile.Dev:0.00}"); + changedProducts.Add((product, db.Cache.RefOf(product).Key)); + } + + if (changedDesigns.Count == 0 && changedProducts.Count == 0) + { + Console.WriteLine("Roles backfill is already authored; nothing to do."); + return 0; + } + Console.WriteLine($"\n{changedDesigns.Count} design(s) and {changedProducts.Count} product(s) to author."); + if (!apply) + { + Console.WriteLine("Dry run. Pass \"apply\" to land them."); + return 0; + } + foreach (var (document, _) in changedDesigns) CultRecordRefs.Validate(document); + foreach (var (document, _) in changedProducts) CultRecordRefs.Validate(document); + db.Cache.Commit(batch => + { + foreach (var (document, key) in changedDesigns) batch.Upsert(document is WeaponItemData ? typeof(WeaponItemData) : typeof(GearData), document, key); + foreach (var (document, key) in changedProducts) batch.Upsert(typeof(FactionProductData), document, key); + }); + Console.WriteLine($"Landed {changedDesigns.Count} design(s) and {changedProducts.Count} product(s) in Aetheria.cc"); + return 0; + } + // Aetheria release, cut pirates-record (rulings pirates-record-content, catalog-grows-generic-designs-branded-products): // the Pirates faction and eight rebrands of other makers' designs, named in emoji. The Pirates own no design: their range // is what they took, so every product points at a design someone else also makes. Each product carries one ProductRole per @@ -1765,7 +1894,7 @@ public static class Program { var design = DesignByName(r.Design); return (Design: design, r.Name, r.Description, - Roles: (design.Roles ?? new List()).Select(role => new ProductRole { Role = role.Name, Mean = .45f, StandardDeviation = .22f }).ToList()); + Roles: (design.Roles ?? new List()).Select(role => new ProductRole { Role = role.Name, Mean = PirateProfile.Mean, StandardDeviation = PirateProfile.Dev }).ToList()); }).ToArray(); Console.WriteLine($"New faction \"{pirates.Name}\": influence {pirates.InfluenceDistance}, names \"{nameFile.Name}\", allegiance 1.0 to {pirates.Allegiance.Count} factions."); From 017da88334f552db0b6253ee0207620ad065d5b7 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 22:11:52 +0200 Subject: [PATCH 045/120] Catalog: roles-backfill apply (6 designs, 6 products) Output of `AetherDb roles-backfill apply` on origin/master 325ef4bc's catalog, run on Yggdrasil at 54999a64. Dry run: Autocannon, LRMM72, SRMM72, Mine Launcher, Large Drive and Small Drive gain roles and quality ranges; Autocannon (Zhestokost, Pirates), LRMM72 (R&D), SRMM72 (DME), Mine Launcher (DME) and True North (Lightsail) author their ProductRoles. A replay prints "already authored". At merge, take master's catalog and rerun `AetherDb roles-backfill apply`. Co-Authored-By: Claude Sonnet 5.5 --- GameData/Aetheria.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GameData/Aetheria.cc b/GameData/Aetheria.cc index 42a50cad4..76685d05d 100644 --- a/GameData/Aetheria.cc +++ b/GameData/Aetheria.cc @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:32a2e401a03b00bae994bf1a26a3be58db7226c7c6c9a4e12fa2c449429d80e8 -size 12712398 +oid sha256:3d531aff9e6c2d1ea65a123e987ea4e2ce79610598c7547e26e3ecb7fb95b94a +size 12713666 From b5ff1ae260e6296fd825142ef96d1f732cd29221 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 22:25:34 +0200 Subject: [PATCH 046/120] Map: ships add-on frame r5, authoring without the repo Probes B1-B9, the owner answer (published AetherDb), the r5 authority map and rationale from the imagination-addon-frame pass. Co-Authored-By: Claude Opus 5.5 --- docs/aetheria-release-map.md | 199 +++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) diff --git a/docs/aetheria-release-map.md b/docs/aetheria-release-map.md index 5528cfd51..1a2452e85 100644 --- a/docs/aetheria-release-map.md +++ b/docs/aetheria-release-map.md @@ -6714,3 +6714,202 @@ vapour-cloud-presenter, consumables-first-set, consumable-ai; modifier-stacking consumable-host) with resolution `cut_spec.cut-modifier-stacking.r1.n1` (Superseded). Follow-ups: `cloud-vs-missiles-in-flight`, `modifier-magnitude-frozen-at-attach`, `consumable-heat-noop`, `station-restock`. Claimed keys: BehaviorData union 41 ThrottleLockData, 42 VapourDumpData (keys 1-3). + +## Ships add-on frame r5: authoring without the repo + +Imagination, agent `imagination-addon-frame`, session `self-2026-10-08b`. Campaign +`aetheria-release`. Builds on `F:\Projects\aetheria-map-addenda-addon-refresh-2026-10-08.md` +(A1 to A14); A14 recorded the tension this page resolves. Numbering continues at B1. + +Pinned heads: Aetheria `07dcb580` (branch `eureka/aetheria-release-ships-addon-frame-r4`, +local and on origin; the brief's name `eureka/aetheria-release-ships-addon-frame` does not +exist), CultLib `05e67014` (main), CultLib `45c2f400` (Aetheria's pinned +`CultLibRevision`, `Directory.Build.props:6`). Every line number below is at `07dcb580`. + +### Body facts + +**B1. What New Ship needs from the repo (source read).** `_new_ship` +(`tools/blender/aetheria_ships/__init__.py:556-605`) finds the repo by walking up for +`tools/AetherDb` (`_aetheria_repo`, 314-325), writes to `/GameData/Mods//ship.cc`, +and runs `dotnet run --project /tools/AetherDb -- ship-authoring create` (`_aetherdb`, +328-336). `create` (`tools/AetherDb/ShipAuthoringCommands.cs:47-67`) reads the reference +hull from `GameData/Aetheria.cc` through `AetherDb.Open()`, whose `FindRoot` +(`tools/AetherDb/AetherDb.cs:26-32`) keys on `Aetheria.Shared/Aetheria.Shared.csproj`, and +builds the hull with `ShipAuthoringStore.HullLike` +(`Assets/Scripts/ServerShared/ShipAuthoring.cs:95-106`): copy every member, reset Name, +Shape (`new Shape()`), Hardpoints, Prefab, Schematic and Visual. `create` does not run the +ship-ID rule; only `Validate` (`ShipAuthoring.cs:189-192`) does. + +The same `_aetherdb` port also carries Package's `validate` (`__init__.py:650`), and the +specs in force add `centre` (package-frame r1) and `paint` (paint r3). So the repo and +dotnet dependency is the port's, not New Ship's alone: Package already needs both today. + +**B2. Where an author's machine gets the shipped catalog (source read).** The game reads +`/GameData/Aetheria.cc` and `/GameData/Mods` +(`Assets/Scripts/Gameplay/ActionGameManager.cs:40,48-49`), with `GameData` beside the +player's data folder. A ship author who has the game has both. No build or release script +in the repo copies `GameData` into a player build (searched `Assets/Scripts/Editor`, +`tools`, workflow files): the runtime contract is the only evidence that a release carries +it. + +**B3. Studio needs Unity (source read).** CultCache Studio is +`CultCacheStudioWindow : EditorWindow` (`CultLib src/GameCult.Unity/Assets/Caching/Editor/ +CultCacheStudioWindow.cs`), opened from a Unity menu item. Ruling +`authoring-host-thin-imgui-then-thing` keeps it an IMGUI skin until a Thing host after the +release. Studio therefore cannot be the repo-free and Unity-free creator before then. + +**B4. The shipped catalog's schema catalog (probe `probes/p1_catalog.py` on the +`07dcb580` LFS object `9e75d9a4...`).** 13 catalog entries, 219 records, 5 hulldata +records (LonginusX, Zenith, Turret, Djinni, Longinus; Djinni, Longinus and LonginusX are +`HullType` 0, Ship). It carries an `aetheria.hulldata` entry and **no +`aetheria.ship_authoring` entry**: the C# store writes catalog entries only for types it +holds records of. + +**B5. cultcache-py cannot derive a C# catalog identity (probe `p3_schema_id.py` and source +read).** For all 13 C#-written entries, `content_hash == sha256(canonical_schema_json)` +but `schema_id != sha256(canonical_schema_json)`: C# derives the id from a semantic +fingerprint over its member descriptors (`CultLib src/GameCult.Caching/CultCache.cs: +655-658`), with type names from `CultSchemaTypeNames.FromType`. cultcache-py's +`DocumentDefinition.catalog_entry` (`packages/cultcache-py/src/cultcache_py/documents.py: +105-143`) defaults the schema id and content hash to the type name, and its store's +default entry for an unregistered envelope is a stub with no members +(`stores.py:356-368`). Python can carry a C#-written entry through a write; it cannot +author one. + +**B6. Encode and decode probe on Yggdrasil (`ygg-verify.sh`, image +mcr.microsoft.com/dotnet/sdk:10.0, Aetheria `07dcb580`, CultLib `45c2f400` staged, +logs `scratchpad/probes/ygg-create-1.log` and `ygg-publish-2.log`).** Reference: +`dotnet run ... ship-authoring create /tmp/o/dn/ship.cc probe.a "Probe A" --like Djinni`. + +| Candidate | catalog hulldata | catalog ship_authoring | hull record | ship record | C# `inspect` | +|---|---|---|---|---|---| +| cultcache-py, no template (`py_create.py`) | identical (copied from the shipped catalog) | **differs**: stub, schema id `aetheria.ship_authoring`, version `aetheria.ship_authoring.v1`, members `[]` | equal values | identical bytes | loads: `probe.a: Probe A, 0 hardpoints` | +| cultcache-py with the C# file's ship_authoring entry | identical | identical | **equal values, different bytes**: 20 `float32` (`0xca`) slots re-encoded `float64` (`0xcb`), 204 to 284 bytes | identical | loads | +| published AetherDb (B7) | identical | identical | identical | identical | loads | + +`stored_at` is ignored throughout. `new Shape()` encodes `[[1, 1, [true]]]`. The C# reader +accepted the stub catalog by schema name (compatible drift, `CultCache.cs:518-522, +803-830`), reporting every slot as `defaulted_missing_slot` while still decoding the +payload's values: that warning text and the observed behaviour disagree, so a file that +relies on it relies on a CultLib behaviour nobody owns. The float widening is the same in +today's `ship_cc.replace_layout`, which re-packs the whole hull body on every Save; C# +reads `float64` into `float` (smoke_frame's Package validated after a Rasterise write), so +it is a wire-parity gap, not a corruption (follow-up `py-float-width-parity`). + +**B7. A published AetherDb needs neither dotnet nor the repo (Yggdrasil, log +`ygg-nodotnet-5.log`).** `dotnet publish tools/AetherDb -c Release -r linux-x64 +--self-contained -p:PublishSingleFile=true` gave one 77,287,607-byte executable (75 MB +folder with pdbs). Run in `debian:bookworm-slim` with no dotnet (`command -v dotnet`: +none), from a folder holding only `GameData/Aetheria.cc` plus an empty +`Aetheria.Shared/Aetheria.Shared.csproj` (the current `FindRoot` marker), `create ... +--like Djinni` took 1.1 s and wrote a file whose catalog and both records are byte-identical +to the dotnet-run reference. The first run aborted with `No usable version of libssl was +found`; with `libssl3` and `libicu72` installed it ran. Windows needs neither package; the +win-x64 publish was not probed. + +**B8. The reserved-name gap is the owner's (same log).** The published tool created +`/out/con/ship.cc` for ID `con` and `inspect` loaded it: the C# rule (`ShipAuthoring.cs: +190-192`) admits Windows device names, and `create` does not run it at all (B1). + +**B9. Downstream specs read (mind, 2026-10-08).** package-frame r1 says "after frame r4 has +merged" and its recentre test edits cells and Packages with no Save; mounts r4's Package +writes derived Positions straight to the .cc through `replace_layout`; gizmos r2's drag +release writes through `replace_layout` from an interactive (undoable) tool. bake r1, +guards r1, mesh-mounts r1, mask-bakes r1 and paint r3 do not depend on what r5 changes +(paint's `paint` command goes through the `_aetherdb` port, which the tool cut moves). + +### The owner answer + +**What can create and write a ship .cc on an author's machine without the repo or +dotnet?** Two things, by probe: cultcache-py, if a C#-written `aetheria.ship_authoring` +catalog entry is handed to it (B5, B6); or AetherDb itself, published self-contained +(B7). Studio cannot before the Thing host (B3). + +Recommendation: the published AetherDb (question `authoring-tool-without-repo`, option +`published-aetherdb`). It keeps C# the single owner of every ship-package rule the add-on +calls (HullLike, the ID rule, Validate, CenterOfMass, Paint) and removes the repo and the +SDK from all four commands at once, which the Python route does not: Package, centre and +paint would still need the repo. The Python route needs a second copy of HullLike's reset +list and a pinned catalog entry, and widens floats until CultLib fixes B6. The repo-free +reference hull is the author's own game install (`GameData/Aetheria.cc`, B2). + +Prior art (from knowledge; no Eyes pass was run in this budget, so confidence is medium): +game modding kits that keep one rule owner ship the game's own code compiled to modders +rather than re-implementing it in the content tool: Valve's Source SDK ships compiled +studiomdl, vbsp and vrad; Bethesda's Creation Kit is built on the engine's record code; +games that ship no tool (Factorio, KSP, Minecraft data packs) validate content at game +load, which Aetheria also does (`ShipModCatalog.ResolveCatalog` excludes a bad package and +names it). Blender add-ons that drive external compilers (the Source Engine tools add-on, +io_scene_valvesource, calling studiomdl) take the executable's path as a preference. + +### Authority map, r5 + +- **Owner of the .cc on the author's machine:** two explicit, non-undo writers. Save + Layout owns cells and hardpoints (and the first Save of a new ship creates the file + through the published AetherDb `create`). Package owns the visual (model asset, anchors, + lines) and, from mounts r5, saves the layout buffer through Save's writer. C# (through + the published AetherDb) owns HullLike, the ship-ID rule, Validate, CenterOfMass and Paint. +- **Owner of the frame:** the .blend. The layout buffer (`scene.aetheria_layout`), + `aetheria.grid_origin` with the grid size it was computed for, Ship Root and the objects + under it all live in the .blend, so Blender's undo moves them together. +- **Demotions.** Rasterise and Flip Nose are no longer .cc writers; they propose cells + into the buffer. New Ship is no longer a .cc writer; it binds a pending ship. The repo is + no longer an input; the game data folder is. `dotnet run` is no longer a path. +- **Package and an unsaved buffer.** Package refuses while the bound collection's buffer + differs from the .cc ("Save Layout first"), so it never packages cells the author has + not saved. The comparison is ship_cc's own layout revision of the buffer against the + revision Load or Save recorded. Mounts r5 derives Positions into the buffer after that + check and saves them through Save's writer. +- **One chooser** decides which collection is the ship; Bind and New Ship accept an + unbound collection through it, everything else requires a bound one. +- **One frame predicate:** the ship's meshes are Ship Root's descendants outside Source + and Generated with no `aetheria.role`. Rasterise reads them; Flip Nose turns Ship Root's + children; package-frame's recentre moves Ship Root. A mesh in the bound collection that + is not under Ship Root is refused by Rasterise, Flip Nose and Package with a count, never + silently left out. + +### Rationale + +**Why Save, not the UNDO operators, writes the .cc.** Blender's memfile undo restores the +.blend and cannot restore a file on disk. Any undoable operator that writes the .cc splits +the grid's truth (finding `undo-splits-origin-from-cells`), and dropping the UNDO flag does +not help: the next undo push still restores the scene to the step before. So no undoable +operator writes the disk, and the .cc changes only on an explicit Save or Package. This is +the editor-buffer pattern Blender itself follows for external files (images are edited in +memory and saved with Image > Save; the .blend is saved explicitly). + +**Why New Ship creates nothing on disk (`new-ship-orphans-cc`).** Creation is deferred to +the first Save, which creates and fills the file in one operator: `create`, then the +buffer through `replace_layout`, and on any failure after `create` it removes the file and +the folder it made. Nothing persists unless the ship is valid enough to save, and an undone +New Ship leaves nothing to orphan. + +**Grid placement against outside edits.** Studio can resize the grid. `grid_origin` is +stored with the width and height it was computed for, and the Grid is drawn only while the +buffer has that size; otherwise the panel asks for Rasterise again. This keeps a stale +origin from placing cells, without moving the origin into the .cc (the runtime derives +alignment from the centre of mass after package-frame and never needs it). + +**Errors (`create-failure-echoes-inputs`).** Operator reports carry fixed text per failure; +AetherDb's own output goes to the system console, not the report. + +**Ship IDs (`ship-id-allows-reserved-names`).** The rule stays C#'s +(`ShipAuthoringStore`), gains the Windows device names (con, prn, aux, nul, com1-9, +lpt1-9, alone or before a dot) and is run by `create`. New Ship checks it before anything +moves through `ship_cc.valid_ship_id`, whose constants ShipSchemaPinTests pins to the C# +rule, as it already pins the slot numbers. + +**Rejected.** Studio owning creation (needs Unity until the Thing host, B3). A local +encoder or catalog entry in the add-on derived by hand (B5: Python cannot derive C#'s +schema id; the brief forbids it). Relying on cultcache-py's stub catalog entry (B6: the C# +reader's behaviour contradicts its own warning). A framework-dependent publish (it needs +the .NET runtime installed, which is the install the ruling removes). + +### Model page rows (changed) + +| Kind | Named by | Over time | Decides | +|---|---|---|---| +| Ship .cc | `GameData/Mods//ship.cc` under the author's game data folder | created by the first Save of a pending ship; changed by Save, Package, Capture Lines | Save (layout), Package (visual), C# rules through the published AetherDb | +| Layout buffer | `scene.aetheria_layout` | filled by Load or New Ship, edited by Rasterise, Flip Nose, Resize and hardpoint ops, undoable | the author; persisted only by Save | +| Grid placement | `aetheria.grid_origin` = [x, y, width, height] on the collection | written by Rasterise only, undoable | Rasterise | +| Pending ship | `aetheria.pending` = [name, reference] on the collection | set by New Ship, cleared by the Save that creates the file | New Ship, then Save | From f3550b67a453062b2a401b72ddc0ead3c5efd993 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 22:26:34 +0200 Subject: [PATCH 047/120] Tests: roles-backfill fallout (Tractor Beam has no stat, Longinus fitted by name) ShippedCatalogRolesAreLive skips gear with no PerformanceStat at all (Tractor Beam is a thruster kind with none). TheEnvelopeComesFromTheGear fits the Longinus by name: the generator's picks follow the item stream and every authored ProductRole draws from it, so the generated hull stopped carrying Large Drives (Medium Drive and RevvITup instead). Its forward bound is the drives' own thrust, since a Large Drive now rolls 0.8x to 1.2x. Co-Authored-By: Claude Sonnet 5.5 --- tests/Aetheria.Shared.Tests/CatalogRoleTests.cs | 4 +++- tests/Aetheria.Shared.Tests/EvasionTermTests.cs | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/CatalogRoleTests.cs b/tests/Aetheria.Shared.Tests/CatalogRoleTests.cs index 2bab3218d..665602de3 100644 --- a/tests/Aetheria.Shared.Tests/CatalogRoleTests.cs +++ b/tests/Aetheria.Shared.Tests/CatalogRoleTests.cs @@ -34,7 +34,9 @@ public sealed class CatalogRoleTests .Where(i => i is WeaponItemData || (i is GearData && QualityGearKinds.Contains(i.HardpointType))) .OrderBy(i => i.Name, StringComparer.Ordinal)) { - var varying = StatsOf(design).Where(s => s.Min != s.Max).ToArray(); + var stats = StatsOf(design).ToArray(); + if (stats.Length == 0) continue; // Tractor Beam: a thruster kind with no stat at all, so nothing for quality to vary + var varying = stats.Where(s => s.Min != s.Max).ToArray(); var read = varying.SelectMany(NamedQualityRoles).Distinct().OrderBy(r => r, StringComparer.Ordinal).ToArray(); var declared = (design.Roles ?? new List()).Select(r => r.Name).OrderBy(r => r, StringComparer.Ordinal).ToArray(); if (varying.Length == 0) broken.Add($"{design.Name}: every stat is flat"); diff --git a/tests/Aetheria.Shared.Tests/EvasionTermTests.cs b/tests/Aetheria.Shared.Tests/EvasionTermTests.cs index 93eef6145..594019d43 100644 --- a/tests/Aetheria.Shared.Tests/EvasionTermTests.cs +++ b/tests/Aetheria.Shared.Tests/EvasionTermTests.cs @@ -283,7 +283,17 @@ public sealed partial class RunStartTests [Fact] public void TheEnvelopeComesFromTheGear() { - var (_, longinus, _) = EvFleet(); + // The Longinus is fitted by name, not generated: what the generator picks follows the item stream, and every role a + // product authors draws from it (roles-backfill), so a generated hull would stop carrying these drives. + Ship longinus = null; + var scenario = new Scripted(false, stage => + { + stage.Player(stage.Bare("Djinni"), float2(-50000, -50000)); + longinus = stage.Place(EvLonginus(stage), float2(-30000, -30000), facing: float2(0, 1)) as Ship; + }); + var (_, _, _, failures) = Launch(scenario, Inputs(() => GalaxySeed)); + Assert.True(failures.Count == 0, string.Join("; ", failures)); + Assert.NotNull(longinus); EvSettle(longinus); // A thruster's Thrust property is refreshed only while it fires: turn each way so the flank thrusters hold what // they push with, then read the envelope at rest. @@ -306,7 +316,8 @@ public sealed partial class RunStartTests var envelope = longinus.Envelope; var mass = longinus.Mass; - Assert.InRange(envelope.Forward, 500000f / mass * .99f, 500000f / mass * 1.01f); + var forward = largeDrives.Sum(t => t.Thrust) / mass; // each drive's own thrust: a Large Drive's roll moves it between 0.8x and 1.2x + Assert.InRange(envelope.Forward, forward * .99f, forward * 1.01f); Assert.Equal(0f, envelope.Reverse); var flankAcceleration = flanks.Max(t => t.Thrust) / mass; Assert.InRange(envelope.Left, flankAcceleration * .99f, flankAcceleration * 1.01f); From 303f73e19f485be9855a31c4dd4b2322c99cd6cc Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 22:39:21 +0200 Subject: [PATCH 048/120] Tests: the Longinus envelope's flank sides are each Talaria's own thrust Each fitted Talaria has its own lot's thrust, so Left and Right are the two flanks' accelerations, not both the larger one. Co-Authored-By: Claude Sonnet 5.5 --- tests/Aetheria.Shared.Tests/EvasionTermTests.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/EvasionTermTests.cs b/tests/Aetheria.Shared.Tests/EvasionTermTests.cs index 594019d43..708f863bb 100644 --- a/tests/Aetheria.Shared.Tests/EvasionTermTests.cs +++ b/tests/Aetheria.Shared.Tests/EvasionTermTests.cs @@ -319,9 +319,11 @@ public sealed partial class RunStartTests var forward = largeDrives.Sum(t => t.Thrust) / mass; // each drive's own thrust: a Large Drive's roll moves it between 0.8x and 1.2x Assert.InRange(envelope.Forward, forward * .99f, forward * 1.01f); Assert.Equal(0f, envelope.Reverse); - var flankAcceleration = flanks.Max(t => t.Thrust) / mass; - Assert.InRange(envelope.Left, flankAcceleration * .99f, flankAcceleration * 1.01f); - Assert.InRange(envelope.Right, flankAcceleration * .99f, flankAcceleration * 1.01f); + // One Talaria pushes each way, and each has its own lot's thrust: the sides are the two flanks' accelerations. + var flankAccelerations = flanks.Select(t => t.Thrust / mass).OrderBy(a => a).ToArray(); + var sides = new[] { envelope.Left, envelope.Right }.OrderBy(a => a).ToArray(); + Assert.InRange(sides[0], flankAccelerations[0] * .99f, flankAccelerations[0] * 1.01f); + Assert.InRange(sides[1], flankAccelerations[1] * .99f, flankAccelerations[1] * 1.01f); var clockwise = thrusters.Where(t => t.Torque > settings.TorqueFloor).Sum(t => t.Torque * t.Thrust) * settings.TorqueMultiplier / mass; var counterClockwise = thrusters.Where(t => t.Torque < -settings.TorqueFloor).Sum(t => -t.Torque * t.Thrust) * settings.TorqueMultiplier / mass; Assert.InRange(envelope.Clockwise, clockwise * .99f, clockwise * 1.01f); From 8e73b5427171bb52f0350fea0a0c4f7109fdb0b9 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 22:45:46 +0200 Subject: [PATCH 049/120] README: Twemoji CC BY 4.0 attribution Co-Authored-By: Claude Sonnet 5.5 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5136180f9..79f0033f2 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,8 @@ Client-Server communication is implemented using [LiteNetLib](https://github.com Game content lives in `GameData/Aetheria.cc`, a [CultCache](https://github.com/GameCult/CultLib) store that is read-only at runtime. Run state and player state (settings and bindings) live in their own `.cc` stores in the same cache. Persistent data types carry MessagePack attributes; some also carry [JSON.Net](https://www.newtonsoft.com/json) attributes. +Emoji are drawn from [Twemoji](https://github.com/jdecked/twemoji) v17.0.3, converted to a colour font (`Assets/Fonts/Emoji/Twemoji.ttf`, built by `tools/emoji/fetch_emoji_font.py`; the artwork itself is not edited). Copyright (c) 2022–present Jason Sofonia & Justine De Caires; copyright (c) 2014–2021 Twitter. The graphics are licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/); the full notice is `Assets/Fonts/Emoji/LICENSE.txt`. + ### Programming Paradigms The codebase makes heavy use of C#'s [Language Integrated Queries (LINQ)](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/linq/), allowing for the concise representation of operations that modify or filter collections (though they do generate some garbage so must be avoided within the update loop). Asynchronous stream processing is often performed using the [functional reactive programming](http://reactivex.io/) paradigm, which is achieved using [Microsoft's Reactive Extensions](https://github.com/dotnet/reactive) on the server and [Reactive Extensions for Unity](https://github.com/neuecc/UniRx). Combining Observables with LINQ allows for extremely powerful expressions of the programmer's intent. From 8d2b18bb178bab7ed446cdc5d206503df3199e97 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 22:45:48 +0200 Subject: [PATCH 050/120] Emoji corpus from Unicode's emoji-test.txt; lock records interpreter and container 1400 single code points (the 2 and 3 digit ones included), Twemoji lacks none; font sha256 unchanged (reproduced in python:3.13-slim on Yggdrasil). Co-Authored-By: Claude Sonnet 5.5 --- Assets/Fonts/Emoji/emoji-font.lock | 12 +- tools/emoji/emoji-singles.txt | 2681 ++++++++++++++-------------- tools/emoji/emoji-unsupported.txt | 0 3 files changed, 1338 insertions(+), 1355 deletions(-) create mode 100644 tools/emoji/emoji-unsupported.txt diff --git a/Assets/Fonts/Emoji/emoji-font.lock b/Assets/Fonts/Emoji/emoji-font.lock index de5012a8b..f05a93bf9 100644 --- a/Assets/Fonts/Emoji/emoji-font.lock +++ b/Assets/Fonts/Emoji/emoji-font.lock @@ -1,14 +1,22 @@ source_url=https://github.com/jdecked/twemoji/archive/refs/tags/v17.0.3.tar.gz source_version=v17.0.3 source_sha256=a0855654b633045ae2337537e77f1bb4361162f7fcd910e613eaab1d6d9c5fca +emoji_test_version=17.0 +emoji_test_url=https://unicode.org/Public/17.0.0/emoji/emoji-test.txt +emoji_test_sha256=1d8a944f88d7952f7ef7c5167fef3c67995bcae24543949710231b03a201acda svg_count=4009 -single_codepoint_count=1425 +single_codepoint_count=1400 +unsupported_count=0 tool_nanoemoji=0.16.0 tool_fonttools=4.66.1 tool_picosvg=0.23.0 tool_ninja=1.13.2 source_date_epoch=1780352497 color_format=glyf_colr_0 +python_version=3.13.15 +platform=linux +container_image=python:3.13-slim font_file=Twemoji.ttf font_sha256=de66290bfa98226e40d8999a3ab024c2242e8d2938f939d9a85c1494844f6aa0 -singles_sha256=d07a0693cef0530acf540cdeb60d341912f639370e7e30271e90452a8aecdc0d +singles_sha256=7ad887a16ef5386df040e1e8988e9bdc55ae5cab461ffc9df4613d0cb5770b4e +unsupported_sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 diff --git a/tools/emoji/emoji-singles.txt b/tools/emoji/emoji-singles.txt index 09fcc972e..f3901bb01 100644 --- a/tools/emoji/emoji-singles.txt +++ b/tools/emoji/emoji-singles.txt @@ -1,1260 +1,6 @@ -1f004 -1f0cf -1f170 -1f171 -1f17e -1f17f -1f18e -1f191 -1f192 -1f193 -1f194 -1f195 -1f196 -1f197 -1f198 -1f199 -1f19a -1f1e6 -1f1e7 -1f1e8 -1f1e9 -1f1ea -1f1eb -1f1ec -1f1ed -1f1ee -1f1ef -1f1f0 -1f1f1 -1f1f2 -1f1f3 -1f1f4 -1f1f5 -1f1f6 -1f1f7 -1f1f8 -1f1f9 -1f1fa -1f1fb -1f1fc -1f1fd -1f1fe -1f1ff -1f201 -1f202 -1f21a -1f22f -1f232 -1f233 -1f234 -1f235 -1f236 -1f237 -1f238 -1f239 -1f23a -1f250 -1f251 -1f300 -1f301 -1f302 -1f303 -1f304 -1f305 -1f306 -1f307 -1f308 -1f309 -1f30a -1f30b -1f30c -1f30d -1f30e -1f30f -1f310 -1f311 -1f312 -1f313 -1f314 -1f315 -1f316 -1f317 -1f318 -1f319 -1f31a -1f31b -1f31c -1f31d -1f31e -1f31f -1f320 -1f321 -1f324 -1f325 -1f326 -1f327 -1f328 -1f329 -1f32a -1f32b -1f32c -1f32d -1f32e -1f32f -1f330 -1f331 -1f332 -1f333 -1f334 -1f335 -1f336 -1f337 -1f338 -1f339 -1f33a -1f33b -1f33c -1f33d -1f33e -1f33f -1f340 -1f341 -1f342 -1f343 -1f344 -1f345 -1f346 -1f347 -1f348 -1f349 -1f34a -1f34b -1f34c -1f34d -1f34e -1f34f -1f350 -1f351 -1f352 -1f353 -1f354 -1f355 -1f356 -1f357 -1f358 -1f359 -1f35a -1f35b -1f35c -1f35d -1f35e -1f35f -1f360 -1f361 -1f362 -1f363 -1f364 -1f365 -1f366 -1f367 -1f368 -1f369 -1f36a -1f36b -1f36c -1f36d -1f36e -1f36f -1f370 -1f371 -1f372 -1f373 -1f374 -1f375 -1f376 -1f377 -1f378 -1f379 -1f37a -1f37b -1f37c -1f37d -1f37e -1f37f -1f380 -1f381 -1f382 -1f383 -1f384 -1f385 -1f386 -1f387 -1f388 -1f389 -1f38a -1f38b -1f38c -1f38d -1f38e -1f38f -1f390 -1f391 -1f392 -1f393 -1f396 -1f397 -1f399 -1f39a -1f39b -1f39e -1f39f -1f3a0 -1f3a1 -1f3a2 -1f3a3 -1f3a4 -1f3a5 -1f3a6 -1f3a7 -1f3a8 -1f3a9 -1f3aa -1f3ab -1f3ac -1f3ad -1f3ae -1f3af -1f3b0 -1f3b1 -1f3b2 -1f3b3 -1f3b4 -1f3b5 -1f3b6 -1f3b7 -1f3b8 -1f3b9 -1f3ba -1f3bb -1f3bc -1f3bd -1f3be -1f3bf -1f3c0 -1f3c1 -1f3c2 -1f3c3 -1f3c4 -1f3c5 -1f3c6 -1f3c7 -1f3c8 -1f3c9 -1f3ca -1f3cb -1f3cc -1f3cd -1f3ce -1f3cf -1f3d0 -1f3d1 -1f3d2 -1f3d3 -1f3d4 -1f3d5 -1f3d6 -1f3d7 -1f3d8 -1f3d9 -1f3da -1f3db -1f3dc -1f3dd -1f3de -1f3df -1f3e0 -1f3e1 -1f3e2 -1f3e3 -1f3e4 -1f3e5 -1f3e6 -1f3e7 -1f3e8 -1f3e9 -1f3ea -1f3eb -1f3ec -1f3ed -1f3ee -1f3ef -1f3f0 -1f3f3 -1f3f4 -1f3f5 -1f3f7 -1f3f8 -1f3f9 -1f3fa -1f3fb -1f3fc -1f3fd -1f3fe -1f3ff -1f400 -1f401 -1f402 -1f403 -1f404 -1f405 -1f406 -1f407 -1f408 -1f409 -1f40a -1f40b -1f40c -1f40d -1f40e -1f40f -1f410 -1f411 -1f412 -1f413 -1f414 -1f415 -1f416 -1f417 -1f418 -1f419 -1f41a -1f41b -1f41c -1f41d -1f41e -1f41f -1f420 -1f421 -1f422 -1f423 -1f424 -1f425 -1f426 -1f427 -1f428 -1f429 -1f42a -1f42b -1f42c -1f42d -1f42e -1f42f -1f430 -1f431 -1f432 -1f433 -1f434 -1f435 -1f436 -1f437 -1f438 -1f439 -1f43a -1f43b -1f43c -1f43d -1f43e -1f43f -1f440 -1f441 -1f442 -1f443 -1f444 -1f445 -1f446 -1f447 -1f448 -1f449 -1f44a -1f44b -1f44c -1f44d -1f44e -1f44f -1f450 -1f451 -1f452 -1f453 -1f454 -1f455 -1f456 -1f457 -1f458 -1f459 -1f45a -1f45b -1f45c -1f45d -1f45e -1f45f -1f460 -1f461 -1f462 -1f463 -1f464 -1f465 -1f466 -1f467 -1f468 -1f469 -1f46a -1f46b -1f46c -1f46d -1f46e -1f46f -1f470 -1f471 -1f472 -1f473 -1f474 -1f475 -1f476 -1f477 -1f478 -1f479 -1f47a -1f47b -1f47c -1f47d -1f47e -1f47f -1f480 -1f481 -1f482 -1f483 -1f484 -1f485 -1f486 -1f487 -1f488 -1f489 -1f48a -1f48b -1f48c -1f48d -1f48e -1f48f -1f490 -1f491 -1f492 -1f493 -1f494 -1f495 -1f496 -1f497 -1f498 -1f499 -1f49a -1f49b -1f49c -1f49d -1f49e -1f49f -1f4a0 -1f4a1 -1f4a2 -1f4a3 -1f4a4 -1f4a5 -1f4a6 -1f4a7 -1f4a8 -1f4a9 -1f4aa -1f4ab -1f4ac -1f4ad -1f4ae -1f4af -1f4b0 -1f4b1 -1f4b2 -1f4b3 -1f4b4 -1f4b5 -1f4b6 -1f4b7 -1f4b8 -1f4b9 -1f4ba -1f4bb -1f4bc -1f4bd -1f4be -1f4bf -1f4c0 -1f4c1 -1f4c2 -1f4c3 -1f4c4 -1f4c5 -1f4c6 -1f4c7 -1f4c8 -1f4c9 -1f4ca -1f4cb -1f4cc -1f4cd -1f4ce -1f4cf -1f4d0 -1f4d1 -1f4d2 -1f4d3 -1f4d4 -1f4d5 -1f4d6 -1f4d7 -1f4d8 -1f4d9 -1f4da -1f4db -1f4dc -1f4dd -1f4de -1f4df -1f4e0 -1f4e1 -1f4e2 -1f4e3 -1f4e4 -1f4e5 -1f4e6 -1f4e7 -1f4e8 -1f4e9 -1f4ea -1f4eb -1f4ec -1f4ed -1f4ee -1f4ef -1f4f0 -1f4f1 -1f4f2 -1f4f3 -1f4f4 -1f4f5 -1f4f6 -1f4f7 -1f4f8 -1f4f9 -1f4fa -1f4fb -1f4fc -1f4fd -1f4ff -1f500 -1f501 -1f502 -1f503 -1f504 -1f505 -1f506 -1f507 -1f508 -1f509 -1f50a -1f50b -1f50c -1f50d -1f50e -1f50f -1f510 -1f511 -1f512 -1f513 -1f514 -1f515 -1f516 -1f517 -1f518 -1f519 -1f51a -1f51b -1f51c -1f51d -1f51e -1f51f -1f520 -1f521 -1f522 -1f523 -1f524 -1f525 -1f526 -1f527 -1f528 -1f529 -1f52a -1f52b -1f52c -1f52d -1f52e -1f52f -1f530 -1f531 -1f532 -1f533 -1f534 -1f535 -1f536 -1f537 -1f538 -1f539 -1f53a -1f53b -1f53c -1f53d -1f549 -1f54a -1f54b -1f54c -1f54d -1f54e -1f550 -1f551 -1f552 -1f553 -1f554 -1f555 -1f556 -1f557 -1f558 -1f559 -1f55a -1f55b -1f55c -1f55d -1f55e -1f55f -1f560 -1f561 -1f562 -1f563 -1f564 -1f565 -1f566 -1f567 -1f56f -1f570 -1f573 -1f574 -1f575 -1f576 -1f577 -1f578 -1f579 -1f57a -1f587 -1f58a -1f58b -1f58c -1f58d -1f590 -1f595 -1f596 -1f5a4 -1f5a5 -1f5a8 -1f5b1 -1f5b2 -1f5bc -1f5c2 -1f5c3 -1f5c4 -1f5d1 -1f5d2 -1f5d3 -1f5dc -1f5dd -1f5de -1f5e1 -1f5e3 -1f5e8 -1f5ef -1f5f3 -1f5fa -1f5fb -1f5fc -1f5fd -1f5fe -1f5ff -1f600 -1f601 -1f602 -1f603 -1f604 -1f605 -1f606 -1f607 -1f608 -1f609 -1f60a -1f60b -1f60c -1f60d -1f60e -1f60f -1f610 -1f611 -1f612 -1f613 -1f614 -1f615 -1f616 -1f617 -1f618 -1f619 -1f61a -1f61b -1f61c -1f61d -1f61e -1f61f -1f620 -1f621 -1f622 -1f623 -1f624 -1f625 -1f626 -1f627 -1f628 -1f629 -1f62a -1f62b -1f62c -1f62d -1f62e -1f62f -1f630 -1f631 -1f632 -1f633 -1f634 -1f635 -1f636 -1f637 -1f638 -1f639 -1f63a -1f63b -1f63c -1f63d -1f63e -1f63f -1f640 -1f641 -1f642 -1f643 -1f644 -1f645 -1f646 -1f647 -1f648 -1f649 -1f64a -1f64b -1f64c -1f64d -1f64e -1f64f -1f680 -1f681 -1f682 -1f683 -1f684 -1f685 -1f686 -1f687 -1f688 -1f689 -1f68a -1f68b -1f68c -1f68d -1f68e -1f68f -1f690 -1f691 -1f692 -1f693 -1f694 -1f695 -1f696 -1f697 -1f698 -1f699 -1f69a -1f69b -1f69c -1f69d -1f69e -1f69f -1f6a0 -1f6a1 -1f6a2 -1f6a3 -1f6a4 -1f6a5 -1f6a6 -1f6a7 -1f6a8 -1f6a9 -1f6aa -1f6ab -1f6ac -1f6ad -1f6ae -1f6af -1f6b0 -1f6b1 -1f6b2 -1f6b3 -1f6b4 -1f6b5 -1f6b6 -1f6b7 -1f6b8 -1f6b9 -1f6ba -1f6bb -1f6bc -1f6bd -1f6be -1f6bf -1f6c0 -1f6c1 -1f6c2 -1f6c3 -1f6c4 -1f6c5 -1f6cb -1f6cc -1f6cd -1f6ce -1f6cf -1f6d0 -1f6d1 -1f6d2 -1f6d5 -1f6d6 -1f6d7 -1f6d8 -1f6dc -1f6dd -1f6de -1f6df -1f6e0 -1f6e1 -1f6e2 -1f6e3 -1f6e4 -1f6e5 -1f6e9 -1f6eb -1f6ec -1f6f0 -1f6f3 -1f6f4 -1f6f5 -1f6f6 -1f6f7 -1f6f8 -1f6f9 -1f6fa -1f6fb -1f6fc -1f7e0 -1f7e1 -1f7e2 -1f7e3 -1f7e4 -1f7e5 -1f7e6 -1f7e7 -1f7e8 -1f7e9 -1f7ea -1f7eb -1f7f0 -1f90c -1f90d -1f90e -1f90f -1f910 -1f911 -1f912 -1f913 -1f914 -1f915 -1f916 -1f917 -1f918 -1f919 -1f91a -1f91b -1f91c -1f91d -1f91e -1f91f -1f920 -1f921 -1f922 -1f923 -1f924 -1f925 -1f926 -1f927 -1f928 -1f929 -1f92a -1f92b -1f92c -1f92d -1f92e -1f92f -1f930 -1f931 -1f932 -1f933 -1f934 -1f935 -1f936 -1f937 -1f938 -1f939 -1f93a -1f93c -1f93d -1f93e -1f93f -1f940 -1f941 -1f942 -1f943 -1f944 -1f945 -1f947 -1f948 -1f949 -1f94a -1f94b -1f94c -1f94d -1f94e -1f94f -1f950 -1f951 -1f952 -1f953 -1f954 -1f955 -1f956 -1f957 -1f958 -1f959 -1f95a -1f95b -1f95c -1f95d -1f95e -1f95f -1f960 -1f961 -1f962 -1f963 -1f964 -1f965 -1f966 -1f967 -1f968 -1f969 -1f96a -1f96b -1f96c -1f96d -1f96e -1f96f -1f970 -1f971 -1f972 -1f973 -1f974 -1f975 -1f976 -1f977 -1f978 -1f979 -1f97a -1f97b -1f97c -1f97d -1f97e -1f97f -1f980 -1f981 -1f982 -1f983 -1f984 -1f985 -1f986 -1f987 -1f988 -1f989 -1f98a -1f98b -1f98c -1f98d -1f98e -1f98f -1f990 -1f991 -1f992 -1f993 -1f994 -1f995 -1f996 -1f997 -1f998 -1f999 -1f99a -1f99b -1f99c -1f99d -1f99e -1f99f -1f9a0 -1f9a1 -1f9a2 -1f9a3 -1f9a4 -1f9a5 -1f9a6 -1f9a7 -1f9a8 -1f9a9 -1f9aa -1f9ab -1f9ac -1f9ad -1f9ae -1f9af -1f9b0 -1f9b1 -1f9b2 -1f9b3 -1f9b4 -1f9b5 -1f9b6 -1f9b7 -1f9b8 -1f9b9 -1f9ba -1f9bb -1f9bc -1f9bd -1f9be -1f9bf -1f9c0 -1f9c1 -1f9c2 -1f9c3 -1f9c4 -1f9c5 -1f9c6 -1f9c7 -1f9c8 -1f9c9 -1f9ca -1f9cb -1f9cc -1f9cd -1f9ce -1f9cf -1f9d0 -1f9d1 -1f9d2 -1f9d3 -1f9d4 -1f9d5 -1f9d6 -1f9d7 -1f9d8 -1f9d9 -1f9da -1f9db -1f9dc -1f9dd -1f9de -1f9df -1f9e0 -1f9e1 -1f9e2 -1f9e3 -1f9e4 -1f9e5 -1f9e6 -1f9e7 -1f9e8 -1f9e9 -1f9ea -1f9eb -1f9ec -1f9ed -1f9ee -1f9ef -1f9f0 -1f9f1 -1f9f2 -1f9f3 -1f9f4 -1f9f5 -1f9f6 -1f9f7 -1f9f8 -1f9f9 -1f9fa -1f9fb -1f9fc -1f9fd -1f9fe -1f9ff -1fa70 -1fa71 -1fa72 -1fa73 -1fa74 -1fa75 -1fa76 -1fa77 -1fa78 -1fa79 -1fa7a -1fa7b -1fa7c -1fa80 -1fa81 -1fa82 -1fa83 -1fa84 -1fa85 -1fa86 -1fa87 -1fa88 -1fa89 -1fa8a -1fa8e -1fa8f -1fa90 -1fa91 -1fa92 -1fa93 -1fa94 -1fa95 -1fa96 -1fa97 -1fa98 -1fa99 -1fa9a -1fa9b -1fa9c -1fa9d -1fa9e -1fa9f -1faa0 -1faa1 -1faa2 -1faa3 -1faa4 -1faa5 -1faa6 -1faa7 -1faa8 -1faa9 -1faaa -1faab -1faac -1faad -1faae -1faaf -1fab0 -1fab1 -1fab2 -1fab3 -1fab4 -1fab5 -1fab6 -1fab7 -1fab8 -1fab9 -1faba -1fabb -1fabc -1fabd -1fabe -1fabf -1fac0 -1fac1 -1fac2 -1fac3 -1fac4 -1fac5 -1fac6 -1fac8 -1facd -1face -1facf -1fad0 -1fad1 -1fad2 -1fad3 -1fad4 -1fad5 -1fad6 -1fad7 -1fad8 -1fad9 -1fada -1fadb -1fadc -1fadf -1fae0 -1fae1 -1fae2 -1fae3 -1fae4 -1fae5 -1fae6 -1fae7 -1fae8 -1fae9 -1faea -1faef -1faf0 -1faf1 -1faf2 -1faf3 -1faf4 -1faf5 -1faf6 -1faf7 -1faf8 -203c +A9 +AE +203C 2049 2122 2139 @@ -1264,79 +10,79 @@ 2197 2198 2199 -21a9 -21aa -231a -231b +21A9 +21AA +231A +231B 2328 -23cf -23e9 -23ea -23eb -23ec -23ed -23ee -23ef -23f0 -23f1 -23f2 -23f3 -23f8 -23f9 -23fa -24c2 -25aa -25ab -25b6 -25c0 -25fb -25fc -25fd -25fe +23CF +23E9 +23EA +23EB +23EC +23ED +23EE +23EF +23F0 +23F1 +23F2 +23F3 +23F8 +23F9 +23FA +24C2 +25AA +25AB +25B6 +25C0 +25FB +25FC +25FD +25FE 2600 2601 2602 2603 2604 -260e +260E 2611 2614 2615 2618 -261d +261D 2620 2622 2623 2626 -262a -262e -262f +262A +262E +262F 2638 2639 -263a +263A 2640 2642 2648 2649 -264a -264b -264c -264d -264e -264f +264A +264B +264C +264D +264E +264F 2650 2651 2652 2653 -265f +265F 2660 2663 2665 2666 2668 -267b -267e -267f +267B +267E +267F 2692 2693 2694 @@ -1344,59 +90,59 @@ 2696 2697 2699 -269b -269c -26a0 -26a1 -26a7 -26aa -26ab -26b0 -26b1 -26bd -26be -26c4 -26c5 -26c8 -26ce -26cf -26d1 -26d3 -26d4 -26e9 -26ea -26f0 -26f1 -26f2 -26f3 -26f4 -26f5 -26f7 -26f8 -26f9 -26fa -26fd +269B +269C +26A0 +26A1 +26A7 +26AA +26AB +26B0 +26B1 +26BD +26BE +26C4 +26C5 +26C8 +26CE +26CF +26D1 +26D3 +26D4 +26E9 +26EA +26F0 +26F1 +26F2 +26F3 +26F4 +26F5 +26F7 +26F8 +26F9 +26FA +26FD 2702 2705 2708 2709 -270a -270b -270c -270d -270f +270A +270B +270C +270D +270F 2712 2714 2716 -271d +271D 2721 2728 2733 2734 2744 2747 -274c -274e +274C +274E 2753 2754 2755 @@ -1406,20 +152,1249 @@ 2795 2796 2797 -27a1 -27b0 -27bf +27A1 +27B0 +27BF 2934 2935 -2b05 -2b06 -2b07 -2b1b -2b1c -2b50 -2b55 +2B05 +2B06 +2B07 +2B1B +2B1C +2B50 +2B55 3030 -303d +303D 3297 3299 -e50a +1F004 +1F0CF +1F170 +1F171 +1F17E +1F17F +1F18E +1F191 +1F192 +1F193 +1F194 +1F195 +1F196 +1F197 +1F198 +1F199 +1F19A +1F201 +1F202 +1F21A +1F22F +1F232 +1F233 +1F234 +1F235 +1F236 +1F237 +1F238 +1F239 +1F23A +1F250 +1F251 +1F300 +1F301 +1F302 +1F303 +1F304 +1F305 +1F306 +1F307 +1F308 +1F309 +1F30A +1F30B +1F30C +1F30D +1F30E +1F30F +1F310 +1F311 +1F312 +1F313 +1F314 +1F315 +1F316 +1F317 +1F318 +1F319 +1F31A +1F31B +1F31C +1F31D +1F31E +1F31F +1F320 +1F321 +1F324 +1F325 +1F326 +1F327 +1F328 +1F329 +1F32A +1F32B +1F32C +1F32D +1F32E +1F32F +1F330 +1F331 +1F332 +1F333 +1F334 +1F335 +1F336 +1F337 +1F338 +1F339 +1F33A +1F33B +1F33C +1F33D +1F33E +1F33F +1F340 +1F341 +1F342 +1F343 +1F344 +1F345 +1F346 +1F347 +1F348 +1F349 +1F34A +1F34B +1F34C +1F34D +1F34E +1F34F +1F350 +1F351 +1F352 +1F353 +1F354 +1F355 +1F356 +1F357 +1F358 +1F359 +1F35A +1F35B +1F35C +1F35D +1F35E +1F35F +1F360 +1F361 +1F362 +1F363 +1F364 +1F365 +1F366 +1F367 +1F368 +1F369 +1F36A +1F36B +1F36C +1F36D +1F36E +1F36F +1F370 +1F371 +1F372 +1F373 +1F374 +1F375 +1F376 +1F377 +1F378 +1F379 +1F37A +1F37B +1F37C +1F37D +1F37E +1F37F +1F380 +1F381 +1F382 +1F383 +1F384 +1F385 +1F386 +1F387 +1F388 +1F389 +1F38A +1F38B +1F38C +1F38D +1F38E +1F38F +1F390 +1F391 +1F392 +1F393 +1F396 +1F397 +1F399 +1F39A +1F39B +1F39E +1F39F +1F3A0 +1F3A1 +1F3A2 +1F3A3 +1F3A4 +1F3A5 +1F3A6 +1F3A7 +1F3A8 +1F3A9 +1F3AA +1F3AB +1F3AC +1F3AD +1F3AE +1F3AF +1F3B0 +1F3B1 +1F3B2 +1F3B3 +1F3B4 +1F3B5 +1F3B6 +1F3B7 +1F3B8 +1F3B9 +1F3BA +1F3BB +1F3BC +1F3BD +1F3BE +1F3BF +1F3C0 +1F3C1 +1F3C2 +1F3C3 +1F3C4 +1F3C5 +1F3C6 +1F3C7 +1F3C8 +1F3C9 +1F3CA +1F3CB +1F3CC +1F3CD +1F3CE +1F3CF +1F3D0 +1F3D1 +1F3D2 +1F3D3 +1F3D4 +1F3D5 +1F3D6 +1F3D7 +1F3D8 +1F3D9 +1F3DA +1F3DB +1F3DC +1F3DD +1F3DE +1F3DF +1F3E0 +1F3E1 +1F3E2 +1F3E3 +1F3E4 +1F3E5 +1F3E6 +1F3E7 +1F3E8 +1F3E9 +1F3EA +1F3EB +1F3EC +1F3ED +1F3EE +1F3EF +1F3F0 +1F3F3 +1F3F4 +1F3F5 +1F3F7 +1F3F8 +1F3F9 +1F3FA +1F3FB +1F3FC +1F3FD +1F3FE +1F3FF +1F400 +1F401 +1F402 +1F403 +1F404 +1F405 +1F406 +1F407 +1F408 +1F409 +1F40A +1F40B +1F40C +1F40D +1F40E +1F40F +1F410 +1F411 +1F412 +1F413 +1F414 +1F415 +1F416 +1F417 +1F418 +1F419 +1F41A +1F41B +1F41C +1F41D +1F41E +1F41F +1F420 +1F421 +1F422 +1F423 +1F424 +1F425 +1F426 +1F427 +1F428 +1F429 +1F42A +1F42B +1F42C +1F42D +1F42E +1F42F +1F430 +1F431 +1F432 +1F433 +1F434 +1F435 +1F436 +1F437 +1F438 +1F439 +1F43A +1F43B +1F43C +1F43D +1F43E +1F43F +1F440 +1F441 +1F442 +1F443 +1F444 +1F445 +1F446 +1F447 +1F448 +1F449 +1F44A +1F44B +1F44C +1F44D +1F44E +1F44F +1F450 +1F451 +1F452 +1F453 +1F454 +1F455 +1F456 +1F457 +1F458 +1F459 +1F45A +1F45B +1F45C +1F45D +1F45E +1F45F +1F460 +1F461 +1F462 +1F463 +1F464 +1F465 +1F466 +1F467 +1F468 +1F469 +1F46A +1F46B +1F46C +1F46D +1F46E +1F46F +1F470 +1F471 +1F472 +1F473 +1F474 +1F475 +1F476 +1F477 +1F478 +1F479 +1F47A +1F47B +1F47C +1F47D +1F47E +1F47F +1F480 +1F481 +1F482 +1F483 +1F484 +1F485 +1F486 +1F487 +1F488 +1F489 +1F48A +1F48B +1F48C +1F48D +1F48E +1F48F +1F490 +1F491 +1F492 +1F493 +1F494 +1F495 +1F496 +1F497 +1F498 +1F499 +1F49A +1F49B +1F49C +1F49D +1F49E +1F49F +1F4A0 +1F4A1 +1F4A2 +1F4A3 +1F4A4 +1F4A5 +1F4A6 +1F4A7 +1F4A8 +1F4A9 +1F4AA +1F4AB +1F4AC +1F4AD +1F4AE +1F4AF +1F4B0 +1F4B1 +1F4B2 +1F4B3 +1F4B4 +1F4B5 +1F4B6 +1F4B7 +1F4B8 +1F4B9 +1F4BA +1F4BB +1F4BC +1F4BD +1F4BE +1F4BF +1F4C0 +1F4C1 +1F4C2 +1F4C3 +1F4C4 +1F4C5 +1F4C6 +1F4C7 +1F4C8 +1F4C9 +1F4CA +1F4CB +1F4CC +1F4CD +1F4CE +1F4CF +1F4D0 +1F4D1 +1F4D2 +1F4D3 +1F4D4 +1F4D5 +1F4D6 +1F4D7 +1F4D8 +1F4D9 +1F4DA +1F4DB +1F4DC +1F4DD +1F4DE +1F4DF +1F4E0 +1F4E1 +1F4E2 +1F4E3 +1F4E4 +1F4E5 +1F4E6 +1F4E7 +1F4E8 +1F4E9 +1F4EA +1F4EB +1F4EC +1F4ED +1F4EE +1F4EF +1F4F0 +1F4F1 +1F4F2 +1F4F3 +1F4F4 +1F4F5 +1F4F6 +1F4F7 +1F4F8 +1F4F9 +1F4FA +1F4FB +1F4FC +1F4FD +1F4FF +1F500 +1F501 +1F502 +1F503 +1F504 +1F505 +1F506 +1F507 +1F508 +1F509 +1F50A +1F50B +1F50C +1F50D +1F50E +1F50F +1F510 +1F511 +1F512 +1F513 +1F514 +1F515 +1F516 +1F517 +1F518 +1F519 +1F51A +1F51B +1F51C +1F51D +1F51E +1F51F +1F520 +1F521 +1F522 +1F523 +1F524 +1F525 +1F526 +1F527 +1F528 +1F529 +1F52A +1F52B +1F52C +1F52D +1F52E +1F52F +1F530 +1F531 +1F532 +1F533 +1F534 +1F535 +1F536 +1F537 +1F538 +1F539 +1F53A +1F53B +1F53C +1F53D +1F549 +1F54A +1F54B +1F54C +1F54D +1F54E +1F550 +1F551 +1F552 +1F553 +1F554 +1F555 +1F556 +1F557 +1F558 +1F559 +1F55A +1F55B +1F55C +1F55D +1F55E +1F55F +1F560 +1F561 +1F562 +1F563 +1F564 +1F565 +1F566 +1F567 +1F56F +1F570 +1F573 +1F574 +1F575 +1F576 +1F577 +1F578 +1F579 +1F57A +1F587 +1F58A +1F58B +1F58C +1F58D +1F590 +1F595 +1F596 +1F5A4 +1F5A5 +1F5A8 +1F5B1 +1F5B2 +1F5BC +1F5C2 +1F5C3 +1F5C4 +1F5D1 +1F5D2 +1F5D3 +1F5DC +1F5DD +1F5DE +1F5E1 +1F5E3 +1F5E8 +1F5EF +1F5F3 +1F5FA +1F5FB +1F5FC +1F5FD +1F5FE +1F5FF +1F600 +1F601 +1F602 +1F603 +1F604 +1F605 +1F606 +1F607 +1F608 +1F609 +1F60A +1F60B +1F60C +1F60D +1F60E +1F60F +1F610 +1F611 +1F612 +1F613 +1F614 +1F615 +1F616 +1F617 +1F618 +1F619 +1F61A +1F61B +1F61C +1F61D +1F61E +1F61F +1F620 +1F621 +1F622 +1F623 +1F624 +1F625 +1F626 +1F627 +1F628 +1F629 +1F62A +1F62B +1F62C +1F62D +1F62E +1F62F +1F630 +1F631 +1F632 +1F633 +1F634 +1F635 +1F636 +1F637 +1F638 +1F639 +1F63A +1F63B +1F63C +1F63D +1F63E +1F63F +1F640 +1F641 +1F642 +1F643 +1F644 +1F645 +1F646 +1F647 +1F648 +1F649 +1F64A +1F64B +1F64C +1F64D +1F64E +1F64F +1F680 +1F681 +1F682 +1F683 +1F684 +1F685 +1F686 +1F687 +1F688 +1F689 +1F68A +1F68B +1F68C +1F68D +1F68E +1F68F +1F690 +1F691 +1F692 +1F693 +1F694 +1F695 +1F696 +1F697 +1F698 +1F699 +1F69A +1F69B +1F69C +1F69D +1F69E +1F69F +1F6A0 +1F6A1 +1F6A2 +1F6A3 +1F6A4 +1F6A5 +1F6A6 +1F6A7 +1F6A8 +1F6A9 +1F6AA +1F6AB +1F6AC +1F6AD +1F6AE +1F6AF +1F6B0 +1F6B1 +1F6B2 +1F6B3 +1F6B4 +1F6B5 +1F6B6 +1F6B7 +1F6B8 +1F6B9 +1F6BA +1F6BB +1F6BC +1F6BD +1F6BE +1F6BF +1F6C0 +1F6C1 +1F6C2 +1F6C3 +1F6C4 +1F6C5 +1F6CB +1F6CC +1F6CD +1F6CE +1F6CF +1F6D0 +1F6D1 +1F6D2 +1F6D5 +1F6D6 +1F6D7 +1F6D8 +1F6DC +1F6DD +1F6DE +1F6DF +1F6E0 +1F6E1 +1F6E2 +1F6E3 +1F6E4 +1F6E5 +1F6E9 +1F6EB +1F6EC +1F6F0 +1F6F3 +1F6F4 +1F6F5 +1F6F6 +1F6F7 +1F6F8 +1F6F9 +1F6FA +1F6FB +1F6FC +1F7E0 +1F7E1 +1F7E2 +1F7E3 +1F7E4 +1F7E5 +1F7E6 +1F7E7 +1F7E8 +1F7E9 +1F7EA +1F7EB +1F7F0 +1F90C +1F90D +1F90E +1F90F +1F910 +1F911 +1F912 +1F913 +1F914 +1F915 +1F916 +1F917 +1F918 +1F919 +1F91A +1F91B +1F91C +1F91D +1F91E +1F91F +1F920 +1F921 +1F922 +1F923 +1F924 +1F925 +1F926 +1F927 +1F928 +1F929 +1F92A +1F92B +1F92C +1F92D +1F92E +1F92F +1F930 +1F931 +1F932 +1F933 +1F934 +1F935 +1F936 +1F937 +1F938 +1F939 +1F93A +1F93C +1F93D +1F93E +1F93F +1F940 +1F941 +1F942 +1F943 +1F944 +1F945 +1F947 +1F948 +1F949 +1F94A +1F94B +1F94C +1F94D +1F94E +1F94F +1F950 +1F951 +1F952 +1F953 +1F954 +1F955 +1F956 +1F957 +1F958 +1F959 +1F95A +1F95B +1F95C +1F95D +1F95E +1F95F +1F960 +1F961 +1F962 +1F963 +1F964 +1F965 +1F966 +1F967 +1F968 +1F969 +1F96A +1F96B +1F96C +1F96D +1F96E +1F96F +1F970 +1F971 +1F972 +1F973 +1F974 +1F975 +1F976 +1F977 +1F978 +1F979 +1F97A +1F97B +1F97C +1F97D +1F97E +1F97F +1F980 +1F981 +1F982 +1F983 +1F984 +1F985 +1F986 +1F987 +1F988 +1F989 +1F98A +1F98B +1F98C +1F98D +1F98E +1F98F +1F990 +1F991 +1F992 +1F993 +1F994 +1F995 +1F996 +1F997 +1F998 +1F999 +1F99A +1F99B +1F99C +1F99D +1F99E +1F99F +1F9A0 +1F9A1 +1F9A2 +1F9A3 +1F9A4 +1F9A5 +1F9A6 +1F9A7 +1F9A8 +1F9A9 +1F9AA +1F9AB +1F9AC +1F9AD +1F9AE +1F9AF +1F9B0 +1F9B1 +1F9B2 +1F9B3 +1F9B4 +1F9B5 +1F9B6 +1F9B7 +1F9B8 +1F9B9 +1F9BA +1F9BB +1F9BC +1F9BD +1F9BE +1F9BF +1F9C0 +1F9C1 +1F9C2 +1F9C3 +1F9C4 +1F9C5 +1F9C6 +1F9C7 +1F9C8 +1F9C9 +1F9CA +1F9CB +1F9CC +1F9CD +1F9CE +1F9CF +1F9D0 +1F9D1 +1F9D2 +1F9D3 +1F9D4 +1F9D5 +1F9D6 +1F9D7 +1F9D8 +1F9D9 +1F9DA +1F9DB +1F9DC +1F9DD +1F9DE +1F9DF +1F9E0 +1F9E1 +1F9E2 +1F9E3 +1F9E4 +1F9E5 +1F9E6 +1F9E7 +1F9E8 +1F9E9 +1F9EA +1F9EB +1F9EC +1F9ED +1F9EE +1F9EF +1F9F0 +1F9F1 +1F9F2 +1F9F3 +1F9F4 +1F9F5 +1F9F6 +1F9F7 +1F9F8 +1F9F9 +1F9FA +1F9FB +1F9FC +1F9FD +1F9FE +1F9FF +1FA70 +1FA71 +1FA72 +1FA73 +1FA74 +1FA75 +1FA76 +1FA77 +1FA78 +1FA79 +1FA7A +1FA7B +1FA7C +1FA80 +1FA81 +1FA82 +1FA83 +1FA84 +1FA85 +1FA86 +1FA87 +1FA88 +1FA89 +1FA8A +1FA8E +1FA8F +1FA90 +1FA91 +1FA92 +1FA93 +1FA94 +1FA95 +1FA96 +1FA97 +1FA98 +1FA99 +1FA9A +1FA9B +1FA9C +1FA9D +1FA9E +1FA9F +1FAA0 +1FAA1 +1FAA2 +1FAA3 +1FAA4 +1FAA5 +1FAA6 +1FAA7 +1FAA8 +1FAA9 +1FAAA +1FAAB +1FAAC +1FAAD +1FAAE +1FAAF +1FAB0 +1FAB1 +1FAB2 +1FAB3 +1FAB4 +1FAB5 +1FAB6 +1FAB7 +1FAB8 +1FAB9 +1FABA +1FABB +1FABC +1FABD +1FABE +1FABF +1FAC0 +1FAC1 +1FAC2 +1FAC3 +1FAC4 +1FAC5 +1FAC6 +1FAC8 +1FACD +1FACE +1FACF +1FAD0 +1FAD1 +1FAD2 +1FAD3 +1FAD4 +1FAD5 +1FAD6 +1FAD7 +1FAD8 +1FAD9 +1FADA +1FADB +1FADC +1FADF +1FAE0 +1FAE1 +1FAE2 +1FAE3 +1FAE4 +1FAE5 +1FAE6 +1FAE7 +1FAE8 +1FAE9 +1FAEA +1FAEF +1FAF0 +1FAF1 +1FAF2 +1FAF3 +1FAF4 +1FAF5 +1FAF6 +1FAF7 +1FAF8 diff --git a/tools/emoji/emoji-unsupported.txt b/tools/emoji/emoji-unsupported.txt new file mode 100644 index 000000000..e69de29bb From 37f40dfdd7447e4ebaecfe17e831a6f7e706681f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 22:45:51 +0200 Subject: [PATCH 051/120] Emoji routing: the global fallback is the only route; the check observes it EmojiFont.Build leaves TMP's emoji fallback list empty. EmojiCheck fails when that list is non-empty, when the global fallback lacks Emoji.asset, or when a glyph is served by a font other than Emoji.asset or the body font; reads the Pirates product names from the shipped catalog; renders the emoji-test.txt corpus and reports what Twemoji lacks; counts only used atlas pages (TMP doubles the array capacity, so trailing slots are null by design). Co-Authored-By: Claude Sonnet 5.5 --- Assets/Scripts/Editor/EmojiCheck.cs | 84 ++++++++++++++----- Assets/Scripts/Editor/EmojiFont.cs | 13 +-- .../TextMesh Pro/Resources/TMP Settings.asset | 4 +- 3 files changed, 70 insertions(+), 31 deletions(-) diff --git a/Assets/Scripts/Editor/EmojiCheck.cs b/Assets/Scripts/Editor/EmojiCheck.cs index 48afc0018..5080315b3 100644 --- a/Assets/Scripts/Editor/EmojiCheck.cs +++ b/Assets/Scripts/Editor/EmojiCheck.cs @@ -6,22 +6,20 @@ using TMPro; using UnityEditor; using UnityEngine; -// Every single-code-point emoji the pinned font set draws, and the Pirates product names, render through an -// ordinary TMP text on the game's body font with no missing code point and with ink in the emoji atlas. +// Every single-code-point emoji of the pinned Unicode Emoji version that Twemoji draws, and the Pirates product +// names read from the shipped catalog, render through an ordinary TMP text on the game's body font with no +// missing code point, no blank glyph, ink in the emoji atlas, and each glyph served by the emoji asset or by the +// body font itself, never by a third font. TMP Settings must route emoji by exactly one entry: the global +// fallback (the emoji fallback list stays empty), so dropping that entry or re-adding the second route fails. // Run as -executeMethod EmojiCheck.Run (exit 1 on failure in batchmode, as EngineAssetCheck). Corpus: -// tools/emoji/emoji-singles.txt; -emojiLimit N spreads N entries evenly across it for the spike. +// tools/emoji/emoji-singles.txt (from emoji-test.txt); the emoji Twemoji lacks are listed in +// tools/emoji/emoji-unsupported.txt and reported, not rendered; -emojiLimit N spreads N entries evenly for a spike. public static class EmojiCheck { const string BodyFontPath = "Assets/Fonts/Ubuntu/Ubuntu-L Small SDF.asset"; + const string PiratesFaction = "Pirates"; const int Chunk = 64; - // The eight Pirates products (addenda "The Pirates record r2"), written as code points. - static readonly string[] PiratesNames = - { - "\U0001FAF3\U0001F52B", "\U0001FAA6\U0001F920", "\U0001F9FE\U0001F6AB", "\U0001F3A8\U0001F648", - "\U0001F525", "\U0001F440\U0001F3AF", "\U0001F511\U0001F3C3\U0001F4A8", "\U0001F4E6\U0001F92B", - }; - [MenuItem("Aetheria/Text/Check Emoji Rendering")] public static void Run() { @@ -32,7 +30,8 @@ public static class EmojiCheck if (Application.isBatchMode) EditorApplication.Exit(failures.Count == 0 ? 0 : 1); } - static string RunCheck(List failures) + // Public so a scratch driver can mutate TMP Settings in memory and watch this fail; adds to failures. + public static string RunCheck(List failures) { var emoji = AssetDatabase.LoadAssetAtPath(EmojiFont.AssetPath); var body = AssetDatabase.LoadAssetAtPath(BodyFontPath); @@ -42,7 +41,14 @@ public static class EmojiCheck return "nothing checked"; } - var corpus = LoadSingles(); + // One route: the global fallback names the emoji asset, and the emoji fallback list is empty. + if (TMP_Settings.emojiFallbackTextAssets != null && TMP_Settings.emojiFallbackTextAssets.Count > 0) + failures.Add("TMP Settings' emoji fallback list is not empty: a second emoji route exists (EmojiFont.Build leaves it empty)."); + if (TMP_Settings.fallbackFontAssets == null || !TMP_Settings.fallbackFontAssets.Contains(emoji)) + failures.Add("TMP Settings' global fallback list does not name Emoji.asset: the only emoji route is gone."); + + var unsupported = new HashSet(LoadCodePoints("emoji-unsupported.txt")); + var corpus = LoadSingles().Where(cp => !unsupported.Contains(cp)).ToList(); var missing = new HashSet(); TMP_Text.MissingCharacterEventCallback onMissing = (unicode, index, source, asset, component) => missing.Add(unicode); TMP_Text.OnMissingCharacter += onMissing; @@ -54,7 +60,8 @@ public static class EmojiCheck text.textWrappingMode = TextWrappingModes.NoWrap; text.rectTransform.sizeDelta = new Vector2(100000, 100000); - int viaEmoji = 0, viaOther = 0, blank = 0; + int viaEmoji = 0, viaBody = 0, blank = 0, pirateNames = 0; + var strays = new HashSet(); try { void Render(string content, string label) @@ -65,7 +72,10 @@ public static class EmojiCheck { var info = text.textInfo.characterInfo[i]; if (info.character == ' ' || info.elementType != TMP_TextElementType.Character) continue; - if (info.fontAsset == emoji) viaEmoji++; else viaOther++; + if (info.fontAsset == emoji) viaEmoji++; + else if (info.fontAsset == body) viaBody++; + else if (strays.Add($"{info.fontAsset.name}:{(int)info.character:X}")) + failures.Add($"U+{(int)info.character:X4} in {label} was served by {info.fontAsset.name}, not Emoji.asset or the body font"); if (info.fontAsset == emoji && info.textElement.glyph.glyphRect.width <= 0) { blank++; @@ -79,7 +89,24 @@ public static class EmojiCheck var slice = corpus.Skip(i).Take(Chunk).ToList(); Render(string.Join(" ", slice.Select(char.ConvertFromUtf32)), $"chunk {i}"); } - foreach (var name in PiratesNames) Render(name, "Pirates name"); + + // The Pirates names as the shipped catalog stores them, through the game's own catalog reader. + var catalogPath = Path.Combine(Directory.GetParent(Application.dataPath).FullName, "GameData", "Aetheria.cc"); + using (var catalog = AetheriaStores.Open(catalogPath)) + { + var pirates = catalog.GetAll().SingleOrDefault(faction => faction.Name == PiratesFaction); + if (pirates == null) failures.Add($"the catalog has no {PiratesFaction} faction"); + else + { + var key = catalog.RefOf(pirates); + foreach (var product in catalog.GetAll().Where(product => product.Manufacturer.Equals(key))) + { + pirateNames++; + Render(product.Name, $"Pirates product name \"{product.Name}\""); + } + if (pirateNames == 0) failures.Add($"the catalog has no {PiratesFaction} products"); + } + } } finally { @@ -89,12 +116,13 @@ public static class EmojiCheck foreach (var cp in missing.OrderBy(c => c)) failures.Add($"missing code point U+{cp:X4}"); + // TMP over-allocates atlasTextures by doubling its capacity when a page is added, so entries past + // atlasTextureCount are null by design (3 pages in a 4-slot array). Only the used pages count. long ink = 0, colour = 0; - int pages = 0, nullPages = 0; - foreach (var page in emoji.atlasTextures) + for (var i = 0; i < emoji.atlasTextureCount; i++) { - if (page == null) { nullPages++; continue; } - pages++; + var page = emoji.atlasTextures[i]; + if (page == null) { failures.Add($"emoji atlas page {i} of {emoji.atlasTextureCount} is null"); continue; } var pixels = page.GetPixels32(); ink += pixels.LongCount(p => p.a != 0); colour += pixels.LongCount(p => p.a != 0 && (p.r != p.g || p.g != p.b)); @@ -102,20 +130,30 @@ public static class EmojiCheck if (ink == 0) failures.Add("the emoji atlas holds no ink after rendering"); else if (colour == 0) failures.Add("the emoji atlas holds no coloured pixel: the glyphs rendered monochrome"); + var pages = emoji.atlasTextureCount; // Dynamic atlas content is cache: drop it so this check leaves the asset as Build wrote it. emoji.ClearFontAssetData(true); - return $"checked {corpus.Count} single code points + {PiratesNames.Length} Pirates names, {missing.Count} missing, " + - $"{viaEmoji} glyphs via the emoji asset, {viaOther} via other fonts, {blank} blank, {ink} atlas ink pixels of which {colour} coloured, {pages} atlas pages ({nullPages} null)"; + if (unsupported.Count > 0) + Debug.LogWarning($"EmojiCheck: Twemoji lacks {unsupported.Count} single-code-point emoji of the Emoji version: " + + string.Join(" ", unsupported.OrderBy(c => c).Select(c => $"U+{c:X4}"))); + return $"checked {corpus.Count} single code points ({unsupported.Count} not drawn by Twemoji, listed in the warning) + {pirateNames} Pirates product names, " + + $"{missing.Count} missing, {viaEmoji} glyphs via the emoji asset, {viaBody} via the body font, {strays.Count} via other fonts, {blank} blank, " + + $"{ink} atlas ink pixels of which {colour} coloured, {pages} atlas pages"; } static List LoadSingles() { - var path = Path.Combine(Directory.GetParent(Application.dataPath).FullName, "tools", "emoji", "emoji-singles.txt"); - var all = File.ReadAllLines(path).Where(l => l.Length > 0).Select(l => Convert.ToInt32(l, 16)).ToList(); + var all = LoadCodePoints("emoji-singles.txt"); var args = Environment.GetCommandLineArgs(); var at = Array.IndexOf(args, "-emojiLimit"); if (at < 0 || at + 1 >= args.Length) return all; var limit = int.Parse(args[at + 1]); return Enumerable.Range(0, limit).Select(i => all[i * all.Count / limit]).ToList(); } + + static List LoadCodePoints(string file) + { + var path = Path.Combine(Directory.GetParent(Application.dataPath).FullName, "tools", "emoji", file); + return File.ReadAllLines(path).Where(l => l.Length > 0).Select(l => Convert.ToInt32(l, 16)).ToList(); + } } diff --git a/Assets/Scripts/Editor/EmojiFont.cs b/Assets/Scripts/Editor/EmojiFont.cs index c7ce97aaf..0c1d4eac2 100644 --- a/Assets/Scripts/Editor/EmojiFont.cs +++ b/Assets/Scripts/Editor/EmojiFont.cs @@ -6,8 +6,10 @@ using UnityEngine.TextCore.LowLevel; // The one writer of the emoji font asset and of TMP Settings' emoji routing. The font file is the // output of tools/emoji/fetch_emoji_font.py; the asset is a dynamic COLOR font asset whose atlas pages -// are cache, filled on first use at runtime and cleared here. Every TMP text reaches emoji through -// TMP Settings' emoji fallback and global fallback list; no component names an emoji font. +// are cache, filled on first use at runtime and cleared here. Every TMP text reaches emoji through ONE +// route, TMP Settings' global fallback list; the emoji fallback list stays empty, and no component names +// an emoji font. (Measured: the global list alone resolves every single code point; the emoji list alone +// leaves 200 text-presentation emoji unresolved. A second route could only disagree with the first.) public static class EmojiFont { public const string FontPath = "Assets/Fonts/Emoji/Twemoji.ttf"; @@ -61,16 +63,15 @@ public static class EmojiFont // The atlas pages are cache: Build leaves them empty, the same state EmojiCheck restores. asset.ClearFontAssetData(true); - // Text-presentation emoji (U+263A without FE0F) are not routed through the emoji list, so the - // asset is also the global fallback. - TMP_Settings.emojiFallbackTextAssets = new List { asset }; + // One route: the global fallback. It also serves text-presentation emoji (U+263A without FE0F). + TMP_Settings.emojiFallbackTextAssets = new List(); TMP_Settings.fallbackFontAssets = new List { asset }; TMP_Settings.defaultSpriteAsset = null; EditorUtility.SetDirty(asset); EditorUtility.SetDirty(TMP_Settings.instance); AssetDatabase.SaveAssets(); - message = "EmojiFont: built " + AssetPath + " and routed TMP Settings' emoji fallback to it."; + message = "EmojiFont: built " + AssetPath + " and made it TMP Settings' only emoji route (the global fallback)."; return true; } } diff --git a/Assets/TextMesh Pro/Resources/TMP Settings.asset b/Assets/TextMesh Pro/Resources/TMP Settings.asset index 0371c3b56..d7c619e31 100644 --- a/Assets/TextMesh Pro/Resources/TMP Settings.asset +++ b/Assets/TextMesh Pro/Resources/TMP Settings.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f00aad0de57d69c610a4a421bf4652e93f45c2a39bacc76dec8a7921bf699ecb -size 1930 +oid sha256:1e678e96aa2e2acd6a205a7c33df946e3a59e8dfae43e0cbbc98ddecc6983a58 +size 1861 From 544807bb858ae37a711e998adf7eb77a05731673 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 22:50:12 +0200 Subject: [PATCH 052/120] PiratesRecordTests: a Pirates rebrand unit titles and brands as its own product through the merged lot-product code Co-Authored-By: Claude Sonnet 5.5 --- .../Aetheria.Shared.Tests/PiratesRecordTests.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/Aetheria.Shared.Tests/PiratesRecordTests.cs b/tests/Aetheria.Shared.Tests/PiratesRecordTests.cs index 7a865d1e4..9d9b79056 100644 --- a/tests/Aetheria.Shared.Tests/PiratesRecordTests.cs +++ b/tests/Aetheria.Shared.Tests/PiratesRecordTests.cs @@ -64,4 +64,20 @@ public sealed partial class RunStartTests .ToArray(); Assert.Empty(twice); } + + // A unit minted from a Pirates rebrand titles and brands as that rebrand: the lot records its product, so the + // emoji name shows although the same design is also sold by its original maker. + [Fact] + public void PiratesRebrandUnitsTitleAsTheirOwnProduct() + { + var pirates = ShippedPirates(); + foreach (var product in PiratesProducts()) + { + var unit = _items.CreateInstance(_items.CreateLot(product)); + Assert.Equal(product.Name, _items.Title(unit)); + var brand = _items.Brand((CraftedItemInstance) unit); + Assert.Same(product, brand.Product); + Assert.Same(pirates, brand.Maker); + } + } } From a7f801c4b94013d0c5dc19fd08792cbda2804483 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 22:54:11 +0200 Subject: [PATCH 053/120] Subtype modifier tests: deactivate the ship before equipping the guns TryEquip refuses on a deployed ship. Co-Authored-By: Claude Sonnet 5.5 --- tests/Aetheria.Shared.Tests/StatResolverTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Aetheria.Shared.Tests/StatResolverTests.cs b/tests/Aetheria.Shared.Tests/StatResolverTests.cs index 5b731215d..9afafcd26 100644 --- a/tests/Aetheria.Shared.Tests/StatResolverTests.cs +++ b/tests/Aetheria.Shared.Tests/StatResolverTests.cs @@ -96,10 +96,10 @@ public sealed class StatResolverTests : IDisposable cache.FlushAsync().Wait(); var items = new ItemManager(cache, new ProvenanceLedger(), Settings(), _ => { }); var ship = BuildActivatedShip(cache, items, withBooster: true); + ship.Deactivate(); // TryEquip refuses while deployed Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName("AutoGun")), FirstGunCell)); Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName("ChargedGun")), SecondGunCell)); // Re-activate so the booster's Initialize sees the guns it did not see at the first activation. - ship.Deactivate(); ship.Activate(); var booster = ship.Equipment.Single(e => e.Data.Name == "Booster"); var behavior = booster.GetBehavior(); From 257a8a4795062b5c54c5fb5ff0fb9570323ca408 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 23:39:30 +0200 Subject: [PATCH 054/120] Sim speed fix: scripted poses are not drawn ahead; a step cannot reset its clock; DrawAhead.Time deleted DrawAhead leaves a ship in a wormhole animation where the step put it. The clock is one static instance; a zone entered inside a step ends that frame's stepping and the clock resets between frames (SimClock.Reset throws inside Advance). DrawAhead.Time had no consumer. Co-Authored-By: Claude Sonnet 5.5 --- Assets/Scripts/Gameplay/ActionGameManager.cs | 12 +++++-- Assets/Scripts/ServerShared/DrawAhead.cs | 12 ++++--- Assets/Scripts/ServerShared/SimClock.cs | 24 +++++++++++--- tests/Aetheria.Shared.Tests/SimClockTests.cs | 34 +++++++++++++++++++- 4 files changed, 68 insertions(+), 14 deletions(-) diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index 0476acde4..97c559d35 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -229,7 +229,8 @@ public class ActionGameManager : MonoBehaviour public ItemManager ItemManager { get; private set; } public Zone Zone { get; private set; } - public static SimClock Clock { get; private set; } + public static SimClock Clock { get; } = new SimClock(1f / 60f); + private bool _zoneEntered; private readonly (float2 direction, string name)[] _directions = { (float2(0, 1), "Front"), @@ -727,7 +728,7 @@ public class ActionGameManager : MonoBehaviour galaxyZone.Contents = new Zone(ItemManager, Settings.PlanetSettings, galaxyZone.PackedContents, galaxyZone, CurrentGalaxy); } Zone = galaxyZone.Contents; - Clock = new SimClock(1f / 60f); + _zoneEntered = true; // the clock forgets its lead between frames, never inside a step PlayMusic(MusicType.Overworld); Zone.Log = s => Debug.Log($"Zone: {s}"); @@ -1328,7 +1329,12 @@ public class ActionGameManager : MonoBehaviour } } // One clock steps the zone, paused or not: a paused frame feeds it no real time. - Clock.Advance(_paused ? 0f : Time.deltaTime, Zone.Update); + // A zone entered inside a step (a wormhole) ends this frame's stepping: the rest of its steps are the old zone's. + void EnterNewZone() { if (_zoneEntered) { Clock.Reset(); _zoneEntered = false; } } + EnterNewZone(); + var steppedZone = Zone; + Clock.Advance(_paused ? 0f : Time.deltaTime, delta => { if (Zone == steppedZone) steppedZone.Update(delta); }); + EnterNewZone(); } private static string ResultLabel(ShotResult result) => result switch diff --git a/Assets/Scripts/ServerShared/DrawAhead.cs b/Assets/Scripts/ServerShared/DrawAhead.cs index 52c92fee8..d26bf91b4 100644 --- a/Assets/Scripts/ServerShared/DrawAhead.cs +++ b/Assets/Scripts/ServerShared/DrawAhead.cs @@ -10,11 +10,16 @@ using static CultMath.math; // one, a previous state or a velocity of its own. public static class DrawAhead { + // A ship in a wormhole animation has its pose scripted by the step, not integrated from Velocity and TurnRate, + // so it is drawn where the step put it: extrapolating a scripted pose draws a streak that resets every step. public static float3 Position(Entity entity, float lead) => - entity.Position + float3(entity.Velocity.x, 0, entity.Velocity.y) * lead; + entity is Ship { WormholeAnimationInProgress: true } + ? entity.Position + : entity.Position + float3(entity.Velocity.x, 0, entity.Velocity.y) * lead; // The ship's rotation turned about its own up axis by the rate its last step turned, for lead seconds. - public static quaternion Rotation(Ship ship, float lead) => Turned(ship.Rotation, ship.TurnRate * lead); + public static quaternion Rotation(Ship ship, float lead) => + ship.WormholeAnimationInProgress ? ship.Rotation : Turned(ship.Rotation, ship.TurnRate * lead); // q turned about its own up axis by the angle, in the sense Direction turns for a positive TurnRate. public static quaternion Turned(quaternion q, float angle) @@ -32,7 +37,4 @@ public static class DrawAhead t.w * q.z + t.x * q.y - t.y * q.x + t.z * q.w, t.w * q.w - t.x * q.x - t.y * q.y - t.z * q.z)); } - - // Zone time for a presenter that evaluates a sim function of time (FireControl.RoundAt). - public static float Time(Zone zone, float lead) => zone.Time + lead; } diff --git a/Assets/Scripts/ServerShared/SimClock.cs b/Assets/Scripts/ServerShared/SimClock.cs index 15e132ac0..6d00c46d0 100644 --- a/Assets/Scripts/ServerShared/SimClock.cs +++ b/Assets/Scripts/ServerShared/SimClock.cs @@ -16,6 +16,7 @@ public sealed class SimClock private readonly double _period; private double _accumulator; + private bool _advancing; public SimClock(float step, float stepsPerRealSecond = 60) { @@ -36,19 +37,32 @@ public sealed class SimClock // Sim seconds since the last step, 0 <= Lead < Step. public float Lead => (float) (_accumulator * StepsPerRealSecond * Step); + // Forgets the real time paid toward the next step (a new zone starts between steps). A step cannot do this to the + // clock that is running it. + public void Reset() + { + if (_advancing) throw new InvalidOperationException("A step cannot reset the clock that is running it."); + _accumulator = 0; + } + // Feeds real seconds (negative counts as none, one frame counts at most MaxFrameSeconds) and runs the whole // steps they pay for. Returns how many ran. public int Advance(float realSeconds, Action step) { _accumulator += Math.Min(Math.Max(realSeconds, 0f), MaxFrameSeconds); var ran = 0; - while (_accumulator >= _period) + _advancing = true; + try { - _accumulator -= _period; - step(Step); - Steps++; - ran++; + while (_accumulator >= _period) + { + _accumulator -= _period; + step(Step); + Steps++; + ran++; + } } + finally { _advancing = false; } return ran; } } diff --git a/tests/Aetheria.Shared.Tests/SimClockTests.cs b/tests/Aetheria.Shared.Tests/SimClockTests.cs index 3a8f72fbd..db79647bb 100644 --- a/tests/Aetheria.Shared.Tests/SimClockTests.cs +++ b/tests/Aetheria.Shared.Tests/SimClockTests.cs @@ -77,6 +77,18 @@ public sealed class SimClockTests Assert.Equal(6, clock.Steps); } + [Fact] + public void AStepCannotResetItsClock() + { + var clock = new SimClock(1f / 60f); + Assert.Throws(() => clock.Advance(1f / 60f, _ => clock.Reset())); + clock.Advance(.005f, _ => { }); + Assert.True(clock.Lead > 0); + clock.Reset(); + Assert.Equal(0f, clock.Lead); + Assert.Equal(1, clock.Advance(1f / 60f, _ => { })); + } + [Fact] public void PauseFeedsNothing() { @@ -252,10 +264,30 @@ public sealed partial class RunStartTests Assert.Equal(player.Position.x + player.Velocity.x * lead, ahead.x, 6); Assert.Equal(player.Position.y, ahead.y); Assert.Equal(player.Position.z + player.Velocity.y * lead, ahead.z, 6); - Assert.Equal(arena.Time + lead, DrawAhead.Time(arena, lead)); Assert.Equal(before, Snapshot(arena, new List())); } + // A pose the step scripts (the wormhole animation) is drawn where the step put it, at any lead. + [Fact] + public void ScriptedPosesAreNotDrawnAhead() + { + var (arena, player) = LaunchDuel(); + for (var step = 0; step < 120; step++) arena.Update(ClockFrame); + player.EnterWormhole(player.Position.xz + float2(50, 0)); + arena.Update(ClockFrame); + Assert.True(player.WormholeAnimationInProgress); + Assert.True(length(player.Velocity) > .01f, "the ship carries speed into the animation"); + var position = DrawAhead.Position(player, ClockFrame); + Assert.Equal(player.Position.x, position.x); + Assert.Equal(player.Position.y, position.y); + Assert.Equal(player.Position.z, position.z); + var rotation = DrawAhead.Rotation(player, ClockFrame); + Assert.Equal(player.Rotation.x, rotation.x); + Assert.Equal(player.Rotation.y, rotation.y); + Assert.Equal(player.Rotation.z, rotation.z); + Assert.Equal(player.Rotation.w, rotation.w); + } + // The turn a presenter draws ahead is the turn the next step makes: a ship held in a steady turn, drawn one whole // step ahead of the pose it was last given, is where the sim puts that pose next. [Fact] From 6a1eb68ddbf7e1fd1000d6896a8f24a7f8143800 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 23:46:26 +0200 Subject: [PATCH 055/120] Roles backfill: one role map per design, the six unnamed non-flat stats named, RolesBackfill takes a root RoleMapFor answers for the Mine Launcher (MineLayerRoles) so roles-migrate and roles-backfill no longer disagree on it; BackfillMapFor is deleted. Count, MinRange, DamageSpread and the reactor Modifier join their kind maps, and the backfill points a varying stat's unnamed Quality term at its role without touching its range. DeathCluster, Flak Gun, GT 3K, plight and both MoveOnPros join the backfill set (CB-R1 forces them). RolesBackfill(apply, root) mirrors PiratesFaction so tests can replay it on a scratch catalog. Co-Authored-By: Claude Sonnet 5.5 --- tools/AetherDb/Program.cs | 46 ++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/tools/AetherDb/Program.cs b/tools/AetherDb/Program.cs index 2900ac4f1..9b87bf9b0 100644 --- a/tools/AetherDb/Program.cs +++ b/tools/AetherDb/Program.cs @@ -161,13 +161,13 @@ public static class Program { ["Damage"] = "barrel", ["Range"] = "barrel", ["Velocity"] = "barrel", ["Penetration"] = "barrel", ["Cooldown"] = "feed mechanism", ["Spread"] = "feed mechanism", ["Heat"] = "feed mechanism", - ["Visibility"] = "feed mechanism", ["Energy"] = "feed mechanism", + ["Visibility"] = "feed mechanism", ["Energy"] = "feed mechanism", ["Count"] = "feed mechanism", }; private static readonly Dictionary EnergyWeaponRoles = new Dictionary { ["Damage"] = "focusing array", ["Range"] = "focusing array", ["Velocity"] = "focusing array", - ["Penetration"] = "focusing array", + ["Penetration"] = "focusing array", ["DamageSpread"] = "focusing array", ["Energy"] = "power coupling", ["Heat"] = "power coupling", ["ChargeTime"] = "power coupling", ["ChargeEnergy"] = "power coupling", ["ChargeHeat"] = "power coupling", ["Cooldown"] = "power coupling", ["Spread"] = "power coupling", ["Visibility"] = "power coupling", @@ -178,6 +178,7 @@ public static class Program ["Damage"] = "warhead", ["Penetration"] = "warhead", ["DamageSpread"] = "warhead", ["LockSpeed"] = "guidance system", ["SensorImpact"] = "guidance system", ["LockAngle"] = "guidance system", ["DirectionImpact"] = "guidance system", ["Decay"] = "guidance system", ["Range"] = "guidance system", + ["MinRange"] = "guidance system", ["Cooldown"] = "guidance system", ["Spread"] = "guidance system", ["Visibility"] = "guidance system", ["Energy"] = "guidance system", ["Heat"] = "guidance system", ["Thrust"] = "thruster", ["MissileVelocity"] = "thruster", ["Velocity"] = "thruster", @@ -201,7 +202,7 @@ public static class Program private static readonly Dictionary ReactorRoles = new Dictionary { - ["Charge"] = "core", ["Efficiency"] = "core", + ["Charge"] = "core", ["Efficiency"] = "core", ["Modifier"] = "core", ["OverloadEfficiency"] = "regulator", ["ThrottlingFactor"] = "regulator", }; @@ -262,7 +263,8 @@ public static class Program // The field->role map for one design, or null when its kind carries no per-part behaviour stat at all // (CargoBay, DockingBay, ControlModule, Hull/Station, Hull/Turret -- none of their behaviours read // StatSource.Quality today, so there is no natural role to author and the report says so per design). - private static Dictionary RoleMapFor(EquippableItemData item, string kind) => kind switch + private static Dictionary RoleMapFor(EquippableItemData item, string kind) => + item.Behaviors != null && item.Behaviors.OfType().Any() ? MineLayerRoles : kind switch { "Weapon/Ballistic" => BallisticWeaponRoles, "Weapon/Energy" => EnergyWeaponRoles, @@ -276,6 +278,9 @@ public static class Program _ => null, }; + // Whether CB-R1 maps this design's stats to roles; the catalog test asks the tool, so it keeps no copy of the kinds. + public static bool HasRoleMap(EquippableItemData item) => RoleMapFor(item, KindOf(item)) != null; + private sealed record RoleAuthoring( EquippableItemData Item, string Kind, List<(string Behavior, string Field, string Role)> StatsAssigned, List RolesDeclared, string SkipReason); @@ -1725,10 +1730,13 @@ public static class Program // roles their kind's map names, and every seller authors a ProductRole per role. Each mapped stat becomes a range // of value x (0.8, 1.2) that rises with quality for a benefit and falls for a cost, read through one Quality term // naming its role (exponent: the mode of the kind's existing designs on that field, 1 when there is none; an - // existing Quality term keeps its exponent). Zero stats stay flat. Small Drive joined the set because it declares - // roles that no non-flat stat reads. Idempotent: a design with no flat mapped stat is left alone, a product with a - // role is left alone. Dry run unless passed "apply". - private static readonly string[] BackfillDesigns = { "Autocannon", "LRMM72", "SRMM72", "Mine Launcher", "Large Drive", "Small Drive" }; + // existing Quality term keeps its exponent). Zero stats stay flat. A mapped stat that already varies keeps its range + // and only has its unnamed Quality term pointed at the role. Small Drive joined the set because it declares roles that + // no non-flat stat reads; DeathCluster, Flak Gun, GT 3K, plight and the two MoveOnPros because each had a non-flat stat + // whose Quality term named no role (CB-R1: each non-flat stat reads exactly one declared role). Idempotent: a design with + // no flat mapped stat and no unnamed one is left alone, a product with a role is left alone. Dry run unless passed "apply". + private static readonly string[] BackfillDesigns = { "Autocannon", "LRMM72", "SRMM72", "Mine Launcher", "Large Drive", "Small Drive", + "DeathCluster", "Flak Gun", "GT 3K", "plight", "MoveOnPro", "MoveOnPro Station Reactor" }; private static readonly HashSet CostStats = new HashSet(StringComparer.Ordinal) { @@ -1736,17 +1744,14 @@ public static class Program "SensorImpact", "DirectionImpact", "Decay", }; - private static Dictionary BackfillMapFor(EquippableItemData item) => - item.Behaviors.OfType().Any() ? MineLayerRoles : RoleMapFor(item, KindOf(item)); - private static IEnumerable StatsOf(EquippableItemData item) => (item.Behaviors ?? new List()).Where(b => b != null) .SelectMany(b => b.GetType().GetFields().Where(f => f.FieldType == typeof(PerformanceStat)).Select(f => f.GetValue(b) as PerformanceStat)) .Where(s => s?.Terms != null); - private static int RolesBackfill(bool apply) + public static int RolesBackfill(bool apply, string root = null) { - var db = AetherDb.Open(catalogWritable: apply); + var db = AetherDb.Open(catalogWritable: apply, root: root); var items = db.Cache.GetAll().ToArray(); var products = db.Cache.GetAll().ToArray(); var targets = BackfillDesigns.Select(name => items.Where(i => i.Name == name).ToArray()) @@ -1768,22 +1773,23 @@ public static class Program foreach (var design in targets) { var kind = KindOf(design); - var map = BackfillMapFor(design) ?? throw new InvalidOperationException($"\"{design.Name}\" has no role map."); + var map = RoleMapFor(design, kind) ?? throw new InvalidOperationException($"\"{design.Name}\" has no role map."); var lines = new List(); foreach (var behavior in design.Behaviors.Where(b => b != null)) foreach (var field in behavior.GetType().GetFields().Where(f => f.FieldType == typeof(PerformanceStat))) { if (!map.TryGetValue(field.Name, out var role) || !(field.GetValue(behavior) is PerformanceStat stat)) continue; - if (stat.Min != stat.Max || stat.Min == 0f || float.IsInfinity(stat.Min)) continue; - var value = stat.Min; - (stat.Min, stat.Max) = CostStats.Contains(field.Name) ? (value * 1.2f, value * .8f) : (value * .8f, value * 1.2f); stat.Terms ??= new List(); var quality = stat.Terms.FirstOrDefault(t => t.Source == StatSource.Quality); - if (quality == null) + var value = stat.Min; + if (stat.Min == stat.Max) { - quality = new StatTerm { Source = StatSource.Quality, Exponent = ExponentFor(kind, field.Name) }; - stat.Terms.Add(quality); + if (stat.Min == 0f || float.IsInfinity(stat.Min)) continue; + (stat.Min, stat.Max) = CostStats.Contains(field.Name) ? (value * 1.2f, value * .8f) : (value * .8f, value * 1.2f); + quality ??= new StatTerm { Source = StatSource.Quality, Exponent = ExponentFor(kind, field.Name) }; + if (!stat.Terms.Contains(quality)) stat.Terms.Add(quality); } + else if (quality == null || !string.IsNullOrEmpty(quality.Role)) continue; // already named, or not quality-driven quality.Role = role; lines.Add($" {behavior.GetType().Name}.{field.Name,-16} {value:0.####} -> {stat.Min:0.####}..{stat.Max:0.####} {role} ^{quality.Exponent:0.##}"); } From 2e1c2c439b898fb8167121d59f067091a4b32867 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 23:46:28 +0200 Subject: [PATCH 056/120] Tests: CB-R1 enforced whole, backfill replay on a scratch catalog, envelope and operability read the catalog ShippedCatalogRolesAreLive checks every design of every kind: each non-flat stat reads one declared role, declared roles are read, sellers author each once, role-mapped designs are not all flat (the tool answers which, via Program.HasRoleMap). RolesBackfillIsAuthored pins ratios and roles, with the Autocannon Penetration, Mine Range and LRMM72 LockAngle asserts. RolesBackfillCommandTests resets the backfilled designs on a scratch catalog, applies, and expects the shipped catalog back, then replays as a no-op. EveryShippedProductHasAMaker. TheEnvelopeComesFromTheGear bounds each drive by the catalog's Large Drive Thrust range; ShippedWeaponOperabilityTests reads the catalog's DefaultEntitySettings. Co-Authored-By: Claude Sonnet 5.5 --- .../Aetheria.Shared.Tests/CatalogRoleTests.cs | 105 ++++++++----- .../Aetheria.Shared.Tests/EvasionTermTests.cs | 6 +- .../RolesBackfillCommandTests.cs | 146 ++++++++++++++++++ .../ShippedWeaponOperabilityTests.cs | 4 +- 4 files changed, 219 insertions(+), 42 deletions(-) create mode 100644 tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs diff --git a/tests/Aetheria.Shared.Tests/CatalogRoleTests.cs b/tests/Aetheria.Shared.Tests/CatalogRoleTests.cs index 665602de3..bb83a23cd 100644 --- a/tests/Aetheria.Shared.Tests/CatalogRoleTests.cs +++ b/tests/Aetheria.Shared.Tests/CatalogRoleTests.cs @@ -4,44 +4,47 @@ using System.Linq; using Xunit; // Aetheria release, cut roles-backfill, rule CB-R1: a crafted design declares roles iff a stat varies with quality, -// every declared role is read by a non-flat stat, and every seller authors one ProductRole per role. Both tests read +// every declared role is read by a non-flat stat, and every seller authors one ProductRole per role. These tests read // the shipped catalog through the real reader. public sealed class CatalogRoleTests { - // Every weapon and every gear of a kind that carries per-part quality stats (the kinds roles-migrate keeps a role - // map for). Cargo bays, docking bays, control modules, tools and hulls carry no quality-reading stat. - private static readonly HardpointType[] QualityGearKinds = - { HardpointType.Radiator, HardpointType.Reactor, HardpointType.Sensors, HardpointType.Thruster }; - - private static IEnumerable StatsOf(EquippableItemData design) => + internal static IEnumerable<(string Name, PerformanceStat Stat)> StatsOf(EquippableItemData design) => design.Behaviors.Where(b => b != null) - .SelectMany(b => b.GetType().GetFields().Where(f => f.FieldType == typeof(PerformanceStat)).Select(f => (PerformanceStat)f.GetValue(b))) - .Where(s => s?.Terms != null); + .SelectMany(b => b.GetType().GetFields().Where(f => f.FieldType == typeof(PerformanceStat)) + .Select(f => ($"{b.GetType().Name}.{f.Name}", (PerformanceStat)f.GetValue(b)))) + .Where(s => s.Item2?.Terms != null); private static PerformanceStat Stat(EquippableItemData design, string field) => Assert.Single(design.Behaviors.SelectMany(b => b.GetType().GetFields().Where(f => f.Name == field && f.FieldType == typeof(PerformanceStat)).Select(f => (PerformanceStat)f.GetValue(b)))); - private static string[] NamedQualityRoles(PerformanceStat stat) => + internal static string[] NamedQualityRoles(PerformanceStat stat) => stat.Terms.Where(t => t.Source == StatSource.Quality && !string.IsNullOrEmpty(t.Role)).Select(t => t.Role).ToArray(); + // The whole of CB-R1 over every design in the catalog: each non-flat stat reads exactly one declared role, every + // declared role is read, and every seller authors each role once. Weapons, and designs of a kind the tool keeps a role + // map for, must also carry a non-flat stat (a design with no stat at all has nothing for quality to vary). [Fact] public void ShippedCatalogRolesAreLive() { - var cache = RestoredHullsTests.OpenCatalog(); + using var cache = RestoredHullsTests.OpenCatalog(); var products = cache.GetAll().ToArray(); var broken = new List(); - foreach (var design in cache.GetAll() - .Where(i => i is WeaponItemData || (i is GearData && QualityGearKinds.Contains(i.HardpointType))) - .OrderBy(i => i.Name, StringComparer.Ordinal)) + foreach (var design in cache.GetAll().OrderBy(i => i.Name, StringComparer.Ordinal)) { var stats = StatsOf(design).ToArray(); - if (stats.Length == 0) continue; // Tractor Beam: a thruster kind with no stat at all, so nothing for quality to vary - var varying = stats.Where(s => s.Min != s.Max).ToArray(); - var read = varying.SelectMany(NamedQualityRoles).Distinct().OrderBy(r => r, StringComparer.Ordinal).ToArray(); + var varying = stats.Where(s => s.Stat.Min != s.Stat.Max).ToArray(); var declared = (design.Roles ?? new List()).Select(r => r.Name).OrderBy(r => r, StringComparer.Ordinal).ToArray(); - if (varying.Length == 0) broken.Add($"{design.Name}: every stat is flat"); + foreach (var (name, stat) in varying) + { + var named = NamedQualityRoles(stat); + if (named.Length != 1 || !declared.Contains(named[0])) + broken.Add($"{design.Name}: non-flat {name} reads [{string.Join(", ", named)}], not one of its declared roles [{string.Join(", ", declared)}]"); + } + var read = varying.SelectMany(s => NamedQualityRoles(s.Stat)).Distinct().OrderBy(r => r, StringComparer.Ordinal).ToArray(); if (!read.SequenceEqual(declared)) broken.Add($"{design.Name}: declares [{string.Join(", ", declared)}] but non-flat stats read [{string.Join(", ", read)}]"); + if (varying.Length == 0 && stats.Length > 0 && (design is WeaponItemData || Program.HasRoleMap(design))) + broken.Add($"{design.Name}: every stat is flat"); foreach (var product in products.Where(p => p.Design.Key.Equals(cache.RefOf(design).Key))) { var authored = (product.Roles ?? new List()).Select(r => r.Role).OrderBy(r => r, StringComparer.Ordinal).ToArray(); @@ -49,45 +52,62 @@ public sealed class CatalogRoleTests broken.Add($"{design.Name}: product \"{product.Name}\" authors [{string.Join(", ", authored)}], not each of [{string.Join(", ", declared)}] once"); } } - Assert.True(broken.Count == 0, string.Join("\n", broken)); + Assert.True(broken.Count == 0, string.Join(" +", broken)); } - // value, then the stat's expected Min and Max: a benefit rises with quality (0.8x to 1.2x), a cost falls (1.2x to 0.8x). - private static void AssertRange(EquippableItemData design, string field, float value, bool cost, string role) + // A product with no maker would title as its design and take no maker profile for its roles. + [Fact] + public void EveryShippedProductHasAMaker() + { + using var cache = RestoredHullsTests.OpenCatalog(); + var makerless = cache.GetAll().Where(p => cache.Get(p.Manufacturer) == null).Select(p => p.Name).OrderBy(n => n, StringComparer.Ordinal).ToArray(); + Assert.True(makerless.Length == 0, "products with no maker: " + string.Join(", ", makerless)); + } + + // A benefit rises with quality across 0.8x to 1.2x of its authored value (Max/Min 1.5), a cost falls across 1.2x to 0.8x + // (Max/Min 2/3). The ratio and the role are pinned, not the value, so an operator retune of a design stays green. + private static void AssertRange(EquippableItemData design, string field, bool cost, string role) { var stat = Stat(design, field); - Assert.Equal(value * (cost ? 1.2f : .8f), stat.Min, 3); - Assert.Equal(value * (cost ? .8f : 1.2f), stat.Max, 3); - Assert.Contains(role, NamedQualityRoles(stat)); + Assert.Equal(cost ? 2f / 3f : 1.5f, stat.Max / stat.Min, 3); + Assert.Equal(new[] { role }, NamedQualityRoles(stat)); } + // Stats that already varied before the backfill keep their range; only their unnamed Quality term gains the role. + private static void AssertNamed(EquippableItemData design, string field, string role) => + Assert.Equal(new[] { role }, NamedQualityRoles(Stat(design, field))); + [Fact] public void RolesBackfillIsAuthored() { - var cache = RestoredHullsTests.OpenCatalog(); + using var cache = RestoredHullsTests.OpenCatalog(); EquippableItemData Design(string name) => Assert.Single(cache.GetAll(), i => i.Name == name); string[] Roles(EquippableItemData d) => d.Roles.Select(r => r.Name).OrderBy(r => r, StringComparer.Ordinal).ToArray(); var autocannon = Design("Autocannon"); Assert.Equal(new[] { "barrel", "feed mechanism" }, Roles(autocannon)); - AssertRange(autocannon, "Damage", 3f, false, "barrel"); - AssertRange(autocannon, "Range", 1000f, false, "barrel"); + AssertRange(autocannon, "Damage", false, "barrel"); + AssertRange(autocannon, "Range", false, "barrel"); + AssertRange(autocannon, "Penetration", false, "barrel"); var lrm = Design("LRMM72"); Assert.Equal(new[] { "guidance system", "thruster", "warhead" }, Roles(lrm)); - AssertRange(lrm, "Damage", 256f, false, "warhead"); - AssertRange(lrm, "Cooldown", 10f, true, "guidance system"); + AssertRange(lrm, "Damage", false, "warhead"); + AssertRange(lrm, "Cooldown", true, "guidance system"); + AssertRange(lrm, "LockAngle", false, "guidance system"); var srm = Design("SRMM72"); Assert.Equal(new[] { "guidance system", "thruster", "warhead" }, Roles(srm)); - AssertRange(srm, "Damage", 128f, false, "warhead"); - AssertRange(srm, "Cooldown", 5f, true, "guidance system"); + AssertRange(srm, "Damage", false, "warhead"); + AssertRange(srm, "Cooldown", true, "guidance system"); var mine = Design("Mine Launcher"); Assert.Equal(new[] { "dispenser", "warhead" }, Roles(mine)); - AssertRange(mine, "Damage", 50f, false, "warhead"); - AssertRange(mine, "Cooldown", 2f, true, "dispenser"); - AssertRange(mine, "Spread", 5f, true, "dispenser"); + AssertRange(mine, "Damage", false, "warhead"); + AssertRange(mine, "Range", false, "dispenser"); + AssertRange(mine, "Cooldown", true, "dispenser"); + AssertRange(mine, "Spread", true, "dispenser"); var layer = Assert.Single(mine.Behaviors.OfType()); Assert.Equal(2f, layer.ArmingDelay); Assert.Equal(2f, layer.FuseDelay); @@ -95,12 +115,19 @@ public sealed class CatalogRoleTests var drive = Design("Large Drive"); Assert.Equal(new[] { "injector", "nozzle" }, Roles(drive)); - AssertRange(drive, "Thrust", 250000f, false, "nozzle"); - AssertRange(drive, "Visibility", 100f, true, "nozzle"); - AssertRange(drive, "Heat", 2500f, true, "injector"); + AssertRange(drive, "Thrust", false, "nozzle"); + AssertRange(drive, "Visibility", true, "nozzle"); + AssertRange(drive, "Heat", true, "injector"); - var smallDrive = Design("Small Drive"); - AssertRange(smallDrive, "Thrust", 75000f, false, "nozzle"); + AssertRange(Design("Small Drive"), "Thrust", false, "nozzle"); + + // Designs whose non-flat stats named no role are pointed at the role their kind's map gives them. + AssertNamed(Design("DeathCluster"), "Count", "feed mechanism"); + AssertNamed(Design("Flak Gun"), "Count", "feed mechanism"); + AssertNamed(Design("GT 3K"), "MinRange", "guidance system"); + AssertNamed(Design("plight"), "DamageSpread", "focusing array"); + AssertNamed(Design("MoveOnPro"), "Modifier", "core"); + AssertNamed(Design("MoveOnPro Station Reactor"), "Modifier", "core"); // Each seller's means come from its maker's profile; the Pirates are middling and inconsistent. var profiles = new Dictionary diff --git a/tests/Aetheria.Shared.Tests/EvasionTermTests.cs b/tests/Aetheria.Shared.Tests/EvasionTermTests.cs index 708f863bb..ea6ce903d 100644 --- a/tests/Aetheria.Shared.Tests/EvasionTermTests.cs +++ b/tests/Aetheria.Shared.Tests/EvasionTermTests.cs @@ -316,7 +316,11 @@ public sealed partial class RunStartTests var envelope = longinus.Envelope; var mass = longinus.Mass; - var forward = largeDrives.Sum(t => t.Thrust) / mass; // each drive's own thrust: a Large Drive's roll moves it between 0.8x and 1.2x + // The shipped Large Drive's Thrust is a range its lot's roll moves it through, so each drive pushes within the catalog's + // range and the envelope forward is what the two push together. + var catalogThrust = _cache.GetAll().Single(gear => gear.Name == "Large Drive").Behaviors.OfType().Single().Thrust; + Assert.All(largeDrives, t => Assert.InRange(t.Thrust, catalogThrust.Min * .99f, catalogThrust.Max * 1.01f)); + var forward = largeDrives.Sum(t => t.Thrust) / mass; Assert.InRange(envelope.Forward, forward * .99f, forward * 1.01f); Assert.Equal(0f, envelope.Reverse); // One Talaria pushes each way, and each has its own lot's thrust: the sides are the two flanks' accelerations. diff --git a/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs b/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs new file mode 100644 index 000000000..923dedbb1 --- /dev/null +++ b/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using GameCult.Caching; +using GameCult.Caching.MessagePack; +using Xunit; + +// AetherDb's roles-backfill command (aetheria-release, cut roles-backfill) run on a scratch copy of the shipped catalog, +// with the designs it backfills put back to the state they shipped in before it: flat stats at the middle of their range, +// no roles, no seller quality, and the unnamed Quality terms of the stats that already varied. The shipped catalog is the +// oracle, since the command produced it. +public sealed class RolesBackfillCommandTests : IDisposable +{ + private static readonly string[] Flattened = { "Autocannon", "LRMM72", "SRMM72", "Mine Launcher", "Large Drive", "Small Drive" }; + + // Designs that shipped with non-flat stats whose Quality term named no role; the field is the stat to unname. + private static readonly (string Design, string Field)[] Unnamed = + { + ("DeathCluster", "Count"), ("Flak Gun", "Count"), ("GT 3K", "MinRange"), ("plight", "DamageSpread"), + ("MoveOnPro", "Modifier"), ("MoveOnPro Station Reactor", "Modifier"), + }; + + private readonly string _root = Path.Combine(Path.GetTempPath(), "roles-backfill-" + Guid.NewGuid().ToString("N")); + private string CatalogPath => Path.Combine(_root, "GameData", "Aetheria.cc"); + private readonly Dictionary _shipped = new Dictionary(); + + public RolesBackfillCommandTests() + { + Directory.CreateDirectory(Path.Combine(_root, "GameData")); + File.Copy(Path.Combine(AetherDb.FindRoot(), "GameData", "Aetheria.cc"), CatalogPath); + // As PiratesFactionCommandTests: edit through a registry scoped to the shipped assembly, then give the catalog the + // record AetherDb's process-wide registry demands in this test host. + var registry = CultDocumentRegistry.ForTypes(typeof(ItemData).Assembly.GetTypes() + .Where(t => t is { IsAbstract: false, IsInterface: false }) + .Where(t => t.GetCustomAttribute() != null)); + using (var cache = new CultCache(registry)) + { + cache.AddBackingStore(new SingleFileMessagePackBackingStore(CatalogPath), AetheriaStores.CatalogTypes); + var designs = cache.GetAll().ToArray(); + var products = cache.GetAll().ToArray(); + var changed = new List(); + foreach (var design in designs.Where(d => Flattened.Contains(d.Name))) + { + _shipped[design.Name] = Describe(cache, design, products); + foreach (var (_, stat) in CatalogRoleTests.StatsOf(design)) + { + if (stat.Min != stat.Max) stat.Min = stat.Max = (stat.Min + stat.Max) / 2f; + stat.Terms.RemoveAll(t => t.Source == StatSource.Quality); + } + design.Roles = new List(); + changed.Add(design); + foreach (var product in products.Where(p => p.Design.Key.Equals(cache.RefOf(design).Key))) + { + product.Roles = new List(); + changed.Add(product); + } + } + foreach (var (name, field) in Unnamed) + { + var design = designs.Single(d => d.Name == name); + _shipped[name] = Describe(cache, design, products); + foreach (var term in Stat(design, field).Terms.Where(t => t.Source == StatSource.Quality)) term.Role = null; + changed.Add(design); + } + cache.Commit(batch => + { + foreach (var document in changed) batch.Upsert(document.GetType(), document, cache.RefOf(document).Key); + }); + cache.FlushAsync().Wait(); + } + using (var cache = new CultCache()) + { + cache.AddBackingStore(new SingleFileMessagePackBackingStore(CatalogPath), AetheriaStores.CatalogTypes); + cache.Upsert(new TestCatalogGlobal { Name = "Temperament" }); + cache.FlushAsync().Wait(); + } + } + + public void Dispose() => Directory.Delete(_root, true); + + private static PerformanceStat Stat(EquippableItemData design, string field) => + Assert.Single(CatalogRoleTests.StatsOf(design), s => s.Name.EndsWith("." + field)).Stat; + + // Everything the command decides about one design: its roles, each stat's range and role, and its sellers' role quality. + private static string Describe(CultCache cache, EquippableItemData design, FactionProductData[] products) + { + static string F(float v) => float.IsInfinity(v) ? v.ToString() : ((double) v).ToString("G5"); + var stats = CatalogRoleTests.StatsOf(design).Select(s => + $"{s.Name} {F(s.Stat.Min)}..{F(s.Stat.Max)} {string.Join("+", CatalogRoleTests.NamedQualityRoles(s.Stat))}"); + var sellers = products.Where(p => p.Design.Key.Equals(cache.RefOf(design).Key)).OrderBy(p => p.Name, StringComparer.Ordinal) + .Select(p => $"{p.Name}: " + string.Join(", ", (p.Roles ?? new List()).OrderBy(r => r.Role, StringComparer.Ordinal) + .Select(r => $"{r.Role} {F(r.Mean)}/{F(r.StandardDeviation)}"))); + var roles = "roles " + string.Join(", ", (design.Roles ?? new List()).Select(r => r.Name).OrderBy(r => r, StringComparer.Ordinal)); + return string.Join("\n", new[] { roles }.Concat(stats).Concat(sellers)); + } + + private Dictionary Read() + { + var db = AetherDb.Open(root: _root); + try + { + var products = db.Cache.GetAll().ToArray(); + return db.Cache.GetAll().Where(d => _shipped.ContainsKey(d.Name)) + .ToDictionary(d => d.Name, d => Describe(db.Cache, d, products)); + } + finally { db.Cache.Dispose(); } + } + + [Fact] + public void A_dry_run_writes_nothing_and_apply_lands_what_the_catalog_ships_and_replays_as_a_no_op() + { + var before = File.ReadAllBytes(CatalogPath); + var start = Read(); + Assert.All(_shipped.Keys, name => Assert.NotEqual(_shipped[name], start[name])); + + Assert.Equal(0, Program.RolesBackfill(apply: false, root: _root)); + Assert.Equal(before, File.ReadAllBytes(CatalogPath)); + + Assert.Equal(0, Program.RolesBackfill(apply: true, root: _root)); + var landed = File.ReadAllBytes(CatalogPath); + Assert.NotEqual(before, landed); + var after = Read(); + foreach (var name in _shipped.Keys.OrderBy(n => n, StringComparer.Ordinal)) + Assert.Equal(_shipped[name], after[name]); + + // A second apply finds every stat ranged and named and every seller authored, and writes nothing. + Assert.Equal(0, Program.RolesBackfill(apply: true, root: _root)); + Assert.Equal(landed, File.ReadAllBytes(CatalogPath)); + } + + [Fact] + public void A_design_the_catalog_does_not_hold_refuses_and_lands_nothing() + { + var db = AetherDb.Open(catalogWritable: true, root: _root); + var autocannon = db.Cache.GetAll().Single(weapon => weapon.Name == "Autocannon"); + autocannon.Name = "Autocannon (renamed)"; + db.Cache.Commit(batch => batch.Upsert(typeof(WeaponItemData), autocannon, db.Cache.RefOf(autocannon).Key)); + db.Cache.Dispose(); + var before = File.ReadAllBytes(CatalogPath); + + Assert.Throws(() => Program.RolesBackfill(apply: true, root: _root)); + Assert.Equal(before, File.ReadAllBytes(CatalogPath)); + } +} diff --git a/tests/Aetheria.Shared.Tests/ShippedWeaponOperabilityTests.cs b/tests/Aetheria.Shared.Tests/ShippedWeaponOperabilityTests.cs index 537fbc3a9..8ac543451 100644 --- a/tests/Aetheria.Shared.Tests/ShippedWeaponOperabilityTests.cs +++ b/tests/Aetheria.Shared.Tests/ShippedWeaponOperabilityTests.cs @@ -2,7 +2,7 @@ using System.Linq; using Xunit; // A weapon is thermally online only while its performance exceeds the shutdown threshold (Entity.UpdatePerformance -// compares against EntitySettings.ShutdownPerformance), and wear runs at full rate below it. Hull cells +// compares against the catalog settings' DefaultEntitySettings.ShutdownPerformance, which production clones), and wear runs at full rate below it. Hull cells // start at Entity.StartingCellTemperature, so every shipped weapon must perform there, or a ship fitted with it // can never fire it. The catalog comes from the shipped GameData/Aetheria.cc and the temperature from the // constant Entity builds its cells with, so a unit slip in authoring (celsius for kelvin) fails here. @@ -15,7 +15,7 @@ public sealed class ShippedWeaponOperabilityTests var weapons = cache.GetAll().OfType().ToArray(); Assert.NotEmpty(weapons); - var threshold = new EntitySettings().ShutdownPerformance; + var threshold = RestoredHullsTests.Settings().DefaultEntitySettings.ShutdownPerformance; Assert.True(threshold > 0f); var dead = weapons.Where(weapon => weapon.Performance(Entity.StartingCellTemperature) <= threshold) From 6218f9fdc1f4db26b15a70ba0465e4071ef3a1f4 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 23:47:54 +0200 Subject: [PATCH 057/120] Roles backfill: Count and MinRange take a role but are never ranged; fix a broken string in the catalog test Co-Authored-By: Claude Sonnet 5.5 --- tools/AetherDb/Program.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/AetherDb/Program.cs b/tools/AetherDb/Program.cs index 9b87bf9b0..97e5c9151 100644 --- a/tools/AetherDb/Program.cs +++ b/tools/AetherDb/Program.cs @@ -1738,6 +1738,10 @@ public static class Program private static readonly string[] BackfillDesigns = { "Autocannon", "LRMM72", "SRMM72", "Mine Launcher", "Large Drive", "Small Drive", "DeathCluster", "Flak Gun", "GT 3K", "plight", "MoveOnPro", "MoveOnPro Station Reactor" }; + // Stats the backfill names but never ranges: a shot count is whole, and which way a launcher's MinRange should fall is + // each design's own call (GT 3K authors it as a cost already). + private static readonly HashSet RoleOnlyStats = new HashSet(StringComparer.Ordinal) { "Count", "MinRange" }; + private static readonly HashSet CostStats = new HashSet(StringComparer.Ordinal) { "Cooldown", "Spread", "Heat", "Visibility", "Energy", "EnergyUsage", "ChargeTime", "ChargeEnergy", "ChargeHeat", @@ -1784,6 +1788,7 @@ public static class Program var value = stat.Min; if (stat.Min == stat.Max) { + if (RoleOnlyStats.Contains(field.Name)) continue; // a count or a per-design bound: never ranged if (stat.Min == 0f || float.IsInfinity(stat.Min)) continue; (stat.Min, stat.Max) = CostStats.Contains(field.Name) ? (value * 1.2f, value * .8f) : (value * .8f, value * 1.2f); quality ??= new StatTerm { Source = StatSource.Quality, Exponent = ExponentFor(kind, field.Name) }; From d2f40617699997d7a26109b5410dfb5f7b3d1e7d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 23:48:23 +0200 Subject: [PATCH 058/120] Tests: fix the broken join string in the catalog roles test Co-Authored-By: Claude Sonnet 5.5 --- tests/Aetheria.Shared.Tests/CatalogRoleTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/CatalogRoleTests.cs b/tests/Aetheria.Shared.Tests/CatalogRoleTests.cs index bb83a23cd..2e606905b 100644 --- a/tests/Aetheria.Shared.Tests/CatalogRoleTests.cs +++ b/tests/Aetheria.Shared.Tests/CatalogRoleTests.cs @@ -52,8 +52,7 @@ public sealed class CatalogRoleTests broken.Add($"{design.Name}: product \"{product.Name}\" authors [{string.Join(", ", authored)}], not each of [{string.Join(", ", declared)}] once"); } } - Assert.True(broken.Count == 0, string.Join(" -", broken)); + Assert.True(broken.Count == 0, string.Join("\n", broken)); } // A product with no maker would title as its design and take no maker profile for its roles. From c5ce2b6409f9c6caddb3522762ed770e578907d8 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 23:50:14 +0200 Subject: [PATCH 059/120] Tests: the replay leaves Small Drive's sellers as they shipped Co-Authored-By: Claude Sonnet 5.5 --- tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs b/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs index 923dedbb1..8e5053291 100644 --- a/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs +++ b/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs @@ -51,7 +51,8 @@ public sealed class RolesBackfillCommandTests : IDisposable } design.Roles = new List(); changed.Add(design); - foreach (var product in products.Where(p => p.Design.Key.Equals(cache.RefOf(design).Key))) + // Small Drive shipped with its sellers' quality already authored (roles-migrate); only its stats were flat. + foreach (var product in products.Where(p => design.Name != "Small Drive" && p.Design.Key.Equals(cache.RefOf(design).Key))) { product.Roles = new List(); changed.Add(product); From 80b51d63eabd830c97f6d7eef87a0983f6d18f26 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 8 Oct 2026 23:50:35 +0200 Subject: [PATCH 060/120] Catalog: roles-backfill apply re-run (6 designs name their unnamed Quality terms) Produced by `AetherDb roles-backfill apply` on Yggdrasil over the previous catalog blob (3d531aff). Dry run: DeathCluster (Weapon/Ballistic) roles [barrel, feed mechanism] AutoWeaponData.Count 12 -> 12..15 feed mechanism ^1 Flak Gun (Weapon/Ballistic) roles [barrel, feed mechanism] AutoWeaponData.Count 6 -> 6..8 feed mechanism ^1 GT 3K (Weapon/Launcher) roles [guidance system, thruster, warhead] LauncherData.MinRange 500 -> 500..50 guidance system ^1.75 plight (Weapon/Energy) roles [focusing array, power coupling] ChargedWeaponData.DamageSpread 1 -> 1..3.3 focusing array ^1.25 MoveOnPro (Reactor) roles [core, regulator] StatModifierData.Modifier 1.1 -> 1.1..1.25 core ^1 MoveOnPro Station Reactor (Reactor) roles [core, regulator] StatModifierData.Modifier 1.1 -> 1.1..1.25 core ^1 6 design(s) and 0 product(s) to author. Dry run. Pass "apply" to land them. Co-Authored-By: Claude Sonnet 5.5 --- GameData/Aetheria.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GameData/Aetheria.cc b/GameData/Aetheria.cc index 76685d05d..82f4c4cd0 100644 --- a/GameData/Aetheria.cc +++ b/GameData/Aetheria.cc @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d531aff9e6c2d1ea65a123e987ea4e2ce79610598c7547e26e3ecb7fb95b94a -size 12713666 +oid sha256:2534838f58c3cb3ccd97d08e7ea81f94bce276b60642a6fad91ccfb64088d0f4 +size 12713731 From e97968e27177da4d872c0fa714dfc1235d4da1ab Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 00:02:46 +0200 Subject: [PATCH 061/120] Roles backfill: read each stat with as, so Stryker can mutate the command Co-Authored-By: Claude Sonnet 5.5 --- tools/AetherDb/Program.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/AetherDb/Program.cs b/tools/AetherDb/Program.cs index 97e5c9151..170a3dfe9 100644 --- a/tools/AetherDb/Program.cs +++ b/tools/AetherDb/Program.cs @@ -1782,7 +1782,8 @@ public static class Program foreach (var behavior in design.Behaviors.Where(b => b != null)) foreach (var field in behavior.GetType().GetFields().Where(f => f.FieldType == typeof(PerformanceStat))) { - if (!map.TryGetValue(field.Name, out var role) || !(field.GetValue(behavior) is PerformanceStat stat)) continue; + var stat = field.GetValue(behavior) as PerformanceStat; + if (stat == null || !map.TryGetValue(field.Name, out var role)) continue; stat.Terms ??= new List(); var quality = stat.Terms.FirstOrDefault(t => t.Source == StatSource.Quality); var value = stat.Min; From 94f4a9872cda3dcd46e2f48d2f55e78f70e88c7f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 00:11:04 +0200 Subject: [PATCH 062/120] Roles backfill: separate the null and map checks (definite assignment survives mutation) Co-Authored-By: Claude Sonnet 5.5 --- tools/AetherDb/Program.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/AetherDb/Program.cs b/tools/AetherDb/Program.cs index 170a3dfe9..22c50d456 100644 --- a/tools/AetherDb/Program.cs +++ b/tools/AetherDb/Program.cs @@ -1783,7 +1783,8 @@ public static class Program foreach (var field in behavior.GetType().GetFields().Where(f => f.FieldType == typeof(PerformanceStat))) { var stat = field.GetValue(behavior) as PerformanceStat; - if (stat == null || !map.TryGetValue(field.Name, out var role)) continue; + if (stat == null) continue; + if (!map.TryGetValue(field.Name, out var role)) continue; stat.Terms ??= new List(); var quality = stat.Terms.FirstOrDefault(t => t.Source == StatSource.Quality); var value = stat.Min; From 2843358430e7e543bdfdee45155ced73319d3d3b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 00:24:58 +0200 Subject: [PATCH 063/120] Tests: a names-only replay, so apply still lands designs when no seller changes Co-Authored-By: Claude Sonnet 5.5 --- .../RolesBackfillCommandTests.cs | 73 ++++++++++++++----- 1 file changed, 53 insertions(+), 20 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs b/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs index 8e5053291..c7dd752e4 100644 --- a/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs +++ b/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs @@ -30,20 +30,38 @@ public sealed class RolesBackfillCommandTests : IDisposable { Directory.CreateDirectory(Path.Combine(_root, "GameData")); File.Copy(Path.Combine(AetherDb.FindRoot(), "GameData", "Aetheria.cc"), CatalogPath); - // As PiratesFactionCommandTests: edit through a registry scoped to the shipped assembly, then give the catalog the - // record AetherDb's process-wide registry demands in this test host. + // The catalog needs the record AetherDb's process-wide registry demands in this test host (as PiratesFactionCommandTests). + using (var cache = new CultCache()) + { + cache.AddBackingStore(new SingleFileMessagePackBackingStore(CatalogPath), AetheriaStores.CatalogTypes); + cache.Upsert(new TestCatalogGlobal { Name = "Temperament" }); + cache.FlushAsync().Wait(); + } + var db = AetherDb.Open(root: _root); + try + { + var products = db.Cache.GetAll().ToArray(); + foreach (var design in db.Cache.GetAll().Where(d => Flattened.Contains(d.Name) || Unnamed.Any(u => u.Design == d.Name))) + _shipped[design.Name] = Describe(db.Cache, design, products); + } + finally { db.Cache.Dispose(); } + } + + // Puts the named designs back to the state they shipped in before the backfill, through a registry scoped to the shipped + // assembly (as PiratesFactionCommandTests strips its faction). + private void Reset(bool flattened, bool unnamed) + { var registry = CultDocumentRegistry.ForTypes(typeof(ItemData).Assembly.GetTypes() .Where(t => t is { IsAbstract: false, IsInterface: false }) .Where(t => t.GetCustomAttribute() != null)); - using (var cache = new CultCache(registry)) - { - cache.AddBackingStore(new SingleFileMessagePackBackingStore(CatalogPath), AetheriaStores.CatalogTypes); - var designs = cache.GetAll().ToArray(); - var products = cache.GetAll().ToArray(); - var changed = new List(); + using var cache = new CultCache(registry); + cache.AddBackingStore(new SingleFileMessagePackBackingStore(CatalogPath), AetheriaStores.CatalogTypes); + var designs = cache.GetAll().ToArray(); + var products = cache.GetAll().ToArray(); + var changed = new List(); + if (flattened) foreach (var design in designs.Where(d => Flattened.Contains(d.Name))) { - _shipped[design.Name] = Describe(cache, design, products); foreach (var (_, stat) in CatalogRoleTests.StatsOf(design)) { if (stat.Min != stat.Max) stat.Min = stat.Max = (stat.Min + stat.Max) / 2f; @@ -58,24 +76,23 @@ public sealed class RolesBackfillCommandTests : IDisposable changed.Add(product); } } + if (unnamed) foreach (var (name, field) in Unnamed) { var design = designs.Single(d => d.Name == name); - _shipped[name] = Describe(cache, design, products); foreach (var term in Stat(design, field).Terms.Where(t => t.Source == StatSource.Quality)) term.Role = null; changed.Add(design); } - cache.Commit(batch => - { - foreach (var document in changed) batch.Upsert(document.GetType(), document, cache.RefOf(document).Key); - }); - cache.FlushAsync().Wait(); - } - using (var cache = new CultCache()) + cache.Commit(batch => { - cache.AddBackingStore(new SingleFileMessagePackBackingStore(CatalogPath), AetheriaStores.CatalogTypes); - cache.Upsert(new TestCatalogGlobal { Name = "Temperament" }); - cache.FlushAsync().Wait(); + foreach (var document in changed) batch.Upsert(document.GetType(), document, cache.RefOf(document).Key); + }); + cache.FlushAsync().Wait(); + using (var plain = new CultCache()) + { + plain.AddBackingStore(new SingleFileMessagePackBackingStore(CatalogPath), AetheriaStores.CatalogTypes); + plain.Upsert(new TestCatalogGlobal { Name = "Temperament" }); + plain.FlushAsync().Wait(); } } @@ -112,6 +129,7 @@ public sealed class RolesBackfillCommandTests : IDisposable [Fact] public void A_dry_run_writes_nothing_and_apply_lands_what_the_catalog_ships_and_replays_as_a_no_op() { + Reset(flattened: true, unnamed: true); var before = File.ReadAllBytes(CatalogPath); var start = Read(); Assert.All(_shipped.Keys, name => Assert.NotEqual(_shipped[name], start[name])); @@ -131,6 +149,21 @@ public sealed class RolesBackfillCommandTests : IDisposable Assert.Equal(landed, File.ReadAllBytes(CatalogPath)); } + // Only unnamed Quality terms to name: no seller and no range changes, and apply must still land the designs. + [Fact] + public void Naming_an_unnamed_term_alone_lands_without_touching_ranges_or_sellers() + { + Reset(flattened: false, unnamed: true); + var before = File.ReadAllBytes(CatalogPath); + var start = Read(); + Assert.All(Unnamed.Select(u => u.Design), name => Assert.NotEqual(_shipped[name], start[name])); + + Assert.Equal(0, Program.RolesBackfill(apply: true, root: _root)); + Assert.NotEqual(before, File.ReadAllBytes(CatalogPath)); + var after = Read(); + foreach (var (name, _) in Unnamed) Assert.Equal(_shipped[name], after[name]); + } + [Fact] public void A_design_the_catalog_does_not_hold_refuses_and_lands_nothing() { From d7ee26d3895270bda5c0fa4e2286e673990225a8 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 00:36:45 +0200 Subject: [PATCH 064/120] Tests: the replay reads the shipped designs and resets through the scoped registry before the catalog gains the sibling record Co-Authored-By: Claude Sonnet 5.5 --- .../RolesBackfillCommandTests.cs | 41 ++++++++----------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs b/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs index c7dd752e4..2bff3143e 100644 --- a/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs +++ b/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs @@ -30,31 +30,23 @@ public sealed class RolesBackfillCommandTests : IDisposable { Directory.CreateDirectory(Path.Combine(_root, "GameData")); File.Copy(Path.Combine(AetherDb.FindRoot(), "GameData", "Aetheria.cc"), CatalogPath); - // The catalog needs the record AetherDb's process-wide registry demands in this test host (as PiratesFactionCommandTests). - using (var cache = new CultCache()) - { - cache.AddBackingStore(new SingleFileMessagePackBackingStore(CatalogPath), AetheriaStores.CatalogTypes); - cache.Upsert(new TestCatalogGlobal { Name = "Temperament" }); - cache.FlushAsync().Wait(); - } - var db = AetherDb.Open(root: _root); - try - { - var products = db.Cache.GetAll().ToArray(); - foreach (var design in db.Cache.GetAll().Where(d => Flattened.Contains(d.Name) || Unnamed.Any(u => u.Design == d.Name))) - _shipped[design.Name] = Describe(db.Cache, design, products); - } - finally { db.Cache.Dispose(); } + using var cache = new CultCache(ScopedRegistry()); + cache.AddBackingStore(new SingleFileMessagePackBackingStore(CatalogPath), AetheriaStores.CatalogTypes); + var products = cache.GetAll().ToArray(); + foreach (var design in cache.GetAll().Where(d => Flattened.Contains(d.Name) || Unnamed.Any(u => u.Design == d.Name))) + _shipped[design.Name] = Describe(cache, design, products); } + // A registry scoped to the shipped assembly, so this test assembly's own documents never reach the real catalog. + private static CultDocumentRegistry ScopedRegistry() => CultDocumentRegistry.ForTypes(typeof(ItemData).Assembly.GetTypes() + .Where(t => t is { IsAbstract: false, IsInterface: false }) + .Where(t => t.GetCustomAttribute() != null)); + // Puts the named designs back to the state they shipped in before the backfill, through a registry scoped to the shipped // assembly (as PiratesFactionCommandTests strips its faction). private void Reset(bool flattened, bool unnamed) { - var registry = CultDocumentRegistry.ForTypes(typeof(ItemData).Assembly.GetTypes() - .Where(t => t is { IsAbstract: false, IsInterface: false }) - .Where(t => t.GetCustomAttribute() != null)); - using var cache = new CultCache(registry); + using var cache = new CultCache(ScopedRegistry()); cache.AddBackingStore(new SingleFileMessagePackBackingStore(CatalogPath), AetheriaStores.CatalogTypes); var designs = cache.GetAll().ToArray(); var products = cache.GetAll().ToArray(); @@ -83,11 +75,13 @@ public sealed class RolesBackfillCommandTests : IDisposable foreach (var term in Stat(design, field).Terms.Where(t => t.Source == StatSource.Quality)) term.Role = null; changed.Add(design); } - cache.Commit(batch => - { - foreach (var document in changed) batch.Upsert(document.GetType(), document, cache.RefOf(document).Key); - }); + if (changed.Count > 0) + cache.Commit(batch => + { + foreach (var document in changed) batch.Upsert(document.GetType(), document, cache.RefOf(document).Key); + }); cache.FlushAsync().Wait(); + // Then the record AetherDb's process-wide registry demands in this test host (as PiratesFactionCommandTests). using (var plain = new CultCache()) { plain.AddBackingStore(new SingleFileMessagePackBackingStore(CatalogPath), AetheriaStores.CatalogTypes); @@ -167,6 +161,7 @@ public sealed class RolesBackfillCommandTests : IDisposable [Fact] public void A_design_the_catalog_does_not_hold_refuses_and_lands_nothing() { + Reset(flattened: false, unnamed: false); var db = AetherDb.Open(catalogWritable: true, root: _root); var autocannon = db.Cache.GetAll().Single(weapon => weapon.Name == "Autocannon"); autocannon.Name = "Autocannon (renamed)"; From 9744d3969f1b33911a394503a6e2f3218398435c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 01:59:25 +0200 Subject: [PATCH 065/120] Roles backfill: the exponent precedent is read before any target changes; the replay sees exponents and a one-of-two-roles seller ExponentFor drew its pool from items.Except(targets), so growing the targets from six to twelve changed the exponents a rerun on master's catalog writes. The pool is now snapshotted once, from every design that declares roles and names a Quality term, before the loop mutates anything. Describe prints each named term's exponent, one test pins Autocannon's and LRMM72's exponents against the shipped blob, and one clears a single role of a two-role seller and expects only that role re-authored. Co-Authored-By: Claude Sonnet 5.5 --- .../RolesBackfillCommandTests.cs | 55 ++++++++++++++++++- tools/AetherDb/Program.cs | 18 ++++-- 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs b/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs index 2bff3143e..40fbe130e 100644 --- a/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs +++ b/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs @@ -95,12 +95,12 @@ public sealed class RolesBackfillCommandTests : IDisposable private static PerformanceStat Stat(EquippableItemData design, string field) => Assert.Single(CatalogRoleTests.StatsOf(design), s => s.Name.EndsWith("." + field)).Stat; - // Everything the command decides about one design: its roles, each stat's range and role, and its sellers' role quality. + // Everything the command decides about one design: its roles, each stat's range, role and exponent, and its sellers' role quality. private static string Describe(CultCache cache, EquippableItemData design, FactionProductData[] products) { static string F(float v) => float.IsInfinity(v) ? v.ToString() : ((double) v).ToString("G5"); var stats = CatalogRoleTests.StatsOf(design).Select(s => - $"{s.Name} {F(s.Stat.Min)}..{F(s.Stat.Max)} {string.Join("+", CatalogRoleTests.NamedQualityRoles(s.Stat))}"); + $"{s.Name} {F(s.Stat.Min)}..{F(s.Stat.Max)} {string.Join("+", s.Stat.Terms.Where(t => t.Source == StatSource.Quality && !string.IsNullOrEmpty(t.Role)).Select(t => $"{t.Role}^{F(t.Exponent)}"))}"); var sellers = products.Where(p => p.Design.Key.Equals(cache.RefOf(design).Key)).OrderBy(p => p.Name, StringComparer.Ordinal) .Select(p => $"{p.Name}: " + string.Join(", ", (p.Roles ?? new List()).OrderBy(r => r.Role, StringComparer.Ordinal) .Select(r => $"{r.Role} {F(r.Mean)}/{F(r.StandardDeviation)}"))); @@ -158,6 +158,57 @@ public sealed class RolesBackfillCommandTests : IDisposable foreach (var (name, _) in Unnamed) Assert.Equal(_shipped[name], after[name]); } + // The exponents a first apply authors do not depend on which designs it targets: applied to the base-shaped catalog, the + // command reproduces the shipped exponents of Autocannon and LRMM72, which differ across stats (the mode is not one value). + [Fact] + public void Apply_on_the_base_shaped_catalog_reproduces_the_shipped_exponents() + { + static Dictionary Exponents(EquippableItemData d) => CatalogRoleTests.StatsOf(d) + .SelectMany(s => s.Stat.Terms.Where(t => t.Source == StatSource.Quality && !string.IsNullOrEmpty(t.Role)).Select(t => (s.Name, t.Exponent))) + .ToDictionary(x => x.Name, x => x.Exponent); + var designs = new[] { "Autocannon", "LRMM72" }; + Dictionary> Read(string root) + { + var db = AetherDb.Open(root: root); + try { return db.Cache.GetAll().Where(d => designs.Contains(d.Name)).ToDictionary(d => d.Name, Exponents); } + finally { db.Cache.Dispose(); } + } + var shipped = Read(AetherDb.FindRoot()); + Assert.All(designs, name => Assert.True(shipped[name].Values.Distinct().Count() > 1, name + " exponents are one value; the test would not tell a mode from a default")); + + Reset(flattened: true, unnamed: true); + Assert.Equal(0, Program.RolesBackfill(apply: true, root: _root)); + var after = Read(_root); + foreach (var name in designs) Assert.Equal(shipped[name].OrderBy(x => x.Key, StringComparer.Ordinal), after[name].OrderBy(x => x.Key, StringComparer.Ordinal)); + } + + // A seller that authors one of a design's two roles gets only the other one added, and keeps the one it authored. + [Fact] + public void A_seller_with_one_of_two_roles_authored_gains_only_the_missing_role() + { + Reset(flattened: false, unnamed: false); + var db = AetherDb.Open(catalogWritable: true, root: _root); + var autocannon = db.Cache.GetAll().Single(weapon => weapon.Name == "Autocannon"); + var seller = db.Cache.GetAll().OrderBy(p => p.Name, StringComparer.Ordinal).First(p => p.Design.Key.Equals(db.Cache.RefOf(autocannon).Key) && p.Roles.Count == 2); + var kept = seller.Roles.OrderBy(r => r.Role, StringComparer.Ordinal).First(); + kept.Mean = .91f; + seller.Roles = new List { kept }; + var sellerName = seller.Name; + var keptRole = kept.Role; + db.Cache.Commit(batch => batch.Upsert(typeof(FactionProductData), seller, db.Cache.RefOf(seller).Key)); + db.Cache.Dispose(); + + Assert.Equal(0, Program.RolesBackfill(apply: true, root: _root)); + db = AetherDb.Open(root: _root); + try + { + var after = db.Cache.GetAll().Single(p => p.Name == sellerName); + Assert.Equal(new[] { "barrel", "feed mechanism" }, after.Roles.Select(r => r.Role).OrderBy(r => r, StringComparer.Ordinal).ToArray()); + Assert.Equal(.91f, after.Roles.Single(r => r.Role == keptRole).Mean, 3); + } + finally { db.Cache.Dispose(); } + } + [Fact] public void A_design_the_catalog_does_not_hold_refuses_and_lands_nothing() { diff --git a/tools/AetherDb/Program.cs b/tools/AetherDb/Program.cs index 22c50d456..953f08e0f 100644 --- a/tools/AetherDb/Program.cs +++ b/tools/AetherDb/Program.cs @@ -1762,15 +1762,21 @@ public static class Program .Select((found, i) => found.Length == 1 ? found[0] : throw new InvalidOperationException( $"Expected exactly one design named \"{BackfillDesigns[i]}\", found {found.Length}.")).ToArray(); + // The precedent is read once, before any target changes: the named Quality exponents of every design that declares + // roles, so a design the command names later never leaves the pool another target draws from, and a design that + // starts with no named term contributes nothing. + var precedent = items.Where(i => i.Roles != null && i.Roles.Count > 0) + .SelectMany(i => (i.Behaviors ?? new List()).Where(b => b != null) + .SelectMany(b => b.GetType().GetFields().Where(f => f.FieldType == typeof(PerformanceStat)).Select(f => (Kind: KindOf(i), Field: f.Name, Stat: f.GetValue(b) as PerformanceStat)))) + .Where(x => x.Stat?.Terms != null && x.Stat.Min != x.Stat.Max) + .SelectMany(x => x.Stat.Terms.Where(t => t.Source == StatSource.Quality && !string.IsNullOrEmpty(t.Role)).Select(t => (x.Kind, x.Field, t.Exponent))) + .ToArray(); + float ExponentFor(string kind, string field) { - var precedent = items.Except(targets).Where(i => KindOf(i) == kind && i.Roles != null && i.Roles.Count > 0) - .SelectMany(i => i.Behaviors ?? new List()).Where(b => b != null) - .SelectMany(b => b.GetType().GetFields().Where(f => f.Name == field && f.FieldType == typeof(PerformanceStat)).Select(f => f.GetValue(b) as PerformanceStat)) - .Where(s => s?.Terms != null && s.Min != s.Max) - .SelectMany(s => s.Terms.Where(t => t.Source == StatSource.Quality && !string.IsNullOrEmpty(t.Role)).Select(t => t.Exponent)) + var modes = precedent.Where(p => p.Kind == kind && p.Field == field).Select(p => p.Exponent) .GroupBy(e => e).OrderByDescending(g => g.Count()).ThenBy(g => g.Key).Select(g => g.Key).ToArray(); - return precedent.Length > 0 ? precedent[0] : 1f; + return modes.Length > 0 ? modes[0] : 1f; } var changedDesigns = new List<(EquippableItemData Document, CultRecordKey Key)>(); From 651f5cdc4fe06a2685bb6eb343a10430e7171cd7 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 01:59:26 +0200 Subject: [PATCH 066/120] Tests: CB-R1 counts every Quality term on a non-flat stat and rejects a role named on a flat one A non-flat stat must carry exactly one Quality term, named, declared, with a positive exponent; a flat stat may not name a role. The check is one function with its own synthetic-stat test. Co-Authored-By: Claude Sonnet 5.5 --- .../Aetheria.Shared.Tests/CatalogRoleTests.cs | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/CatalogRoleTests.cs b/tests/Aetheria.Shared.Tests/CatalogRoleTests.cs index 2e606905b..9978f081d 100644 --- a/tests/Aetheria.Shared.Tests/CatalogRoleTests.cs +++ b/tests/Aetheria.Shared.Tests/CatalogRoleTests.cs @@ -20,6 +20,21 @@ public sealed class CatalogRoleTests internal static string[] NamedQualityRoles(PerformanceStat stat) => stat.Terms.Where(t => t.Source == StatSource.Quality && !string.IsNullOrEmpty(t.Role)).Select(t => t.Role).ToArray(); + // CB-R1 for one stat. A non-flat stat carries exactly one Quality term, it names a declared role, and its exponent is + // positive (a role that cannot move the stat is not read). A flat stat carries no Quality term that names a role. + // Returns the fault, or null. + internal static string StatFault(PerformanceStat stat, string[] declared) + { + var quality = stat.Terms.Where(t => t.Source == StatSource.Quality).ToArray(); + if (stat.Min == stat.Max) + return quality.Any(t => !string.IsNullOrEmpty(t.Role)) ? $"is flat but a Quality term names [{string.Join(", ", NamedQualityRoles(stat))}]" : null; + if (quality.Length != 1) + return $"is non-flat with {quality.Length} Quality terms, not one"; + if (string.IsNullOrEmpty(quality[0].Role) || !declared.Contains(quality[0].Role)) + return $"is non-flat and reads [{quality[0].Role}], not one of its declared roles [{string.Join(", ", declared)}]"; + return quality[0].Exponent > 0f ? null : $"is non-flat but its role \"{quality[0].Role}\" has exponent {quality[0].Exponent}"; + } + // The whole of CB-R1 over every design in the catalog: each non-flat stat reads exactly one declared role, every // declared role is read, and every seller authors each role once. Weapons, and designs of a kind the tool keeps a role // map for, must also carry a non-flat stat (a design with no stat at all has nothing for quality to vary). @@ -34,11 +49,10 @@ public sealed class CatalogRoleTests var stats = StatsOf(design).ToArray(); var varying = stats.Where(s => s.Stat.Min != s.Stat.Max).ToArray(); var declared = (design.Roles ?? new List()).Select(r => r.Name).OrderBy(r => r, StringComparer.Ordinal).ToArray(); - foreach (var (name, stat) in varying) + foreach (var (name, stat) in stats) { - var named = NamedQualityRoles(stat); - if (named.Length != 1 || !declared.Contains(named[0])) - broken.Add($"{design.Name}: non-flat {name} reads [{string.Join(", ", named)}], not one of its declared roles [{string.Join(", ", declared)}]"); + var fault = StatFault(stat, declared); + if (fault != null) broken.Add($"{design.Name}: {name} {fault}"); } var read = varying.SelectMany(s => NamedQualityRoles(s.Stat)).Distinct().OrderBy(r => r, StringComparer.Ordinal).ToArray(); if (!read.SequenceEqual(declared)) @@ -55,6 +69,23 @@ public sealed class CatalogRoleTests Assert.True(broken.Count == 0, string.Join("\n", broken)); } + private static PerformanceStat Ranged(params StatTerm[] terms) => new PerformanceStat { Min = 1f, Max = 2f, Terms = terms.ToList() }; + private static StatTerm Quality(string role, float exponent = 1f) => new StatTerm { Source = StatSource.Quality, Role = role, Exponent = exponent }; + + // The stat check refuses what the shipped catalog does not hold but a later content cut could author. + [Fact] + public void TheStatCheckRefusesGenericQualityDeadRolesAndNamedFlatStats() + { + var declared = new[] { "barrel" }; + Assert.Null(StatFault(Ranged(Quality("barrel")), declared)); + Assert.Null(StatFault(new PerformanceStat { Min = 3f, Max = 3f, Terms = new List() }, declared)); + Assert.NotNull(StatFault(Ranged(Quality("barrel"), Quality(null)), declared)); + Assert.NotNull(StatFault(Ranged(Quality("barrel", 0f)), declared)); + Assert.NotNull(StatFault(Ranged(Quality(null)), declared)); + Assert.NotNull(StatFault(Ranged(Quality("bogus")), declared)); + Assert.NotNull(StatFault(new PerformanceStat { Min = 3f, Max = 3f, Terms = new List { Quality("bogus") } }, declared)); + } + // A product with no maker would title as its design and take no maker profile for its roles. [Fact] public void EveryShippedProductHasAMaker() From a91bd1f7cfa849c18a55a4ac35983a177bdbdf46 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 02:13:15 +0200 Subject: [PATCH 067/120] Tests: the replay resets the drives to unnamed terms, not stripped ones, as they shipped Large Drive and Small Drive shipped with unnamed Quality terms on their flat stats; the command keeps those exponents. The reset stripped them, so the new exponent check read the Thruster precedent instead. Co-Authored-By: Claude Sonnet 5.5 --- tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs b/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs index 40fbe130e..a34248df0 100644 --- a/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs +++ b/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs @@ -57,7 +57,10 @@ public sealed class RolesBackfillCommandTests : IDisposable foreach (var (_, stat) in CatalogRoleTests.StatsOf(design)) { if (stat.Min != stat.Max) stat.Min = stat.Max = (stat.Min + stat.Max) / 2f; - stat.Terms.RemoveAll(t => t.Source == StatSource.Quality); + // The drives shipped with unnamed Quality terms on their flat stats (roles-migrate); the command keeps those + // exponents and names them. The other four shipped with no term at all. + if (design.Name.EndsWith(" Drive", StringComparison.Ordinal)) foreach (var term in stat.Terms.Where(t => t.Source == StatSource.Quality)) term.Role = null; + else stat.Terms.RemoveAll(t => t.Source == StatSource.Quality); } design.Roles = new List(); changed.Add(design); From 450e371f748edbd455b9ebbbee8f609ae5dea68e Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 02:14:54 +0200 Subject: [PATCH 068/120] Tests: the exponent pin reads the shipped designs through the catalog opener AetherDb.Open refuses a catalog without the test global record the scoped copies lack. Co-Authored-By: Claude Sonnet 5.5 --- .../RolesBackfillCommandTests.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs b/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs index a34248df0..034ce7455 100644 --- a/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs +++ b/tests/Aetheria.Shared.Tests/RolesBackfillCommandTests.cs @@ -170,18 +170,17 @@ public sealed class RolesBackfillCommandTests : IDisposable .SelectMany(s => s.Stat.Terms.Where(t => t.Source == StatSource.Quality && !string.IsNullOrEmpty(t.Role)).Select(t => (s.Name, t.Exponent))) .ToDictionary(x => x.Name, x => x.Exponent); var designs = new[] { "Autocannon", "LRMM72" }; - Dictionary> Read(string root) - { - var db = AetherDb.Open(root: root); - try { return db.Cache.GetAll().Where(d => designs.Contains(d.Name)).ToDictionary(d => d.Name, Exponents); } - finally { db.Cache.Dispose(); } - } - var shipped = Read(AetherDb.FindRoot()); + Dictionary> shipped; + Dictionary> Read(EquippableItemData[] all) => all.Where(d => designs.Contains(d.Name)).ToDictionary(d => d.Name, Exponents); + using (var catalog = RestoredHullsTests.OpenCatalog()) shipped = Read(catalog.GetAll().ToArray()); Assert.All(designs, name => Assert.True(shipped[name].Values.Distinct().Count() > 1, name + " exponents are one value; the test would not tell a mode from a default")); Reset(flattened: true, unnamed: true); Assert.Equal(0, Program.RolesBackfill(apply: true, root: _root)); - var after = Read(_root); + var db = AetherDb.Open(root: _root); + Dictionary> after; + try { after = Read(db.Cache.GetAll().ToArray()); } + finally { db.Cache.Dispose(); } foreach (var name in designs) Assert.Equal(shipped[name].OrderBy(x => x.Key, StringComparer.Ordinal), after[name].OrderBy(x => x.Key, StringComparer.Ordinal)); } From 0c944a5a513f22ba932918e13aec899a79c57f8f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 03:23:55 +0200 Subject: [PATCH 069/120] Catalog: replay roles-backfill apply on master's catalog AetherDb roles-backfill apply on master's blob 32a2e401: 12 designs and 6 products authored; a second apply is a no-op. Co-Authored-By: Claude Sonnet 5.5 --- GameData/Aetheria.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GameData/Aetheria.cc b/GameData/Aetheria.cc index 42a50cad4..d43d20702 100644 --- a/GameData/Aetheria.cc +++ b/GameData/Aetheria.cc @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:32a2e401a03b00bae994bf1a26a3be58db7226c7c6c9a4e12fa2c449429d80e8 -size 12712398 +oid sha256:193a4d252f9ef3189886f2a477db82bb23df0d36d6d8255b398769a6f036152e +size 12713731 From 80d2c7f305e2830daa1bbbb8a824b07ba7dcfec3 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 03:35:13 +0200 Subject: [PATCH 070/120] Tests: TurnRateIsTheActuatorsTurn idles the drives before it checks a Direction write The backfilled Large Drives differ in quality, so a held forward thrust is a net torque and TurnRate never settled to zero. Release the movement demand with the turn demand. Co-Authored-By: Claude Sonnet 5.5 --- tests/Aetheria.Shared.Tests/SimClockTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Aetheria.Shared.Tests/SimClockTests.cs b/tests/Aetheria.Shared.Tests/SimClockTests.cs index db79647bb..ade676bfc 100644 --- a/tests/Aetheria.Shared.Tests/SimClockTests.cs +++ b/tests/Aetheria.Shared.Tests/SimClockTests.cs @@ -239,6 +239,7 @@ public sealed partial class RunStartTests // A write to Direction between steps (staging, a load, a wormhole exit) is not a turn. player.Turn = 0; + player.MovementDirection = float2(0, 0); // two backfilled drives differ in quality, so a held thrust is a net torque for (var step = 0; step < 600 && abs(player.TurnRate) > 0; step++) arena.Update(ClockFrame); player.Direction = normalize(float2(3, -4)); arena.Update(ClockFrame); From 439d488ac8eafb106adbe28f5ed6a17245dee76e Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 13:42:38 +0200 Subject: [PATCH 071/120] Map: flight control allocates thrust (body facts, authority map, rationale); locomotion-cut.md superseded for Cuts 2-3 Imagination pass, aetheria-release, session self-2026-10-09-morning. Co-Authored-By: Claude Opus 5.5 --- docs/aetheria-release-map.md | 155 +++++++++++++++++++++++++++++++++++ docs/locomotion-cut.md | 4 + 2 files changed, 159 insertions(+) diff --git a/docs/aetheria-release-map.md b/docs/aetheria-release-map.md index 1a2452e85..4be47de1c 100644 --- a/docs/aetheria-release-map.md +++ b/docs/aetheria-release-map.md @@ -6913,3 +6913,158 @@ the .NET runtime installed, which is the install the ruling removes). | Layout buffer | `scene.aetheria_layout` | filled by Load or New Ship, edited by Rasterise, Flip Nose, Resize and hardpoint ops, undoable | the author; persisted only by Save | | Grid placement | `aetheria.grid_origin` = [x, y, width, height] on the collection | written by Rasterise only, undoable | Rasterise | | Pending ship | `aetheria.pending` = [name, reference] on the collection | set by New Ship, cleared by the Save that creates the file | New Ship, then Save | + +## Flight control allocates thrust + +Pass: Imagination, session `self-2026-10-09-morning`. Body: `GameCult/Aetheria` origin/master `80d2c7f3`, +detached worktree, removed at the end. Ruling: `flight-control-allocates-thrust` (operator words in its +`operator_quote`). Prior art: `F:\Projects\aetheria-thrust-allocation-prior-art-2026-10-09.md` (Eyes; cited +as PA). This section supersedes the body facts, consumer audit and Cuts 2-3 of `docs/locomotion-cut.md` +(2026-09-22), which predate the aether drive's deletion (`2cfdc728`) and the LookDirection-to-Turn change. +That file's Cut 4 (AI heading planner) and Cut 5 (combat facing) are carried as follow-ups, not as specs. + +### Body facts + +- **TA1. The plant.** 2D translation plus kinematic yaw. `Thruster` is the only propulsor + (`Ship.cs:138-139`). Per unit throttle a thruster adds `-push * Thrust / Mass` to velocity along its mount + and turns `Direction` by `Torque * Thrust * TorqueMultiplier / Mass` rad/s (`Thruster.cs:127-130`). + `Torque` is the sine of the moment arm (`Thruster.cs:65-70`). `Axis` saturates to [0,1] + (`Thruster.cs:46-50`). Authored `TorqueFloor` 0.5, `TorqueMultiplier` 0.1 (`Settings.asset`, read by + the probe through `AuthoredSettings`). +- **TA2. The mixer and its satellites** (`Ship.cs` at `80d2c7f3`): bucket sets `:33-40`, built in + `Activate` `:97-116` from mount rotation and from `Torque` against `TorqueFloor`; aggregates `:51-60`; + `TurnTime` `:62-67` (no caller); `ItemDestroyed` pruning `:121`, `:124-134`; `RecalculateThrust` and six + `Recalculate*` `:150-227`; the mixer `:236-259` (strafe trim with an unguarded division by the trim + count at `:237`, `:246`). Readers outside `Ship.cs`: the aggregates only in + `EvasionTermTests.cs:708-709` and `:997-1056`; `TurnTime` none; `IAnalogBehavior` + (`Behaviors.cs:96-99`) only `Thruster.cs:40` and a comment at `Entity.cs:2343`. `Thruster.Manoeuvre` + (`Thruster.cs:98-114`) is the envelope's per-thruster term and the only other `TorqueFloor` reader in + source (`:109`); tests read the floor at `EvasionTermTests.cs:331-332,383-384` and + `SteeringTests.cs:304-305`. +- **TA3. The Duel ship, probed** (scratch test in `RunStartTests`, Yggdrasil job, CultMath `614fd445`, + 2026-10-09). Longinus, mass 4235, four thrusters. Per unit throttle (starboard m/s^2, forward m/s^2, + clockwise deg/s): + + | Thruster | Cell, mount | Thrust | Column | + |---|---|---|---| + | Large Drive | (1,0) Reversed | 228441 | (0, 53.94, +49.24) | + | Large Drive | (3,0) Reversed | 259158 | (0, 61.19, -55.86) | + | Talaria | (2,14) CounterClockwise | 123144 | (+29.08, 0, +166.26) | + | Talaria | (3,14) Clockwise | 92312 | (-21.80, 0, -124.63) | + + The two drives share a design and a lot quality (0.300) and still differ by 13% in live thrust, so + held forward thrust is a 6.6 deg/s turn. The two Talarias have equal torque-to-sideways ratios, so + together they cannot make a pure torque: any turn on this hull pushes it sideways. +- **TA4. Allocation probe** (same job; `player.Update` stepped alone at 1/60 s; a scratch hook replaced + the mixer with a `BoundedLeastSquares` solve; rows normalised per axis by the box extreme, ridge 1e-3; + "prio" is a yaw row weight of 1000; heading in degrees, clockwise positive; axes are the four throttles + in the table's order): + + | Case | mixer | WLS w=1 | WLS w=3 | prio | + |---|---|---|---|---| + | forward, intact, 2 s heading | -12.09 | -6.41 | -1.38 | **0.017** | + | forward, strong drive off, 2 s | 97.41 | 59.06 | 14.24 | **-0.014** (axes .92 0 0 .36) | + | forward, weak drive off, 2 s | -109.65 | -56.22 | -11.47 | **0.026** (axes 0 .90 .29 0) | + | forward, nose Talaria (2,14) off, 2 s | -12.09 | -8.83 | -2.97 | **0.012** | + | strafe right, 2 s | 0 (no strafe at all) | 181.39 | 39.29 | **0.023** (axes 0 .85 .28 0) | + | forward + Turn 1, mean deg/s | 159.9 | 94.2 | 189.0 | **214.9** (axes 1 0 1 0) | + | Turn 1 in place, mean deg/s | 166.1 (axes 0 0 1 0) | 97.8 (.57 0 .42 0) | 188.9 (1 0 .84 0) | **214.9** (1 0 1 0) | + | Turn -1 in place | -124.6 | -81.8 | -158.8 | **-179.6** | + | Turn 1, Talaria (2,14) off | 0 (stuck) | 12.4 | 36.8 | **48.8** (drive only) | + | intent zero | 0 | 0 | 0 | 0 | + + Box extremes: clockwise 215.5 deg/s, counter-clockwise 180.5 deg/s, forward 115.1 m/s^2, starboard + 29.1, port 21.8. A yaw-first solve holds heading to hundredths of a degree whatever is lost; a weighted + solve at the old map's default (w = 3) leaves a drift the ruling forbids. Solve cost: 14.2 us mean + (including per-call allocation, n = 4), at most 11 active-set iterations over 2000 random intents. +- **TA5. The Execute gate drops small balancing throttles.** `Thruster.Execute` and `PowerRequest` act only + above throttle .01 (`Thruster.cs:87,124`). Probe: weighted solve, full forward, 10 s: heading -7.49 deg + with the gate, -4.56 without; forward 0.3, 10 s: -0.213 deg with, -0.028 without. A balancing throttle + below .01 on a 166 deg/s Talaria leaves up to 1.7 deg/s unbalanced. +- **TA6. A power-gated column deadlocks.** `PowerRequest` is proportional to the throttle + (`Thruster.cs:87`) and `PowerBus.Step` grants after `Ship.Update` (`Entity.cs:1463`), so a column zeroed + for `PowerSupply <= 1e-4` gets throttle 0, requests nothing, and is never granted. The column's thrust is + therefore `EvaluateNominalPower(Thrust)` (full grant; heat, durability, quality and modifiers live, + `Entity.cs:1856`) and its liveness is `Item.Active` only. A browned-out thruster still under-delivers + after the solve (follow-up `allocator-brownout-columns`). +- **TA7. The Duel arena moves the player by itself.** With forward thrust the player's velocity swings + between about +28 and -28 m/s along the nose every ~60 ticks with no `VelocityLimit` clamp (limit 100, + drag 0.1), while intent zero leaves it at rest. Translation measured in the Duel is not thrust; tests that + measure translation use `RestoredHullsTests.BuildThrustedShip`. Cause not traced (the zone's field at + the spawn is the first suspect). +- **TA8. CultMath 0.3.0 re-rolls generated content.** Full suite at `80d2c7f3` with only + `CultMathRevision` moved to `614fd445` (tag `cultmath-unity-v0.3.0`): 1057 pass, 5 fail. + `TutorialGalaxy_unchanged` (faction homes differ), three `RestoredHullsTests` that pick zones by name + (`RestoredHullsTests.cs:624,740,807`: "EAC-7089", "EAC-2733" no longer exist) and + `ACoastingAgileShipIsEasierToHitThanTheSameShipJinking` (hits 95 jinking against 78 coasting; its target + is `stage.Generated("Djinni")`, `EvasionTermTests.cs:520`, so the target's fit follows the galaxy). The + cause is 0.3.0's breaking `snoise` kernel (its CHANGELOG), read in C# by `Settings.cs:72-73` (galaxy + cloud density), `GlobalData.cs:30` and `Environment.cs:132,144`. Shaders use the GPU Noise plugin, not + CultMath (`Background.shader:39`), so nothing on the GPU moves. Every other DemoTerminus test passes. + Between the tags CultMath also adds `phacelle` (MPL-2.0 files), `cellular`, `smin_grad`, the `*_grad` + noises and `BoundedLeastSquares`; no existing signature changes. +- **TA9. The pin guard.** `Directory.Build.targets:21` conditions `VerifyCultLibRevision` on a + backslash-spelled project identity; `docs/mining-cut-refresh.md:155-157` infers it never fires on Linux. + Yggdrasil's `~/eureka-verify/pins` holds `cultlib-45c2f40` and `cultlib-6d5e209` only; + `~/eureka-verify/repos/CultLib.git` contains `614fd445`. + +### Authority map + +- **Owner:** `ThrustAllocator` (new, one per `Ship`, stepped in `Ship.Update`) turns intent into every + thruster's throttle. +- **Inputs:** intent (`MovementDirection`, or full forward while `Entity.ThrottleLocked`; `Turn`), and one + column per thruster: its body push and turn per unit throttle at full grant, zero when the item is not + `Active`. +- **Outputs:** `Thruster.Axis` for the tick, read only by `Execute`, `PowerRequest` and presentation. +- **Derived:** the box extremes (what intent is a fraction of) are derived from the columns each tick; the + envelope becomes the same extremes (cut `envelope-from-columns`). The warm start is command state, never + truth. +- **Demoted or dead:** the bucket sets, aggregates, strafe trim, `TurnTime`, `RemoveThruster` pruning and + `IAnalogBehavior` are deleted. `TorqueFloor` is no longer an owner of anything; it dies with + `Thruster.Manoeuvre`. A thruster's capability is no longer classified by mount or by a floor; it is its + column. +- **Forbidden writers:** any `Axis` write outside `Ship.Update`; any rule that classifies a thruster as + "for" rotation or translation; any second solve or fallback mixer; any liveness set kept across ticks. +- **Shared paths:** player helm, agents, the throttle lock and tests all reach the throttles only through + intent. + +### Model page rows (changed) + +| Kind | Named by | Over time | Decides | +|---|---|---|---| +| Thruster column (runtime) | the `Thruster` behaviour | rebuilt every tick from live stats and `Item.Active` | `ThrustAllocator` reads; item state stays `EquippedItem`'s and `PowerBus`'s | +| Allocator warm start (runtime) | the ship's `ThrustAllocator` | last tick's throttles; reset when the thruster count changes | `ThrustAllocator`; never saved | +| `GameplaySettings.TorqueFloor` (authored) | field name | deleted by `envelope-from-columns`; the `Settings.asset` line is left for Unity to drop (it lands in `AuthoredSettings.Unplaced`) | nobody after the cut | + +### Rationale + +- **Yaw before translation for a hold.** The ruling forbids yaw from asymmetry, and TA4 shows a weighted + solve cannot deliver that at any weight that also lets a turn trade: holding is exact only when the yaw + row dominates. Prior art agrees for spacecraft-like plants: MechJeb weights torque 200 times translation, + and the Space Engineers allocation proposal serves rotation authority first (PA A3). Multirotors do the + opposite (PX4 and ArduPilot sacrifice yaw, PA A2) because yaw is not their stability axis. Translation + first was rejected: it reproduces the mixer's drift. What gives under saturation is translation, so a + damaged ship crabs rather than spins; the operator expected exactly that ("crab walking just became + optimal", `docs/locomotion-cut.md:25-29` at `80d2c7f3`). +- **What a full turn spends is the operator's call** (question `turn-authority`), because the operator's + 2026-09-22 words on rotation demand (`docs/locomotion-cut.md:82-87` at `80d2c7f3`) and TA4's numbers pull different + ways. +- **One solve with a large yaw weight, not two sequential solves.** The large-weight form approximates + sequential least squares (PA A1, Schofield) with one call, deterministic and allocation-free; a yaw + weight of 1000 against a ridge of 1e-4 keeps the conditioning near 1e5, inside the range the solver + documents as exact (`BoundedLeastSquares.cs` remarks). +- **The ridge** makes the optimum unique, so the warm start never changes the answer and two equal + thrusters share a demand. It costs 0.01% of full thrust. +- **No actuator interface.** One kind of actuator exists. The allocator takes columns and returns + throttles, so a vectored row (follow-up `vectored-thruster-rows`) is more columns: a row tilting within + +-theta becomes an axial variable in [0,1] and a lateral variable in [-sin theta, sin theta], and its slew + rate narrows the lateral variable's bounds each tick. Bounds stay boxes, which is what the solver takes; + the row maps its two solved variables back to a throttle and an angle. +- **The pin bump is its own cut.** The solver exists only from 0.3.0, and 0.3.0 re-rolls generated content + (TA8). Backporting the solver to a 0.2.5 was rejected: CultLib releases from main, and a branch release + would carry the old noise kernel's seam bug forward. `no-save-compatibility-before-players` covers the + re-rolled galaxy. +- **Order.** `cultmath-0-3-pin`, then `thrust-allocator-core`; `throttle-lock` lands on the mixer as + specified, then `thrust-allocator` deletes the mixer, keeps throttle-lock's one intent line as the + allocator's input and rewrites throttle-lock's two mixer-shaped tests in allocator terms. One owner of + `Ship.Update`'s thruster block at every step. diff --git a/docs/locomotion-cut.md b/docs/locomotion-cut.md index 84a143ea9..c358e1ecb 100644 --- a/docs/locomotion-cut.md +++ b/docs/locomotion-cut.md @@ -2,6 +2,10 @@ Date: 2026-09-22 +Superseded on 2026-10-09 for its body facts, consumer audit and Cuts 2-3 by `docs/aetheria-release-map.md`, +section "Flight control allocates thrust": the aether drive was deleted and `Ship.Turn` replaced `LookDirection` +as the facing command after this was written. Cuts 4 and 5 remain design input only. + Status: target and cut map, from an Imagination pass revised on 2026-09-22. Cut 1 (the Longinus and Djinni hulls, five thruster designs, and tutorial entrance station rule) landed on `codex/locomotion-cut1` and was merged into `codex/fire-control-12` at `e160bbe5` on 2026-09-26. Cuts 2 to 5 remain target work. The operator's Q1, Q2, From 97dd38dfb8344285854cc0c8644aa0cc82bf5a49 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 14:02:36 +0200 Subject: [PATCH 072/120] Pin CultMath 0.3.0 and make the pin guard fire on Linux Directory.Build.props, Packages/manifest.json and the lock move to cultmath-unity-v0.3.0 (614fd445) together. VerifyCultLibRevision matched the CultMath ProjectReference by a backslash-spelled identity and was skipped on Linux; it now matches by project file name. Does not pass tests: generated content re-rolls with the new noise kernel, fixtures follow in later commits. Co-Authored-By: Claude Opus 5.5 --- Directory.Build.props | 4 ++-- Directory.Build.targets | 2 +- Packages/manifest.json | 2 +- Packages/packages-lock.json | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index e1f4acc37..8c27ee3ec 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,8 +6,8 @@ 45c2f4006f5f7dab357426cabd3197e8d0a17f66 $(CULTMATH_ROOT) $(CultLibRoot) - - 6d5e2096563a320933b64ddfd6ded8f2e846ac91 + + 614fd445845f6251ee2986707beecea0bb211791 C:\Program Files\Git\cmd\git.exe git diff --git a/Directory.Build.targets b/Directory.Build.targets index c9c079a6c..809f01c86 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -18,7 +18,7 @@ + Condition="'$(SkipCultLibRevisionCheck)' != 'true' and '@(ProjectReference->WithMetadataValue('Filename','CultMath'))' != ''"> Date: Fri, 9 Oct 2026 14:54:42 +0200 Subject: [PATCH 073/120] Tests: TutorialSeed1 follows the galaxy CultMath 0.3.0 rolls The noise kernel changed at cultmath-unity-v0.3.0, so the fixed seed gives a different tutorial. TutorialGalaxy_unchanged keeps its claim (a fixed seed gives a fixed tutorial) against the regenerated shape. Does not pass the whole suite: the RestoredHullsTests and the evasion test follow. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/DemoTerminusTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/DemoTerminusTests.cs b/tests/Aetheria.Shared.Tests/DemoTerminusTests.cs index 1f282f48a..ae1895e75 100644 --- a/tests/Aetheria.Shared.Tests/DemoTerminusTests.cs +++ b/tests/Aetheria.Shared.Tests/DemoTerminusTests.cs @@ -220,7 +220,7 @@ public sealed partial class RunStartTests " | zones " + g.Zones.Length + " | owners " + string.Join(",", g.Zones.Select(z => z.Owner == null ? "-" : z.Owner.ShortName)); - // Measured on 228f241e, before the demo cut, at seed 1. + // Measured at seed 1 under CultMath 0.3.0 (cultmath-unity-v0.3.0); the noise kernel changed there, which re-rolled the galaxy. private const string TutorialSeed1 = - "Miss Terri’s Sugariffic Snack Company,Zhestokost,Lucent Media,Adrasteia,Aeronautics Unlimited,Finch Cybernetics | homes 47,10,17,49,35,1 | entrance 25 | zones 64 | owners Zhestokost,Finch,Lucent,Zhestokost,Finch,Adrasteia,Zhestokost,Adrasteia,Zhestokost,Zhestokost,Zhestokost,Zhestokost,Finch,Lucent,Adrasteia,Adrasteia,Zhestokost,Lucent,Finch,Finch,Lucent,Lucent,Finch,AU,Adrasteia,-,-,Adrasteia,Lucent,-,AU,Lucent,Zhestokost,Lucent,Lucent,AU,Adrasteia,AU,Lucent,AU,Finch,Finch,-,Lucent,Zhestokost,Zhestokost,Miss Terri's,Miss Terri's,Miss Terri's,Adrasteia,Finch,-,-,Miss Terri's,Lucent,AU,Miss Terri's,Zhestokost,Lucent,Lucent,Lucent,Finch,-,-"; + "Miss Terri’s Sugariffic Snack Company,Zhestokost,Lucent Media,Adrasteia,Aeronautics Unlimited,Finch Cybernetics | homes 3,44,40,19,0,30 | entrance 12 | zones 64 | owners AU,AU,Finch,Miss Terri's,Lucent,Zhestokost,-,Lucent,AU,AU,Lucent,Adrasteia,-,Finch,Zhestokost,-,Adrasteia,-,AU,Adrasteia,AU,Zhestokost,Miss Terri's,-,AU,Finch,Miss Terri's,-,Zhestokost,-,Finch,-,-,AU,-,-,-,-,Miss Terri's,AU,Lucent,-,Finch,-,Zhestokost,AU,-,Zhestokost,Adrasteia,Adrasteia,AU,Miss Terri's,Finch,AU,Finch,-,Lucent,Zhestokost,-,AU,Miss Terri's,Zhestokost,-,Lucent"; } From 0ccf02746d0909d09374230dc0d1b253c8954dc1 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 14:54:43 +0200 Subject: [PATCH 074/120] Tests: restored-hull station tests pick their zone by the property they test EAC-7089 and EAC-2733 were generated-content names that CultMath 0.3.0 re-rolled away. The no-Lagrange-candidate zone is now found by its property (planets that are all rosette members, so no genuine candidate) by scanning seeds, and the zero-natural-station zone by its isTutorial: false roll. Every assertion keeps its claim; the candidate predicate is shared with the leak test's fixture check. Co-Authored-By: Claude Opus 5.5 --- .../RestoredHullsTests.cs | 53 +++++++++++++++---- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs b/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs index 7447e8037..b1ce60093 100644 --- a/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs +++ b/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs @@ -116,6 +116,36 @@ public sealed class RestoredHullsTests return planetOrbits.Count(o => o.Parent.IsSet() && o.Parent.Key.Equals(orbit.Parent.Key) && abs(o.Distance - orbit.Distance) < .1f) > 1; } + // The zone a test needs is chosen by the property its claim is about, from the galaxy the generator rolled, because + // which zone has that property follows the noise kernel (CultMath 0.3.0 re-rolled "EAC-7089" and "EAC-2733" away). + // A planet orbit is a genuine Lagrange candidate when it is the only planet at its distance under its parent -- + // exactly what ZoneGenerator's potentialLagrangePoints filter keeps. + private static OrbitData[] ParentedPlanetOrbits(CultCache cache, ZonePack pack) => + pack.Planets.Select(p => cache.Get(cache.Get(p).Orbit)).Where(o => o.Parent.IsSet()).ToArray(); + + private static bool HasGenuineLagrangeCandidate(OrbitData[] planetOrbits) => + planetOrbits.Any(o => planetOrbits.Count(c => c.Parent.Key.Equals(o.Parent.Key) && abs(c.Distance - o.Distance) < .1f) == 1); + + // The first seed, and in its tutorial galaxy the first non-entrance zone, that has planets every one of which is a + // rosette member: no genuine Lagrange candidate, so only the entrance's widened candidate set can seat a station + // there. Such a zone is rare, so seeds are tried in order until one has it. + private static (uint seed, string zoneName) NoLagrangeCandidateZone(CultCache cache) + { + var items = new ItemManager(cache, new ProvenanceLedger(), Settings(), _ => { }); + for (var seed = 1u; seed <= 64u; seed++) + { + var galaxy = new Galaxy(TutorialGalaxySettings(), TutorialBackgroundSettings(), TutorialNameSettings(), + cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed); + foreach (var zone in galaxy.Zones.Where(z => !ReferenceEquals(z, galaxy.Entrance))) + { + var orbits = ParentedPlanetOrbits(cache, ZoneGenerator.GenerateZone(items, TutorialZoneSettings(), galaxy, zone, isTutorial: false, ambient: false)); + if (orbits.Length > 0 && !HasGenuineLagrangeCandidate(orbits)) return (seed, zone.Name); + } + } + Assert.True(false, "fixture: no tutorial galaxy at seeds 1-64 has a zone whose planets are all rosette members."); + return default; + } + // ZoneGenerator.GenerateZone writes OrbitData/BodyData (run-store types, AetheriaStores.RunTypes), so a // catalog-only cache has no home for them. A scratch run file, deleted after the seed that used it. private static CultCache OpenReadOnlyRealCatalogWithScratchRun(string catalogPath, string runPath) @@ -652,8 +682,8 @@ public sealed class RestoredHullsTests return new Galaxy(cache, game, _ => { }); } - // Cut 1 fix batch 2 (Soul's finding #2): the tutorial entrance's widened candidate set. Seed 3's real tutorial - // galaxy has a zone, "EAC-7089", whose planets are entirely one rosette with no captured satellites at all: 4 + // Cut 1 fix batch 2 (Soul's finding #2): the tutorial entrance's widened candidate set. A real tutorial + // galaxy has a zone (NoLagrangeCandidateZone finds one; under CultMath 0.2.x it was seed 3's "EAC-7089"), whose planets are entirely one rosette with no captured satellites at all: 4 // planets sharing one Distance under one Parent, and nothing else -- potentialLagrangePoints is empty for it // (verified directly against this exact fixture, not carried over from Soul's own forced-fallback probe, whose // own candidate check undercounted planet orbits by using pack.Planets.Count -- which excludes Empty rosette @@ -678,7 +708,8 @@ public sealed class RestoredHullsTests try { using var cache = OpenReadOnlyRealCatalogWithScratchRun(gameData, scratchRun); - var galaxy = BuildGalaxyWithForcedEntrance(cache, seed: 3u, "EAC-7089"); + var (seed, zoneName) = NoLagrangeCandidateZone(cache); + var galaxy = BuildGalaxyWithForcedEntrance(cache, seed, zoneName); var items = new ItemManager(cache, new ProvenanceLedger(), Settings(), _ => { }); var pack = ZoneGenerator.GenerateZone(items, TutorialZoneSettings(), galaxy, galaxy.Entrance, isTutorial: true); @@ -716,7 +747,7 @@ public sealed class RestoredHullsTests // Cut 1 fix batch 2 (Soul's finding #2, leak check): the entrance override must not leak to (a) other // tutorial zones or (b) a non-tutorial game's own entrance zone. Both are pinned against a seed-1 galaxy -- - // the widened-candidate test above uses a different fixture, seed 3's "EAC-7089" -- so this test and the one + // the widened-candidate test above uses a different fixture, NoLagrangeCandidateZone's -- so this test and the one // below it stand or fall on this one known fixture, not that one. [Fact] public void EntranceOverrideDoesNotLeakToOtherTutorialZonesOrNonTutorialGames() @@ -737,8 +768,8 @@ public sealed class RestoredHullsTests // (a) A non-entrance tutorial zone that naturally rolls zero stations keeps that roll under // isTutorial: true -- the floor must not leak past the galaxy.Entrance reference check. - var otherZone = galaxy.Zones.Single(z => z.Name == "EAC-2733"); - Assert.NotSame(galaxy.Entrance, otherZone); + // The zone is chosen by its natural roll: zero stations without the tutorial floor (isTutorial: false). + var otherZone = galaxy.Zones.First(z => !ReferenceEquals(z, galaxy.Entrance) && StationCount(z, isTutorial: false) == 0); Assert.Equal(0, StationCount(otherZone, isTutorial: true)); // (b) The entrance zone itself, generated for a non-tutorial game (isTutorial: false), keeps its @@ -792,7 +823,7 @@ public sealed class RestoredHullsTests } // Cut 1 fix batch 3 (F6, N2): the entrance-only widening must not leak to a non-entrance zone that would - // otherwise get zero stations. Seed 3's "EAC-7089" (the same no-Lagrange fixture the widened-candidate test + // otherwise get zero stations. NoLagrangeCandidateZone's zone (the same no-Lagrange fixture the widened-candidate test // above uses, this time NOT retargeted as Entrance) rolls a real, nonzero station count on its own -- forced // here by boosting one faction's presence there (HomeZone distance 0, InfluenceDistance 50, so the roll's // floor(rand * (factionPresence+1)) is overwhelmingly likely nonzero) via the same public SavedGame round @@ -848,8 +879,9 @@ public sealed class RestoredHullsTests try { using var cache = OpenReadOnlyRealCatalogWithScratchRun(gameData, scratchRun); - var galaxy = BuildGalaxyWithBoostedPresence(cache, seed: 3u, "EAC-7089"); - var zone = galaxy.Zones.Single(z => z.Name == "EAC-7089"); + var (seed, zoneName) = NoLagrangeCandidateZone(cache); + var galaxy = BuildGalaxyWithBoostedPresence(cache, seed, zoneName); + var zone = galaxy.Zones.Single(z => z.Name == zoneName); Assert.NotSame(galaxy.Entrance, zone); var items = new ItemManager(cache, new ProvenanceLedger(), Settings(), _ => { }); @@ -857,8 +889,7 @@ public sealed class RestoredHullsTests // Confirms this fixture still has no genuine (non-rosette) Lagrange candidate -- a regression here // would mean the fixture no longer probes what this test claims to probe. - var planetOrbits = pack.Planets.Select(p => cache.Get(cache.Get(p).Orbit)).Where(o => o.Parent.IsSet()).ToArray(); - var hasGenuineCandidate = planetOrbits.Any(o => planetOrbits.Count(c => c.Parent.Key.Equals(o.Parent.Key) && abs(c.Distance - o.Distance) < .1f) == 1); + var hasGenuineCandidate = HasGenuineLagrangeCandidate(ParentedPlanetOrbits(cache, pack)); Assert.False(hasGenuineCandidate, "the entrance-adjacent fixture zone now has a genuine Lagrange candidate; it no longer tests the no-Lagrange leak path."); var stationCount = pack.Entities.OfType().Count(e => (cache.Get(e.Hull.Data) as HullData)?.HullType == HullType.Station); From c724001467c1c19754ccdbf8fd5435476ef2142e Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 14:54:44 +0200 Subject: [PATCH 075/120] Tests: the evasion range's target is a written-out Djinni fit EvRangeLaunch placed stage.Generated("Djinni"), whose fit follows the galaxy; at CultMath 0.3.0 the generated target gave 95 hits jinking against 78 coasting. The target now carries an explicit Talaria/cockpit/reactor fit, so the hit comparison does not depend on generated content. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/EvasionTermTests.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/EvasionTermTests.cs b/tests/Aetheria.Shared.Tests/EvasionTermTests.cs index ea6ce903d..1d4337380 100644 --- a/tests/Aetheria.Shared.Tests/EvasionTermTests.cs +++ b/tests/Aetheria.Shared.Tests/EvasionTermTests.cs @@ -502,7 +502,7 @@ public sealed partial class RunStartTests } // Two shooters on opposite sides of the same target, 200 m away: one with a projectile gun and one with a laser - // (a beam's authored velocity is 0, a flight time of zero). The target is the generated Djinni, the catalog's one ship that strafes. + // (a beam's authored velocity is 0, a flight time of zero). The target is the fitted Djinni, the catalog's one ship that strafes. private sealed class EvRange { public Zone Arena; @@ -510,6 +510,18 @@ public sealed partial class RunStartTests public Weapon Gun, Laser, GunnerLaser; } + // The target's fit, written out: the Djinni's own hardpoints filled with the Talaria it is generated with, its + // reactor, cockpit and radiators. What the evasion tests price is this ship's strafing, not whatever the generator + // rolls for it from the galaxy. + private static ScenarioFit EvDjinni(ScenarioStage stage) => stage.Fit("Djinni", + ("Cockpit 2x2", int2(6, 3), ItemRotation.None), ("Core Power", int2(6, 11), ItemRotation.None), + ("Talaria", int2(6, 0), ItemRotation.Reversed), ("Talaria", int2(5, 1), ItemRotation.Reversed), ("Talaria", int2(7, 1), ItemRotation.Reversed), + ("Talaria", int2(4, 4), ItemRotation.CounterClockwise), ("Talaria", int2(4, 11), ItemRotation.CounterClockwise), + ("Talaria", int2(9, 4), ItemRotation.Clockwise), ("Talaria", int2(9, 11), ItemRotation.Clockwise), + ("Talaria", int2(6, 14), ItemRotation.None), + ("Iapyx", int2(1, 4), ItemRotation.Reversed), ("Iapyx", int2(3, 1), ItemRotation.Reversed), + ("Iapyx", int2(9, 1), ItemRotation.Reversed), ("Iapyx", int2(11, 4), ItemRotation.Reversed)); + private EvRange EvRangeLaunch() { Ship gunner = null, lasing = null, target = null; @@ -517,7 +529,7 @@ public sealed partial class RunStartTests { stage.Player(stage.Bare("Djinni"), float2(-50000, -50000)); gunner = stage.Place(EvLonginus(stage, "FastBlast+-", "ColdFire"), float2(0, 0), facing: float2(0, 1)) as Ship; - target = stage.Place(stage.Generated("Djinni"), float2(0, 200), facing: float2(0, 1)) as Ship; + target = stage.Place(EvDjinni(stage), float2(0, 200), facing: float2(0, 1)) as Ship; lasing = stage.Place(EvLonginus(stage, "ColdFire"), float2(0, 400), facing: float2(0, -1)) as Ship; }); var (_, arena, _, failures) = Launch(scenario, Inputs(() => GalaxySeed)); From ab396c786a3370f105d489f929f01127f26393d5 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 14:57:48 +0200 Subject: [PATCH 076/120] Tests: the evasion volleys fire enough shots to resolve the price gap With the target's fit written out the jinking price is .383 against .440 coasting, but 200 rolled shots gave 93 hits jinking against 78 coasting: the gap between the prices is smaller than the noise of two 200-shot volleys. 2000 shots put the expected gap near four standard deviations; the assertion is unchanged. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/EvasionTermTests.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/EvasionTermTests.cs b/tests/Aetheria.Shared.Tests/EvasionTermTests.cs index 1d4337380..9723cad90 100644 --- a/tests/Aetheria.Shared.Tests/EvasionTermTests.cs +++ b/tests/Aetheria.Shared.Tests/EvasionTermTests.cs @@ -584,6 +584,10 @@ public sealed partial class RunStartTests return (hits, price); } + // The two volleys' hit counts are rolls, so their gap is the price gap plus noise. At the price gap this fixture + // has (about .06 on .44) 200 shots flip the comparison one seed in eight; this many keeps it near four standard deviations. + private const int EvVolleySize = 2000; + // The evasion term is in the live price, the HUD's forecast and the roll: a ship crossing at 150 m/s that has been // jinking is harder to hit than the same ship coasting, for a projectile gun. [Fact] @@ -597,7 +601,7 @@ public sealed partial class RunStartTests EvCross(range, jinking: false); var coastingDiagnostic = FireControl.Inspect(gun, shooter, target); var coastingPrice = FireControl.HitProbability(gun, shooter, target); - var coasting = EvVolley(range, gun, shooter, 200); + var coasting = EvVolley(range, gun, shooter, EvVolleySize); Assert.Equal(0f, coastingDiagnostic.Evasion); Assert.Equal(1f, coastingDiagnostic.PEvasion); Assert.True(coastingPrice > .2f, $"fixture: a coasting target is hittable ({coastingPrice})"); @@ -605,10 +609,10 @@ public sealed partial class RunStartTests EvCross(range, jinking: true); var jinkingDiagnostic = FireControl.Inspect(gun, shooter, target); var jinkingPrice = FireControl.HitProbability(gun, shooter, target); - var jinking = EvVolley(range, gun, shooter, 200); - Console.WriteLine($"EVASION price: coasting {coastingPrice:F3} hits {coasting.hits}/200 committed {coasting.price:F3}; " + + var jinking = EvVolley(range, gun, shooter, EvVolleySize); + Console.WriteLine($"EVASION price: coasting {coastingPrice:F3} hits {coasting.hits}/{EvVolleySize} committed {coasting.price:F3}; " + $"jinking {jinkingPrice:F3} (evasion {jinkingDiagnostic.Evasion:F2} m, tracking {jinkingDiagnostic.Tracking:F1}, PEvasion {jinkingDiagnostic.PEvasion:F3}) " + - $"hits {jinking.hits}/200 committed {jinking.price:F3}"); + $"hits {jinking.hits}/{EvVolleySize} committed {jinking.price:F3}"); Assert.True(jinkingDiagnostic.Evasion > 0f); Assert.True(jinkingDiagnostic.PEvasion < 1f); From aac2cf91e449e94915aacecf8cff996b49418666 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 15:28:29 +0200 Subject: [PATCH 077/120] Map: spec refresh 2026-10-09 (scarce consumables, fog-bank presenter, multiply, tiered boosters, shipped AetherDb, player build) Body facts RF1-RF8, model rows, booster and consumable tables, and rationale for the revisions admitted under follow-up rulings-2026-10-09-spec-refresh. Sweeps the stacking and hoarding lines the 2026-10-09 rulings made stale. Co-Authored-By: Claude Opus 5.5 --- docs/aetheria-release-map.md | 188 ++++++++++++++++++++++++++++++++++- 1 file changed, 184 insertions(+), 4 deletions(-) diff --git a/docs/aetheria-release-map.md b/docs/aetheria-release-map.md index 4be47de1c..b30ea7141 100644 --- a/docs/aetheria-release-map.md +++ b/docs/aetheria-release-map.md @@ -6418,7 +6418,9 @@ After these, every cell of CB3 is filled except the SplitMissile row (follow-up **Stat boosters** (Tool 1x1, template Targeting Computer `c07ea205`'s EnergyDraw and Heat, then a StatModifierData Multiplier in the same group so power gates it; roles amplifier (the magnitude) -and regulator (EnergyDraw, Heat); magnitude per question `boost-strength`): +and regulator (EnergyDraw, Heat); magnitude per question `boost-strength`). Superseded 2026-10-09 by +ruling `boost-strength-tiered-by-size`: each stat gets a modest 1x1 and a strong 2x2 design, tabled in +'Spec refresh 2026-10-09' below): | Design | Targets | Products (maker) | Cut | |---|---|---|---| @@ -6612,8 +6614,8 @@ Prior art: `F:\Projects\aetheria-consumables-prior-art-2026-10-08.md` (cited bel - **CS-R1, the consumable mechanic.** A consumable is a cargo item, single use, activated from the action bar (player) or by an agent rule (AI). Its effect is timed (`Duration`) and carries its own drawback in the same record. A non-Stackable design allows one active effect at a time, so its Duration is also its - cooldown; no second cooldown mechanism. Boosts from a consumable stack with gear under the one resolver - rule (`modifier-stacking`). + cooldown; no second cooldown mechanism. Boosts from a consumable multiply with gear under the one resolver + rule (ruling `boost-stacking-multiply`; pinned by cut `modifier-stacking` r3). - **CS-R2, roles.** Per CB-R1, every non-flat effect stat reads one declared role and every product authors one ProductRole per role. Benefit magnitudes rise with quality, cost magnitudes fall. - **CS-R3, the throttle lock.** While locked, the effective movement intent is full forward, no strafe, @@ -6687,7 +6689,9 @@ denies the lock before launch; seekers in flight are follow-up `cloud-vs-missile consumables combine (vent, then overdrive out of the cloud) and their drawbacks bite each other (the overdrive plume is bright; leaving the cloud ends the cover). The symmetric rule makes hiding blind. -**Hoarding.** The elixir literature (PA 10) names scarcity, unlimited inventory and unclear purpose as +**Hoarding** (written for the abundant option; the operator ruled `consumables-scarce` on 2026-10-09 and +accepted the hoarding risk, so the cheap, everywhere-stocked half of this paragraph no longer holds: see +'Spec refresh 2026-10-09'). The elixir literature (PA 10) names scarcity, unlimited inventory and unclear purpose as causes, and caps, abundance and situational need as cures. Here: cargo cells and mass are the cap (CC7), both designs are cheap (6,000 and 4,000 against 25,000-250,000 for a drive) and stocked at every station under question `consumable-supply`, AI pilots use them (cut `consumable-ai`) so the player sees what @@ -7068,3 +7072,179 @@ That file's Cut 4 (AI heading planner) and Cut 5 (combat facing) are carried as specified, then `thrust-allocator` deletes the mixer, keeps throttle-lock's one intent line as the allocator's input and rewrites throttle-lock's two mixer-shaped tests in allocator terms. One owner of `Ship.Update`'s thruster block at every step. + +## Spec refresh 2026-10-09: scarce consumables, the fog bank, multiply, tiered boosters, the shipped AetherDb + +Pass: Imagination `imagination-spec-refresh`, session `self-2026-10-09-morning`, follow-up +`rulings-2026-10-09-spec-refresh`. Body: `GameCult/Aetheria` origin/master `439d488a` (and `07dcb580`, the +frame-r4 branch, for the authoring tool), detached worktree `C:\aeth-wt\imag-refresh`, removed at the end. +Rulings applied (read their `operator_quote` in the mind): `consumables-scarce`, `vapour-cloud-obscuration`, +`boost-stacking-multiply`, `boost-strength-tiered-by-size`, `components-are-roles-for-demo`, +`published-aetherdb-with-game`, `overdrive-keep-steering`; composed with `flight-control-allocates-thrust` +and `turn-authority-full` (section 'Flight control allocates thrust' above). No question is raised: every +choice below is a project default or authored content that follow-up `combat-pace-pass` tunes. + +### Body facts + +- **RF1. The cloud patch layer (scene and source read).** `ARPG.unity` holds a camera named `Patch` + (GameObject 559840914, local y 10 under the fog camera parent, culling mask 2048 = layer 11 `Patch` + in `ProjectSettings/TagManager.asset`) whose target is `Assets/Resources/Fog Patch.renderTexture` + (guid `c618a6b2...`). `VolumeSampling.cs:69` publishes it as `_NebulaPatch`; `Volumetric.cginc:95-98` + reads it as the patch's vertical thickness: `saturate((-abs(y + disp - FloorOffset) + patch) / + PatchBlend) * PatchDensity`. The one environment is `Settings.DefaultEnvironment` + (`ActionGameManager.cs:216-222`): PatchDensity .35, FloorOffset -20, PatchBlend 25 + (`Assets/Resources/Settings.asset:389-392`). What paints the layer today: two quads under + `SectorBrushes`, `Depth Micro` and `Depth Macro`, with `Sector Patch Depth Small/Large.mat` on + `Brushes/Simplex Brush` (additive `Blend One One`, world-space noise, `_Depth` 10 and 20). A fog-bank + brush already exists and nothing references it: `Assets/Prefabs/RPG/Fog Patch.prefab` (layer 11, a quad + rotated to face up, `Fog Brush.mat` on `Brushes/Power Brush`, `_Depth` 80, `_Power` 16, `_Cutoff` 1; + prefab guid `4084aa3d...`, grep of every scene, prefab and asset: no reference). A quad of side 2R + carries the brush's pulse out to radius R (`PowerBrush.shader`: `dist = length(uv - .5) * 2`). +- **RF2. Drawing a zone body ahead.** `DrawAhead.cs:15-18` draws an entity at `Position + Velocity * + Lead`; `KinematicBody.TotalVelocity` (`FloatingBodies.cs:31-32`) is commented "What a presenter draws + ahead with". `MineInstance.Place` draws the latest body with no lead. The mine presenter's hooks at + `439d488a`: `ZoneRenderer.cs:47` (MinePrefab), `:97` (`_mines`), `:227-235` (load and subscribe), + `:259-264` (LoadMine), `:289-291` (ClearZone). +- **RF3. Generation draws (source read).** `LoadoutGenerator` holds its own copy of the caller's + `Random` (`LoadoutGenerator.cs:27`), and the wanderer generator is reused across ships + (`ZoneGenerator.cs:378`), so one extra draw in `GenerateShipLoadout` shifts every later wanderer. + `ItemManager.CreateInstance` rolls lot quality on `ItemManager.Random` (`ItemManager.cs:123`), so + every extra instance shifts later lots. The catalog at `439d488a` holds no consumable record (CC1), so + a consumable step that draws only when a consumable is on offer leaves every current golden untouched. +- **RF4. Death drops carry.** Today `EntityInstance.cs:296-316` drops each cargo item on death; cut + `loot-1` r2 moves this into `Zone.DropLoot` ("every cargo stack drops"). A consumable an AI ship + carries is therefore loot when it dies, through the owner that already decides loot. +- **RF5. Multiply is the rule and nothing pins it.** `StatResolver.ScaleModifier` + (`StatResolver.cs:108-114`) multiplies every attached scale; `ConstantModifier` (`:116-122`) sums. + `StatResolverTests.cs` (455 lines) has no test with two scales on one stat. No spec in force names + `modifier-stacking` in `depends_on` (specs-with-no-report recipe, 2026-10-09). +- **RF6. No player build entry point exists.** No `BuildPipeline`, `BuildPlayerOptions` or + `IPostprocessBuild` anywhere in `Assets/Scripts` or `Assets/Editor` (which holds only + `Epiphany/EpiphanyEditorBridge.cs`). Ruling `adopt-build-cut-1` (2026-10-03) ordered + `docs/build-delivery-cut.md` Cut 1 mapped as one cut; none was admitted, and `ships-player` r2 adds its + Mods staging to "the committed entry point". The game reads `/../GameData` + (`ActionGameManager.cs:37-41`, `:48-49`). `GameData` at `439d488a` holds `Aetheria.cc`, `Narrative/` + (read by `MainMenu.cs:183`) and `SoundbanksInfo.json`, which no C# file reads (grep) and which + `audio-1` retires with Wwise. Addressables: `m_BuildAddressablesWithPlayerBuild: 0` + (`AddressableAssetSettings.asset:61`, PreferencesValue), so whether content builds with the player is + a per-machine preference today. Build-delivery 0.2 (probe, 2026-09-17) found a read-only + `AetheriaStores.Open` of a missing catalog silent; not re-probed at `439d488a` (`AetheriaStores.cs:16-31` + is unchanged in kind), so its test is written first and must fail first. +- **RF7. The install's tool folder cannot be `Tools`.** The repo has `tools/`; Windows paths are + case-insensitive, so a developer publishing AetherDb into `/Tools` would write into `tools/`. + The install folder is `ModTools/`, gitignored at the repo root. +- **RF8. The add-on anchors at `07dcb580` are r1's** (`__init__.py:29` timeout, `:302-311` + preference, `:314-325` `_aetheria_repo`, `:328-336` `_aetherdb`, `:576-577` New Ship's target; + `AetherDb.cs:26-32` FindRoot, identical on `439d488a`). + +### Model page rows (changed) + +| Kind | Named by | Over time | Decides | +|---|---|---|---| +| Consumable stock (runtime, station cargo) | the station | rolled once at generation: a station stocks with chance `StationConsumableChance`, then one product, 1 to `StationConsumableUnits` units | `LoadoutGenerator`, through `AvailableProducts` | +| Carried consumable (runtime, ship cargo) | the ship | rolled once at generation with chance `ShipConsumableChance`, one unit; spent by `consumable-ai` or dropped as loot on death | `LoadoutGenerator`; then the consumable host or `Zone.DropLoot` | +| Fog bank (presentation) | its `VapourCloud` | one patch brush per cloud, depth = authored depth x the cloud's opacity fraction, destroyed when the cloud leaves `Zone.Clouds` | `ZoneRenderer`'s one subscription; writes nothing to the sim | +| Player directory | the build output root | made by one `AetheriaBuild` run: player, `GameData/Aetheria.cc`, `GameData/Narrative/**`, `GameData/Mods/**`, `ModTools/AetherDb.exe` | `AetheriaBuild` alone | +| Ship author's game folder | the add-on's one preference | the folder whose `GameData` holds `Aetheria.cc`; `ModTools/AetherDb` is found under it | the author | + +### Design tables + +**Stat boosters, tiered by size** (ruling `boost-strength-tiered-by-size`). Shape and template per CB-R2 +and the r1 table: Tool gear, Targeting Computer `c07ea205`'s EnergyDraw and Heat, then a StatModifierData +Multiplier in the same group; roles amplifier (magnitude) and regulator (EnergyDraw, Heat). The 1x1 keeps +the template's EnergyDraw, Heat, Mass and Price; the 2x2 takes 3x EnergyDraw and Heat and 4x Mass and +Price. Magnitude Min-Max (rises with amplifier quality): 1x1 **1.03-1.08**, 2x2 **1.20-1.40**. Both +designs on one stat multiply (ruling `boost-stacking-multiply`), so a 1x1 and a 2x2 at best quality give +1.512. + +| Stat | 1x1 design: products | 2x2 design: products | Cut | +|---|---|---|---| +| ThrusterData.Thrust | Thrust Amplifier: Tailwind (Finch), Second Wind (Lightsail) | Thrust Amplifier Array: Jetstream (Finch), Trade Wind (Lightsail) | boost-gear-systems | +| SensorData.Sensitivity | Signal Amplifier: Earful (Finch), Lookout (AU) | Signal Amplifier Array: Big Ears (Finch), Watchtower (AU) | boost-gear-systems | +| RadiatorData.Emissivity | Emissivity Booster: Cold Read (Adrasteia), Cool Head (Lightsail) | Emissivity Booster Array: Dead Calm (Adrasteia), Cold Storage (Lightsail) | boost-gear-systems | +| ReactorData.Charge | Reactor Overclock: Afterhours (NiteLife), redline (DME) | Reactor Overclock Array: Last Call (NiteLife), meltdown (DME) | boost-gear-systems | +| WeaponData.Damage | Damage Amplifier: Showstopper (Lucent), Heavy Hand (Zhestokost) | Damage Amplifier Array: Encore (Lucent), Iron Fist (Zhestokost) | boost-gear-combat | +| WeaponData.Range | Range Extender: Far Sight (Adrasteia), Long Look (Finch) | Range Extender Array: Horizon Line (Adrasteia), Far Flight (Finch) | boost-gear-combat | +| ShieldData.Capacity | Shield Booster: Thick Skin (AU), SB-2 Bastion (R&D) | Shield Booster Array: Hard Shell (AU), SB-8 Rampart (R&D) | boost-gear-combat | + +Both boost cuts are held behind follow-up `role-targeted-adjacent-boosts`: if the operator rules that +boosters address a role (and adjacency), the Target column changes and these specs get another revision. + +**The first consumables, strengthened** (ruling `consumables-scarce`: rare, stronger). Changes from the +'Consumables' tables above; everything not named is unchanged. + +| Design | Field | Was | Now | +|---|---|---|---| +| Thruster Overdrive | Duration | 5 s | 6 s | +| | Price | 6,000 | 18,000 | +| | Thrust (propellant) | 2.0-3.0 | 3.0-4.5 | +| | TopSpeed (propellant) | 1.5-2.0 | 2.0-3.0 | +| | Thruster Heat, Visibility (regulator, falling) | 4.0-2.5 | 5.0-3.0 | +| Coolant Vent | Duration | 12 s | 15 s | +| | Price | 4,000 | 12,000 | +| | Radius Min | half the Djinni diagonal x 1.25 | x 1.5 (Max stays 1.5x Min) | +| | Opacity (nozzle) | .80-.95 | .90-.98 | +| | Lifetime (coolant) | 10-14 s | 14-20 s | +| | Radiator Emissivity penalty (coolant) | .4-.6 | .3-.5 | + +**Supply, scarce.** `StationConsumableChance` .25, `StationConsumableUnits` 2 (one product, 1-2 units), +`ShipConsumableChance` .10, `ShipConsumableUnits` 1. Named constants in `LoadoutGenerator` beside the gear +stock's literal 16, as r1 had them. + +### Rationale + +**Scarcity through the owners that exist.** "Rare loot and a few stations" needs no loot table: a ship +that carries a consumable drops it on death through the one loot owner (RF4), and only a quarter of +stations stock any. The same roll makes AI use rare, so when the player meets a vent or an overdrive in +an AI's hands it is an event, and killing that ship before it fires is a reason to loot it. Stronger +effects at a higher price follow the ruling; the hoarding risk is the operator's, accepted. The consumable +step draws nothing when no consumable is on offer (RF3), so this cut re-rolls no current fixture; once +`consumables-first-set` lands the generated galaxy re-rolls once, which +`no-save-compatibility-before-players` covers. + +**The fog bank is a patch brush.** The operator's words in `vapour-cloud-obscuration`: painting into the +patch layer is the presenter. The volumetric pass already turns patch thickness into fog the ship flies +inside (RF1), so a cloud is one more additive brush on layer `Patch`, and `Fog Patch.prefab` is that brush, +already authored. Its depth is the authored depth times `OpacityAt / Opacity`, so the fog thins exactly as +the sim's obscuration does and is gone when the cloud is removed (opacity reaches 0 at Lifetime). r1's +particle system is dropped: a second renderer for a thing the nebula already draws. + +**Multiply needs a test, not code.** The ruling keeps today's arithmetic (RF5), so `modifier-stacking` +shrinks to the tests that pin it, gear with gear and consumable with gear. A rule nobody pins is a rule a +later "balance" patch removes silently. + +**The boost tiers.** A strong 2x2 and a modest 1x1 per stat lets cell budget, not a stacking curve, bound a +ship: four cells buy x1.20-1.40 from one array or x1.13-1.36 from four 1x1s (1.03^4 to 1.08^4) at 4x the +power and heat, so at equal quality the array is the better use of cells and power and a hold full of +smalls is the expensive fit. Under multiply nothing else bounds stacking, so these two ranges are the +bound; combat-pace-pass tunes them. + +**How the boosts and the overdrive compose with flight control.** A Thrust multiplier (Thrust Amplifier, +Thruster Overdrive) changes only the resolved `ThrusterData.Thrust`. From `thrust-allocator` on, the +allocator's columns are the thrusters' `EvaluateNominalPower(Thrust)` each tick (TA6), so a boosted drive +is a stronger column and the yaw-first solve still holds heading when one drive is boosted and its twin +is not; the evasion envelope reads the same columns (`envelope-from-columns`). The overdrive's lock is +intent (throttle-lock), not throttles: under the allocator "throttle at max" means full forward intent, +and a drive may be trimmed below 1 to hold heading. So no booster or consumable spec writes an `Axis`, +touches `Ship.Update` or `Thruster.cs`, or asserts per-thruster throttles; their tests observe resolved +stats, `ThrottleLocked` and motion, which hold before and after the allocator lands. + +**The shipped tool and the player build are two owners, in order.** `ships-authoring-tool` owns where the +add-on finds AetherDb (the game folder's `ModTools/`) and what AetherDb treats as its root (a folder whose +`GameData` holds `Aetheria.cc`). `player-build` owns what a player directory contains, the published +AetherDb included. A self-contained single-file publish was probed at 77 MB for linux-x64 (B7); the +Windows player carries the win-x64 one. One preference replaces r1's two: the tool's path is derived from +the game folder, so the two cannot disagree. `ships-player` r3 drops its own Mods staging line and builds +through `player-build`, so one cut decides the staging list. + +**Component records** are owed after the demo cut (ruling `components-are-roles-for-demo`); follow-up +`component-records-after-demo` carries the mapping so it outlives this refresh. + +### Admitted + +Receipts `mind-commit-d41d568c...`, `mind-commit-dc5c5a2a...`, `mind-commit-c2edfe6d...` (2026-10-09 +13:24-13:28 UTC). Revisions, each with the resolution superseding its predecessor: consumable-supply r2, +vapour-cloud-presenter r2, modifier-stacking r3 (shrunk to tests), consumables-first-set r2 (not named in the +follow-up; reshaped by `consumables-scarce`), boost-gear-systems r2 and boost-gear-combat r2 (held), +ships-authoring-tool r2, ships-player r3. New: cut player-build r1, follow-up +`component-records-after-demo`. No question raised. From 549c1b0253cf7ca608bd2ed49dad4a6a6ebe5667 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 15:39:05 +0200 Subject: [PATCH 078/120] Tests: the two no-Lagrange zone tests share one scan; restore NotSame, use Assert.Fail The tutorial-seed scan for a zone with no genuine Lagrange candidate ran once per test; it now runs once behind a Lazy. The leak test regains Assert.NotSame(galaxy.Entrance, otherZone), and the scan's miss path uses Assert.Fail (xUnit2020). Co-Authored-By: Claude Opus 5.5 --- .../RestoredHullsTests.cs | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs b/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs index b1ce60093..61c5fad1f 100644 --- a/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs +++ b/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs @@ -129,7 +129,27 @@ public sealed class RestoredHullsTests // The first seed, and in its tutorial galaxy the first non-entrance zone, that has planets every one of which is a // rosette member: no genuine Lagrange candidate, so only the entrance's widened candidate set can seat a station // there. Such a zone is rare, so seeds are tried in order until one has it. - private static (uint seed, string zoneName) NoLagrangeCandidateZone(CultCache cache) + // The scan is the cost (tens of seconds), and it depends only on the generator, so both tests share one result + // through a lazy computed under its own scratch run store. + private static readonly Lazy<(uint seed, string zoneName)> NoLagrangeCandidateZoneScan = new(ScanForNoLagrangeCandidateZone); + + private static (uint seed, string zoneName) NoLagrangeCandidateZone() => NoLagrangeCandidateZoneScan.Value; + + private static (uint seed, string zoneName) ScanForNoLagrangeCandidateZone() + { + var scratchRun = Path.Combine(Path.GetTempPath(), $"aetheria-nolagrange-scan-{Guid.NewGuid():N}.cc"); + try + { + using var cache = OpenReadOnlyRealCatalogWithScratchRun(Path.Combine(FindRepoRoot(), "GameData", "Aetheria.cc"), scratchRun); + return ScanForNoLagrangeCandidateZone(cache); + } + finally + { + if (File.Exists(scratchRun)) File.Delete(scratchRun); + } + } + + private static (uint seed, string zoneName) ScanForNoLagrangeCandidateZone(CultCache cache) { var items = new ItemManager(cache, new ProvenanceLedger(), Settings(), _ => { }); for (var seed = 1u; seed <= 64u; seed++) @@ -142,8 +162,7 @@ public sealed class RestoredHullsTests if (orbits.Length > 0 && !HasGenuineLagrangeCandidate(orbits)) return (seed, zone.Name); } } - Assert.True(false, "fixture: no tutorial galaxy at seeds 1-64 has a zone whose planets are all rosette members."); - return default; + return Assert.Fail("fixture: no tutorial galaxy at seeds 1-64 has a zone whose planets are all rosette members."); } // ZoneGenerator.GenerateZone writes OrbitData/BodyData (run-store types, AetheriaStores.RunTypes), so a @@ -708,7 +727,7 @@ public sealed class RestoredHullsTests try { using var cache = OpenReadOnlyRealCatalogWithScratchRun(gameData, scratchRun); - var (seed, zoneName) = NoLagrangeCandidateZone(cache); + var (seed, zoneName) = NoLagrangeCandidateZone(); var galaxy = BuildGalaxyWithForcedEntrance(cache, seed, zoneName); var items = new ItemManager(cache, new ProvenanceLedger(), Settings(), _ => { }); @@ -770,6 +789,7 @@ public sealed class RestoredHullsTests // isTutorial: true -- the floor must not leak past the galaxy.Entrance reference check. // The zone is chosen by its natural roll: zero stations without the tutorial floor (isTutorial: false). var otherZone = galaxy.Zones.First(z => !ReferenceEquals(z, galaxy.Entrance) && StationCount(z, isTutorial: false) == 0); + Assert.NotSame(galaxy.Entrance, otherZone); Assert.Equal(0, StationCount(otherZone, isTutorial: true)); // (b) The entrance zone itself, generated for a non-tutorial game (isTutorial: false), keeps its @@ -879,7 +899,7 @@ public sealed class RestoredHullsTests try { using var cache = OpenReadOnlyRealCatalogWithScratchRun(gameData, scratchRun); - var (seed, zoneName) = NoLagrangeCandidateZone(cache); + var (seed, zoneName) = NoLagrangeCandidateZone(); var galaxy = BuildGalaxyWithBoostedPresence(cache, seed, zoneName); var zone = galaxy.Zones.Single(z => z.Name == zoneName); Assert.NotSame(galaxy.Entrance, zone); From a3ea1d7ba1f20a6041abe6555c03648f353f2105 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 15:40:42 +0200 Subject: [PATCH 079/120] Tests: scan's miss path fails then returns default (Assert.Fail is void) Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/RestoredHullsTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs b/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs index 61c5fad1f..b5622b268 100644 --- a/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs +++ b/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs @@ -162,7 +162,8 @@ public sealed class RestoredHullsTests if (orbits.Length > 0 && !HasGenuineLagrangeCandidate(orbits)) return (seed, zone.Name); } } - return Assert.Fail("fixture: no tutorial galaxy at seeds 1-64 has a zone whose planets are all rosette members."); + Assert.Fail("fixture: no tutorial galaxy at seeds 1-64 has a zone whose planets are all rosette members."); + return default; } // ZoneGenerator.GenerateZone writes OrbitData/BodyData (run-store types, AetheriaStores.RunTypes), so a From 1b2e9c1258b41b4d71b0929b284df7bcc1394aec Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 16:37:59 +0200 Subject: [PATCH 080/120] Map: allocator translation cost is linear (core r2 probes, tolerance, chosen constants) Imagination pass, aetheria-release, session self-2026-10-09-morning. Co-Authored-By: Claude Opus 5.5 --- docs/aetheria-release-map.md | 60 ++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/docs/aetheria-release-map.md b/docs/aetheria-release-map.md index b30ea7141..3cd61747c 100644 --- a/docs/aetheria-release-map.md +++ b/docs/aetheria-release-map.md @@ -7073,6 +7073,66 @@ That file's Cut 4 (AI heading planner) and Cut 5 (combat facing) are carried as allocator's input and rewrites throttle-lock's two mixer-shaped tests in allocator terms. One owner of `Ship.Update`'s thruster block at every step. +### Revision: translation error costs linearly (core r2) + +Pass: Imagination, session `self-2026-10-09-morning`, after Hands stopped on `cut-thrust-allocator-core.r1` +(question `allocator-translation-cost-l2-has-no-onset`, ruling `allocator-l1-translation-cost`, operator +ruling `turn-authority-full`). Probes ran the real `CultMath.BoundedLeastSquares` (CultLib `614fd445`, the +pinned 0.3.0) in a scratch console on Yggdrasil, logs `l1probe1.log` to `l1probe3.log` in the session +scratchpad (`imag-allocator-r2`). Column sets: TA3 (Duel Longinus); TA3 with the clockwise Talaria zeroed; +a symmetric Djinni-like hull (mains (0,50,+-20), fore laterals (+-20,0,+-80), aft laterals (+-20,0,-+60), +bow (0,-30,0)); drives 0.3% apart for the tiny-imbalance hold. + +- **TA10. A squared translation cost has no onset** (Hands' probe, confirmed): r1's TA-R2 mixes the + drive into a turn from Turn 0 for any weights, so it cannot meet `turn-authority-full`. +- **TA11. The split form works only when the solver resolves it.** Variables: throttles and four error + variables e+x, e-x, e+y, e-y in [0,2]; rows H*(B_r u / s_r - e+ + e-) = H d_r / s_r, the yaw row + Wy * B_z u / s_z = Wy d_z / s_z, a cost row w*e = -w*kappa per error variable (linear slope + L = 2 w^2 kappa, quadratic part at most e/(2 kappa) of it), a ridge row rho*u per throttle. + At the solver's fixed KKT tolerance (1e-6 of the gradient at the origin) every one of 84 settings in two + grids failed somewhere: Hands' (H 300, Wy 1, w .01, kappa 2, rho .01) and the first choice (H 100, Wy 10, + w .01, kappa 10, rho 1e-3) gave all-zero throttles for move (0,1) and for a strafe, because the cost + signal L is below the tolerance against the H^2 and Wy^2 terms; larger L settings served translation + but gave throttles that depended on the warm start by up to 0.62 (ties the ridge cannot resolve below + the tolerance). The chain the form needs, tolerance << rho^2 << L << H^2, Wy^2, with L/H^2 about 1e-4 + for a sharp onset and L/Wy^2 about 1e-6 for the hold, spans more than the 1e6 that tolerance leaves. +- **TA12. With the tolerance at 1e-12 everything holds** (`l1probe3.log`, same grid, scratch change of + the constant): warm-start difference 0 in every setting, IterationLimit never reached, at most 10 + iterations. Chosen H 100, Wy 1000, w .3, kappa 10, rho .1: + + | Check | Result | + |---|---| + | Duel onset, clockwise / counter-clockwise | .772 / .691 (columns: 166.26/215.50 = .7715, 124.63/180.49 = .6905) | + | Duel, clockwise Talaria lost | drive from Turn > 0, Turn .1 gives drive .0999; Turn 1 gives 49.23 deg/s | + | Symmetric hull | no translation up to Turn .88 (pure-torque pairs, then mains cancelled by the bow); strafe .95 of the extreme | + | Hold, forward, yaw rate (deg/s) | intact -.0003, strong drive off .0003, weak drive off -.0002, drives .3% apart .0003 | + | Forward served, move (0,1) | 1.000 of the forward extreme | + | Largest throttle step per .001 of Turn over [-1,1] | .0133 (the steepest column ratio; Duel's drive slope is .0044) | + | Largest yaw residual in the sweeps | .0006 deg/s | + | cond(A), estimated from A^T A | 1.0e4; the solver documents exact results through 3e5 | + + Neighbouring settings fail one check each: H 30 leaks the drive at Turn .001; w 1 or 3 leaks it at + .0014 throttle before the onset (L/H^2 too large); rho .03 makes throttles jump up to .12 per .001 of + Turn (near-ties resolved too steeply); Wy 300 lets the hold drift .003 deg/s. +- **TA13. The Duel Longinus does not strafe.** Under the linear cost a starboard strafe would need the + counter-turning drive at full to hold heading, which costs more forward error than the strafe gains, + so move (1,0) gives no throttle. Today's mixer gives none either (TA4). The symmetric hull strafes. + +Rationale changes: + +- **The solve keeps one call; the tolerance is the solver owner's to expose.** CultMath's + `BoundedLeastSquares.Solve` gets an optional relative KKT tolerance (default unchanged, 1e-6), filled in + CultLib, never worked around in Aetheria; the allocator passes 1e-12 and relies on the solver's + progress rule to stop at its noise floor. Follow-up `cultmath-bls-kkt-tolerance` carries it; Aetheria + takes it by a pin bump in the core cut's first commit. +- **The onset is an ordering, not a label.** Under a linear cost the actuator with the least normalised + translation per unit of turn serves first and the next joins only when it saturates; on the Duel that + is the Talaria, so the onset is the Talaria's share of the turn extreme and moves with the columns + (damage, heat, brownout). Nothing classifies a thruster as attitude or drive. +- The bullets "One solve with a large yaw weight" and "The ridge" above describe r1 and are superseded + by this revision: the yaw weight is 1000 against a ridge of .1, and uniqueness of the answer depends on + the tolerance as well as the ridge. + ## Spec refresh 2026-10-09: scarce consumables, the fog bank, multiply, tiered boosters, the shipped AetherDb Pass: Imagination `imagination-spec-refresh`, session `self-2026-10-09-morning`, follow-up From 739eee4a8ed63deb5f2c9f92b67ef6ea3f0f267d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:17:36 +0200 Subject: [PATCH 081/120] Consumable host: tests first (red at the base; they build, three fail) Adds AConsumableModifierBoostsUntilExpiry, ABoughtConsumableActivates and ANonStackableConsumableRefusesASecondActivation. This commit builds; its tests fail until the host cut lands. Co-Authored-By: Claude Opus 5.5 --- .../Aetheria.Shared.Tests/ConsumableTests.cs | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 tests/Aetheria.Shared.Tests/ConsumableTests.cs diff --git a/tests/Aetheria.Shared.Tests/ConsumableTests.cs b/tests/Aetheria.Shared.Tests/ConsumableTests.cs new file mode 100644 index 000000000..3fdd6aad3 --- /dev/null +++ b/tests/Aetheria.Shared.Tests/ConsumableTests.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using CultMath; +using GameCult.Caching; +using Xunit; + +// A consumable is a cargo item with a timed effect (aetheria-release CS-R1). Its modifiers exist in the resolver +// exactly while its effect is active: the host (Entity) initialises the effect's behaviours at activation and +// disposes them at expiry, and ItemManager.CreateInstance is the one way a lot becomes a ConsumableItem. +public sealed class ConsumableTests : IDisposable +{ + private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-consumable-" + Guid.NewGuid().ToString("N")); + private string Catalog => Path.Combine(_root, "Aetheria.cc"); + private static readonly int2 EngineCell = new int2(0, 0); + private static readonly int2 HoldCell = new int2(1, 1); + + public ConsumableTests() => Directory.CreateDirectory(_root); + public void Dispose() => Directory.Delete(_root, true); + + private sealed class Rig + { + public CultCache Cache; + public ItemManager Items; + public Ship Ship; + public EquippedItem Engine; + public PerformanceStat Thrust; + public ConsumableItemData Consumable; + public CultRecordKey Key; + public CultRecordRef Maker; + + public ConsumableItem Mint() => Assert.IsType(Items.CreateInstance(Items.CreateLot(Consumable, Maker, .5f))); + public EquippedCargoBay Hold => Ship.CargoBays.Single(); + public int InCargo => Hold.Cargo.Keys.Count(i => i is ConsumableItem); + } + + // A ship with a thruster (Thrust 10) and a hold; the consumable doubles ThrusterData.Thrust for one second. + private Rig Build(bool stackable = false) + { + var thrust = new PerformanceStat { Min = 10, Max = 10 }; + var cache = AetheriaStores.Open(Catalog, catalogWritable: true); + cache.Upsert(new TestCatalogGlobal { Name = "Temperament" }); + cache.Upsert(new Faction { Name = "Maker", ShortName = "MKR" }); + var hullShape = new Shape(3, 3); + foreach (var cell in hullShape.AllCoordinates) hullShape[cell] = true; + var hullRef = cache.Upsert(new HullData + { + Name = "Skiff", HullType = HullType.Ship, Shape = hullShape, Durability = 10, + Hardpoints = { new HardpointData { Type = HardpointType.Sensors, Position = EngineCell, Shape = new Shape() } } + }); + var engineRef = cache.Upsert(new GearData + { + Name = "Engine", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Durability = 10, + Behaviors = { new ThrusterData { Thrust = thrust } } + }); + var interior = new Shape(1, 1); + foreach (var cell in interior.AllCoordinates) interior[cell] = true; + var holdRef = cache.Upsert(new CargoBayData { Name = "Hold", Shape = new Shape(), InteriorShape = interior, Durability = 10 }); + var consumableRef = cache.Upsert(new ConsumableItemData + { + Name = "Overdrive", Duration = 1f, Stackable = stackable, Shape = new Shape(), + Behaviors = + { + new StatModifierData + { + Stat = new StatReference { Target = nameof(ThrusterData), Stat = nameof(ThrusterData.Thrust) }, + Modifier = new PerformanceStat { Min = 2, Max = 2 }, + Type = StatModifierType.Multiplier + } + } + }); + cache.FlushAsync().Wait(); + + var items = new ItemManager(cache, new ProvenanceLedger(), RunSaveTests.TestSettings(), _ => { }); + var makerRef = cache.RefOf(cache.GetByName("Maker")); + EquippableItem Gear(EquippableItemData design) => (EquippableItem) items.CreateInstance(items.CreateLot(design, makerRef, .5f)); + var zone = new Zone(items, new PlanetSettings(), new ZonePack(), new GalaxyZone { Name = "Test Zone", Owner = null }, null); + var ship = new Ship(items, zone, Gear(cache.Get(hullRef)), new EntitySettings()); + Assert.True(ship.TryEquip(Gear(cache.Get(engineRef)), EngineCell)); + Assert.True(ship.TryEquip(Gear(cache.Get(holdRef)), HoldCell)); + zone.Entities.Add(ship); + ship.Activate(); + var data = cache.Get(consumableRef); + return new Rig + { + Cache = cache, Items = items, Ship = ship, Thrust = thrust, Consumable = data, + Key = items.ItemData.RefOf(data).Key, + Engine = ship.Equipment.Single(e => e.Data.Name == "Engine"), + Maker = makerRef + }; + } + + // Two ticks after activation the modifier has attached (the first tick executes it, the second applies it). + private static void Tick(Rig rig, int times, float dt) + { + for (var i = 0; i < times; i++) rig.Ship.Update(dt); + } + + // The rule: a consumable's modifiers exist in the resolver exactly while its effect is active, never after. + // Observed at the resolved stat and at the resolver's modifier entry count, not at a flag. + [Fact] + public void AConsumableModifierBoostsUntilExpiry() + { + var rig = Build(); + using var _ = rig.Cache; + var resolver = rig.Ship.Resolver; + Assert.Equal(10f, rig.Engine.Evaluate(rig.Thrust), 3); + var baseline = resolver.ModifierEntryCount; + + rig.Ship.ActivateConsumable(rig.Mint()); + Tick(rig, 2, .3f); // 0.4s of the 1s remain; the modifier has attached + Assert.Equal(20f, rig.Engine.Evaluate(rig.Thrust), 3); + Assert.True(resolver.ModifierEntryCount > baseline); + + Tick(rig, 3, .3f); // 0.9s elapsed in all: expired on the third of these ticks + Assert.Equal(10f, rig.Engine.Evaluate(rig.Thrust), 3); + Assert.Equal(baseline, resolver.ModifierEntryCount); + } + + // A lot minted through the one production path is a ConsumableItem, and a hold holding it activates it. + [Fact] + public void ABoughtConsumableActivates() + { + var rig = Build(); + using var _ = rig.Cache; + Assert.True(rig.Hold.TryStore(rig.Mint())); + Assert.Equal(1, rig.InCargo); + + Assert.True(rig.Ship.TryActivateConsumable(rig.Consumable)); + Assert.Equal(0, rig.InCargo); + Assert.NotNull(rig.Ship.FindActiveConsumable(rig.Consumable)); + } + + // CS-R1: a non-stackable design allows one active effect at a time, so its Duration is its cooldown. + [Fact] + public void ANonStackableConsumableRefusesASecondActivation() + { + var rig = Build(); + using var _ = rig.Cache; + Assert.True(rig.Hold.TryStore(rig.Mint())); + Assert.True(rig.Hold.TryStore(rig.Mint())); + + Assert.True(rig.Ship.TryActivateConsumable(rig.Consumable)); + Assert.False(rig.Ship.TryActivateConsumable(rig.Consumable)); + Assert.Equal(1, rig.InCargo); + + Tick(rig, 5, .3f); // the first effect has run out + Assert.True(rig.Ship.TryActivateConsumable(rig.Consumable)); + Assert.Equal(0, rig.InCargo); + } +} From c6bca1a52234d737943f07b7e00f1da1b057d26f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:17:47 +0200 Subject: [PATCH 082/120] ThrottleLock behaviour: an active consumable forces full forward intent, read once in Ship.Update ThrottleLockData (union 41) marks the lock; Entity.ThrottleLocked is derived from the active effects; Ship.Update reads one `move` in place of MovementDirection. Turn is untouched (ruling overdrive-keep-steering). Not yet verified by a build when committed. Co-Authored-By: Claude Opus 5.5 --- .../ServerShared/Behaviors/Behaviors.cs | 1 + .../ServerShared/Behaviors/ThrottleLock.cs | 35 ++++ Assets/Scripts/ServerShared/Entity.cs | 3 + Assets/Scripts/ServerShared/Ship.cs | 14 +- .../ThrottleLockTests.cs | 174 ++++++++++++++++++ 5 files changed, 221 insertions(+), 6 deletions(-) create mode 100644 Assets/Scripts/ServerShared/Behaviors/ThrottleLock.cs create mode 100644 tests/Aetheria.Shared.Tests/ThrottleLockTests.cs diff --git a/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs b/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs index 9f2e292a6..4b07d7bb8 100644 --- a/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs +++ b/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs @@ -187,6 +187,7 @@ public interface IPowerConsumer // superseded draft quoted -- checked directly here rather than trusted from that document. Union(39, typeof(TargetingSystemData)), Union(40, typeof(MineLayerData)), +Union(41, typeof(ThrottleLockData)), JsonObject(MemberSerialization.OptIn)] public abstract class BehaviorData { diff --git a/Assets/Scripts/ServerShared/Behaviors/ThrottleLock.cs b/Assets/Scripts/ServerShared/Behaviors/ThrottleLock.cs new file mode 100644 index 000000000..d33ef07cb --- /dev/null +++ b/Assets/Scripts/ServerShared/Behaviors/ThrottleLock.cs @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +using MessagePack; +using Newtonsoft.Json; + +// Presence marks the lock: Entity.ThrottleLocked is true while an active effect carries one, and Ship.Update +// then reads full forward intent in place of MovementDirection. The behaviour holds no state and does nothing +// itself; turning stays with the pilot. +[Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn), RuntimeInspectable] +public class ThrottleLockData : BehaviorData +{ + public override Behavior CreateInstance(EquippedItem item) + { + return new ThrottleLock(this, item); + } + public override Behavior CreateInstance(ConsumableItemEffect item) + { + return new ThrottleLock(this, item); + } +} + +public class ThrottleLock : Behavior +{ + public ThrottleLock(ThrottleLockData data, EquippedItem item) : base(data, item) + { + } + + public ThrottleLock(ThrottleLockData data, ConsumableItemEffect item) : base(data, item) + { + } + + public override bool Execute(float dt) => true; +} diff --git a/Assets/Scripts/ServerShared/Entity.cs b/Assets/Scripts/ServerShared/Entity.cs index 1da6e68d8..4855db616 100644 --- a/Assets/Scripts/ServerShared/Entity.cs +++ b/Assets/Scripts/ServerShared/Entity.cs @@ -863,6 +863,9 @@ public abstract class Entity _activeConsumables.Add(new ConsumableItemEffect(item, this)); } + // Derived, never written: true iff an active consumable effect carries a ThrottleLock. + public bool ThrottleLocked => _activeConsumables.Any(e => e.Behaviors.OfType().Any()); + public ConsumableItemEffect FindActiveConsumable(ConsumableItemData data) { return _activeConsumables.FirstOrDefault(ac => ac.Data == data); diff --git a/Assets/Scripts/ServerShared/Ship.cs b/Assets/Scripts/ServerShared/Ship.cs index 98dfa4fb3..993d92940 100644 --- a/Assets/Scripts/ServerShared/Ship.cs +++ b/Assets/Scripts/ServerShared/Ship.cs @@ -233,27 +233,29 @@ public class Ship : Entity if (_active && !_exitingWormhole && !_enteringWormhole) { RecalculateThrust(); + // The lock replaces intent here, once; player and agent intent both pass through this one read. + var move = ThrottleLocked ? float2(0, 1) : MovementDirection; foreach (var thruster in _allThrusters) thruster.Axis = 0; var rightThrusterTorqueCompensation = abs(RightStrafeTotalTorque) / RightStrafeTorqueThrusters.Count; foreach (var thruster in _rightThrusters) { var thrust = 0f; - thrust += MovementDirection.x; + thrust += move.x; if (RightStrafeTorqueThrusters.Contains(thruster)) - thrust -= MovementDirection.x * (rightThrusterTorqueCompensation / (abs(thruster.Torque) * thruster.Thrust)); + thrust -= move.x * (rightThrusterTorqueCompensation / (abs(thruster.Torque) * thruster.Thrust)); thruster.Axis = thrust; } var leftThrusterTorqueCompensation = abs(LeftStrafeTotalTorque) / LeftStrafeTorqueThrusters.Count; foreach (var thruster in _leftThrusters) { var thrust = 0f; - thrust += -MovementDirection.x; + thrust += -move.x; if (LeftStrafeTorqueThrusters.Contains(thruster)) - thrust += MovementDirection.x * (leftThrusterTorqueCompensation / (abs(thruster.Torque) * thruster.Thrust)); + thrust += move.x * (leftThrusterTorqueCompensation / (abs(thruster.Torque) * thruster.Thrust)); thruster.Axis = thrust; } - foreach (var thruster in _forwardThrusters) thruster.Axis += MovementDirection.y; - foreach (var thruster in _reverseThrusters) thruster.Axis += -MovementDirection.y; + foreach (var thruster in _forwardThrusters) thruster.Axis += move.y; + foreach (var thruster in _reverseThrusters) thruster.Axis += -move.y; foreach (var thruster in _clockwiseThrusters) thruster.Axis += Turn; foreach (var thruster in _counterClockwiseThrusters) thruster.Axis += -Turn; diff --git a/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs b/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs new file mode 100644 index 000000000..1673e2be1 --- /dev/null +++ b/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using CultMath; +using GameCult.Caching; +using Xunit; +using static CultMath.math; + +// A ThrottleLock on an active consumable replaces the pilot's movement intent with full forward, read once at +// the top of Ship.Update's active branch. Turn is untouched. Every observation is a thruster's Axis after a real +// Ship.Update, with the effect started through Entity.ActivateConsumable. +public sealed class ThrottleLockTests : IDisposable +{ + private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-throttlelock-" + Guid.NewGuid().ToString("N")); + private string Catalog => Path.Combine(_root, "Aetheria.cc"); + + public ThrottleLockTests() => Directory.CreateDirectory(_root); + public void Dispose() => Directory.Delete(_root, true); + + private static GameplaySettings Settings() => new GameplaySettings + { + DefaultEntitySettings = new EntitySettings(), + Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } }, + QualityPriceModifier = new ExponentialLerp() + }; + + private static PerformanceStat Constant(float v) => new PerformanceStat { Min = v, Max = v }; + + private static EquippableItem Mint(CultCache cache, ItemManager items, ItemData design, ItemRotation rotation = ItemRotation.None) => new EquippableItem + { + Data = cache.RefOf(design), + Durability = design is EquippableItemData equippable ? equippable.Durability : 0, + Rotation = rotation, + Lot = items.Lots.Add(new Lot { Design = cache.RefOf(design), Origin = new Attributed(), Quality = .5f, Roles = new List() }) + }; + + private sealed class Fixture + { + public CultCache Cache; + public ItemManager Items; + public Ship Ship; + public ConsumableItemData Lock; + public Thruster Forward, Reverse, Right, Left, Clockwise; + + public void ActivateLock() => Ship.ActivateConsumable(new ConsumableItem + { + Data = Cache.RefOf(Lock), + Lot = Items.Lots.Add(new Lot { Design = Cache.RefOf(Lock), Origin = new Attributed(), Quality = .5f, Roles = new List() }) + }); + } + + // One thruster per intent direction, plus two reverse thrusters either side of the centre line, one of + // which has clockwise torque (the one Turn reaches). Thrusters are found by their item's rotation, as Ship + // sorts them. + private Fixture Build(float lockDuration) + { + var cache = AetheriaStores.Open(Catalog, catalogWritable: true); + cache.Upsert(new TestCatalogGlobal { Name = "Temperament" }); + var hullShape = new Shape(5, 5); + foreach (var cell in hullShape.AllCoordinates) hullShape[cell] = true; + cache.Upsert(new HullData { Name = "Skiff", HullType = HullType.Ship, Shape = hullShape, Durability = 10, Mass = 1000 }); + cache.Upsert(new GearData + { + Name = "Thruster", Hardpoint = HardpointType.Tool, Shape = new Shape(), Durability = 10, + MinimumTemperature = 0, MaximumTemperature = 1000, OptimalTemperature = 280, PlateauWidth = 400, + Behaviors = { new ThrusterData { Thrust = Constant(100), Visibility = Constant(0), Heat = Constant(0), EnergyUsage = Constant(0) } } + }); + var lockData = new ConsumableItemData { Name = "Overdrive", Duration = lockDuration, Behaviors = { new ThrottleLockData() } }; + cache.Upsert(lockData); + cache.FlushAsync().Wait(); + + var items = new ItemManager(cache, new ProvenanceLedger(), Settings(), _ => { }); + var zone = new Zone(items, new PlanetSettings(), new ZonePack(), new GalaxyZone { Name = "Test Zone", Owner = null }, null); + var ship = new Ship(items, zone, Mint(cache, items, cache.GetByName("Skiff")), new EntitySettings()); + var gear = cache.GetByName("Thruster"); + Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.Reversed), new int2(2, 1))); + Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.None), new int2(0, 2))); + Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.None), new int2(4, 2))); + Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.CounterClockwise), new int2(3, 3))); + Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.Clockwise), new int2(1, 3))); + zone.Entities.Add(ship); + ship.Aim = float3(0, 0, 1); + ship.Activate(); + + var thrusters = ship.GetBehaviors().ToArray(); + return new Fixture + { + Cache = cache, Items = items, Ship = ship, Lock = cache.GetByName("Overdrive"), + Forward = thrusters.Single(t => t.Item.EquippableItem.Rotation == ItemRotation.Reversed), + Right = thrusters.Single(t => t.Item.EquippableItem.Rotation == ItemRotation.CounterClockwise), + Left = thrusters.Single(t => t.Item.EquippableItem.Rotation == ItemRotation.Clockwise), + Reverse = thrusters.First(t => t.Item.EquippableItem.Rotation == ItemRotation.None && t.Torque <= 0), + Clockwise = thrusters.First(t => t.Item.EquippableItem.Rotation == ItemRotation.None && t.Torque > 0) + }; + } + + // Spec ALockedThrottleIgnoresIntent. Mutation: read MovementDirection in place of move at any of the six + // sites in Ship.Update and the matching assertion goes red. + [Fact] + public void ALockedThrottleIgnoresIntent() + { + var f = Build(100f); + using var _ = f.Cache; + f.ActivateLock(); + Assert.True(f.Ship.ThrottleLocked); + + foreach (var intent in new[] { float2(0, -1), float2(1, 0), float2(-1, 0) }) + { + f.Ship.MovementDirection = intent; + f.Ship.Turn = 0; + f.Ship.Update(0.01f); + Assert.Equal(1f, f.Forward.Axis); + Assert.Equal(0f, f.Reverse.Axis); + Assert.Equal(0f, f.Right.Axis); + Assert.Equal(0f, f.Left.Axis); + } + + // Turning stays with the pilot: the clockwise-torque thruster follows Turn while the lock holds. + f.Ship.MovementDirection = float2(0, -1); + f.Ship.Turn = 1; + f.Ship.Update(0.01f); + Assert.Equal(1f, f.Clockwise.Axis); + f.Ship.Turn = 0; + f.Ship.Update(0.01f); + Assert.Equal(0f, f.Clockwise.Axis); + } + + // Spec TheLockEndsWithItsEffect. Mutation: make ThrottleLocked a latch (set on activation, never cleared) + // and the post-expiry reverse axis stays 0. + [Fact] + public void TheLockEndsWithItsEffect() + { + var f = Build(1f); + using var _ = f.Cache; + f.ActivateLock(); + f.Ship.MovementDirection = float2(0, -1); + f.Ship.Update(0.5f); + Assert.Equal(0f, f.Reverse.Axis); + + f.Ship.Update(2f); // runs past the Duration; the effect leaves at the end of this tick + Assert.False(f.Ship.ThrottleLocked); + + f.Ship.Update(0.01f); + Assert.Equal(1f, f.Reverse.Axis); + Assert.Equal(0f, f.Forward.Axis); + } + + // Spec AnUnlockedShipFollowsIntent. Mutation: replace the conditional by an unconditional float2(0, 1) and + // the reverse and strafe assertions go red. + [Fact] + public void AnUnlockedShipFollowsIntent() + { + var f = Build(1f); + using var _ = f.Cache; + Assert.False(f.Ship.ThrottleLocked); + + f.Ship.MovementDirection = float2(0, -1); + f.Ship.Update(0.01f); + Assert.Equal(1f, f.Reverse.Axis); + Assert.Equal(0f, f.Forward.Axis); + + f.Ship.MovementDirection = float2(1, 0); + f.Ship.Update(0.01f); + Assert.Equal(1f, f.Right.Axis); + Assert.Equal(0f, f.Left.Axis); + Assert.Equal(0f, f.Forward.Axis); + + f.Ship.MovementDirection = float2(0, 1); + f.Ship.Update(0.01f); + Assert.Equal(1f, f.Forward.Axis); + Assert.Equal(0f, f.Reverse.Axis); + } +} From 0e89dd16731b84a157bc071169bf65ddc00adfe6 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:19:22 +0200 Subject: [PATCH 083/120] FireControl.RoundAt: the one statement of where a fired round is at a sim time PendingShot freezes the weapon's Velocity as Speed at Fire; RoundAt(shot, time) reads the frozen origin, direction, speed and fire time, clamped to the FireRange point. Step, Commit and Apply do not call it. BallisticFlightTests pin the line against the shooter position, aim and authored velocity. Builds: yes. Presenters still fly their own rounds at this commit. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/FireControl.cs | 13 ++ .../BallisticFlightTests.cs | 127 ++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 tests/Aetheria.Shared.Tests/BallisticFlightTests.cs diff --git a/Assets/Scripts/ServerShared/FireControl.cs b/Assets/Scripts/ServerShared/FireControl.cs index 138776ce9..097c3cc79 100644 --- a/Assets/Scripts/ServerShared/FireControl.cs +++ b/Assets/Scripts/ServerShared/FireControl.cs @@ -514,6 +514,16 @@ public static class FireControl return shot.Target == null ? 1f : PDeviation(deviation, shot.Tracking); } + // Where a ballistic round is at a sim time: the one statement of the round's line, from its frozen origin along its + // frozen direction at its frozen speed, held at the FireRange point once it has flown that far. Pure: it reads the + // shot alone, and the simulation's own arithmetic (Step, Commit, Apply) does not call it. Presenters draw rounds here. + public static float2 RoundAt(in PendingShot shot, float time) + { + if (shot.Speed <= .01f) return shot.FireOrigin.xz; + var flown = clamp(time - shot.FireTime, 0f, shot.FireRange / shot.Speed); + return shot.FireOrigin.xz + shot.TravelDirection * (shot.Speed * flown); + } + // How far a target can and does move off the line of fire in the solution window: the term DeviationProbability // and HitProbability both add. Behaviour (its observed unpredictability across the line of sight) realises the // evasion and capability (its envelope's reach) caps it, so a coasting ship evades nothing whatever it could do @@ -755,6 +765,7 @@ public static class FireControl Spread = weapon.Spread, FireRange = fireRange, FireTime = now, + Speed = weapon.Velocity, FireOrigin = origin, FireTargetPosition = targetPosition, FireTargetVelocity = targetVelocity, @@ -1999,6 +2010,8 @@ public struct PendingShot public float3 FireTargetPosition; public float3 FireTargetVelocity; public float FireTime; + // The weapon's Velocity at Fire: frozen with the rest, so RoundAt states the same line whatever happens to the stat later. + public float Speed; public float CommitTime; public float ArrivalTime; diff --git a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs new file mode 100644 index 000000000..cd3582d70 --- /dev/null +++ b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs @@ -0,0 +1,127 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +using System.Linq; +using Xunit; +using static CultMath.math; +using float2 = CultMath.float2; +using float3 = CultMath.float3; + +// Ballistic flight: FireControl.RoundAt is the one statement of where a fired round is at a sim time, from the +// PendingShot's frozen origin, direction, speed and fire time. Unity draws rounds there and integrates nothing, so +// these tests pin the line itself against first principles (the shooter's position, the aim, the authored velocity) +// rather than against the code that computes it. Shares the Cut 12.4 engagement fixture. +public sealed partial class FireControlCut124Tests +{ + private const float BallisticSpeed = 20f; + + // A no-lock proximity round: flies the aim line (2,1) to max range, bursting there. + private Engagement FireBurstRound(out float3 origin) + { + var e = Build(TestSettings(), SolidShape(5, 4), velocity: BallisticSpeed, fuse: WeaponFuse.Proximity, blastRadius: 4f, + damage: 100f, weaponRange: NoLockRange); + Aim(e, float2(2, 1)); + e.Shooter.SetTarget(TargetRef.None); + origin = e.Shooter.Position; + FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); + return e; + } + + [Fact] + public void RoundAtIsTheFiredLine() + { + // Burst round: leaves the shooter, is where its own speed puts it a second in, and ends on the burst point. + var e = FireBurstRound(out var origin); + var burst = SafeAssert.OnlyShot(e.Zone); + var aim = normalize(float2(2, 1)); + + var start = FireControl.RoundAt(burst, burst.FireTime); + Assert.Equal(origin.x, start.x, 3); + Assert.Equal(origin.z, start.y, 3); + + var second = FireControl.RoundAt(burst, burst.FireTime + 1f); + Assert.Equal(origin.x + aim.x * BallisticSpeed, second.x, 3); + Assert.Equal(origin.z + aim.y * BallisticSpeed, second.y, 3); + + var arrival = FireControl.RoundAt(burst, burst.ArrivalTime); + Assert.Equal(burst.BurstPosition.x, arrival.x, 3); + Assert.Equal(burst.BurstPosition.z, arrival.y, 3); + + // Past the range it is held at the FireRange point, however late it is asked. + var held = FireControl.RoundAt(burst, burst.FireTime + NoLockRange / BallisticSpeed + 50f); + Assert.Equal(origin.x + aim.x * NoLockRange, held.x, 3); + Assert.Equal(origin.z + aim.y * NoLockRange, held.y, 3); + // Before it was fired it is still at the origin, not behind it. + var before = FireControl.RoundAt(burst, burst.FireTime - 5f); + Assert.Equal(origin.x, before.x, 3); + Assert.Equal(origin.z, before.y, 3); + + // Direct round at a stationary target 100 ahead (+z): arrives on the target, and holds at the range point. + var d = Build(TestSettings(), SolidShape(5, 4), velocity: BallisticSpeed, weaponRange: 1000f); + var dOrigin = d.Shooter.Position; + FireControl.Fire(d.Weapon, d.WeaponItem, d.Shooter); + var direct = SafeAssert.OnlyShot(d.Zone); + var hit = FireControl.RoundAt(direct, direct.ArrivalTime); + Assert.Equal(d.Target.Position.x, hit.x, 2); + Assert.Equal(d.Target.Position.z, hit.y, 2); + Assert.Equal(dOrigin.z + 100f / 2f, FireControl.RoundAt(direct, direct.FireTime + 2.5f).y, 2); + var heldDirect = FireControl.RoundAt(direct, direct.FireTime + direct.FireRange / BallisticSpeed + 99f); + Assert.Equal(dOrigin.z + direct.FireRange, heldDirect.y, 2); + } + + // A round with no speed (the weapon authors Velocity 0, or effectively 0) does not fly: it is at its origin at any + // time, and is not stretched along its line by a vanishing speed over the range it would need forever to cover. + [Fact] + public void ARoundWithNoSpeedStaysAtItsOrigin() + { + var e = Build(TestSettings(), SolidShape(5, 4), velocity: .005f, weaponRange: 1000f); + var origin = e.Shooter.Position; + FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); + var shot = SafeAssert.OnlyShot(e.Zone); + + var late = FireControl.RoundAt(shot, shot.FireTime + 5000f); + Assert.Equal(origin.x, late.x, 3); + Assert.Equal(origin.z, late.y, 3); + } + + [Fact] + public void SpeedIsFrozenAtFire() + { + var e = FireBurstRound(out var origin); + var shot = SafeAssert.OnlyShot(e.Zone); + Assert.Equal(BallisticSpeed, shot.Speed, 4); + + // The stat changes after the round left, and the weapon re-reads it. + ((InstantWeaponData) e.Weapon.WeaponData).Velocity = Constant(BallisticSpeed * 5f); + e.Weapon.Execute(0f); + Assert.Equal(BallisticSpeed * 5f, e.Weapon.Velocity, 4); + + var after = SafeAssert.OnlyShot(e.Zone); + Assert.Equal(BallisticSpeed, after.Speed, 4); + var aim = normalize(float2(2, 1)); + var at = FireControl.RoundAt(after, after.FireTime + 1f); + Assert.Equal(origin.x + aim.x * BallisticSpeed, at.x, 3); + Assert.Equal(origin.z + aim.y * BallisticSpeed, at.y, 3); + } + + [Fact] + public void RoundAtIsPure() + { + var e = FireBurstRound(out _); + var shot = SafeAssert.OnlyShot(e.Zone); + var time = e.Zone.Time; + var entities = e.Zone.Entities.ToList(); + var positions = entities.Select(x => x.Position).ToList(); + var velocities = entities.Select(x => x.Velocity).ToList(); + + for (var i = 0; i < 25; i++) + FireControl.RoundAt(shot, shot.FireTime + i * .37f); + + Assert.Equal(time, e.Zone.Time); + Assert.True(e.Zone.PendingShots.Count == 1 && e.Zone.PendingShots[0].Equals(shot), "the pending shot changed"); + Assert.True(e.Zone.Entities.SequenceEqual(entities), "the zone's entities changed"); + Assert.True(entities.Select(x => x.Position).SequenceEqual(positions), "an entity moved"); + Assert.True(entities.Select(x => x.Velocity).SequenceEqual(velocities), "an entity's velocity changed"); + } +} From aaab561f988090e10f72784838d9f0722dee53e2 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:19:22 +0200 Subject: [PATCH 084/120] Consumable host: initialise at activation, dispose at expiry, instantiate consumables ActivateConsumable runs Initialize on the effect's behaviours; expiry disposes them before Resolver.Forget, so a consumable's modifiers exist exactly while its effect is active. ItemManager.CreateInstance returns a ConsumableItem for a consumable design. StatResolver.DetachModifier drops an entry left with nothing attached, so ModifierEntryCount returns to baseline. LoadoutTests' hand-built ConsumableItem goes through CreateInstance. Builds. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/Entity.cs | 13 ++++++++++++- Assets/Scripts/ServerShared/ItemManager.cs | 3 +++ Assets/Scripts/ServerShared/StatResolver.cs | 3 +++ tests/Aetheria.Shared.Tests/ConsumableTests.cs | 2 +- tests/Aetheria.Shared.Tests/LoadoutTests.cs | 6 +----- 5 files changed, 20 insertions(+), 7 deletions(-) diff --git a/Assets/Scripts/ServerShared/Entity.cs b/Assets/Scripts/ServerShared/Entity.cs index 1da6e68d8..9c4929058 100644 --- a/Assets/Scripts/ServerShared/Entity.cs +++ b/Assets/Scripts/ServerShared/Entity.cs @@ -860,7 +860,13 @@ public abstract class Entity public void ActivateConsumable(ConsumableItem item) { - _activeConsumables.Add(new ConsumableItemEffect(item, this)); + var effect = new ConsumableItemEffect(item, this); + // The same hook Activate runs over Equipment: a behaviour that computes its targets there (StatModifier) + // would otherwise reach its second tick uninitialised. + foreach (var behavior in effect.Behaviors) + if (behavior is IInitializableBehavior initializable) + initializable.Initialize(); + _activeConsumables.Add(effect); } public ConsumableItemEffect FindActiveConsumable(ConsumableItemData data) @@ -1453,6 +1459,11 @@ public abstract class Entity // Cut 2 Gate 1 fix (docs/stats-and-power-cut.md): an expired consumable dropped out of this // list without ever telling the resolver, leaving its generation/cache/modifier entries // reachable (keyed by this ConsumableItemEffect instance) for the rest of the process. + // A StatModifier's attachments are keyed by its target items, not by the effect, so Forget alone + // leaves a boost attached to its targets: dispose each behaviour first. + foreach (var behavior in _activeConsumables[i].Behaviors) + if (behavior is IDisposable disposable) + disposable.Dispose(); Resolver.Forget(_activeConsumables[i]); _activeConsumables.RemoveAt(i--); } diff --git a/Assets/Scripts/ServerShared/ItemManager.cs b/Assets/Scripts/ServerShared/ItemManager.cs index d9c42d02a..884179e4e 100644 --- a/Assets/Scripts/ServerShared/ItemManager.cs +++ b/Assets/Scripts/ServerShared/ItemManager.cs @@ -183,6 +183,9 @@ public class ItemManager }; } + if (design is ConsumableItemData) + return new ConsumableItem { Data = l.Design, Lot = lot }; + return new CompoundCommodity { Data = l.Design, Lot = lot diff --git a/Assets/Scripts/ServerShared/StatResolver.cs b/Assets/Scripts/ServerShared/StatResolver.cs index bbd3ea542..7062fbd9e 100644 --- a/Assets/Scripts/ServerShared/StatResolver.cs +++ b/Assets/Scripts/ServerShared/StatResolver.cs @@ -141,6 +141,9 @@ public class StatResolver { set.Scale.Remove(modifierKey); set.Constant.Remove(modifierKey); + // An entry with nothing attached is no entry: a modifier that is gone leaves no trace in the resolver. + if (set.Scale.Count == 0 && set.Constant.Count == 0) + _modifiers.Remove(entry); } _cache.Remove(entry); } diff --git a/tests/Aetheria.Shared.Tests/ConsumableTests.cs b/tests/Aetheria.Shared.Tests/ConsumableTests.cs index 3fdd6aad3..885e4e111 100644 --- a/tests/Aetheria.Shared.Tests/ConsumableTests.cs +++ b/tests/Aetheria.Shared.Tests/ConsumableTests.cs @@ -113,7 +113,7 @@ public sealed class ConsumableTests : IDisposable Assert.Equal(20f, rig.Engine.Evaluate(rig.Thrust), 3); Assert.True(resolver.ModifierEntryCount > baseline); - Tick(rig, 3, .3f); // 0.9s elapsed in all: expired on the third of these ticks + Tick(rig, 3, .3f); // 1.5s elapsed in all: the effect ran out on the fourth tick Assert.Equal(10f, rig.Engine.Evaluate(rig.Thrust), 3); Assert.Equal(baseline, resolver.ModifierEntryCount); } diff --git a/tests/Aetheria.Shared.Tests/LoadoutTests.cs b/tests/Aetheria.Shared.Tests/LoadoutTests.cs index 1f2c9c842..303ca8f06 100644 --- a/tests/Aetheria.Shared.Tests/LoadoutTests.cs +++ b/tests/Aetheria.Shared.Tests/LoadoutTests.cs @@ -760,11 +760,7 @@ public sealed class LoadoutTests : IDisposable var items = new ItemManager(cache, new ProvenanceLedger(), RunSaveTests.TestSettings(), _ => { }); var hullItem = (EquippableItem) items.CreateInstance(items.CreateLot(hullData, maker, .5f)); var ship = new Ship(items, null, hullItem, new EntitySettings()); - // ItemManager.CreateInstance has no ConsumableItemData branch (it only special-cases EquippableItemData, - // falling back to CompoundCommodity otherwise); build the instance directly, as that method does for - // EquippableItem. - var consumableLot = items.CreateLot(consumableData, maker, .5f); - var consumableItem = new ConsumableItem { Data = cache.RefOf(consumableData), Lot = consumableLot }; + var consumableItem = (ConsumableItem) items.CreateInstance(items.CreateLot(consumableData, maker, .5f)); var effect = new ConsumableItemEffect(consumableItem, ship); effect.Update(5f); // half the duration elapsed -> progress .5 -> effectiveness .5 on a linear ramp From aa9288d9deb564c58ea3fb239f21e30859a2f2af Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:19:23 +0200 Subject: [PATCH 085/120] Projectile is drawn on FireControl.RoundAt and integrates nothing Deletes the Time.deltaTime gravity/drag/position integration and the range-distance kill from Projectile and the UnityEngine.Random jitter, barrel direction and inherited velocity from ProjectileManager. A round holds a copy of its PendingShot and draws at RoundAt(Zone.Time + Clock.Lead) with a barrel offset decaying over BlendTime; ShotResolved ends a hit or burst at the outcome's BurstPoint, a miss flies to the FireRange clamp. Unity compile not yet run at this commit. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/Gameplay/Weapons/Projectile.cs | 100 +++++++++++------- .../Gameplay/Weapons/ProjectileManager.cs | 27 ++--- 2 files changed, 70 insertions(+), 57 deletions(-) diff --git a/Assets/Scripts/Gameplay/Weapons/Projectile.cs b/Assets/Scripts/Gameplay/Weapons/Projectile.cs index 70b591649..4e38ad549 100644 --- a/Assets/Scripts/Gameplay/Weapons/Projectile.cs +++ b/Assets/Scripts/Gameplay/Weapons/Projectile.cs @@ -1,59 +1,87 @@ -using System.Collections; +using System; +using System.Collections; +using UniRx; using UnityEngine; using static CultMath.math; -// Cut 3 (docs/fire-control-cut.md): the raycast, shield branch and SendHit are deleted -- FireControl already -// decided this shot's fate before this object was ever spawned (R8). What is left is pure flight-path -// presentation: fly, and disappear at Range or ShotId's resolution, whichever comes first. -// Cut 6b, 6.2 (12.4(b): Apply's own switch on the frozen Fuse decides this now, not Step): the flak-round -// blast resolves in the simulation, through FireControl.Detonate -- a MonoBehaviour calling it would be -// exactly the authority Cut 3 spent itself deleting, so the blast-distance/blast-radius fields this object -// used to carry for that are gone. This object's job stays fly-and-disappear. +// A ballistic round, drawn on FireControl.RoundAt and deciding nothing. The simulation owns the round's whole flight +// (origin, direction, speed, fire time, arrival, range: the PendingShot copy taken at spawn), so this object stores no +// velocity or position of its own and integrates nothing: each frame it asks RoundAt where the round is at sim time +// plus the clock's Lead (the same draw-ahead ships use), and the simulation's ShotResolved ends a round that hit or +// burst. A round that missed flies on to the FireRange point RoundAt clamps at, and is done there. The barrel is only +// where the round is first seen: its offset from the sim line decays to zero over BlendTime sim seconds. The trail's +// fade after the end stays real time (pure presentation, ruling sim-speed-presentation). public class Projectile : MonoBehaviour { public TrailRenderer Trail; - public float Gravity; - public float Drag = .1f; public Prototype HitEffect; + // Sim seconds over which the barrel's offset from the sim line decays to nothing. + public float BlendTime = .1f; + + private PendingShot _shot; + private Zone _zone; + private Vector3 _barrelOffset; private bool _alive; + private IDisposable _binding; - // Cut 3: FireControl.Fire's ShotId -- this projectile's handle onto Zone.ShotCommitted/ShotResolved. - public int ShotId { get; set; } - public Zone Zone { get; set; } - - public Vector3 StartPosition { get; set; } - public Vector3 Velocity { get; set; } - public Entity SourceEntity { get; set; } - public float Range { get; set; } - - private void OnEnable() + // Takes the frozen shot, the zone whose sim time it is drawn at, and where the barrel is as this round is first seen. + public void Launch(Zone zone, PendingShot shot, Vector3 barrel) { + _zone = zone; + _shot = shot; + var origin = shot.FireOrigin; + _barrelOffset = barrel - new Vector3(origin.x, origin.y, origin.z); + _binding?.Dispose(); + _binding = zone.ShotResolved.Where(o => o.ShotId == shot.ShotId).Subscribe(Resolve); _alive = true; + transform.forward = new Vector3(shot.TravelDirection.x, 0, shot.TravelDirection.y); + transform.position = barrel; + } + + private Vector3 At(CultMath.float2 planar, float time) + { + var blend = BlendTime > 0 ? saturate(1 - (time - _shot.FireTime) / BlendTime) : 0f; + return new Vector3(planar.x, _shot.FireOrigin.y, planar.y) + _barrelOffset * blend; } - // Update is called once per frame void Update() { - if (SourceEntity == null) return; - - if(_alive) - { - var t = transform; - Velocity -= Vector3.up * (Gravity * Time.deltaTime); - Velocity *= max(0, 1 - Drag * Time.deltaTime); - t.forward = Velocity.normalized; - - transform.position += Velocity * Time.deltaTime; - var distanceTraveled = (transform.position - StartPosition).magnitude; - if(distanceTraveled > Range) - StartCoroutine(Kill()); - } + if (!_alive) return; + var time = _zone.Time + ActionGameManager.Clock.Lead; + transform.position = At(FireControl.RoundAt(_shot, time), time); + var end = _shot.Speed > .01f ? _shot.FireTime + _shot.FireRange / _shot.Speed : _shot.FireTime; + if (time >= end) Finish(); } - IEnumerator Kill() + // The simulation's verdict. A round that hit or burst stops where the sim put it, at the outcome's burst point, + // else where it arrives on its line; a miss keeps flying. + private void Resolve(ShotOutcome outcome) + { + if (!_alive || (outcome.Result != ShotResult.Hit && outcome.Result != ShotResult.Burst)) return; + var planar = outcome.HasBurstPoint ? outcome.BurstPoint : FireControl.RoundAt(_shot, _shot.ArrivalTime); + transform.position = At(planar, _shot.ArrivalTime); + if (HitEffect != null) HitEffect.Instantiate().position = transform.position; + Finish(); + } + + private void Finish() { _alive = false; + _binding?.Dispose(); + _binding = null; + StartCoroutine(Fade()); + } + + void OnDisable() + { + _alive = false; + _binding?.Dispose(); + _binding = null; + } + + IEnumerator Fade() + { var startTime = Time.time; var lifetime = Trail.time; while (Time.time - startTime < lifetime) diff --git a/Assets/Scripts/Gameplay/Weapons/ProjectileManager.cs b/Assets/Scripts/Gameplay/Weapons/ProjectileManager.cs index 56cfdcf80..83eba9ea4 100644 --- a/Assets/Scripts/Gameplay/Weapons/ProjectileManager.cs +++ b/Assets/Scripts/Gameplay/Weapons/ProjectileManager.cs @@ -1,32 +1,17 @@ -using System.Collections; -using System.Collections.Generic; -using UnityEngine; -using Random = UnityEngine.Random; +using UnityEngine; public class ProjectileManager : InstantWeaponEffectManager { public Prototype ProjectilePrototype; - public bool InheritVelocity; + // The round is drawn on the line the simulation flies it (FireControl.RoundAt), so there is nothing to aim, jitter + // or inherit here: spread is priced by FireControl, and the sim's round inherits nothing from its shooter. public override void Fire(InstantWeapon weapon, EquippedItem item, EntityInstance source, EntityInstance target, int shotId) { + var zone = source.Entity.Zone; + if (!zone.TryGetShot(shotId, out var shot)) return; var p = ProjectilePrototype.Instantiate(); - var hp = item.Hardpoint; - var barrel = source.GetBarrel(hp); - var angle = weapon.Spread / 2; - p.ShotId = shotId; - p.SourceEntity = source.Entity; - p.Velocity = Quaternion.Euler( - Random.Range(-angle, angle), - Random.Range(-angle, angle), - Random.Range(-angle, angle)) * - barrel.forward * - weapon.Velocity; - p.StartPosition = p.transform.position = barrel.position + p.Velocity * (Random.value * Time.deltaTime); - if(InheritVelocity) - p.Velocity += new Vector3(source.Entity.Velocity.x, 0, source.Entity.Velocity.y); - p.Range = weapon.Range; - p.Zone = source.Entity.Zone; + p.Launch(zone, shot, source.GetBarrel(item.Hardpoint).position); p.Trail.Clear(); } } From 6dcf33d11cefd4b65c0e2f958eedf54ff103cd58 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:22:00 +0200 Subject: [PATCH 086/120] Vapour clouds: Zone owns them, a VapourDump behaviour vents one (union 42) Zone.Vent is the one adder, StepClouds the one remover, Zone.Obscuration the one reader: the product of (1 - opacity) over clouds whose disc the segment touches (closest point of the segment to the centre within Radius), 1f when none. Derived from live clouds at each call, never cached. VapourDump vents once on its first run, drifting with its venter. Nothing reads Obscuration yet (Sensor.Gain takes it in the next commit); this commit builds. Co-Authored-By: Claude Opus 5.5 --- .../ServerShared/Behaviors/Behaviors.cs | 1 + .../ServerShared/Behaviors/VapourDump.cs | 61 ++++ .../ServerShared/Behaviors/VapourDump.cs.meta | 11 + Assets/Scripts/ServerShared/VapourClouds.cs | 31 ++ .../Scripts/ServerShared/VapourClouds.cs.meta | 11 + Assets/Scripts/ServerShared/Zone.cs | 38 +++ .../Aetheria.Shared.Tests/VapourCloudTests.cs | 273 ++++++++++++++++++ 7 files changed, 426 insertions(+) create mode 100644 Assets/Scripts/ServerShared/Behaviors/VapourDump.cs create mode 100644 Assets/Scripts/ServerShared/Behaviors/VapourDump.cs.meta create mode 100644 Assets/Scripts/ServerShared/VapourClouds.cs create mode 100644 Assets/Scripts/ServerShared/VapourClouds.cs.meta create mode 100644 tests/Aetheria.Shared.Tests/VapourCloudTests.cs diff --git a/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs b/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs index 9f2e292a6..7515d55e7 100644 --- a/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs +++ b/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs @@ -187,6 +187,7 @@ public interface IPowerConsumer // superseded draft quoted -- checked directly here rather than trusted from that document. Union(39, typeof(TargetingSystemData)), Union(40, typeof(MineLayerData)), + Union(42, typeof(VapourDumpData)), JsonObject(MemberSerialization.OptIn)] public abstract class BehaviorData { diff --git a/Assets/Scripts/ServerShared/Behaviors/VapourDump.cs b/Assets/Scripts/ServerShared/Behaviors/VapourDump.cs new file mode 100644 index 000000000..3e035f9b8 --- /dev/null +++ b/Assets/Scripts/ServerShared/Behaviors/VapourDump.cs @@ -0,0 +1,61 @@ +using MessagePack; +using Newtonsoft.Json; + +// A coolant dump that becomes a vapour cloud: the first time it runs it vents one cloud around its venter, +// drifting with the venter's velocity. Radius, Opacity and Lifetime are evaluated through the host, so role +// quality applies as for any other stat. Later runs vent nothing. +[Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn), RuntimeInspectable] +public class VapourDumpData : BehaviorData +{ + [Inspectable, JsonProperty("radius"), Key(1), RuntimeInspectable] + public PerformanceStat Radius = new PerformanceStat(); + + // How much of a sight line's signal a fresh cloud removes, in [0, 1]. + [Inspectable, JsonProperty("opacity"), Key(2), RuntimeInspectable] + public PerformanceStat Opacity = new PerformanceStat(); + + // Seconds from venting until the cloud has faded away and is removed. + [Inspectable, JsonProperty("lifetime"), Key(3), RuntimeInspectable] + public PerformanceStat Lifetime = new PerformanceStat(); + + public override Behavior CreateInstance(EquippedItem item) + { + return new VapourDump(this, item); + } + + public override Behavior CreateInstance(ConsumableItemEffect item) + { + return new VapourDump(this, item); + } +} + +public class VapourDump : Behavior +{ + private VapourDumpData _data; + private bool _vented; + + public VapourDump(VapourDumpData data, EquippedItem item) : base(data, item) + { + _data = data; + } + + public VapourDump(VapourDumpData data, ConsumableItemEffect item) : base(data, item) + { + _data = data; + } + + public override bool Execute(float dt) + { + if (_vented) return true; + _vented = true; + Entity.Zone.Vent(new VapourCloud + { + Body = new KinematicBody { Position = Entity.Position.xz, Velocity = Entity.Velocity }, + Radius = Evaluate(_data.Radius), + Opacity = Evaluate(_data.Opacity), + Lifetime = Evaluate(_data.Lifetime), + Venter = Entity + }); + return true; + } +} diff --git a/Assets/Scripts/ServerShared/Behaviors/VapourDump.cs.meta b/Assets/Scripts/ServerShared/Behaviors/VapourDump.cs.meta new file mode 100644 index 000000000..86ef08913 --- /dev/null +++ b/Assets/Scripts/ServerShared/Behaviors/VapourDump.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 961a74683c574807a300eb26694f206d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/VapourClouds.cs b/Assets/Scripts/ServerShared/VapourClouds.cs new file mode 100644 index 000000000..d6d1aaa24 --- /dev/null +++ b/Assets/Scripts/ServerShared/VapourClouds.cs @@ -0,0 +1,31 @@ +using CultMath; +using static CultMath.math; +using float2 = CultMath.float2; + +// A vented vapour cloud: a free-floating disc that thins every sensor sight line through it. Not an Entity, +// never priced or rolled, never saved. Zone.Vent is the only adder and Zone's cloud step the only remover; +// Zone.Obscuration is the one reader detection uses. +public sealed class VapourCloud +{ + public FloatingBodyId Id; + public KinematicBody Body; + public double VentedAt; + public float Lifetime; + public float Radius; + // The opacity at the moment of venting; it fades linearly to nothing over Lifetime. + public float Opacity; + public Entity Venter; + + public float OpacityAt(double now) => + Lifetime <= 0f ? 0f : saturate(Opacity) * saturate(1f - (float) (now - VentedAt) / Lifetime); + + // Whether the segment a-b reaches the disc: the closest point of the segment to the centre lies within the + // radius. A cloud past either end of the segment touches it only if it is near that end. + public bool Touches(float2 a, float2 b) + { + var ab = b - a; + var length2 = lengthsq(ab); + var t = length2 > 0f ? saturate(dot(Body.Position - a, ab) / length2) : 0f; + return lengthsq(Body.Position - (a + ab * t)) <= Radius * Radius; + } +} diff --git a/Assets/Scripts/ServerShared/VapourClouds.cs.meta b/Assets/Scripts/ServerShared/VapourClouds.cs.meta new file mode 100644 index 000000000..cdaa4b80d --- /dev/null +++ b/Assets/Scripts/ServerShared/VapourClouds.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ced45a9738974452b0065b57ee527e8a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/Zone.cs b/Assets/Scripts/ServerShared/Zone.cs index df07d1f57..0aa822775 100644 --- a/Assets/Scripts/ServerShared/Zone.cs +++ b/Assets/Scripts/ServerShared/Zone.cs @@ -20,6 +20,10 @@ public class Zone // Laid mines, in lay order. Zone owns their existence: Lay is the one adder, the mine step the one remover, // and every removal is a blast. Runtime state, never saved. public ReactiveCollection Mines = new ReactiveCollection(); + + // Vented vapour clouds, in vent order. Zone owns their existence: Vent is the one adder, StepClouds the one + // remover, and Obscuration the one reader that detection uses. Runtime state, never saved. + public ReactiveCollection Clouds = new ReactiveCollection(); public Dictionary Planets = new Dictionary(); public Dictionary PlanetInstances = new Dictionary(); @@ -225,6 +229,7 @@ public class Zone foreach (var entity in Entities.ToArray()) entity.Update(deltaTime); StepMines(deltaTime); + StepClouds(deltaTime); // Cut 3: after every entity has had its chance to fire this tick, age and resolve the shots that // firing queued. A shot fired this tick with a flight time shorter than CommitHorizon commits and @@ -238,6 +243,39 @@ public class Zone Mines.Add(mine); } + public void Vent(VapourCloud cloud) + { + cloud.Id = NextBodyId(); + cloud.VentedAt = _time; + Clouds.Add(cloud); + } + + // The fraction of a sight line's signal that survives the clouds between `a` and `b`: the product of + // (1 - opacity now) over every cloud whose disc the segment touches, so 1f when none does. Derived from the + // live clouds at each call and stored nowhere; Sensor.Gain is where it enters detection. + public float Obscuration(float2 a, float2 b) + { + var through = 1f; + for (var i = 0; i < Clouds.Count; i++) + { + var cloud = Clouds[i]; + if (cloud.Touches(a, b)) through *= 1f - cloud.OpacityAt(_time); + } + return through; + } + + // After StepMines: each cloud drifts as a KinematicBody on the zone's force, as a mine does, and is removed + // once its lifetime has run out. Nothing else removes a cloud. + private void StepClouds(float deltaTime) + { + var settings = _itemManager.GameplaySettings; + foreach (var cloud in Clouds.ToArray()) + { + cloud.Body.Step(deltaTime, GetForce(cloud.Body.Position), settings); + if (cloud.VentedAt + cloud.Lifetime <= _time) Clouds.Remove(cloud); + } + } + // After the entity loop and before FireControl.Step, in lay order: each mine drifts as a KinematicBody, is // triggered by the first eligible hull its armed disc touches, and detonates through FireControl.Detonate at // its trigger plus fuse delay or at its lifetime, whatever its layer does (ruling mine-owner-leash). The diff --git a/tests/Aetheria.Shared.Tests/VapourCloudTests.cs b/tests/Aetheria.Shared.Tests/VapourCloudTests.cs new file mode 100644 index 000000000..d829de565 --- /dev/null +++ b/tests/Aetheria.Shared.Tests/VapourCloudTests.cs @@ -0,0 +1,273 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using CultMath; +using GameCult.Caching; +using UniRx; +using Xunit; +using static CultMath.math; +using float2 = CultMath.float2; +using float3 = CultMath.float3; +using int2 = CultMath.int2; + +// Vapour clouds in the sim (aetheria-release cut vapour-cloud, ruling vapour-cloud-obscuration): a VapourDump +// behaviour vents a zone cloud that thins every sensor sight line through it, both ways, through Sensor.Gain's +// one detection rule; a lock falls by the existing detection-threshold rule and nothing else. The scene is two +// ships facing each other along z with a fixed visibility, so what a sensor gathers is a number the clouds +// alone change: steady info is visibility x sensitivity / (distance x TargetInfoDecay), and visibility 100, +// sensitivity 1, decay .5 puts it at .5 at range 400 (above the .1 threshold) and saturated at range 40. +public sealed class VapourCloudTests : IDisposable +{ + private const float Dt = .05f; + private const float Threshold = .1f; + private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-vapour-" + Guid.NewGuid().ToString("N")); + private readonly List _caches = new List(); + private static readonly object Glow = new object(); + + public VapourCloudTests() => Directory.CreateDirectory(_root); + + public void Dispose() + { + foreach (var c in _caches) c.Dispose(); + Directory.Delete(_root, true); + } + + private static PerformanceStat Constant(float v) => new PerformanceStat { Min = v, Max = v }; + + private static Shape Solid(int w, int h) + { + var shape = new Shape(w, h); + foreach (var cell in shape.AllCoordinates) shape[cell] = true; + return shape; + } + + private sealed class Lab + { + public ItemManager Items; + public Zone Zone; + public Ship Observer; + public Ship Target; + public EquippedItem Dump; + public LockWeapon Lock; + public int Lot = 1; + public bool HoldTarget = true; + } + + private Lab Build(float distance, float radius = 20f, float opacity = .9f, float lifetime = 1000f) + { + var settings = new GameplaySettings + { + DefaultEntitySettings = new EntitySettings(), + Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } }, + QualityPriceModifier = new ExponentialLerp(), + TargetDetectionInfoThreshold = Threshold, TargetArmorInfoThreshold = .2f, TargetGearInfoThreshold = .8f, + TargetInfoDecay = .5f, VisibilityDecay = 0f, + FiringArc = 170, CommitHorizon = .5f, SchematicCellSize = 1f + }; + var cache = AetheriaStores.Open(Path.Combine(_root, Guid.NewGuid().ToString("N") + ".cc"), catalogWritable: true); + _caches.Add(cache); + cache.Upsert(new TestCatalogGlobal { Name = "Temperament" }); + cache.Upsert(new HullData + { + Name = "Hull", HullType = HullType.Ship, Shape = Solid(5, 5), Durability = 1000000, Mass = 1000, + Hardpoints = { new HardpointData { Type = HardpointType.Sensors, Position = new int2(0, 0), Shape = new Shape() } } + }); + GearData Gear(string name, HardpointType hardpoint, BehaviorData behavior) => new GearData + { + Name = name, Hardpoint = hardpoint, Shape = new Shape(), Durability = 1, + MinimumTemperature = -1000, MaximumTemperature = 1000, OptimalTemperature = 0, PlateauWidth = 2000, + Behaviors = { behavior } + }; + cache.Upsert(Gear("Eye", HardpointType.Tool, new SensorData + { + Sensitivity = Constant(1f), + SensitivityCurve = new BezierCurve { Keys = new[] { float4(0, 1, 0, 0), float4(1, 1, 0, 0) } }, + PingBoost = Constant(.02f), PingEnergy = Constant(0f), PingVisibility = Constant(5f), + PingRange = Constant(300f), PingCooldown = Constant(4f) + })); + cache.Upsert(Gear("Vent", HardpointType.Tool, new VapourDumpData + { + Radius = Constant(radius), Opacity = Constant(opacity), Lifetime = Constant(lifetime) + })); + cache.Upsert(Gear("LockGun", HardpointType.Sensors, new LockWeaponData + { + Damage = Constant(0), Range = Constant(1000), MinRange = Constant(0), Velocity = Constant(0), + Spread = Constant(0), DamageSpread = Constant(0), Penetration = Constant(0), Count = Constant(1), + BurstTime = Constant(0), Cooldown = Constant(1000), + DamageCurve = new BezierCurve { Keys = new[] { float4(0, 1, 0, 0), float4(1, 1, 0, 0) } }, + LockSpeed = Constant(1), SensorImpact = Constant(1), LockAngle = Constant(170), + DirectionImpact = Constant(1), Decay = Constant(1) + })); + cache.FlushAsync().Wait(); + + var ledger = new ProvenanceLedger(); + for (var i = 1; i <= 50; i++) ledger.Lots[i] = new Lot { Origin = new Attributed(), Quality = 1 }; + var items = new ItemManager(cache, ledger, settings, _ => { }); + var zone = new Zone(items, new PlanetSettings(), new ZonePack { Radius = 5000 }, new GalaxyZone { Name = "Vapour", Owner = null }, null); + var lab = new Lab { Items = items, Zone = zone }; + + EquippableItem Make(string name) => new EquippableItem + { + Data = items.ItemData.RefOf(items.ItemData.GetByName(name)), Durability = 1, Lot = lab.Lot++ + }; + Ship NewShip() + { + var hull = items.ItemData.RefOf(items.ItemData.GetByName("Hull")); + var ship = new Ship(items, zone, new EquippableItem { Data = hull, Durability = 1000000, Lot = lab.Lot++ }, new EntitySettings()); + Assert.True(ship.TryEquip(Make("Eye"))); + return ship; + } + + lab.Observer = NewShip(); + Assert.True(lab.Observer.TryEquip(Make("LockGun"), new int2(0, 0))); + lab.Target = NewShip(); + var dump = Make("Vent"); + Assert.True(lab.Target.TryEquip(dump)); + lab.Dump = lab.Target.Equipment.Single(x => x.EquippableItem == dump); + // Held off until a test pulls the trigger: the dump vents on its first run. + lab.Dump.Enabled.Value = false; + + zone.Entities.Add(lab.Observer); + zone.Entities.Add(lab.Target); + lab.Observer.Activate(); + lab.Target.Activate(); + lab.Observer.Position = float3(0, 0, 0); + lab.Observer.Aim = float3(0, 0, 1); + lab.Target.Position = float3(0, 0, distance); + lab.Target.Aim = float3(0, 0, -1); + lab.Observer.SetIff(lab.Target, true); + lab.Target.SetIff(lab.Observer, true); + zone.Update(0f); + lab.Lock = lab.Observer.Weapons.OfType().Single(); + return lab; + } + + // One tick: both ships stay where they were put, facing each other, each a fixed visibility to the other's sensor. + private static void Tick(Lab lab) + { + foreach (var ship in new[] { lab.Observer, lab.Target }) + { + ship.VisibilitySources[Glow] = 100f; + if (ship != lab.Target || lab.HoldTarget) ship.Velocity = float2.zero; + } + lab.Observer.Direction = float2(0, 1); + lab.Target.Direction = float2(0, -1); + lab.Zone.Update(Dt); + } + + private static void Run(Lab lab, int ticks) + { + for (var i = 0; i < ticks; i++) Tick(lab); + } + + private static float Info(Ship watcher, Ship watched) => watcher.EntityInfoGathered[watched]; + + // The observer has settled on the target and holds a full lock on it: the state a cloud has to break. + private static void Establish(Lab lab) + { + Run(lab, 100); + Assert.True(Info(lab.Observer, lab.Target) > Threshold, "the observer never gathered the target"); + Assert.Contains(lab.Target, lab.Observer.VisibleEnemies); + lab.Observer.SetTarget(lab.Target); + Run(lab, 100); + Assert.True(lab.Lock.IsLocked, "the observer never locked the target"); + Assert.Empty(lab.Zone.Clouds); + } + + private static void Vent(Lab lab) + { + lab.Dump.Enabled.Value = true; + for (var i = 0; i < 5 && lab.Zone.Clouds.Count == 0; i++) Tick(lab); + Assert.Single(lab.Zone.Clouds); + } + + private static VapourCloud Hand(Zone zone, float2 at, float radius, float opacity, float lifetime = 1000f) + { + var cloud = new VapourCloud { Body = new KinematicBody { Position = at }, Radius = radius, Opacity = opacity, Lifetime = lifetime }; + zone.Vent(cloud); + return cloud; + } + + [Fact] + public void ACloudFadesAndIsRemoved() + { + var lab = Build(400f); + var cloud = Hand(lab.Zone, float2(0, 0), 10f, .8f, lifetime: 10f); + var a = float2(-5, 0); + var b = float2(5, 0); + Assert.Equal(.2f, lab.Zone.Obscuration(a, b), 5); + + for (var i = 0; i < 5; i++) lab.Zone.Update(1f); + Assert.Equal(.4f, cloud.OpacityAt(lab.Zone.Time), 5); + Assert.Equal(.6f, lab.Zone.Obscuration(a, b), 5); + Assert.Single(lab.Zone.Clouds); + + for (var i = 0; i < 5; i++) lab.Zone.Update(1f); + Assert.Empty(lab.Zone.Clouds); + Assert.Equal(1f, lab.Zone.Obscuration(a, b)); + } + + [Fact] + public void ACloudWithNoLifetimeObscuresNothing() + { + var lab = Build(400f); + var cloud = Hand(lab.Zone, float2(0, 0), 10f, .8f, lifetime: 0f); + Assert.Equal(0f, cloud.OpacityAt(lab.Zone.Time)); + Assert.Equal(1f, lab.Zone.Obscuration(float2(-5, 0), float2(5, 0))); + } + + [Fact] + public void AnEffectVentsOnce() + { + var lab = Build(400f); + lab.HoldTarget = false; + Run(lab, 2); + lab.Target.Velocity = float2(3, 0); + VapourCloud vented = null; + float2 venterVelocity = float2.zero; + float2 venterAt = float2.zero; + using var sub = lab.Zone.Clouds.ObserveAdd().Subscribe(add => + { + vented = add.Value; + venterVelocity = lab.Target.Velocity; + venterAt = lab.Target.Position.xz; + }); + + lab.Dump.Enabled.Value = true; + Run(lab, 100); + + Assert.Single(lab.Zone.Clouds); + Assert.Same(vented, lab.Zone.Clouds.Single()); + Assert.True(length(venterVelocity) > 0f, "the venter was not moving"); + Assert.Equal(venterVelocity, vented.Body.Velocity); + Assert.Equal(venterAt, vented.Body.Position); + Assert.Same(lab.Target, vented.Venter); + } + + [Fact] + public void ACloudThinsOnlyTheSightLinesItTouches() + { + var lab = Build(400f); + Hand(lab.Zone, float2(0, 0), 10f, .6f); + float Through(float2 a, float2 b) => lab.Zone.Obscuration(a, b); + + Assert.Equal(.4f, Through(float2(-50, 0), float2(50, 0)), 5); + Assert.Equal(.4f, Through(float2(50, 0), float2(-50, 0)), 5); + // Passing wide of the centre but within the radius, neither end inside the disc. + Assert.Equal(.4f, Through(float2(-50, 9.9f), float2(50, 9.9f)), 5); + Assert.Equal(1f, Through(float2(-50, 10.1f), float2(50, 10.1f))); + // Ending inside the disc, and a point inside it (the observer inside its own cloud). + Assert.Equal(.4f, Through(float2(-50, 0), float2(5, 0)), 5); + Assert.Equal(.4f, Through(float2(3, 0), float2(3, 0)), 5); + Assert.Equal(1f, Through(float2(30, 0), float2(30, 0))); + // A segment that stops short of the disc, or starts past it, touches nothing even on the same line. + Assert.Equal(1f, Through(float2(-50, 0), float2(-20, 0))); + Assert.Equal(1f, Through(float2(20, 0), float2(50, 0))); + + // Two clouds on one sight line multiply. + Hand(lab.Zone, float2(30, 0), 5f, .5f); + Assert.Equal(.2f, Through(float2(-50, 0), float2(50, 0)), 5); + Assert.Equal(.4f, Through(float2(-50, 0), float2(15, 0)), 5); + } +} From 0fb5ac1dfc19fad4e2095aabf693820140394529 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:22:16 +0200 Subject: [PATCH 087/120] Sensor.Gain takes the obscuration of its sight line Gain stays the one detection rule: both branches are scaled by the fraction of the signal that survives the clouds between observer and target (Zone.Obscuration, exactly 1f with none, so the pre-cloud trace is bit-identical). The entity loop passes it for pings and passive ticks and PassiveRate for the chunk position. Entity.cs is untouched: a lock falls by the existing detection-threshold rule when info does. Adds the sensor-level facts to VapourCloudTests. Builds; not yet run (verify job follows). Co-Authored-By: Claude Opus 5.5 --- .../Scripts/ServerShared/Behaviors/Sensor.cs | 20 +++-- .../Aetheria.Shared.Tests/VapourCloudTests.cs | 79 +++++++++++++++++++ 2 files changed, 91 insertions(+), 8 deletions(-) diff --git a/Assets/Scripts/ServerShared/Behaviors/Sensor.cs b/Assets/Scripts/ServerShared/Behaviors/Sensor.cs index 8eeadbb7d..d1af7645e 100644 --- a/Assets/Scripts/ServerShared/Behaviors/Sensor.cs +++ b/Assets/Scripts/ServerShared/Behaviors/Sensor.cs @@ -160,17 +160,18 @@ public class Sensor : Behavior, IEventBehavior, IPowerConsumer var diff = entity.Position.xz - Entity.Position.xz; var dist = length(diff); + var obscuration = Entity.Zone.Obscuration(Entity.Position.xz, entity.Position.xz); float previous, next; Entity.EntityInfoGathered.TryGetValue(entity, out previous); if (!_pingedEntities.Contains(entity) && dist < _pingRadius) { _pingedEntities.Add(entity); - next = saturate(previous + Gain(entity.Visibility, Evaluate(_data.Sensitivity), Evaluate(_data.PingBoost), dist, dt, true)); + next = saturate(previous + Gain(entity.Visibility, Evaluate(_data.Sensitivity), Evaluate(_data.PingBoost), dist, dt, true, obscuration)); } else { var angle = acos(dot(forward, normalize(diff))); - next = saturate(previous + Gain(entity.Visibility, Evaluate(_data.Sensitivity), _data.SensitivityCurve.Evaluate(angle / PI), dist, dt, false)); + next = saturate(previous + Gain(entity.Visibility, Evaluate(_data.Sensitivity), _data.SensitivityCurve.Evaluate(angle / PI), dist, dt, false, obscuration)); } next *= 1 - ItemManager.GameplaySettings.TargetInfoDecay * dt; //Context.Log($"{entity.Name} visibility {(int)(previous * 100)}% -> {(int)(next * 100)}%"); @@ -182,12 +183,14 @@ public class Sensor : Behavior, IEventBehavior, IPowerConsumer // Mining Cut 3 (docs/mining-cut-refresh.md): the one detection gain rule -- the info one observation of a // target adds. A ping adds `visibility × sensitivity × PingBoost × distance` once; a passive tick adds // `visibility × sensitivity × curve(angle/π) × dt / distance`. `response` is PingBoost for a ping and the - // sensitivity curve at the bearing for a passive tick. The entity loop above and the chunk query - // (Entity.ChunkInfo, through PassiveRate) both call this; nothing else multiplies these terms. - public static float Gain(float visibility, float sensitivity, float response, float distance, float dt, bool ping) => - ping + // sensitivity curve at the bearing for a passive tick. `obscuration` is the fraction of the signal that + // survives the vapour clouds on the sight line (Zone.Obscuration, 1f with none), scaling both. The entity + // loop above and the chunk query (Entity.ChunkInfo, through PassiveRate) both call this; nothing else + // multiplies these terms. + public static float Gain(float visibility, float sensitivity, float response, float distance, float dt, bool ping, float obscuration) => + (ping ? visibility * sensitivity * response * distance - : visibility * sensitivity * response * dt / distance; + : visibility * sensitivity * response * dt / distance) * obscuration; // The passive gain per second this sensor draws from a target of `visibility` at the planar `position`: // Gain over one second, with the bearing measured from this sensor's own facing exactly as Execute does. @@ -195,7 +198,8 @@ public class Sensor : Behavior, IEventBehavior, IPowerConsumer { var diff = position - Entity.Position.xz; var angle = acos(dot(Direction.xz, normalize(diff))); - return Gain(visibility, Evaluate(_data.Sensitivity), _data.SensitivityCurve.Evaluate(angle / PI), length(diff), 1f, false); + return Gain(visibility, Evaluate(_data.Sensitivity), _data.SensitivityCurve.Evaluate(angle / PI), length(diff), 1f, false, + Entity.Zone.Obscuration(Entity.Position.xz, position)); } // An upper bound on sensitivity x response over every bearing, so PassiveRate(v, p) <= v x this / distance diff --git a/tests/Aetheria.Shared.Tests/VapourCloudTests.cs b/tests/Aetheria.Shared.Tests/VapourCloudTests.cs index d829de565..af80b0143 100644 --- a/tests/Aetheria.Shared.Tests/VapourCloudTests.cs +++ b/tests/Aetheria.Shared.Tests/VapourCloudTests.cs @@ -189,6 +189,56 @@ public sealed class VapourCloudTests : IDisposable return cloud; } + [Fact] + public void ACloudBreaksALockAtRange() + { + var lab = Build(400f); + Establish(lab); + + Vent(lab); + var ticks = 0; + while (ticks++ < 600 && !lab.Observer.Target.Value.IsNone) Tick(lab); + + Assert.True(lab.Observer.Target.Value.IsNone, "the cloud never broke the target"); + Assert.True(Info(lab.Observer, lab.Target) < Threshold, $"info {Info(lab.Observer, lab.Target)} is above the threshold"); + Run(lab, 3); + Assert.Equal(0f, lab.Lock.Lock); + Assert.DoesNotContain(lab.Target, lab.Observer.VisibleEnemies); + } + + [Fact] + public void AStrongCloseSensorBurnsThrough() + { + var lab = Build(40f); + Establish(lab); + var before = Info(lab.Observer, lab.Target); + + Vent(lab); + Run(lab, 600); + + var after = Info(lab.Observer, lab.Target); + Assert.True(after < before - .2f, $"the cloud thinned nothing: {before} -> {after}"); + Assert.True(after > Threshold, $"info {after} fell below the threshold"); + Assert.Equal(lab.Target, lab.Observer.Target.Value.Entity); + Assert.True(lab.Lock.IsLocked); + } + + [Fact] + public void TheCloudBlindsItsVenter() + { + var lab = Build(400f); + Run(lab, 100); + Assert.True(Info(lab.Target, lab.Observer) > Threshold, "the venter never gathered the observer"); + Assert.Contains(lab.Observer, lab.Target.VisibleEnemies); + + Vent(lab); + Run(lab, 300); + + Assert.True(Info(lab.Target, lab.Observer) < Threshold); + Assert.DoesNotContain(lab.Observer, lab.Target.VisibleEnemies); + Assert.Equal(Info(lab.Observer, lab.Target), Info(lab.Target, lab.Observer), 5); + } + [Fact] public void ACloudFadesAndIsRemoved() { @@ -270,4 +320,33 @@ public sealed class VapourCloudTests : IDisposable Assert.Equal(.2f, Through(float2(-50, 0), float2(50, 0)), 5); Assert.Equal(.4f, Through(float2(-50, 0), float2(15, 0)), 5); } + + // Where the rule is decided: one tick from nothing, the info gathered is exactly what survives the cloud + // times what the open sight line gathers, in both directions. + [Fact] + public void TheGainIsScaledByWhatSurvivesTheCloud() + { + var clear = Build(100f); + Tick(clear); + var cloudy = Build(100f); + Hand(cloudy.Zone, float2(0, 100), 20f, .6f); + Tick(cloudy); + + Assert.True(Info(clear.Observer, clear.Target) > 0f); + Assert.Equal(.4f, Info(cloudy.Observer, cloudy.Target) / Info(clear.Observer, clear.Target), 3); + Assert.Equal(.4f, Info(cloudy.Target, cloudy.Observer) / Info(clear.Target, clear.Observer), 3); + } + + // The chunk path reads the same rule: the passive rate toward a point is scaled by the same survivor fraction. + [Fact] + public void TheChunkRateIsScaledByTheSameFactor() + { + var lab = Build(100f); + var at = float2(0, 100); + var open = lab.Observer.Sensor.PassiveRate(100f, at); + Assert.True(open > 0f); + + Hand(lab.Zone, at, 20f, .6f); + Assert.Equal(.4f, lab.Observer.Sensor.PassiveRate(100f, at) / open, 5); + } } From 2640faa9a6dd035c5987a41f3faceedc7d416b5b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:24:06 +0200 Subject: [PATCH 088/120] VapourCloudTests: drift, ping, wire shape and authored stats Pins the cloud's drift and fade-from-vent-time, the ping branch of Gain, the dump's authored radius/opacity/lifetime and its union 42 keys 1-3. Test-only; builds. Co-Authored-By: Claude Opus 5.5 --- .../Aetheria.Shared.Tests/VapourCloudTests.cs | 82 +++++++++++++++++-- 1 file changed, 76 insertions(+), 6 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/VapourCloudTests.cs b/tests/Aetheria.Shared.Tests/VapourCloudTests.cs index af80b0143..c2300264e 100644 --- a/tests/Aetheria.Shared.Tests/VapourCloudTests.cs +++ b/tests/Aetheria.Shared.Tests/VapourCloudTests.cs @@ -4,6 +4,8 @@ using System.IO; using System.Linq; using CultMath; using GameCult.Caching; +using GameCult.Caching.MessagePack; +using MessagePack; using UniRx; using Xunit; using static CultMath.math; @@ -83,7 +85,7 @@ public sealed class VapourCloudTests : IDisposable { Sensitivity = Constant(1f), SensitivityCurve = new BezierCurve { Keys = new[] { float4(0, 1, 0, 0), float4(1, 1, 0, 0) } }, - PingBoost = Constant(.02f), PingEnergy = Constant(0f), PingVisibility = Constant(5f), + PingBoost = Constant(.00002f), PingEnergy = Constant(0f), PingVisibility = Constant(5f), PingRange = Constant(300f), PingCooldown = Constant(4f) })); cache.Upsert(Gear("Vent", HardpointType.Tool, new VapourDumpData @@ -243,6 +245,8 @@ public sealed class VapourCloudTests : IDisposable public void ACloudFadesAndIsRemoved() { var lab = Build(400f); + // Vented 3s into the zone's life: the fade runs from the vent, not from the zone's start. + for (var i = 0; i < 3; i++) lab.Zone.Update(1f); var cloud = Hand(lab.Zone, float2(0, 0), 10f, .8f, lifetime: 10f); var a = float2(-5, 0); var b = float2(5, 0); @@ -258,6 +262,21 @@ public sealed class VapourCloudTests : IDisposable Assert.Equal(1f, lab.Zone.Obscuration(a, b)); } + [Fact] + public void ACloudDriftsWithItsVelocityUntilItFades() + { + var lab = Build(400f); + var cloud = new VapourCloud { Body = new KinematicBody { Position = float2(0, 0), Velocity = float2(10, 0) }, Radius = 5f, Opacity = .5f, Lifetime = 1000f }; + lab.Zone.Vent(cloud); + Assert.Equal(.5f, lab.Zone.Obscuration(float2(0, -20), float2(0, 20)), 5); + + lab.Zone.Update(1f); + + Assert.Equal(10f, cloud.Body.Position.x, 3); + Assert.Equal(1f, lab.Zone.Obscuration(float2(0, -20), float2(0, 20))); + Assert.Equal(.5f, lab.Zone.Obscuration(float2(10, -20), float2(10, 20)), 2); + } + [Fact] public void ACloudWithNoLifetimeObscuresNothing() { @@ -270,7 +289,7 @@ public sealed class VapourCloudTests : IDisposable [Fact] public void AnEffectVentsOnce() { - var lab = Build(400f); + var lab = Build(400f, radius: 17f, opacity: .7f, lifetime: 123f); lab.HoldTarget = false; Run(lab, 2); lab.Target.Velocity = float2(3, 0); @@ -293,13 +312,17 @@ public sealed class VapourCloudTests : IDisposable Assert.Equal(venterVelocity, vented.Body.Velocity); Assert.Equal(venterAt, vented.Body.Position); Assert.Same(lab.Target, vented.Venter); + // Each stat is the authored one, evaluated through the host. + Assert.Equal(17f, vented.Radius); + Assert.Equal(.7f, vented.Opacity); + Assert.Equal(123f, vented.Lifetime); } [Fact] public void ACloudThinsOnlyTheSightLinesItTouches() { var lab = Build(400f); - Hand(lab.Zone, float2(0, 0), 10f, .6f); + var first = Hand(lab.Zone, float2(0, 0), 10f, .6f); float Through(float2 a, float2 b) => lab.Zone.Obscuration(a, b); Assert.Equal(.4f, Through(float2(-50, 0), float2(50, 0)), 5); @@ -316,7 +339,8 @@ public sealed class VapourCloudTests : IDisposable Assert.Equal(1f, Through(float2(20, 0), float2(50, 0))); // Two clouds on one sight line multiply. - Hand(lab.Zone, float2(30, 0), 5f, .5f); + var second = Hand(lab.Zone, float2(30, 0), 5f, .5f); + Assert.NotEqual(first.Id, second.Id); Assert.Equal(.2f, Through(float2(-50, 0), float2(50, 0)), 5); Assert.Equal(.4f, Through(float2(-50, 0), float2(15, 0)), 5); } @@ -329,7 +353,8 @@ public sealed class VapourCloudTests : IDisposable var clear = Build(100f); Tick(clear); var cloudy = Build(100f); - Hand(cloudy.Zone, float2(0, 100), 20f, .6f); + // Mid-way between them and clear of both: only the segment, not either end, touches it. + Hand(cloudy.Zone, float2(0, 50), 10f, .6f); Tick(cloudy); Assert.True(Info(clear.Observer, clear.Target) > 0f); @@ -337,6 +362,33 @@ public sealed class VapourCloudTests : IDisposable Assert.Equal(.4f, Info(cloudy.Target, cloudy.Observer) / Info(clear.Target, clear.Observer), 3); } + // A ping is gathered through the same rule: with a cloud on the sight line the whole trace, ping included, is + // what survives the cloud times the open trace (info is linear in the gain while nothing saturates). + [Fact] + public void ThePingGainIsScaledToo() + { + var clear = Build(100f); + var cloudy = Build(100f); + Hand(cloudy.Zone, float2(0, 50), 10f, .6f); + Run(clear, 2); + Run(cloudy, 2); + clear.Observer.Sensor.Ping(); + cloudy.Observer.Sensor.Ping(); + + var biggestJump = 0f; + var last = Info(clear.Observer, clear.Target); + for (var i = 0; i < 12; i++) + { + Tick(clear); + Tick(cloudy); + var open = Info(clear.Observer, clear.Target); + biggestJump = max(biggestJump, open - last); + last = open; + Assert.Equal(.4f, Info(cloudy.Observer, cloudy.Target) / open, 2); + } + Assert.True(biggestJump > .15f, $"the ping left no mark on the trace ({biggestJump})"); + } + // The chunk path reads the same rule: the passive rate toward a point is scaled by the same survivor fraction. [Fact] public void TheChunkRateIsScaledByTheSameFactor() @@ -346,7 +398,25 @@ public sealed class VapourCloudTests : IDisposable var open = lab.Observer.Sensor.PassiveRate(100f, at); Assert.True(open > 0f); - Hand(lab.Zone, at, 20f, .6f); + Hand(lab.Zone, float2(0, 50), 10f, .6f); Assert.Equal(.4f, lab.Observer.Sensor.PassiveRate(100f, at) / open, 5); } + + // The dump's wire shape: BehaviorData union 42, its radius, opacity and lifetime at keys 1, 2 and 3. + [Fact] + public void TheDumpIsUnion42WithRadiusOpacityLifetimeAtKeys1To3() + { + var options = CultDocumentMessagePackSerialization.OptionsFor(typeof(ZonePack).Assembly); + var data = new VapourDumpData { Radius = Constant(1f), Opacity = Constant(2f), Lifetime = Constant(3f) }; + var bytes = MessagePackSerializer.Serialize(data, options); + + var reader = new MessagePackReader(bytes); + Assert.Equal(2, reader.ReadArrayHeader()); + Assert.Equal(42, reader.ReadInt32()); + Assert.Equal(4, reader.ReadArrayHeader()); + reader.Skip(); + Assert.Equal(1f, MessagePackSerializer.Deserialize(ref reader, options).Min); + Assert.Equal(2f, MessagePackSerializer.Deserialize(ref reader, options).Min); + Assert.Equal(3f, MessagePackSerializer.Deserialize(ref reader, options).Min); + } } From f79cf8b7a77dfb742f55592033a47c67ec1d3284 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:29:27 +0200 Subject: [PATCH 089/120] ConsumableTests: a hold with room for two consumables The 1x1 interior fixture could not store the second consumable the non-stackable test needs. Builds. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/ConsumableTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Aetheria.Shared.Tests/ConsumableTests.cs b/tests/Aetheria.Shared.Tests/ConsumableTests.cs index 885e4e111..740f739ff 100644 --- a/tests/Aetheria.Shared.Tests/ConsumableTests.cs +++ b/tests/Aetheria.Shared.Tests/ConsumableTests.cs @@ -54,7 +54,7 @@ public sealed class ConsumableTests : IDisposable Name = "Engine", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Durability = 10, Behaviors = { new ThrusterData { Thrust = thrust } } }); - var interior = new Shape(1, 1); + var interior = new Shape(2, 1); // room for two consumables foreach (var cell in interior.AllCoordinates) interior[cell] = true; var holdRef = cache.Upsert(new CargoBayData { Name = "Hold", Shape = new Shape(), InteriorShape = interior, Durability = 10 }); var consumableRef = cache.Upsert(new ConsumableItemData From 0c29ea1768ecb8f43fe992e3fbc7cbab7dd32db3 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:32:00 +0200 Subject: [PATCH 090/120] ThrottleLockTests: place thrusters on interior hull cells The first commit's tests failed in setup (border cells are not interior); it builds, its three new tests were red. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/ThrottleLockTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs b/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs index 1673e2be1..db2ad9361 100644 --- a/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs +++ b/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs @@ -75,8 +75,8 @@ public sealed class ThrottleLockTests : IDisposable var ship = new Ship(items, zone, Mint(cache, items, cache.GetByName("Skiff")), new EntitySettings()); var gear = cache.GetByName("Thruster"); Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.Reversed), new int2(2, 1))); - Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.None), new int2(0, 2))); - Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.None), new int2(4, 2))); + Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.None), new int2(1, 2))); + Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.None), new int2(3, 2))); Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.CounterClockwise), new int2(3, 3))); Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.Clockwise), new int2(1, 3))); zone.Entities.Add(ship); From 2b03d908127ffda1f640daf9f70b6792284cea59 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:35:37 +0200 Subject: [PATCH 091/120] RoundAt clamps at the weapon's frozen Range (PendingShot.MaxRange), not FireRange FireRange is the distance to the engaged target and 0 for a round with none, so a no-lock burst round and a round that misses never flew. MaxRange = weapon.Range is frozen at Fire beside Speed. Unity compile not yet run. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/Gameplay/Weapons/Projectile.cs | 4 ++-- Assets/Scripts/ServerShared/FireControl.cs | 8 ++++++-- tests/Aetheria.Shared.Tests/BallisticFlightTests.cs | 8 ++++---- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Assets/Scripts/Gameplay/Weapons/Projectile.cs b/Assets/Scripts/Gameplay/Weapons/Projectile.cs index 4e38ad549..64d84fb4e 100644 --- a/Assets/Scripts/Gameplay/Weapons/Projectile.cs +++ b/Assets/Scripts/Gameplay/Weapons/Projectile.cs @@ -8,7 +8,7 @@ using static CultMath.math; // (origin, direction, speed, fire time, arrival, range: the PendingShot copy taken at spawn), so this object stores no // velocity or position of its own and integrates nothing: each frame it asks RoundAt where the round is at sim time // plus the clock's Lead (the same draw-ahead ships use), and the simulation's ShotResolved ends a round that hit or -// burst. A round that missed flies on to the FireRange point RoundAt clamps at, and is done there. The barrel is only +// burst. A round that missed flies on to the MaxRange point RoundAt clamps at, and is done there. The barrel is only // where the round is first seen: its offset from the sim line decays to zero over BlendTime sim seconds. The trail's // fade after the end stays real time (pure presentation, ruling sim-speed-presentation). public class Projectile : MonoBehaviour @@ -50,7 +50,7 @@ public class Projectile : MonoBehaviour if (!_alive) return; var time = _zone.Time + ActionGameManager.Clock.Lead; transform.position = At(FireControl.RoundAt(_shot, time), time); - var end = _shot.Speed > .01f ? _shot.FireTime + _shot.FireRange / _shot.Speed : _shot.FireTime; + var end = _shot.Speed > .01f ? _shot.FireTime + _shot.MaxRange / _shot.Speed : _shot.FireTime; if (time >= end) Finish(); } diff --git a/Assets/Scripts/ServerShared/FireControl.cs b/Assets/Scripts/ServerShared/FireControl.cs index 097c3cc79..40e3e25a8 100644 --- a/Assets/Scripts/ServerShared/FireControl.cs +++ b/Assets/Scripts/ServerShared/FireControl.cs @@ -515,12 +515,12 @@ public static class FireControl } // Where a ballistic round is at a sim time: the one statement of the round's line, from its frozen origin along its - // frozen direction at its frozen speed, held at the FireRange point once it has flown that far. Pure: it reads the + // frozen direction at its frozen speed, held at the MaxRange point once it has flown that far. Pure: it reads the // shot alone, and the simulation's own arithmetic (Step, Commit, Apply) does not call it. Presenters draw rounds here. public static float2 RoundAt(in PendingShot shot, float time) { if (shot.Speed <= .01f) return shot.FireOrigin.xz; - var flown = clamp(time - shot.FireTime, 0f, shot.FireRange / shot.Speed); + var flown = clamp(time - shot.FireTime, 0f, shot.MaxRange / shot.Speed); return shot.FireOrigin.xz + shot.TravelDirection * (shot.Speed * flown); } @@ -766,6 +766,7 @@ public static class FireControl FireRange = fireRange, FireTime = now, Speed = weapon.Velocity, + MaxRange = weapon.Range, FireOrigin = origin, FireTargetPosition = targetPosition, FireTargetVelocity = targetVelocity, @@ -2012,6 +2013,9 @@ public struct PendingShot public float FireTime; // The weapon's Velocity at Fire: frozen with the rest, so RoundAt states the same line whatever happens to the stat later. public float Speed; + // The weapon's Range at Fire, likewise frozen: how far the round can fly, which RoundAt clamps at. FireRange above is the + // distance to the engaged target (0 for a round with none), not how far a round that misses flies on. + public float MaxRange; public float CommitTime; public float ArrivalTime; diff --git a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs index cd3582d70..c9589a0db 100644 --- a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs +++ b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs @@ -48,7 +48,7 @@ public sealed partial class FireControlCut124Tests Assert.Equal(burst.BurstPosition.x, arrival.x, 3); Assert.Equal(burst.BurstPosition.z, arrival.y, 3); - // Past the range it is held at the FireRange point, however late it is asked. + // Past the weapon's range it is held at the range point, however late it is asked. var held = FireControl.RoundAt(burst, burst.FireTime + NoLockRange / BallisticSpeed + 50f); Assert.Equal(origin.x + aim.x * NoLockRange, held.x, 3); Assert.Equal(origin.z + aim.y * NoLockRange, held.y, 3); @@ -57,7 +57,7 @@ public sealed partial class FireControlCut124Tests Assert.Equal(origin.x, before.x, 3); Assert.Equal(origin.z, before.y, 3); - // Direct round at a stationary target 100 ahead (+z): arrives on the target, and holds at the range point. + // Direct round at a stationary target 100 ahead (+z): arrives on the target, and a miss would fly on to the weapon's range. var d = Build(TestSettings(), SolidShape(5, 4), velocity: BallisticSpeed, weaponRange: 1000f); var dOrigin = d.Shooter.Position; FireControl.Fire(d.Weapon, d.WeaponItem, d.Shooter); @@ -66,8 +66,8 @@ public sealed partial class FireControlCut124Tests Assert.Equal(d.Target.Position.x, hit.x, 2); Assert.Equal(d.Target.Position.z, hit.y, 2); Assert.Equal(dOrigin.z + 100f / 2f, FireControl.RoundAt(direct, direct.FireTime + 2.5f).y, 2); - var heldDirect = FireControl.RoundAt(direct, direct.FireTime + direct.FireRange / BallisticSpeed + 99f); - Assert.Equal(dOrigin.z + direct.FireRange, heldDirect.y, 2); + var heldDirect = FireControl.RoundAt(direct, direct.FireTime + 1000f / BallisticSpeed + 99f); + Assert.Equal(dOrigin.z + 1000f, heldDirect.y, 2); } // A round with no speed (the weapon authors Velocity 0, or effectively 0) does not fly: it is at its origin at any From 42c403dc326960f00c6affc75c69ba7311eb2506 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:39:24 +0200 Subject: [PATCH 092/120] VapourCloudTests: settled-state ratios, drag-aware drift, capture at vent Fixes the fixtures the first verify run falsified: the sensor's start-up seed made one-tick ratios meaningless (compare settled infos and the settled ping extra), launch drag moves a cloud less than its velocity, and the vent's own velocity is read when the cloud is added, not after it has drifted. Test-only; builds. Co-Authored-By: Claude Opus 5.5 --- .../Aetheria.Shared.Tests/VapourCloudTests.cs | 77 +++++++++++-------- 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/VapourCloudTests.cs b/tests/Aetheria.Shared.Tests/VapourCloudTests.cs index c2300264e..08f24848a 100644 --- a/tests/Aetheria.Shared.Tests/VapourCloudTests.cs +++ b/tests/Aetheria.Shared.Tests/VapourCloudTests.cs @@ -85,7 +85,7 @@ public sealed class VapourCloudTests : IDisposable { Sensitivity = Constant(1f), SensitivityCurve = new BezierCurve { Keys = new[] { float4(0, 1, 0, 0), float4(1, 1, 0, 0) } }, - PingBoost = Constant(.00002f), PingEnergy = Constant(0f), PingVisibility = Constant(5f), + PingBoost = Constant(.000004f), PingEnergy = Constant(0f), PingVisibility = Constant(5f), PingRange = Constant(300f), PingCooldown = Constant(4f) })); cache.Upsert(Gear("Vent", HardpointType.Tool, new VapourDumpData @@ -238,7 +238,8 @@ public sealed class VapourCloudTests : IDisposable Assert.True(Info(lab.Target, lab.Observer) < Threshold); Assert.DoesNotContain(lab.Observer, lab.Target.VisibleEnemies); - Assert.Equal(Info(lab.Observer, lab.Target), Info(lab.Target, lab.Observer), 5); + // (The observer updates before the venter in the tick that vents, so it is one tick behind.) + Assert.Equal(Info(lab.Observer, lab.Target), Info(lab.Target, lab.Observer), 3); } [Fact] @@ -272,9 +273,10 @@ public sealed class VapourCloudTests : IDisposable lab.Zone.Update(1f); - Assert.Equal(10f, cloud.Body.Position.x, 3); + // The launch velocity decays on the zone's launch drag, so it has moved most of the way and no further. + Assert.InRange(cloud.Body.Position.x, 5f, 9.9f); Assert.Equal(1f, lab.Zone.Obscuration(float2(0, -20), float2(0, 20))); - Assert.Equal(.5f, lab.Zone.Obscuration(float2(10, -20), float2(10, 20)), 2); + Assert.Equal(.5f, lab.Zone.Obscuration(float2(cloud.Body.Position.x, -20), float2(cloud.Body.Position.x, 20)), 2); } [Fact] @@ -296,9 +298,13 @@ public sealed class VapourCloudTests : IDisposable VapourCloud vented = null; float2 venterVelocity = float2.zero; float2 venterAt = float2.zero; + float2 cloudVelocity = float2.zero; + float2 cloudAt = float2.zero; using var sub = lab.Zone.Clouds.ObserveAdd().Subscribe(add => { vented = add.Value; + cloudVelocity = add.Value.Body.Velocity; + cloudAt = add.Value.Body.Position; venterVelocity = lab.Target.Velocity; venterAt = lab.Target.Position.xz; }); @@ -309,8 +315,8 @@ public sealed class VapourCloudTests : IDisposable Assert.Single(lab.Zone.Clouds); Assert.Same(vented, lab.Zone.Clouds.Single()); Assert.True(length(venterVelocity) > 0f, "the venter was not moving"); - Assert.Equal(venterVelocity, vented.Body.Velocity); - Assert.Equal(venterAt, vented.Body.Position); + Assert.Equal(venterVelocity, cloudVelocity); + Assert.Equal(venterAt, cloudAt); Assert.Same(lab.Target, vented.Venter); // Each stat is the authored one, evaluated through the host. Assert.Equal(17f, vented.Radius); @@ -345,48 +351,51 @@ public sealed class VapourCloudTests : IDisposable Assert.Equal(.4f, Through(float2(-50, 0), float2(15, 0)), 5); } - // Where the rule is decided: one tick from nothing, the info gathered is exactly what survives the cloud - // times what the open sight line gathers, in both directions. + // Where the rule is decided: once the start-up seed has decayed away, the info a sensor settles at is what + // survives the cloud times what the open sight line settles at, in both directions. [Fact] public void TheGainIsScaledByWhatSurvivesTheCloud() { - var clear = Build(100f); - Tick(clear); - var cloudy = Build(100f); + var clear = Build(400f); + var cloudy = Build(400f); // Mid-way between them and clear of both: only the segment, not either end, touches it. - Hand(cloudy.Zone, float2(0, 50), 10f, .6f); - Tick(cloudy); + Hand(cloudy.Zone, float2(0, 200), 10f, .6f, lifetime: 1e6f); + Run(clear, 400); + Run(cloudy, 400); - Assert.True(Info(clear.Observer, clear.Target) > 0f); + Assert.True(Info(clear.Observer, clear.Target) > Threshold); Assert.Equal(.4f, Info(cloudy.Observer, cloudy.Target) / Info(clear.Observer, clear.Target), 3); Assert.Equal(.4f, Info(cloudy.Target, cloudy.Observer) / Info(clear.Target, clear.Observer), 3); } - // A ping is gathered through the same rule: with a cloud on the sight line the whole trace, ping included, is - // what survives the cloud times the open trace (info is linear in the gain while nothing saturates). + // A ping is gathered through the same rule. Settled, the tick that a ping reaches the target adds the ping gain + // in place of the passive one; that extra, in a lab with a cloud on the sight line, is what survives the + // cloud times the extra in the open (info is linear in the gain while nothing saturates). [Fact] public void ThePingGainIsScaledToo() { - var clear = Build(100f); - var cloudy = Build(100f); - Hand(cloudy.Zone, float2(0, 50), 10f, .6f); - Run(clear, 2); - Run(cloudy, 2); - clear.Observer.Sensor.Ping(); - cloudy.Observer.Sensor.Ping(); - - var biggestJump = 0f; - var last = Info(clear.Observer, clear.Target); - for (var i = 0; i < 12; i++) + float PingExtra(Lab lab) { - Tick(clear); - Tick(cloudy); - var open = Info(clear.Observer, clear.Target); - biggestJump = max(biggestJump, open - last); - last = open; - Assert.Equal(.4f, Info(cloudy.Observer, cloudy.Target) / open, 2); + Run(lab, 400); + lab.Observer.Sensor.Ping(); + var biggest = 0f; + var last = Info(lab.Observer, lab.Target); + for (var i = 0; i < 38; i++) + { + Tick(lab); + biggest = max(biggest, Info(lab.Observer, lab.Target) - last); + last = Info(lab.Observer, lab.Target); + } + return biggest; } - Assert.True(biggestJump > .15f, $"the ping left no mark on the trace ({biggestJump})"); + + var clear = Build(250f); + var cloudy = Build(250f); + Hand(cloudy.Zone, float2(0, 125), 10f, .6f, lifetime: 1e6f); + + var open = PingExtra(clear); + Assert.True(open > .05f, $"the ping left no mark on the open trace ({open})"); + Assert.Equal(.4f, PingExtra(cloudy) / open, 2); } // The chunk path reads the same rule: the passive rate toward a point is scaled by the same survivor fraction. From 84f9cd39e5232937a1ba3dddc22d962f83f35af3 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:40:48 +0200 Subject: [PATCH 093/120] VapourCloudTests: settled ratio to two places The settled ratio carries the seed's last residue (.4007 against .4); two places still separates .4 from the squared .16. Test-only; builds. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/VapourCloudTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/VapourCloudTests.cs b/tests/Aetheria.Shared.Tests/VapourCloudTests.cs index 08f24848a..71fc3cc2a 100644 --- a/tests/Aetheria.Shared.Tests/VapourCloudTests.cs +++ b/tests/Aetheria.Shared.Tests/VapourCloudTests.cs @@ -364,8 +364,8 @@ public sealed class VapourCloudTests : IDisposable Run(cloudy, 400); Assert.True(Info(clear.Observer, clear.Target) > Threshold); - Assert.Equal(.4f, Info(cloudy.Observer, cloudy.Target) / Info(clear.Observer, clear.Target), 3); - Assert.Equal(.4f, Info(cloudy.Target, cloudy.Observer) / Info(clear.Target, clear.Observer), 3); + Assert.Equal(.4f, Info(cloudy.Observer, cloudy.Target) / Info(clear.Observer, clear.Target), 2); + Assert.Equal(.4f, Info(cloudy.Target, cloudy.Observer) / Info(clear.Target, clear.Observer), 2); } // A ping is gathered through the same rule. Settled, the tick that a ping reaches the target adds the ping gain From 015e6d86bd2abbf68b4d9627689d62a155c39918 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:51:30 +0200 Subject: [PATCH 094/120] ThrottleLockTests: strafe thrusters in mirrored pairs A lone off-axis strafe thruster cancels its own torque in Ship.Update, so its axis read 0 unlocked; pairs leave the strafe axis plain. The previous commit's AnUnlockedShipFollowsIntent was red for this. Co-Authored-By: Claude Opus 5.5 --- .../ThrottleLockTests.cs | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs b/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs index db2ad9361..9a1b7ac6d 100644 --- a/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs +++ b/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs @@ -41,7 +41,8 @@ public sealed class ThrottleLockTests : IDisposable public ItemManager Items; public Ship Ship; public ConsumableItemData Lock; - public Thruster Forward, Reverse, Right, Left, Clockwise; + public Thruster Forward, Reverse, Clockwise; + public Thruster[] Right, Left; // pairs on either side of the centre line: a lone off-axis strafe thruster cancels its own torque public void ActivateLock() => Ship.ActivateConsumable(new ConsumableItem { @@ -77,8 +78,10 @@ public sealed class ThrottleLockTests : IDisposable Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.Reversed), new int2(2, 1))); Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.None), new int2(1, 2))); Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.None), new int2(3, 2))); + Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.CounterClockwise), new int2(1, 3))); Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.CounterClockwise), new int2(3, 3))); - Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.Clockwise), new int2(1, 3))); + Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.Clockwise), new int2(1, 1))); + Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.Clockwise), new int2(3, 1))); zone.Entities.Add(ship); ship.Aim = float3(0, 0, 1); ship.Activate(); @@ -88,8 +91,8 @@ public sealed class ThrottleLockTests : IDisposable { Cache = cache, Items = items, Ship = ship, Lock = cache.GetByName("Overdrive"), Forward = thrusters.Single(t => t.Item.EquippableItem.Rotation == ItemRotation.Reversed), - Right = thrusters.Single(t => t.Item.EquippableItem.Rotation == ItemRotation.CounterClockwise), - Left = thrusters.Single(t => t.Item.EquippableItem.Rotation == ItemRotation.Clockwise), + Right = thrusters.Where(t => t.Item.EquippableItem.Rotation == ItemRotation.CounterClockwise).ToArray(), + Left = thrusters.Where(t => t.Item.EquippableItem.Rotation == ItemRotation.Clockwise).ToArray(), Reverse = thrusters.First(t => t.Item.EquippableItem.Rotation == ItemRotation.None && t.Torque <= 0), Clockwise = thrusters.First(t => t.Item.EquippableItem.Rotation == ItemRotation.None && t.Torque > 0) }; @@ -112,8 +115,8 @@ public sealed class ThrottleLockTests : IDisposable f.Ship.Update(0.01f); Assert.Equal(1f, f.Forward.Axis); Assert.Equal(0f, f.Reverse.Axis); - Assert.Equal(0f, f.Right.Axis); - Assert.Equal(0f, f.Left.Axis); + Assert.All(f.Right, t => Assert.Equal(0f, t.Axis)); + Assert.All(f.Left, t => Assert.Equal(0f, t.Axis)); } // Turning stays with the pilot: the clockwise-torque thruster follows Turn while the lock holds. @@ -162,10 +165,15 @@ public sealed class ThrottleLockTests : IDisposable f.Ship.MovementDirection = float2(1, 0); f.Ship.Update(0.01f); - Assert.Equal(1f, f.Right.Axis); - Assert.Equal(0f, f.Left.Axis); + Assert.All(f.Right, t => Assert.Equal(1f, t.Axis)); + Assert.All(f.Left, t => Assert.Equal(0f, t.Axis)); Assert.Equal(0f, f.Forward.Axis); + f.Ship.MovementDirection = float2(-1, 0); + f.Ship.Update(0.01f); + Assert.All(f.Left, t => Assert.Equal(1f, t.Axis)); + Assert.All(f.Right, t => Assert.Equal(0f, t.Axis)); + f.Ship.MovementDirection = float2(0, 1); f.Ship.Update(0.01f); Assert.Equal(1f, f.Forward.Axis); From 8c549719c0ce74f5a4518cb2c1e10bbaf3cdcbf5 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:54:20 +0200 Subject: [PATCH 095/120] ThrottleLockTests: strafe pairs placed for opposing torque Pairs mirrored across the ship's width had equal torque and the compensation still cancelled one; the previous commit's AnUnlockedShipFollowsIntent was red. Mirror across its length instead. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/ThrottleLockTests.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs b/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs index 9a1b7ac6d..03f7b1060 100644 --- a/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs +++ b/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs @@ -42,7 +42,7 @@ public sealed class ThrottleLockTests : IDisposable public Ship Ship; public ConsumableItemData Lock; public Thruster Forward, Reverse, Clockwise; - public Thruster[] Right, Left; // pairs on either side of the centre line: a lone off-axis strafe thruster cancels its own torque + public Thruster[] Right, Left; // pairs with opposing torque: a lone off-axis strafe thruster cancels its own torque public void ActivateLock() => Ship.ActivateConsumable(new ConsumableItem { @@ -78,10 +78,11 @@ public sealed class ThrottleLockTests : IDisposable Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.Reversed), new int2(2, 1))); Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.None), new int2(1, 2))); Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.None), new int2(3, 2))); + // Strafe pairs sit either side of the centre line across the ship's length, so their torques oppose. + Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.CounterClockwise), new int2(1, 1))); Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.CounterClockwise), new int2(1, 3))); - Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.CounterClockwise), new int2(3, 3))); - Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.Clockwise), new int2(1, 1))); Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.Clockwise), new int2(3, 1))); + Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.Clockwise), new int2(3, 3))); zone.Entities.Add(ship); ship.Aim = float3(0, 0, 1); ship.Activate(); From e56f9c8be52ec340fb5c499b338a801b318a445c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:56:01 +0200 Subject: [PATCH 096/120] BallisticFlightTests: move the shooter after Fire so a live-origin RoundAt fails The origin-from-live-shooter hand mutant survived against a stationary shooter. Builds: yes. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/BallisticFlightTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs index c9589a0db..3468a3034 100644 --- a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs +++ b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs @@ -35,6 +35,8 @@ public sealed partial class FireControlCut124Tests var e = FireBurstRound(out var origin); var burst = SafeAssert.OnlyShot(e.Zone); var aim = normalize(float2(2, 1)); + // The shooter moves on after the round left: the line is the one frozen at Fire, not one drawn from where it is now. + e.Shooter.Position += float3(7, 0, 3); var start = FireControl.RoundAt(burst, burst.FireTime); Assert.Equal(origin.x, start.x, 3); From 950b0e3ddcdfabae3a311bbc4a0f08f08f12ce48 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 21:17:35 +0200 Subject: [PATCH 097/120] VapourCloud: drop the zero-lifetime guard saturate already covers The mutation table let the guard's removal survive: with saturate's NaN rule (max(0, NaN) = 0) a zero-lifetime cloud has no opacity without it, and ACloudWithNoLifetimeObscuresNothing still pins that behaviour. Builds. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/VapourClouds.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Assets/Scripts/ServerShared/VapourClouds.cs b/Assets/Scripts/ServerShared/VapourClouds.cs index d6d1aaa24..9f4a44603 100644 --- a/Assets/Scripts/ServerShared/VapourClouds.cs +++ b/Assets/Scripts/ServerShared/VapourClouds.cs @@ -16,8 +16,9 @@ public sealed class VapourCloud public float Opacity; public Entity Venter; + // A cloud with no lifetime divides by zero; saturate maps the NaN and the infinity to nothing left. public float OpacityAt(double now) => - Lifetime <= 0f ? 0f : saturate(Opacity) * saturate(1f - (float) (now - VentedAt) / Lifetime); + saturate(Opacity) * saturate(1f - (float) (now - VentedAt) / Lifetime); // Whether the segment a-b reaches the disc: the closest point of the segment to the centre lies within the // radius. A cloud past either end of the segment touches it only if it is near that end. From 6d83597b77673e91a7bb51ab5d5ba4952c779a22 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 22:27:59 +0200 Subject: [PATCH 098/120] Map: ballistic-flight r2 body facts and why a round's end is one sim statement Co-Authored-By: Claude Opus 5.5 --- docs/aetheria-release-map.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/aetheria-release-map.md b/docs/aetheria-release-map.md index 3cd61747c..b095dc870 100644 --- a/docs/aetheria-release-map.md +++ b/docs/aetheria-release-map.md @@ -5835,6 +5835,15 @@ factor), which waits on all of them. a smaller step means more decisions per sim second, the same per real second. No ruling fixes a per-step count. +**Body facts for ballistic-flight r2** (source reads at e56f9c8b, 2026-10-09, imagination-aeth-ballistic-r2): + +- B-r2-1. FireControl.Step writes a committed shot back (`shots[i] = shot`, FireControl.cs:868) before `ShotCommitted` fires (:869). A no-lock fused round stopped by a hull has its `ArrivalTime` shortened at that commit (:867; CommitBurst :1024). A presenter's fire-time copy of the shot therefore carries a stale arrival unless it re-reads the record at `ShotCommitted`. +- B-r2-2. `FireControl.Designated` measures `range` to any non-null target before the in-range test (:293-297), and a direct round's FlightDistance is that range. A direct round can therefore be fired at a target beyond Range and arrive after `FireTime + MaxRange/Speed`. +- B-r2-3. `InstantWeapon.Execute` calls `FireControl.Fire` and then `OnFire` (:271-272) inside the entity loop, and `FireControl.Step` runs after it (Zone.cs:232). A presenter spawned from `OnFire` binds `ShotCommitted` and `ShotResolved` before its shot can commit or resolve, including a round fired at nothing that commits and misses in the same step. +- B-r2-4. Solve aims a direct or engaged fused round at the predicted intercept (`Solution().Direction`, :135-156) but sets FlightDistance to the current planar range (:654-666), so on a moving target the arrival point on the frozen line is not where the target is. Open as question direct-arrival-at-intercept. + +**Why a round's end is one sim statement.** The sim resolves a round at `ArrivalTime`. A drawn round that ends anywhere else needs a second owner of the end, which is what ballistic-flight s1 found in Projectile. `FireControl.RoundEnd(shot, known)` states the end for what the sim has published about the outcome: arrival for a hit, a burst or an unknown outcome, and the frozen weapon range for a miss, never short of arrival. `RoundAt` clamps there. A round whose outcome is not yet published waits at its arrival point for at most one step instead of overshooting. The drawing rules (placement, barrel blend, stop, done) are pure statics in DrawAhead, beside the ships' draw-ahead, so the headless suite tests them by behaviour rather than by grep. + ### Census Every Unity-side place that integrates, times or decides a sim fact at `6d427b7a`, and the cut From 60aacfdc358c7a706b9020e0c5fa1c1c738884a3 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 22:31:12 +0200 Subject: [PATCH 099/120] FireControl.RoundEnd states a round's end; DrawAhead draws rounds as pure statics RoundEnd(shot, known) is the one statement of when a ballistic round's drawn flight ends: arrival for a hit, burst or unpublished outcome, the frozen range (never short of arrival) for a miss. RoundAt takes the known outcome and clamps at RoundEnd. DrawAhead gains Round, RoundStop, RoundOver (pure, no state) so the blend, stop and done rules are headless-testable. Seven new tests (ten cases). Unity build: Projectile.cs still calls the old RoundAt, so this commit does not compile in Unity (the headless test project does not include Projectile). The next commit moves Projectile onto the statics. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/DrawAhead.cs | 25 +- Assets/Scripts/ServerShared/FireControl.cs | 24 +- .../BallisticFlightTests.cs | 290 ++++++++++++++++-- 3 files changed, 313 insertions(+), 26 deletions(-) diff --git a/Assets/Scripts/ServerShared/DrawAhead.cs b/Assets/Scripts/ServerShared/DrawAhead.cs index d26bf91b4..77365cebe 100644 --- a/Assets/Scripts/ServerShared/DrawAhead.cs +++ b/Assets/Scripts/ServerShared/DrawAhead.cs @@ -5,8 +5,9 @@ using CultMath; using static CultMath.math; -// Where a presenter draws the latest sim state when the frame falls Lead sim seconds after the last step. Pure reads -// of the simulation for presenters (never called by ServerShared): the sim owns every pose and no presenter stores +// Where a presenter draws the latest sim state when the frame falls Lead sim seconds after the last step: ships by +// their last step's velocity, ballistic rounds on FireControl's RoundAt and RoundEnd. Pure reads of the simulation for +// presenters (never called by ServerShared, no state of their own): the sim owns every pose and no presenter stores // one, a previous state or a velocity of its own. public static class DrawAhead { @@ -37,4 +38,24 @@ public static class DrawAhead t.w * q.z + t.x * q.y - t.y * q.x + t.z * q.w, t.w * q.w - t.x * q.x - t.y * q.y - t.z * q.z)); } + + // A ballistic round at a sim time (the clock's Zone.Time + Lead): on FireControl's line for what the sim has published + // of its outcome, with the barrel's offset from that line decaying to nothing over blendTime sim seconds from FireTime. + public static float3 Round(in PendingShot shot, ShotResult? known, float3 barrel, float blendTime, float time) => + Lifted(shot, FireControl.RoundAt(shot, known, time), barrel, blendTime, time); + + // Where a round that hit or burst stops: the outcome's burst point, else where it arrives on its line. + public static float3 RoundStop(in PendingShot shot, ShotOutcome outcome, float3 barrel, float blendTime) => + Lifted(shot, outcome.HasBurstPoint ? outcome.BurstPoint : FireControl.RoundAt(shot, outcome.Result, shot.ArrivalTime), + barrel, blendTime, shot.ArrivalTime); + + // Whether the drawn flight is over by time: only a published miss ends by the clock. A hit or burst ends on ShotResolved. + public static bool RoundOver(in PendingShot shot, ShotResult? known, float time) => + known == ShotResult.Miss && time >= FireControl.RoundEnd(shot, ShotResult.Miss); + + private static float3 Lifted(in PendingShot shot, float2 planar, float3 barrel, float blendTime, float time) + { + var blend = blendTime > 0 ? saturate(1 - (time - shot.FireTime) / blendTime) : 0f; + return float3(planar.x, shot.FireOrigin.y, planar.y) + (barrel - shot.FireOrigin) * blend; + } } diff --git a/Assets/Scripts/ServerShared/FireControl.cs b/Assets/Scripts/ServerShared/FireControl.cs index 40e3e25a8..c1c4e8ddd 100644 --- a/Assets/Scripts/ServerShared/FireControl.cs +++ b/Assets/Scripts/ServerShared/FireControl.cs @@ -514,13 +514,23 @@ public static class FireControl return shot.Target == null ? 1f : PDeviation(deviation, shot.Tracking); } + // The sim time a ballistic round's drawn flight ends, for what is known of its outcome: the one statement of a round's + // end. A round that hit or burst (or whose outcome is not yet published) ends at its ArrivalTime. A round the sim has + // published as a Miss flies on to the weapon's frozen range and never ends short of its own arrival (a direct round's + // FireRange is measured to any target, in range or not). Pure: it reads the shot alone, and the simulation's own + // arithmetic (Step, Commit, Apply) does not call it. Presenters end rounds here, through DrawAhead. + public static float RoundEnd(in PendingShot shot, ShotResult? known) => + known == ShotResult.Miss + ? max(shot.ArrivalTime, shot.FireTime + (shot.Speed > .01f ? shot.MaxRange / shot.Speed : 0f)) + : shot.ArrivalTime; + // Where a ballistic round is at a sim time: the one statement of the round's line, from its frozen origin along its - // frozen direction at its frozen speed, held at the MaxRange point once it has flown that far. Pure: it reads the - // shot alone, and the simulation's own arithmetic (Step, Commit, Apply) does not call it. Presenters draw rounds here. - public static float2 RoundAt(in PendingShot shot, float time) + // frozen direction at its frozen speed, held at RoundEnd once it has flown that far. With no outcome known the round is + // drawn no further than its arrival point and waits there for the sim to publish one. Pure, like RoundEnd. + public static float2 RoundAt(in PendingShot shot, ShotResult? known, float time) { if (shot.Speed <= .01f) return shot.FireOrigin.xz; - var flown = clamp(time - shot.FireTime, 0f, shot.MaxRange / shot.Speed); + var flown = clamp(time - shot.FireTime, 0f, RoundEnd(shot, known) - shot.FireTime); return shot.FireOrigin.xz + shot.TravelDirection * (shot.Speed * flown); } @@ -865,6 +875,8 @@ public static class FireControl // A contact burst short of max range arrives when the round has flown that far, not at the frozen // max-range time (CommitBurst). Never later than the frozen time. if (shot.Outcome.Result == ShotResult.Burst) shot.ArrivalTime = min(shot.ArrivalTime, now + shot.Outcome.ArrivalIn); + // Written back before ShotCommitted fires: a presenter re-reading the record there (Zone.TryGetShot) sees + // the committed ArrivalTime a contact burst has just shortened. shots[i] = shot; zone.ShotCommitted.OnNext(shot.Outcome); } @@ -2013,8 +2025,8 @@ public struct PendingShot public float FireTime; // The weapon's Velocity at Fire: frozen with the rest, so RoundAt states the same line whatever happens to the stat later. public float Speed; - // The weapon's Range at Fire, likewise frozen: how far the round can fly, which RoundAt clamps at. FireRange above is the - // distance to the engaged target (0 for a round with none), not how far a round that misses flies on. + // The weapon's Range at Fire, likewise frozen: how far a round that misses flies on, read only by RoundEnd. FireRange above + // is the distance to the engaged target (0 for a round with none), not how far a round that misses flies on. public float MaxRange; public float CommitTime; public float ArrivalTime; diff --git a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs index 3468a3034..0781d2c06 100644 --- a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs +++ b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs @@ -9,9 +9,11 @@ using float2 = CultMath.float2; using float3 = CultMath.float3; // Ballistic flight: FireControl.RoundAt is the one statement of where a fired round is at a sim time, from the -// PendingShot's frozen origin, direction, speed and fire time. Unity draws rounds there and integrates nothing, so -// these tests pin the line itself against first principles (the shooter's position, the aim, the authored velocity) -// rather than against the code that computes it. Shares the Cut 12.4 engagement fixture. +// PendingShot's frozen origin, direction, speed and fire time, and FireControl.RoundEnd the one statement of when its +// drawn flight ends for what the sim has published of its outcome. Unity draws rounds through DrawAhead's round statics +// and integrates nothing, so these tests pin the line, the end and the drawing against first principles (the shooter's +// position, the aim, the authored velocity and range) rather than against the code that computes them. Shares the +// Cut 12.4 engagement fixture. public sealed partial class FireControlCut124Tests { private const float BallisticSpeed = 20f; @@ -38,24 +40,28 @@ public sealed partial class FireControlCut124Tests // The shooter moves on after the round left: the line is the one frozen at Fire, not one drawn from where it is now. e.Shooter.Position += float3(7, 0, 3); - var start = FireControl.RoundAt(burst, burst.FireTime); + var start = FireControl.RoundAt(burst, null, burst.FireTime); Assert.Equal(origin.x, start.x, 3); Assert.Equal(origin.z, start.y, 3); - var second = FireControl.RoundAt(burst, burst.FireTime + 1f); + var second = FireControl.RoundAt(burst, null, burst.FireTime + 1f); Assert.Equal(origin.x + aim.x * BallisticSpeed, second.x, 3); Assert.Equal(origin.z + aim.y * BallisticSpeed, second.y, 3); - var arrival = FireControl.RoundAt(burst, burst.ArrivalTime); - Assert.Equal(burst.BurstPosition.x, arrival.x, 3); - Assert.Equal(burst.BurstPosition.z, arrival.y, 3); + foreach (ShotResult? known in new ShotResult?[] { null, ShotResult.Burst }) + foreach (var later in new[] { 0f, 1f, 50f }) + { + var arrival = FireControl.RoundAt(burst, known, burst.ArrivalTime + later); + Assert.Equal(burst.BurstPosition.x, arrival.x, 3); + Assert.Equal(burst.BurstPosition.z, arrival.y, 3); + } // Past the weapon's range it is held at the range point, however late it is asked. - var held = FireControl.RoundAt(burst, burst.FireTime + NoLockRange / BallisticSpeed + 50f); + var held = FireControl.RoundAt(burst, ShotResult.Miss, burst.FireTime + NoLockRange / BallisticSpeed + 50f); Assert.Equal(origin.x + aim.x * NoLockRange, held.x, 3); Assert.Equal(origin.z + aim.y * NoLockRange, held.y, 3); // Before it was fired it is still at the origin, not behind it. - var before = FireControl.RoundAt(burst, burst.FireTime - 5f); + var before = FireControl.RoundAt(burst, null, burst.FireTime - 5f); Assert.Equal(origin.x, before.x, 3); Assert.Equal(origin.z, before.y, 3); @@ -64,11 +70,16 @@ public sealed partial class FireControlCut124Tests var dOrigin = d.Shooter.Position; FireControl.Fire(d.Weapon, d.WeaponItem, d.Shooter); var direct = SafeAssert.OnlyShot(d.Zone); - var hit = FireControl.RoundAt(direct, direct.ArrivalTime); - Assert.Equal(d.Target.Position.x, hit.x, 2); - Assert.Equal(d.Target.Position.z, hit.y, 2); - Assert.Equal(dOrigin.z + 100f / 2f, FireControl.RoundAt(direct, direct.FireTime + 2.5f).y, 2); - var heldDirect = FireControl.RoundAt(direct, direct.FireTime + 1000f / BallisticSpeed + 99f); + foreach (ShotResult? known in new ShotResult?[] { null, ShotResult.Hit }) + // A round not known to miss waits at its arrival point however late it is asked. + foreach (var at in new[] { direct.ArrivalTime, direct.FireTime + 99f }) + { + var hit = FireControl.RoundAt(direct, known, at); + Assert.Equal(d.Target.Position.x, hit.x, 2); + Assert.Equal(d.Target.Position.z, hit.y, 2); + } + Assert.Equal(dOrigin.z + 100f / 2f, FireControl.RoundAt(direct, null, direct.FireTime + 2.5f).y, 2); + var heldDirect = FireControl.RoundAt(direct, ShotResult.Miss, direct.FireTime + 1000f / BallisticSpeed + 99f); Assert.Equal(dOrigin.z + 1000f, heldDirect.y, 2); } @@ -82,7 +93,7 @@ public sealed partial class FireControlCut124Tests FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); var shot = SafeAssert.OnlyShot(e.Zone); - var late = FireControl.RoundAt(shot, shot.FireTime + 5000f); + var late = FireControl.RoundAt(shot, ShotResult.Miss, shot.FireTime + 5000f); Assert.Equal(origin.x, late.x, 3); Assert.Equal(origin.z, late.y, 3); } @@ -102,7 +113,7 @@ public sealed partial class FireControlCut124Tests var after = SafeAssert.OnlyShot(e.Zone); Assert.Equal(BallisticSpeed, after.Speed, 4); var aim = normalize(float2(2, 1)); - var at = FireControl.RoundAt(after, after.FireTime + 1f); + var at = FireControl.RoundAt(after, null, after.FireTime + 1f); Assert.Equal(origin.x + aim.x * BallisticSpeed, at.x, 3); Assert.Equal(origin.z + aim.y * BallisticSpeed, at.y, 3); } @@ -117,8 +128,18 @@ public sealed partial class FireControlCut124Tests var positions = entities.Select(x => x.Position).ToList(); var velocities = entities.Select(x => x.Velocity).ToList(); + var barrel = shot.FireOrigin + float3(1, .5f, -2); for (var i = 0; i < 25; i++) - FireControl.RoundAt(shot, shot.FireTime + i * .37f); + { + var at = shot.FireTime + i * .37f; + foreach (ShotResult? known in new ShotResult?[] { null, ShotResult.Miss }) + { + FireControl.RoundAt(shot, known, at); + FireControl.RoundEnd(shot, known); + DrawAhead.Round(shot, known, barrel, .1f, at); + DrawAhead.RoundOver(shot, known, at); + } + } Assert.Equal(time, e.Zone.Time); Assert.True(e.Zone.PendingShots.Count == 1 && e.Zone.PendingShots[0].Equals(shot), "the pending shot changed"); @@ -126,4 +147,237 @@ public sealed partial class FireControlCut124Tests Assert.True(entities.Select(x => x.Position).SequenceEqual(positions), "an entity moved"); Assert.True(entities.Select(x => x.Velocity).SequenceEqual(velocities), "an entity's velocity changed"); } + + // The weapon's Range is frozen with the rest: a round that misses flies on to the range it left with, whatever the stat + // becomes after. + [Fact] + public void RangeIsFrozenAtFire() + { + var e = FireBurstRound(out var origin); + var shot = SafeAssert.OnlyShot(e.Zone); + Assert.Equal(NoLockRange, shot.MaxRange, 4); + var endBefore = FireControl.RoundEnd(shot, ShotResult.Miss); + + // The stat changes after the round left, and the weapon re-reads it. + ((InstantWeaponData) e.Weapon.WeaponData).Range = Constant(NoLockRange * 5f); + e.Weapon.Execute(0f); + Assert.Equal(NoLockRange * 5f, e.Weapon.Range, 4); + + var after = SafeAssert.OnlyShot(e.Zone); + Assert.Equal(NoLockRange, after.MaxRange, 4); + Assert.Equal(endBefore, FireControl.RoundEnd(after, ShotResult.Miss), 4); + var aim = normalize(float2(2, 1)); + var held = FireControl.RoundAt(after, ShotResult.Miss, after.FireTime + 1000f); + Assert.Equal(origin.x + aim.x * NoLockRange, held.x, 3); + Assert.Equal(origin.z + aim.y * NoLockRange, held.y, 3); + } + + // A direct round at a stationary target `targetRange` ahead (+z) of the shooter, weapon Range `range`, at `speed`; + // or, with atNothing, the same weapon fired along +z at no target. + private Engagement FireDirectRound(float speed, float range, float targetRange = 100f, bool atNothing = false) + { + var e = Build(TestSettings(), SolidShape(5, 4), velocity: speed, weaponRange: range, targetRange: targetRange); + if (atNothing) + { + Aim(e, float2(0, 1)); + e.Shooter.SetTarget(TargetRef.None); + } + FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); + return e; + } + + // A hit, a burst or an outcome not yet known ends at the round's arrival; a miss flies on to the weapon's range and + // never ends short of its own arrival; only a published miss is over by the clock. + [Fact] + public void RoundEndAndRoundOverAreTheOutcomesEnd() + { + foreach (var speed in new[] { 20f, 200f }) + { + var d = FireDirectRound(speed, 1000f); + var direct = SafeAssert.OnlyShot(d.Zone); + Assert.Equal(direct.FireTime + 100f / speed, direct.ArrivalTime, 4); + Assert.Equal(direct.FireTime + 100f / speed, FireControl.RoundEnd(direct, null), 4); + Assert.Equal(direct.FireTime + 100f / speed, FireControl.RoundEnd(direct, ShotResult.Hit), 4); + Assert.Equal(direct.FireTime + 100f / speed, FireControl.RoundEnd(direct, ShotResult.Burst), 4); + Assert.Equal(direct.FireTime + 1000f / speed, FireControl.RoundEnd(direct, ShotResult.Miss), 4); + + foreach (ShotResult? known in new ShotResult?[] { null, ShotResult.Hit, ShotResult.Burst }) + Assert.False(DrawAhead.RoundOver(direct, known, direct.FireTime + 10000f)); + var missEnd = direct.FireTime + 1000f / speed; + Assert.False(DrawAhead.RoundOver(direct, ShotResult.Miss, missEnd - .01f)); + Assert.True(DrawAhead.RoundOver(direct, ShotResult.Miss, missEnd)); + + // A target beyond the weapon's range: a miss never ends short of its own arrival. + var far = FireDirectRound(speed, 100f, targetRange: 150f); + var beyond = SafeAssert.OnlyShot(far.Zone); + Assert.Equal(beyond.FireTime + 150f / speed, beyond.ArrivalTime, 4); + Assert.Equal(beyond.FireTime + 150f / speed, FireControl.RoundEnd(beyond, ShotResult.Miss), 4); + + // A round fired at nothing arrives where it left, and a miss flies on to the range. + var none = FireDirectRound(speed, 100f, atNothing: true); + var nothing = SafeAssert.OnlyShot(none.Zone); + Assert.Equal(nothing.FireTime, nothing.ArrivalTime, 4); + Assert.Equal(nothing.FireTime, FireControl.RoundEnd(nothing, null), 4); + Assert.Equal(nothing.FireTime + 100f / speed, FireControl.RoundEnd(nothing, ShotResult.Miss), 4); + + } + } + + // The frame falls Lead (< one step) after the last step, so a hit or burst is drawn at Zone.Time + Lead; it must stay + // on the arrival point until the sim resolves it, and only a published miss flies on. + [Theory] + [InlineData(20f)] + [InlineData(200f)] + public void ARoundIsDrawnNoFurtherThanItsArrivalUntilItMisses(float speed) + { + var e = FireDirectRound(speed, 1000f); + var shot = SafeAssert.OnlyShot(e.Zone); + var origin = shot.FireOrigin; + const float step = 1f / 30f; + + foreach (var lead in new[] { 0f, step / 2f, .999f * step }) + { + var time = shot.ArrivalTime + lead; + foreach (ShotResult? known in new ShotResult?[] { null, ShotResult.Hit, ShotResult.Burst }) + { + var drawn = DrawAhead.Round(shot, known, shot.FireOrigin, 0f, time); + Assert.Equal(origin.x, drawn.x, 3); + Assert.Equal(origin.z + 100f, drawn.z, 3); + } + var missed = DrawAhead.Round(shot, ShotResult.Miss, shot.FireOrigin, 0f, time); + Assert.Equal(origin.x, missed.x, 3); + Assert.Equal(origin.z + 100f + lead * speed, missed.z, 3); + } + + for (var time = shot.FireTime; time <= shot.ArrivalTime + 1f; time += 1f / 144f) + { + var drawn = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, time); + Assert.True(drawn.z - origin.z <= 100f + 1e-3f, "the round was drawn past its arrival point"); + } + } + + // A no-lock contact round stopped by a hull short of max range has its arrival shortened at the commit; the + // simulation writes the record back before ShotCommitted fires, so a presenter re-reading it there draws the stop + // at the committed arrival, not the fire-time copy's. + [Fact] + public void TheCommittedRecordCarriesTheCommittedArrival() + { + var e = Build(TestSettings(), SolidShape(5, 4), velocity: 20f, fuse: WeaponFuse.Contact, blastRadius: 4f, damage: 100f, + weaponRange: NoLockRange); + e.Shooter.SetTarget(TargetRef.None); + Aim(e, float2(0, 1)); + AddShip(e, PointAlong(e, float2(0, 1), 25f), 1f, 300); + ShotOutcome outcome = null; + PendingShot record = default; + var found = false; + using var c = e.Zone.ShotCommitted.Subscribe(o => + { + outcome = o; + found = e.Zone.TryGetShot(o.ShotId, out record); + }); + + FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); + var fired = SafeAssert.OnlyShot(e.Zone); + Assert.False(fired.Committed); + e.Zone.Update(.01f); + + Assert.True(found, "the committed record was not readable when ShotCommitted fired"); + Assert.True(record.Committed); + Assert.Equal(fired.FireTime + 3f, fired.ArrivalTime, 2); + Assert.Equal(fired.FireTime + 1.05f, record.ArrivalTime, 2); + Assert.True(record.ArrivalTime < fired.ArrivalTime); + + var stop = DrawAhead.RoundStop(record, outcome, record.FireOrigin, 0f); + var expected = outcome.HasBurstPoint + ? outcome.BurstPoint + : record.FireOrigin.xz + record.TravelDirection * (20f * (record.ArrivalTime - record.FireTime)); + Assert.Equal(expected.x, stop.x, 3); + Assert.Equal(expected.y, stop.z, 3); + foreach (var later in new[] { 0f, .5f, 5f }) + { + var drawn = DrawAhead.Round(record, ShotResult.Burst, record.FireOrigin, 0f, record.ArrivalTime + later); + Assert.Equal(expected.x, drawn.x, 3); + Assert.Equal(expected.y, drawn.z, 3); + } + } + + // The barrel is only where the round is first seen: its offset from the sim line decays linearly to nothing over + // blendTime sim seconds from FireTime. + [Theory] + [InlineData(.1f)] + [InlineData(.25f)] + public void TheBarrelBlendDecaysOnSimTime(float blend) + { + var e = FireBurstRound(out var origin); + var shot = SafeAssert.OnlyShot(e.Zone); + var aim = normalize(float2(2, 1)); + var offset = float3(1, .5f, -2); + var barrel = shot.FireOrigin + offset; + float3 Line(float dt) => float3(origin.x + aim.x * BallisticSpeed * dt, origin.y, origin.z + aim.y * BallisticSpeed * dt); + + void Close(float3 want, float3 got) + { + Assert.Equal(want.x, got.x, 3); + Assert.Equal(want.y, got.y, 3); + Assert.Equal(want.z, got.z, 3); + } + + Close(barrel, DrawAhead.Round(shot, null, barrel, blend, shot.FireTime)); + Close(Line(blend / 2f) + offset * .5f, DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + blend / 2f)); + Close(Line(blend), DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + blend)); + Close(Line(2f * blend), DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + 2f * blend)); + Close(Line(0f), DrawAhead.Round(shot, null, barrel, 0f, shot.FireTime)); + } + + // Placement depends on sim time alone: any history of frames walked to a checkpoint draws what a fresh call draws, which + // is the frozen line at the frozen speed. + [Theory] + [InlineData(20f)] + [InlineData(200f)] + public void ARoundIsAFunctionOfSimTime(float speed) + { + var e = FireDirectRound(speed, 1000f); + var shot = SafeAssert.OnlyShot(e.Zone); + var checkpoints = new[] { .05f, .3f, 1f, 3f }; + var histories = new[] { new[] { 1f / 144f }, new[] { 1f / 30f }, new[] { 1f / 144f, 1f / 30f, 1f / 90f } }; + + foreach (var frames in histories) + { + var elapsed = 0f; + var frame = 0; + foreach (var checkpoint in checkpoints) + { + while (elapsed + 1e-4f < checkpoint) + { + elapsed = min(checkpoint, elapsed + frames[frame++ % frames.Length]); + DrawAhead.Round(shot, ShotResult.Miss, shot.FireOrigin, .1f, shot.FireTime + elapsed); + } + var drawn = DrawAhead.Round(shot, ShotResult.Miss, shot.FireOrigin, .1f, shot.FireTime + checkpoint); + var fresh = DrawAhead.Round(shot, ShotResult.Miss, shot.FireOrigin, .1f, shot.FireTime + checkpoint); + Assert.Equal(fresh.z, drawn.z, 3); + Assert.Equal(shot.FireOrigin.z + speed * min(checkpoint, 1000f / speed), drawn.z, 3); + Assert.Equal(shot.FireOrigin.x, drawn.x, 3); + Assert.Equal(shot.FireOrigin.y, drawn.y, 3); + } + } + } + + // Today's sim geometry: the round flies the line frozen at Fire and stops at the arrival point on it, whatever the + // target does afterwards. (Where a leading round's arrival should lie is the sim's question, never Projectile's.) + [Fact] + public void AHitOnAMovingTargetStopsOnItsFrozenLine() + { + var e = Build(TestSettings(), SolidShape(5, 4), velocity: BallisticSpeed, weaponRange: 1000f); + e.Target.Velocity = float2(30, 0); + FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); + var shot = SafeAssert.OnlyShot(e.Zone); + Assert.True(shot.TravelDirection.x > 0f, "the round did not lead the target"); + + e.Target.Position += float3(40, 0, 0); + var stop = DrawAhead.RoundStop(shot, new ShotOutcome { ShotId = shot.ShotId, Result = ShotResult.Hit }, shot.FireOrigin, 0f); + var line = shot.FireOrigin.xz + shot.TravelDirection * (BallisticSpeed * (shot.ArrivalTime - shot.FireTime)); + Assert.Equal(line.x, stop.x, 3); + Assert.Equal(shot.FireOrigin.y, stop.y, 3); + Assert.Equal(line.y, stop.z, 3); + } } From 85e57aef785ce4e1a4338a7ea1e3ab124fe562d5 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 22:32:08 +0200 Subject: [PATCH 100/120] BallisticFlightTests: using System for the Subscribe lambda; DrawAhead comment names no frame clock Builds headless; Unity still on the old Projectile at this commit. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/DrawAhead.cs | 2 +- tests/Aetheria.Shared.Tests/BallisticFlightTests.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Assets/Scripts/ServerShared/DrawAhead.cs b/Assets/Scripts/ServerShared/DrawAhead.cs index 77365cebe..f594b33fe 100644 --- a/Assets/Scripts/ServerShared/DrawAhead.cs +++ b/Assets/Scripts/ServerShared/DrawAhead.cs @@ -40,7 +40,7 @@ public static class DrawAhead } // A ballistic round at a sim time (the clock's Zone.Time + Lead): on FireControl's line for what the sim has published - // of its outcome, with the barrel's offset from that line decaying to nothing over blendTime sim seconds from FireTime. + // of its outcome, with the barrel's offset from that line decaying to nothing over blendTime sim seconds from the moment of firing. public static float3 Round(in PendingShot shot, ShotResult? known, float3 barrel, float blendTime, float time) => Lifted(shot, FireControl.RoundAt(shot, known, time), barrel, blendTime, time); diff --git a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs index 0781d2c06..e1957e2db 100644 --- a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs +++ b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs @@ -2,6 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +using System; using System.Linq; using Xunit; using static CultMath.math; From a9f11964725bf5899aca4008d68d2643b18b6242 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 22:33:51 +0200 Subject: [PATCH 101/120] BallisticFlightTests: using UniRx for the Subscribe lambda Builds headless; Unity still on the old Projectile at this commit. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/BallisticFlightTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs index e1957e2db..5b1b24dd9 100644 --- a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs +++ b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs @@ -4,6 +4,7 @@ using System; using System.Linq; +using UniRx; using Xunit; using static CultMath.math; using float2 = CultMath.float2; From f9483181f2680d732d6d28b49b6b8e15af26aa09 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 22:35:37 +0200 Subject: [PATCH 102/120] BallisticFlightTests: the moving target crosses at 10, below the round's speed, so an intercept exists Spec r2 said 30 against a round speed of 20: no first-order intercept exists there (the target outruns the round sideways), so the round did not lead. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/BallisticFlightTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs index 5b1b24dd9..3aadc3e39 100644 --- a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs +++ b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs @@ -364,13 +364,13 @@ public sealed partial class FireControlCut124Tests } } - // Today's sim geometry: the round flies the line frozen at Fire and stops at the arrival point on it, whatever the + // Today's sim geometry (a target crossing at 10 against a speed of 20, so an intercept exists): the round flies the line frozen at Fire and stops at the arrival point on it, whatever the // target does afterwards. (Where a leading round's arrival should lie is the sim's question, never Projectile's.) [Fact] public void AHitOnAMovingTargetStopsOnItsFrozenLine() { var e = Build(TestSettings(), SolidShape(5, 4), velocity: BallisticSpeed, weaponRange: 1000f); - e.Target.Velocity = float2(30, 0); + e.Target.Velocity = float2(10, 0); FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); var shot = SafeAssert.OnlyShot(e.Zone); Assert.True(shot.TravelDirection.x > 0f, "the round did not lead the target"); From ba2b4a7377a54c4d233c1c69321975145140c931 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 22:35:46 +0200 Subject: [PATCH 103/120] Projectile draws through DrawAhead and computes no position, time or range Deletes At, the end formula (Unity restating the range) and Resolve's own stop arithmetic. Launch binds ShotCommitted (refresh the record, learn the outcome) and ShotResolved; Draw is DrawAhead.Round plus RoundOver; a hit or burst stops at DrawAhead.RoundStop. Projectile names no flight quantity. Builds headless (Projectile is not in the test project); the Unity compile is run separately at the final head. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/Gameplay/Weapons/Projectile.cs | 71 +++++++++++-------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/Assets/Scripts/Gameplay/Weapons/Projectile.cs b/Assets/Scripts/Gameplay/Weapons/Projectile.cs index 64d84fb4e..2fe4129d1 100644 --- a/Assets/Scripts/Gameplay/Weapons/Projectile.cs +++ b/Assets/Scripts/Gameplay/Weapons/Projectile.cs @@ -2,15 +2,15 @@ using System.Collections; using UniRx; using UnityEngine; -using static CultMath.math; -// A ballistic round, drawn on FireControl.RoundAt and deciding nothing. The simulation owns the round's whole flight -// (origin, direction, speed, fire time, arrival, range: the PendingShot copy taken at spawn), so this object stores no -// velocity or position of its own and integrates nothing: each frame it asks RoundAt where the round is at sim time -// plus the clock's Lead (the same draw-ahead ships use), and the simulation's ShotResolved ends a round that hit or -// burst. A round that missed flies on to the MaxRange point RoundAt clamps at, and is done there. The barrel is only -// where the round is first seen: its offset from the sim line decays to zero over BlendTime sim seconds. The trail's -// fade after the end stays real time (pure presentation, ruling sim-speed-presentation). +// A ballistic round, drawn by DrawAhead's round statics and deciding nothing. The simulation owns the round's whole +// flight (the PendingShot copy taken at spawn, replaced by the sim's record at ShotCommitted, and the outcome it +// publishes), so this object stores no motion state of its own and computes no position, time or range: each frame it +// asks DrawAhead.Round where the round is at sim time plus the clock's Lead (the same draw-ahead ships use), and the +// round ends on the sim's word: ShotResolved stops a round that hit or burst where DrawAhead.RoundStop puts it, and +// DrawAhead.RoundOver ends one the sim published as a miss. The barrel is only where the round is first seen: its +// offset from the sim line decays to zero over BlendTime sim seconds. The trail's fade after the end stays real time +// (pure presentation, ruling sim-speed-presentation). public class Projectile : MonoBehaviour { public TrailRenderer Trail; @@ -21,50 +21,59 @@ public class Projectile : MonoBehaviour private PendingShot _shot; private Zone _zone; - private Vector3 _barrelOffset; + private CultMath.float3 _barrel; + private ShotResult? _known; private bool _alive; - private IDisposable _binding; + private CompositeDisposable _binding; - // Takes the frozen shot, the zone whose sim time it is drawn at, and where the barrel is as this round is first seen. + // Takes the shot as the sim holds it, the zone whose sim time it is drawn at, and where the barrel is as this round is first seen. public void Launch(Zone zone, PendingShot shot, Vector3 barrel) { _zone = zone; _shot = shot; - var origin = shot.FireOrigin; - _barrelOffset = barrel - new Vector3(origin.x, origin.y, origin.z); + _barrel = new CultMath.float3(barrel.x, barrel.y, barrel.z); + _known = null; _binding?.Dispose(); - _binding = zone.ShotResolved.Where(o => o.ShotId == shot.ShotId).Subscribe(Resolve); + _binding = new CompositeDisposable( + zone.ShotCommitted.Where(o => o.ShotId == shot.ShotId).Subscribe(Committed), + zone.ShotResolved.Where(o => o.ShotId == shot.ShotId).Subscribe(Resolve)); _alive = true; transform.forward = new Vector3(shot.TravelDirection.x, 0, shot.TravelDirection.y); - transform.position = barrel; - } - - private Vector3 At(CultMath.float2 planar, float time) - { - var blend = BlendTime > 0 ? saturate(1 - (time - _shot.FireTime) / BlendTime) : 0f; - return new Vector3(planar.x, _shot.FireOrigin.y, planar.y) + _barrelOffset * blend; + Draw(); } void Update() { - if (!_alive) return; - var time = _zone.Time + ActionGameManager.Clock.Lead; - transform.position = At(FireControl.RoundAt(_shot, time), time); - var end = _shot.Speed > .01f ? _shot.FireTime + _shot.MaxRange / _shot.Speed : _shot.FireTime; - if (time >= end) Finish(); + if (_alive) Draw(); } - // The simulation's verdict. A round that hit or burst stops where the sim put it, at the outcome's burst point, - // else where it arrives on its line; a miss keeps flying. + private void Draw() + { + var time = _zone.Time + ActionGameManager.Clock.Lead; + transform.position = V(DrawAhead.Round(_shot, _known, _barrel, BlendTime, time)); + if (DrawAhead.RoundOver(_shot, _known, time)) Finish(); + } + + // The sim's commit: the record it holds now (a contact burst's arrival is shortened here) and what it decided. + private void Committed(ShotOutcome outcome) + { + if (_zone.TryGetShot(outcome.ShotId, out var record)) _shot = record; + _known = outcome.Result; + } + + // The simulation's verdict. A round that hit or burst stops where the sim put it; a miss keeps flying. private void Resolve(ShotOutcome outcome) { - if (!_alive || (outcome.Result != ShotResult.Hit && outcome.Result != ShotResult.Burst)) return; - var planar = outcome.HasBurstPoint ? outcome.BurstPoint : FireControl.RoundAt(_shot, _shot.ArrivalTime); - transform.position = At(planar, _shot.ArrivalTime); + if (!_alive) return; + _known = outcome.Result; + if (outcome.Result == ShotResult.Miss) return; + transform.position = V(DrawAhead.RoundStop(_shot, outcome, _barrel, BlendTime)); if (HitEffect != null) HitEffect.Instantiate().position = transform.position; Finish(); } + private static Vector3 V(CultMath.float3 p) => new Vector3(p.x, p.y, p.z); + private void Finish() { _alive = false; From d98818ea0c1c669995f3326559f7722ce78287a7 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 22:56:51 +0200 Subject: [PATCH 104/120] BallisticFlightTests: a receding proximity target puts the burst point off the arrival line The contact-burst fixture cannot tell RoundStop's burst point from the line at arrival (CommitBurst puts both at the same distance), so a stop that ignored the burst point survived. A targeted proximity round at a target receding faster than the round closes bursts at the pulled-back range point while arriving at the fired-at distance; this test pins the stop to the sim's burst point. Co-Authored-By: Claude Opus 5.5 --- .../BallisticFlightTests.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs index 3aadc3e39..f2c5b45f7 100644 --- a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs +++ b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs @@ -382,4 +382,31 @@ public sealed partial class FireControlCut124Tests Assert.Equal(shot.FireOrigin.y, stop.y, 3); Assert.Equal(line.y, stop.z, 3); } + + // A proximity round at a target that recedes faster than the round closes bursts at the weapon's range, pulled back + // along its line, while it arrives when it has flown the distance the target was fired at: its burst point is not + // the line at its arrival. The stop is the sim's burst point, so a presenter that stopped at the arrival line would + // end the tracer short of where the round burst. + [Fact] + public void ABurstRoundStopsAtItsBurstPointNotItsArrivalLine() + { + var e = Build(TestSettings(), SolidShape(5, 4), velocity: BallisticSpeed, fuse: WeaponFuse.Proximity, blastRadius: 4f, + damage: 100f, weaponRange: NoLockRange, targetRange: 50f); + e.Target.Velocity = float2(0, 15); + ShotOutcome outcome = null; + using var c = e.Zone.ShotCommitted.Subscribe(o => outcome = o); + FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); + var origin = SafeAssert.OnlyShot(e.Zone).FireOrigin; + for (var i = 0; i < 100 && outcome == null; i++) e.Zone.Update(.05f); + Assert.NotNull(outcome); + Assert.True(outcome.HasBurstPoint); + Assert.True(e.Zone.TryGetShot(outcome.ShotId, out var record)); + + Assert.Equal(origin.x, outcome.BurstPoint.x, 3); + Assert.Equal(origin.z + NoLockRange, outcome.BurstPoint.y, 3); + Assert.Equal(origin.z + 50f, FireControl.RoundAt(record, outcome.Result, record.ArrivalTime).y, 3); + var stop = DrawAhead.RoundStop(record, outcome, record.FireOrigin, 0f); + Assert.Equal(outcome.BurstPoint.x, stop.x, 3); + Assert.Equal(outcome.BurstPoint.y, stop.z, 3); + } } From ce4bb199a7f368921983dd5d492aeeaeeb8de8e0 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 23:23:24 +0200 Subject: [PATCH 105/120] Map: ballistic flight is presentation (r3 probes and rationale) Co-Authored-By: Claude Opus 5.5 --- docs/aetheria-release-map.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/aetheria-release-map.md b/docs/aetheria-release-map.md index b095dc870..255ed25a5 100644 --- a/docs/aetheria-release-map.md +++ b/docs/aetheria-release-map.md @@ -5844,6 +5844,17 @@ factor), which waits on all of them. **Why a round's end is one sim statement.** The sim resolves a round at `ArrivalTime`. A drawn round that ends anywhere else needs a second owner of the end, which is what ballistic-flight s1 found in Projectile. `FireControl.RoundEnd(shot, known)` states the end for what the sim has published about the outcome: arrival for a hit, a burst or an unknown outcome, and the frozen weapon range for a miss, never short of arrival. `RoundAt` clamps there. A round whose outcome is not yet published waits at its arrival point for at most one step instead of overshooting. The drawing rules (placement, barrel blend, stop, done) are pure statics in DrawAhead, beside the ships' draw-ahead, so the headless suite tests them by behaviour rather than by grep. +**Ballistic flight is presentation (r3, 2026-10-09).** After rulings projectile-flight-is-presentation, sim-hooks-presentation-agnostic and direct-arrival-keep-timing, FireControl owns only a shot's facts: the outcome, when damage lands (ArrivalTime, a contact burst's shortened arrival, the commit tick), the impact cell with its target, and a fused round's burst point. DrawAhead owns the drawn round, as pure statics tested headless. + +Probes at d98818ea: +(1) The impact cell is already on the outcome: on a hit, Commit stores the first cell of the shot's lane (FireControl.cs:960-988), MakeOutcome copies Cell and Target (:1527-1544), and ShotCommitted and ShotResolved publish the outcome (:881, :894). No sim hook was needed. +(2) Entity.ToWorldPoint (Entity.cs:717-725) is the one schematic-to-world frame. The step turns Direction by `mul(Direction, float2x2.Rotate(TurnRate*dt))` (Thruster.cs:129), so the cell on the drawn pose is `Position(target, lead).xz + Rotate(ToWorldPoint(cell) - Position.xz, TurnRate*lead)`, exactly. +(3) The commit tick equals `record.ArrivalTime - outcome.ArrivalIn` for every result (:877-881, :1541). +(4) An engaged proximity round is Hit or Burst, never Miss; it bursts at BurstPosition frozen at Fire (:1009-1017, :1148-1150), and it is drawn there. +(5) A target that leaves the zone mid-flight resolves as a fresh Miss (:857-868). + +Rationale: a hit follows its frozen line until the commit tick, then blends onto its impact cell on the target's drawn pose, so it lands on the hull rather than at the frozen line's arrival point. The sim's damage timing is unchanged. A miss flies on to the weapon's frozen range, which is DrawAhead.RoundEnd's choice. A kink at the commit frame (at most one step's share of the line-to-hull gap) and a snap back to the line after a target-gone Miss are accepted presentation. Rejected: publishing the lane's entry point as a new sim field; the ruling names the cell, so a hit effect may sit up to half a cell inside the hull skin. Risk: a hull mesh not aligned with its schematic makes hits land beside the mesh; that is a content fault, and the operator's moving-target check shows it. + ### Census Every Unity-side place that integrates, times or decides a sim fact at `6d427b7a`, and the cut From 29c5f7aba154a24a44c74edd36b8aa13878a64e9 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 23:24:54 +0200 Subject: [PATCH 106/120] Consumable host: one initialiser for gear and running effects; tests pin overlap, lifetime, per-design cooldown, refit Entity.Activate re-initialises the running consumable effects through the same InitializeBehaviors that initialises gear and ActivateConsumable, so a consumable active across deactivate/refit/activate retargets to the new items. ConsumableTests gains the stackable overlap/multiply, shared-entry detach, expiry-boundary, per-design cooldown and refit facts. StatResolverTests' expiry test builds its consumable through ItemManager.CreateInstance. Not yet built or run; verification follows in a later commit message. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/Entity.cs | 19 ++-- .../Aetheria.Shared.Tests/ConsumableTests.cs | 99 ++++++++++++++++++- .../StatResolverTests.cs | 2 +- 3 files changed, 109 insertions(+), 11 deletions(-) diff --git a/Assets/Scripts/ServerShared/Entity.cs b/Assets/Scripts/ServerShared/Entity.cs index 9c4929058..d7152b172 100644 --- a/Assets/Scripts/ServerShared/Entity.cs +++ b/Assets/Scripts/ServerShared/Entity.cs @@ -203,12 +203,10 @@ public abstract class Entity return presencePermitted; }), initialValue: true); - foreach (var item in Equipment) - foreach (var behavior in item.Behaviors) - { - if(behavior is IInitializableBehavior initializableBehavior) - initializableBehavior.Initialize(); - } + // One initialiser for everything that computes targets here: gear and the effects still running, so a + // consumable active across a refit retargets to the new items exactly as gear does. + InitializeBehaviors(Equipment.SelectMany(item => item.Behaviors)); + InitializeBehaviors(_activeConsumables.SelectMany(effect => effect.Behaviors)); foreach(var entity in Zone.Entities) { EntityInfoGathered[entity] = 0; @@ -863,10 +861,15 @@ public abstract class Entity var effect = new ConsumableItemEffect(item, this); // The same hook Activate runs over Equipment: a behaviour that computes its targets there (StatModifier) // would otherwise reach its second tick uninitialised. - foreach (var behavior in effect.Behaviors) + InitializeBehaviors(effect.Behaviors); + _activeConsumables.Add(effect); + } + + private static void InitializeBehaviors(IEnumerable behaviors) + { + foreach (var behavior in behaviors.ToList()) if (behavior is IInitializableBehavior initializable) initializable.Initialize(); - _activeConsumables.Add(effect); } public ConsumableItemEffect FindActiveConsumable(ConsumableItemData data) diff --git a/tests/Aetheria.Shared.Tests/ConsumableTests.cs b/tests/Aetheria.Shared.Tests/ConsumableTests.cs index 740f739ff..843b0f221 100644 --- a/tests/Aetheria.Shared.Tests/ConsumableTests.cs +++ b/tests/Aetheria.Shared.Tests/ConsumableTests.cs @@ -27,10 +27,13 @@ public sealed class ConsumableTests : IDisposable public EquippedItem Engine; public PerformanceStat Thrust; public ConsumableItemData Consumable; + public ConsumableItemData Other; // a second, unrelated non-stackable design + public Func NewEngine; public CultRecordKey Key; public CultRecordRef Maker; - public ConsumableItem Mint() => Assert.IsType(Items.CreateInstance(Items.CreateLot(Consumable, Maker, .5f))); + public ConsumableItem Mint(ConsumableItemData design = null) => + Assert.IsType(Items.CreateInstance(Items.CreateLot(design ?? Consumable, Maker, .5f))); public EquippedCargoBay Hold => Ship.CargoBays.Single(); public int InCargo => Hold.Cargo.Keys.Count(i => i is ConsumableItem); } @@ -70,6 +73,7 @@ public sealed class ConsumableTests : IDisposable } } }); + var otherRef = cache.Upsert(new ConsumableItemData { Name = "Flare", Duration = 1f, Shape = new Shape() }); cache.FlushAsync().Wait(); var items = new ItemManager(cache, new ProvenanceLedger(), RunSaveTests.TestSettings(), _ => { }); @@ -84,7 +88,8 @@ public sealed class ConsumableTests : IDisposable var data = cache.Get(consumableRef); return new Rig { - Cache = cache, Items = items, Ship = ship, Thrust = thrust, Consumable = data, + Cache = cache, Items = items, Ship = ship, Thrust = thrust, Consumable = data, Other = cache.Get(otherRef), + NewEngine = () => Gear(cache.Get(engineRef)), Key = items.ItemData.RefOf(data).Key, Engine = ship.Equipment.Single(e => e.Data.Name == "Engine"), Maker = makerRef @@ -149,4 +154,94 @@ public sealed class ConsumableTests : IDisposable Assert.True(rig.Ship.TryActivateConsumable(rig.Consumable)); Assert.Equal(0, rig.InCargo); } + + // The expiry boundary: Duration 1 with 0.3 s ticks leaves 0.1 s after the third tick (still active) and + // -0.2 s after the fourth (gone). An expiry one tick early or one tick late changes one of these two reads. + [Fact] + public void AConsumableLivesForItsDurationAndNoLonger() + { + var rig = Build(); + using var _ = rig.Cache; + rig.Ship.ActivateConsumable(rig.Mint()); + Tick(rig, 3, .3f); + Assert.Equal(20f, rig.Engine.Evaluate(rig.Thrust), 3); + Tick(rig, 1, .3f); + Assert.Equal(10f, rig.Engine.Evaluate(rig.Thrust), 3); + } + + // CS-R1, the stackable side: a stackable design activates again while its first effect runs, and every + // multiplier on the stat multiplies. + [Fact] + public void TwoStackableConsumablesOverlapAndMultiply() + { + var rig = Build(stackable: true); + using var _ = rig.Cache; + Assert.True(rig.Hold.TryStore(rig.Mint())); + Assert.True(rig.Hold.TryStore(rig.Mint())); + + Assert.True(rig.Ship.TryActivateConsumable(rig.Consumable)); + Assert.True(rig.Ship.TryActivateConsumable(rig.Consumable)); + Assert.Equal(0, rig.InCargo); + Tick(rig, 2, .3f); + Assert.Equal(40f, rig.Engine.Evaluate(rig.Thrust), 3); + } + + // Two modifiers share one resolver entry. The first expires while the second still runs: the second's + // boost stays, and when it expires too nothing is left behind. + [Fact] + public void AnExpiringConsumableLeavesAnOverlappingOnesBoost() + { + var rig = Build(stackable: true); + using var _ = rig.Cache; + var resolver = rig.Ship.Resolver; + var baseline = resolver.ModifierEntryCount; + Assert.True(rig.Hold.TryStore(rig.Mint())); + Assert.True(rig.Hold.TryStore(rig.Mint())); + + Assert.True(rig.Ship.TryActivateConsumable(rig.Consumable)); + Tick(rig, 2, .3f); + Assert.True(rig.Ship.TryActivateConsumable(rig.Consumable)); + Tick(rig, 2, .3f); // the first ran out on the fourth tick overall; the second has 0.4 s left + Assert.Equal(20f, rig.Engine.Evaluate(rig.Thrust), 3); + Assert.True(resolver.ModifierEntryCount > baseline); + + Tick(rig, 2, .3f); + Assert.Equal(10f, rig.Engine.Evaluate(rig.Thrust), 3); + Assert.Equal(baseline, resolver.ModifierEntryCount); + } + + // CS-R1: the cooldown is per design. A running effect of one design does not refuse another design. + [Fact] + public void ADesignsCooldownDoesNotBlockAnotherDesign() + { + var rig = Build(); + using var _ = rig.Cache; + Assert.True(rig.Hold.TryStore(rig.Mint())); + Assert.True(rig.Hold.TryStore(rig.Mint(rig.Other))); + + Assert.True(rig.Ship.TryActivateConsumable(rig.Consumable)); + Assert.True(rig.Ship.TryActivateConsumable(rig.Other)); + Assert.Equal(0, rig.InCargo); + } + + // Docking: an effect still running when the entity deactivates, is refitted and activates again boosts the + // items now fitted, as gear modifiers do, not the thruster that was swapped out. + [Fact] + public void AConsumableActiveAcrossARefitBoostsTheNewItems() + { + var rig = Build(); + using var _ = rig.Cache; + rig.Ship.ActivateConsumable(rig.Mint()); + Tick(rig, 2, .3f); + Assert.Equal(20f, rig.Engine.Evaluate(rig.Thrust), 3); + + rig.Ship.Deactivate(); + Assert.NotNull(rig.Ship.TryUnequip(rig.Engine)); + Assert.True(rig.Ship.TryEquip(rig.NewEngine(), EngineCell)); + rig.Ship.Activate(); + Tick(rig, 2, .1f); // 0.2 s more: the effect still has 0.2 s of its second + var fitted = rig.Ship.Equipment.Single(e => e.Data.Name == "Engine"); + Assert.NotSame(rig.Engine, fitted); + Assert.Equal(20f, fitted.Evaluate(rig.Thrust), 3); + } } diff --git a/tests/Aetheria.Shared.Tests/StatResolverTests.cs b/tests/Aetheria.Shared.Tests/StatResolverTests.cs index 9afafcd26..0c0283f20 100644 --- a/tests/Aetheria.Shared.Tests/StatResolverTests.cs +++ b/tests/Aetheria.Shared.Tests/StatResolverTests.cs @@ -441,7 +441,7 @@ public sealed class StatResolverTests : IDisposable for (var i = 0; i < 20; i++) { var lot = items.Lots.Add(new Lot { Design = cache.RefOf(boosterData), Origin = new Attributed(), Quality = .5f, Roles = new List() }); - var consumableItem = new ConsumableItem { Data = cache.RefOf(boosterData), Lot = lot }; + var consumableItem = Assert.IsType(items.CreateInstance(lot)); ship.ActivateConsumable(consumableItem); // Duration is 0.01s; one 1-second tick expires it and drives Entity.Update's expiry branch. From 87ec736f045aabbf0c23ce1e1562e069f4c5c3e8 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 23:25:25 +0200 Subject: [PATCH 107/120] Map: articulated mounts (body facts AM1-AM11, model-page rows, authority map, rationale) Co-Authored-By: Claude Opus 5.5 --- docs/aetheria-release-map.md | 188 +++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) diff --git a/docs/aetheria-release-map.md b/docs/aetheria-release-map.md index 255ed25a5..fe79044d6 100644 --- a/docs/aetheria-release-map.md +++ b/docs/aetheria-release-map.md @@ -7328,3 +7328,191 @@ vapour-cloud-presenter r2, modifier-stacking r3 (shrunk to tests), consumables-f follow-up; reshaped by `consumables-scarce`), boost-gear-systems r2 and boost-gear-combat r2 (held), ships-authoring-tool r2, ships-player r3. New: cut player-build r1, follow-up `component-records-after-demo`. No question raised. + +## Articulated mounts + +Mapped 2026-10-09 by Imagination (`imagination-aeth-articulated`, session `self-2026-10-09-ag`) for follow-up +`articulated-mounts`, against `origin/master` `6d83597b`. Rulings: `arc-and-traverse-on-the-link`, +`rig-is-presentation`, `sim-hooks-presentation-agnostic`, `projectile-flight-is-presentation`, +`ship-roles-are-tags`, `thrusters-radiators-are-meshes`; follow-up `articulated-mount-traverse-min`. +Anchors are exact at `6d83597b`. + +**Scope tension for Self.** Target r4's `not_in_scope` lists "articulated mounts". The operator asked for this work +now (her hermit-crab hull carries its big gun on a jointed arm). The cuts are admitted; the target needs a revision +that takes articulated mounts out of `not_in_scope` before Hands is briefed. + +### Body facts + +**AM1. Arc is per hardpoint today, not per weapon.** `FireControl.ArcFor` (`FireControl.cs:36-41`) reads +`HardpointData.FiringArc` (`ItemData.cs:567-569`, key 6) when above zero, else `GameplaySettings.FiringArc` +(`Settings.cs:222`, initializer 120). `InArc` (`:50-58`) passes 360 or more unconditionally. The only shipped writer +was `AetherDb firing-arc-migrate` (`tools/AetherDb/Program.cs:998-1047`), which set 360 on the Turret hull's +Ballistic hardpoints. Twenty-three hardpoint initializers author `FiringArc`, all in tests (`git grep -nE +"[{,] *FiringArc = |^ +FiringArc = "` over tests, Assets/Scripts and tools: 23), and eight accessors read or write +it (`hardpoint\??\.FiringArc|Mount\(s\)\.FiringArc|Hardpoints\[0\]\.FiringArc`: 8). The brief's "per weapon, +default 120, turret 360" is this per-hardpoint rule. + +**AM2. Traverse is not simulated; it only prices.** `Solution` (`FireControl.cs:128-152`) says turrets traverse +instantly in the simulation and that link traverse "will make this stateful behind the same signature". +`TrackingRate` (`:238-245`) is the gun's `Weapon.Tracking` times the ship's targeting gear over `UnaidedTracking`, ++infinity for an unauthored gun, and only `PMount` reads it (`:383`, `:427`, `:720`). Follow-up +`articulated-mount-traverse-min` puts the link's traverse inside `TrackingRate` as the slower of the two. These cuts +add no slew state: arc and aim stay derived on every read. + +**AM3. The visual record reserves a pivot role nobody uses.** `ShipAuthoringStore.Roles` (`ShipAuthoring.cs:50-58`) +includes `articulation`, "a pivot, never a mount". No add-on code tags it, and `ShipModShips.Assemble` gives every +mod ship `ArticulationPoints = Array.Empty()` (`ShipModShips.cs:164`). The authoring doc says no +pivot is planned "until pivots move onto `HullData`" (`docs/moddable-ship-authoring.md:188-189`). + +**AM4. `ArticulationPoint` is one yaw/pitch pivot with no yaw clamp.** `ArticulationPoint.cs:9-48` slews yaw and +pitch toward `Target` at `Speed` and clamps only pitch. `ShipPrefabAuthoring.cs:139-149` parses +`Pivot.Group.YawMin.YawMax.PitchMin.PitchMax.Speed` but stores no yaw limit, so the Controls section's C9 line +("clamps yaw to YawMin..YawMax") is wrong on this base. A single pivot needs no IK. `EntityInstance.cs:282-289` +gives each articulation group an aim transform, and `LateUpdate` places it (`:410-417`) from +`FireControl.Solution(...).Direction` of the group's first weapon. A link presenter follows that read. The Eyes IK +report (AM11) found the same two facts. + +**AM5. The GLB path maps every node that carries `aetheria.id`.** `ShipModCatalog.ReadNodeIds` +(`ShipModCatalog.cs:195-218`) reads node extras, refuses a duplicate id and records whether the node has a mesh; it +keeps no parent relation. `Bind` (`:168-191`) checks each anchor's node. `ShipModVisual.LoadAsync` maps anchors to +Transforms through glTFast's instantiator (`ShipModVisual.cs:51-54`). A joint is an ordinary glTF node, so the same +map reaches it once it carries an id. + +**AM6. Probe: the add-on's export keeps an armature, its skin and data-bone extras.** Run on Starfire 2026-10-09 +with Blender 5.2.2 LTS at `C:\Program Files (x86)\Steam\steamapps\common\Blender\blender.exe` +(`--background --factory-startup`). Script `probe_rig_export.py` in the session scratchpad +(`...\scratchpad\imagination-aeth-articulated\`). It builds a collection with a hull cube; an armature +Shoulder > Elbow > Wrist; a cylinder skinned to Wrist through an Armature modifier; an empty `weapon-mount` +parented to the Wrist bone (`parent_type = BONE`) with a muzzle child; a Limit Rotation constraint on Elbow; and +custom properties on the Wrist data bone (`aetheria.role=link`, `aetheria.id=link.claw`) and on the Wrist pose bone. +It exports with the add-on's options (`__init__.py:359-361`: GLB, `use_active_collection`, `use_selection`, +`export_extras`, `export_yup`, `export_apply`). Result: +- One skin with joints `Shoulder, Elbow, Wrist`; the barrel node has `skin: 0` and a mesh. `export_apply` did not + bake the armature away. +- Joints are ordinary nodes in the hierarchy (`Arm > Shoulder > Elbow > Wrist > ClawGun > ClawGun.Muzzle`): the + bone-parented empty is a child of the Wrist joint node and keeps its extras. +- Data-bone custom properties export as the joint node's `extras` (`Wrist` carried `aetheria.role` and + `aetheria.id`). The pose-bone property did **not** export. A tag set by hand in Pose Mode's custom-properties + panel is lost; the add-on must write data-bone properties. +- The Limit Rotation constraint left no trace in the GLB (no extension, no extras). glTF carries no joint limits, + so the add-on must package them. +- The armature object's own custom property exported as extras on the `Arm` node. + +**AM7. The ship file carries unknown slots through.** `ship_cc.replace_layout` (`ship_cc.py:161-195`) and +`replace_visual` (`:116-140`) append each row's slots past the known members by mount or anchor id, and edit only +their own slots. `ShipSchemaPinTests` pins the Python slot numbers and member orders to the C# keys and asserts +`HARDPOINT_MEMBERS` has 7 names. A C# cut that adds `HardpointData` key 7 or `HullData` key 33 therefore needs no +Python change: the add-on carries both untouched until the add-on cut names them. + +**AM8. The schema version string is not a gate.** CultCache resolves a persisted record by schema name when the +local type has one version (CultLib `src/GameCult.Caching/CultCache.cs:518-523`). `aetheria.hulldata` and +`aetheria.ship_authoring` are both `"1"` (`ItemData.cs:510`, `ShipAuthoring.cs:12`). Bumping either buys no +refusal. A retired slot is guarded by a raw-payload refusal instead, as `RefuseLegacyEmbeddedHull` +(`ShipAuthoring.cs:124-151`) guards ShipAuthoring key 1. Every cut here keeps both versions at `"1"`. + +**AM9. Calibre is a footprint band, held only in a test so far.** `WeaponItemData.WeaponCaliber` +(`ItemData.cs:482-483`) runs Small to ExtraLarge (`Enums.cs:77-83`). Cut `caliber-bands` (unlanded) states the bands +(Small 1-2 cells, Medium 3-4, Large 5-6, ExtraLarge 7-9) only inside its test. No balance source names an arc cap +per calibre: AetheriaLore `Game Design/Ship Play Concepts.md` fork 2 (line 24, read in the ship-language +worktree) gives the trade without numbers. + +**AM10. In-flight add-on specs that touch arcs.** Among in-force specs with no report, only `ships-addon-gizmos` r3 +(arc fan from the row's arc) and `ships-addon-mounts` r5 (row defaults include `FiringArc`; a negative grep on +`.firing_arc =`) name a hardpoint arc. `controls-hud` r2, `controls-ai-bearing` r1 and `munition-shots` r5 read +`ArcFor` or `TrackingRate`, whose signatures these cuts keep. + +**AM11. Eyes prior art on IK** (`C:\Users\Meta\AppData\Local\Temp\claude\F--Projects-CultLib\f24b705c-99b0-4915-8cd0-d31d652edbbf\scratchpad\eyes-ik-prior-art\ik-prior-art.md`). +Damped least squares, min `||[J; lambda I] dtheta - [e; 0]||^2` with the joint bounds as the box, has exactly the +form of CultMath's existing `BoundedLeastSquares`. Every engine Eyes checked (Unity Animation Rigging, Godot 4.6, +Unreal Control Rig) keeps IK in the animation layer; none was found in a math library. Aim-an-axis leaves twist +about the aim free, so a chain aims with two effective degrees of freedom. + +### Model page + +| Kind | Identity | Lifecycle | Authority | +|---|---|---|---| +| Link (`LinkData` in `HullData.Links`, key 33) | `Id`: the `aetheria.id` that Tag Link writes on the data bone or object (ruling `ship-roles-are-tags`). Renaming never changes it. For a shipped hull, the id the migration writes. | Edited in Blender's layout buffer and written by Save or Package. A package edited after a save changes the arc or traverse a run's guns use on Continue. No save names a link, because the sim holds no slew state (AM2). If a link is removed while a hardpoint names it, the judge fails the package, compose excludes it, and Continue refuses the run, naming the missing design (`RunSave.RequireDesigns`). It adds nothing to `mod-hull-changed-under-run`, because a link change moves no cell. | The author, through the add-on, for arc and traverse. `HullLinks.Validate` judges it. `FireControl` reads it, only through `ArcFor` and `TrackingRate`. | +| Hardpoint on a link (`HardpointData.Link`, key 7) | The hardpoint's mount id (`Transform`) plus the link id. Empty means a fixed mount: the default arc and the gun's own traverse. | Derived at Package from the mount object's ancestry (the nearest ancestor tagged link, through a bone parent), as mounted `Position` is derived from the object. A mount re-parented in Blender changes link at the next Package. For `ambiguous-hardpoint-origin`, a link is one more way two hardpoints at one origin differ (arc, as rotation already does), so it neither fixes nor worsens that defect. The fix there is to save the hardpoint's identity: C4's element id on `HardpointData`. | The mount's place in the Blender hierarchy; Package derives it. The panel and Studio show it read-only for a mounted row. The judge refuses a name that is not a link of the hull. | +| Rig chain (`ShipJoint` in `ShipAuthoring.Joints`, key 5, and the `link` anchors) | Each joint's `Id` is its bone's `aetheria.id`, assigned once by Package (`joint.`) and kept on the data bone. A link anchor's id is its link's id. | Re-rigging changes only the visual record, and every Package and compose judges it again. No save, sim fact or catalog record names a joint. The Unity rig components are cache-only and rebuilt each boot with the prototype. | The author's Blender bone IK settings (lock, limit, min, max), copied by Package. The judge checks the tree and that the yaw reach covers the link's arc. Unity reads and drives it. No joint feeds a sim fact (ruling `rig-is-presentation`). | + +**C4 (follow-up `c4-ship-element-ids`).** Links are a fourth element kind for variants: `LinkData` rows keyed by +`Id`, as hardpoints are keyed by `Transform`. A heavy variant that narrows a link's arc is a tail-slot patch by link +id. `ship_cc` carries link rows' unknown slots by id, as it does for hardpoints and anchors; `ships-addon-links` +lands that. + +### Authority map + +- **Owner.** `LinkData`, authored per hull, owns arc width and traverse speed. The Blender hierarchy owns whether a + hardpoint is on a link, derived into `HardpointData.Link` at Package. The Blender bone IK settings own the joint + limits, packaged into `ShipAuthoring.Joints`. +- **Sim inputs.** `HullData.Links` and `HardpointData.Link`, nothing else. +- **Outputs.** `ArcFor`, `InArc`, `AimDirection` and `TrackingRate`, signatures unchanged. Presentation reads + `GunSolution.Direction` of a link's lead weapon and the link's traverse. +- **Derived.** `HardpointData.Link`, from the hierarchy. A fixed mount's arc, which is `GameplaySettings.FiringArc`. + An object link's joint (one yaw degree of freedom, plus or minus half the arc), at Package. The Unity link rigs + (cache-only). +- **Demoted, then cut.** `HardpointData.FiringArc` is no longer an owner; the link is. `links-record` refuses a + hardpoint that names a link while its `FiringArc` is set. `links-arc` stops reading it and refuses it on packages. + `retire-firing-arc` deletes it and retires key 6. `link-rig-record` replaces the `articulation` role with `link`. +- **Forbidden writers.** No joint limit, bone length, IK result or Unity transform feeds `ArcFor` or + `TrackingRate`. No panel or Studio edit writes a mounted row's `Link`. No helper invents an arc or a traverse. + +### Rationale + +**Links are flat in `HullData`; chains live in the visual record.** The sim reads only the link a hardpoint names +(ruling `rig-is-presentation`). A turret on a turret is a chain of joints, and a chain is presentation, so `LinkData` +has no parent: its arc is the whole coverage the author grants that part. Parenting lives in `ShipJoint.Parent`. A +link's chain runs from its joint up to the nearest ancestor that is a link, or to the root, and parent links solve +first. + +**One facing per link.** Weapon hardpoints on one link must share a `Rotation`. `MountDirection` stays derived from +the item's rotation (`FireControl.cs:28-33`), the link needs no facing field, and the presenter's lead weapon aims +for every gun on the part. + +**Why `FiringArc` is retired rather than kept for fixed mounts.** Two places that state an arc are two owners. A +fixed mount is the degenerate case: no link, the default arc, the gun's own traverse. Any other arc is a link, +including the shipped Turret hull's, which migrates to one link with arc 360 and traverse +infinity, so its behaviour +is unchanged. The subtraction is its own cut and lands after the add-on cuts that edit the same panel, so no open +cut's hunks are stranded. + +**Traverse +infinity means no mechanical limit.** It matches `Weapon.Tracking`'s convention ("an unauthored gun is ++infinity", `FireControl.cs:238-245`), so `min` needs no special case. The add-on shows it as a Free traverse +toggle; zero and NaN are refused. + +**Joint limits come from Blender's bone IK settings, not from Limit Rotation constraints.** Blender's own IK solver +honours `lock_ik_*`, `use_ik_limit_*` and `ik_min_*`/`ik_max_*`, so an author previewing with an IK constraint sees +the limits the game uses. The semantics follow Blender's: an axis is free unless locked or limited. Package writes +each degree of freedom as a unit axis in Ship Root space at rest, converted to glTF axes, with a min and max in +degrees. This keeps the record independent of the exporter's bone-axis correction, which this map did not verify. + +**The reach check is yaw only, and conservative.** The sim is planar. A link's chain covers its arc when the summed +ranges of the chain's degrees of freedom whose axis lies within 5 degrees of Ship Root up reach the arc (an arc of +360 needs a summed 360). Serial joints about parallel axes add, so the sum is exact for the arms the add-on makes. A +chain whose yaw comes only from tilted axes is refused with a message naming the link. That is the price of a check +the judge can state in one line. + +**The solver is behind a port.** Aetheria's presenter needs only this: a chain (joints root-first, their rest +offsets, their degrees of freedom with limits), an effector (the muzzle offset and its local aim axis), the current +angles and a target direction; it gets back new angles. `link-rig-presenter` codes against that port. Question +`solver-owner` decides who implements it. + +**Thrusters on links stay a follow-up.** A gimballed thruster changes where thrust points. That is a sim fact, and +the thrust allocator (cuts `thrust-allocator-core`, `thrust-allocator` and `envelope-from-columns`, all in flight) +reads thrusters as fixed columns. Putting thrusters on links now would reopen those cuts. The judge refuses a +non-weapon hardpoint on a link and names follow-up `gimballed-thrusters`. + +**Cut order.** `links-record` (types and judge), then `links-arc` (FireControl reads the link; the Turret hull and +the test fixtures migrate), carry the sim. `link-rig-record` adds the joint record and the GLB checks. +`ships-addon-links` lands the Blender side after `ships-addon-mounts`. Mounts r5 needs no revision: it writes no +arc, and `ships-addon-links` extends its derivation. `ships-addon-gizmos` r4 replaces r3. `link-rig-presenter` +waits on `solver-owner`, `link-calibre-judge` waits on `calibre-arc-caps`, and `retire-firing-arc` goes last. + +### Admitted + +Receipts `mind-commit-b0c6b373...`, `mind-commit-c00a16b5...`, `mind-commit-08fce00f...` and `mind-commit-06e09a45...` +(2026-10-09 21:18-21:24 UTC). New cuts: links-record r1, links-arc r1, link-rig-record r2 (r1 superseded for one +out-of-range anchor), ships-addon-link-rows r1, ships-addon-link-package r1, link-rig-presenter r1, link-calibre-judge +r1, retire-firing-arc r1. Revised: ships-addon-gizmos r5 (r3 superseded by r4 and r4 by r5 for an untested negative +pattern). ships-addon-mounts r5 is unchanged. Questions: `solver-owner` (raised in link-rig-presenter) and +`calibre-arc-caps` (raised in link-calibre-judge). Follow-ups: `gimballed-thrusters`, `caliber-bands-one-table`. +context-pack read every anchor SAME at `6d83597b`. From 2dcb7b2078af66c9666bcabc2ca86e8e4955913b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 23:27:22 +0200 Subject: [PATCH 108/120] Ballistic flight r3: DrawAhead draws a round from the sim's facts, a hit ends at its impact cell; FireControl draws nothing FireControl.RoundEnd and RoundAt leave FireControl (RoundEnd to DrawAhead verbatim, RoundAt as DrawAhead's private Line); DrawAhead.Round takes the published outcome, steers a hit or burst from its frozen line at the commit tick onto its end (burst point, else the impact cell on the target's drawn pose), and RoundStop is deleted. Projectile caches the last published ShotOutcome and calls DrawAhead only. BallisticFlightTests move to the new signatures; the frozen-line moving-target test is replaced by impact-cell, steer and no-drawing-function tests. Unverified: not yet built or run. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/Gameplay/Weapons/Projectile.cs | 28 +- .../Gameplay/Weapons/ProjectileManager.cs | 2 +- Assets/Scripts/ServerShared/DrawAhead.cs | 67 +++- Assets/Scripts/ServerShared/FireControl.cs | 24 +- .../BallisticFlightTests.cs | 309 ++++++++++++------ 5 files changed, 286 insertions(+), 144 deletions(-) diff --git a/Assets/Scripts/Gameplay/Weapons/Projectile.cs b/Assets/Scripts/Gameplay/Weapons/Projectile.cs index 2fe4129d1..66a444d89 100644 --- a/Assets/Scripts/Gameplay/Weapons/Projectile.cs +++ b/Assets/Scripts/Gameplay/Weapons/Projectile.cs @@ -3,13 +3,13 @@ using System.Collections; using UniRx; using UnityEngine; -// A ballistic round, drawn by DrawAhead's round statics and deciding nothing. The simulation owns the round's whole -// flight (the PendingShot copy taken at spawn, replaced by the sim's record at ShotCommitted, and the outcome it -// publishes), so this object stores no motion state of its own and computes no position, time or range: each frame it -// asks DrawAhead.Round where the round is at sim time plus the clock's Lead (the same draw-ahead ships use), and the -// round ends on the sim's word: ShotResolved stops a round that hit or burst where DrawAhead.RoundStop puts it, and +// A ballistic round, drawn by DrawAhead.Round from what the simulation publishes and deciding nothing. The simulation +// owns the round's facts (the PendingShot copy taken at spawn, replaced by the sim's record at ShotCommitted, and the +// outcome it publishes), so this object stores no motion state of its own and computes no position, time, range or +// impact point: each frame it asks DrawAhead.Round where the round is at sim time plus the clock's Lead (the same +// draw-ahead ships use). The round ends on the sim's word: ShotResolved ends one that hit or burst, and // DrawAhead.RoundOver ends one the sim published as a miss. The barrel is only where the round is first seen: its -// offset from the sim line decays to zero over BlendTime sim seconds. The trail's fade after the end stays real time +// offset from the drawn line decays to zero over BlendTime sim seconds. The trail's fade after the end stays real time // (pure presentation, ruling sim-speed-presentation). public class Projectile : MonoBehaviour { @@ -22,7 +22,7 @@ public class Projectile : MonoBehaviour private PendingShot _shot; private Zone _zone; private CultMath.float3 _barrel; - private ShotResult? _known; + private ShotOutcome _known; private bool _alive; private CompositeDisposable _binding; @@ -49,25 +49,25 @@ public class Projectile : MonoBehaviour private void Draw() { - var time = _zone.Time + ActionGameManager.Clock.Lead; - transform.position = V(DrawAhead.Round(_shot, _known, _barrel, BlendTime, time)); - if (DrawAhead.RoundOver(_shot, _known, time)) Finish(); + var lead = ActionGameManager.Clock.Lead; + transform.position = V(DrawAhead.Round(_shot, _known, _barrel, BlendTime, _zone.Time, lead)); + if (DrawAhead.RoundOver(_shot, _known, _zone.Time, lead)) Finish(); } // The sim's commit: the record it holds now (a contact burst's arrival is shortened here) and what it decided. private void Committed(ShotOutcome outcome) { if (_zone.TryGetShot(outcome.ShotId, out var record)) _shot = record; - _known = outcome.Result; + _known = outcome; } - // The simulation's verdict. A round that hit or burst stops where the sim put it; a miss keeps flying. + // The simulation's verdict. A round that hit or burst is drawn at its end and stops; a miss keeps flying. private void Resolve(ShotOutcome outcome) { if (!_alive) return; - _known = outcome.Result; + _known = outcome; if (outcome.Result == ShotResult.Miss) return; - transform.position = V(DrawAhead.RoundStop(_shot, outcome, _barrel, BlendTime)); + Draw(); if (HitEffect != null) HitEffect.Instantiate().position = transform.position; Finish(); } diff --git a/Assets/Scripts/Gameplay/Weapons/ProjectileManager.cs b/Assets/Scripts/Gameplay/Weapons/ProjectileManager.cs index 83eba9ea4..216756cf9 100644 --- a/Assets/Scripts/Gameplay/Weapons/ProjectileManager.cs +++ b/Assets/Scripts/Gameplay/Weapons/ProjectileManager.cs @@ -4,7 +4,7 @@ public class ProjectileManager : InstantWeaponEffectManager { public Prototype ProjectilePrototype; - // The round is drawn on the line the simulation flies it (FireControl.RoundAt), so there is nothing to aim, jitter + // The round is drawn by DrawAhead from what the simulation publishes, so there is nothing to aim, jitter // or inherit here: spread is priced by FireControl, and the sim's round inherits nothing from its shooter. public override void Fire(InstantWeapon weapon, EquippedItem item, EntityInstance source, EntityInstance target, int shotId) { diff --git a/Assets/Scripts/ServerShared/DrawAhead.cs b/Assets/Scripts/ServerShared/DrawAhead.cs index f594b33fe..98b70656e 100644 --- a/Assets/Scripts/ServerShared/DrawAhead.cs +++ b/Assets/Scripts/ServerShared/DrawAhead.cs @@ -5,10 +5,11 @@ using CultMath; using static CultMath.math; -// Where a presenter draws the latest sim state when the frame falls Lead sim seconds after the last step: ships by -// their last step's velocity, ballistic rounds on FireControl's RoundAt and RoundEnd. Pure reads of the simulation for -// presenters (never called by ServerShared, no state of their own): the sim owns every pose and no presenter stores -// one, a previous state or a velocity of its own. +// Pure presentation statics over sim state, never called by ServerShared, with no state of their own: the sim owns every +// pose and no presenter stores one, a previous state or a velocity of its own. Ships are drawn where the frame falls Lead +// sim seconds after the last step, by their last step's velocity and turn. A ballistic round's flight is presentation +// (ruling projectile-flight-is-presentation), drawn from the sim's facts: its frozen line, the published outcome, the +// commit tick, the impact cell on the target's drawn pose, the burst point. public static class DrawAhead { // A ship in a wormhole animation has its pose scripted by the step, not integrated from Velocity and TurnRate, @@ -39,19 +40,57 @@ public static class DrawAhead t.w * q.w - t.x * q.x - t.y * q.y - t.z * q.z)); } - // A ballistic round at a sim time (the clock's Zone.Time + Lead): on FireControl's line for what the sim has published - // of its outcome, with the barrel's offset from that line decaying to nothing over blendTime sim seconds from the moment of firing. - public static float3 Round(in PendingShot shot, ShotResult? known, float3 barrel, float blendTime, float time) => - Lifted(shot, FireControl.RoundAt(shot, known, time), barrel, blendTime, time); + // A ballistic round at a sim time (the clock's Zone.Time + Lead): on its frozen line while the outcome is unknown or a + // miss, with the barrel's offset from that line decaying to nothing over blendTime sim seconds from the moment of firing. + // A round the sim published as a hit or burst steers from its line, from the commit tick, onto where it ends by the + // time damage lands: a burst's frozen point, else the impact cell on the target's drawn pose; and is held there after. + public static float3 Round(in PendingShot shot, ShotOutcome known, float3 barrel, float blendTime, float simTime, float lead) + { + var time = simTime + lead; + var drawn = Lifted(shot, Line(shot, known?.Result, time), barrel, blendTime, time); + if (known == null || known.Result == ShotResult.Miss) return drawn; + var end = known.HasBurstPoint + ? float3(known.BurstPoint.x, shot.FireOrigin.y, known.BurstPoint.y) + : Impact(known.Target, known.Cell, lead); + var commit = shot.ArrivalTime - known.ArrivalIn; + var steer = shot.ArrivalTime > commit ? saturate((time - commit) / (shot.ArrivalTime - commit)) : 1f; + return drawn + (end - drawn) * steer; + } - // Where a round that hit or burst stops: the outcome's burst point, else where it arrives on its line. - public static float3 RoundStop(in PendingShot shot, ShotOutcome outcome, float3 barrel, float blendTime) => - Lifted(shot, outcome.HasBurstPoint ? outcome.BurstPoint : FireControl.RoundAt(shot, outcome.Result, shot.ArrivalTime), - barrel, blendTime, shot.ArrivalTime); + // The impact cell's centre on the target's drawn pose: the cell at the sim pose, drawn ahead by the target's last + // step velocity and turned by its last step turn for lead seconds, as Position and Rotation draw the entity. + public static float3 Impact(Entity target, int2 cell, float lead) + { + var at = Position(target, lead); + var offset = target.ToWorldPoint((float2) cell) - target.Position.xz; + var turned = mul(offset, float2x2.Rotate(Turn(target, lead))); + return float3(at.x + turned.x, at.y, at.z + turned.y); + } // Whether the drawn flight is over by time: only a published miss ends by the clock. A hit or burst ends on ShotResolved. - public static bool RoundOver(in PendingShot shot, ShotResult? known, float time) => - known == ShotResult.Miss && time >= FireControl.RoundEnd(shot, ShotResult.Miss); + public static bool RoundOver(in PendingShot shot, ShotOutcome known, float simTime, float lead) => + known != null && known.Result == ShotResult.Miss && simTime + lead >= RoundEnd(shot, ShotResult.Miss); + + // The sim time a round's drawn line ends, for what is known of its outcome. A round that hit or burst (or whose outcome + // is not yet published) ends at its ArrivalTime; a round the sim has published as a Miss flies on to the weapon's frozen + // range and never ends short of its own arrival (a direct round's FireRange is measured to any target, in range or not). + public static float RoundEnd(in PendingShot shot, ShotResult? known) => + known == ShotResult.Miss + ? max(shot.ArrivalTime, shot.FireTime + (shot.Speed > .01f ? shot.MaxRange / shot.Speed : 0f)) + : shot.ArrivalTime; + + // The frozen line a round follows until it steers: from its frozen origin along its frozen direction at its frozen + // speed, held at RoundEnd once it has flown that far. With no outcome known the round is drawn no further than its + // arrival point and waits there for the sim to publish one. + private static float2 Line(in PendingShot shot, ShotResult? known, float time) + { + if (shot.Speed <= .01f) return shot.FireOrigin.xz; + var flown = clamp(time - shot.FireTime, 0f, RoundEnd(shot, known) - shot.FireTime); + return shot.FireOrigin.xz + shot.TravelDirection * (shot.Speed * flown); + } + + private static float Turn(Entity entity, float lead) => + entity is Ship { WormholeAnimationInProgress: true } ? 0f : entity.TurnRate * lead; private static float3 Lifted(in PendingShot shot, float2 planar, float3 barrel, float blendTime, float time) { diff --git a/Assets/Scripts/ServerShared/FireControl.cs b/Assets/Scripts/ServerShared/FireControl.cs index c1c4e8ddd..fbe2e0d13 100644 --- a/Assets/Scripts/ServerShared/FireControl.cs +++ b/Assets/Scripts/ServerShared/FireControl.cs @@ -514,26 +514,6 @@ public static class FireControl return shot.Target == null ? 1f : PDeviation(deviation, shot.Tracking); } - // The sim time a ballistic round's drawn flight ends, for what is known of its outcome: the one statement of a round's - // end. A round that hit or burst (or whose outcome is not yet published) ends at its ArrivalTime. A round the sim has - // published as a Miss flies on to the weapon's frozen range and never ends short of its own arrival (a direct round's - // FireRange is measured to any target, in range or not). Pure: it reads the shot alone, and the simulation's own - // arithmetic (Step, Commit, Apply) does not call it. Presenters end rounds here, through DrawAhead. - public static float RoundEnd(in PendingShot shot, ShotResult? known) => - known == ShotResult.Miss - ? max(shot.ArrivalTime, shot.FireTime + (shot.Speed > .01f ? shot.MaxRange / shot.Speed : 0f)) - : shot.ArrivalTime; - - // Where a ballistic round is at a sim time: the one statement of the round's line, from its frozen origin along its - // frozen direction at its frozen speed, held at RoundEnd once it has flown that far. With no outcome known the round is - // drawn no further than its arrival point and waits there for the sim to publish one. Pure, like RoundEnd. - public static float2 RoundAt(in PendingShot shot, ShotResult? known, float time) - { - if (shot.Speed <= .01f) return shot.FireOrigin.xz; - var flown = clamp(time - shot.FireTime, 0f, RoundEnd(shot, known) - shot.FireTime); - return shot.FireOrigin.xz + shot.TravelDirection * (shot.Speed * flown); - } - // How far a target can and does move off the line of fire in the solution window: the term DeviationProbability // and HitProbability both add. Behaviour (its observed unpredictability across the line of sight) realises the // evasion and capability (its envelope's reach) caps it, so a coasting ship evades nothing whatever it could do @@ -2023,9 +2003,9 @@ public struct PendingShot public float3 FireTargetPosition; public float3 FireTargetVelocity; public float FireTime; - // The weapon's Velocity at Fire: frozen with the rest, so RoundAt states the same line whatever happens to the stat later. + // The weapon's Velocity at Fire, frozen with the rest so a presenter draws the same line whatever happens to the stat later. public float Speed; - // The weapon's Range at Fire, likewise frozen: how far a round that misses flies on, read only by RoundEnd. FireRange above + // The weapon's Range at Fire, likewise frozen: how far a presenter may draw a round that misses. FireRange above // is the distance to the engaged target (0 for a round with none), not how far a round that misses flies on. public float MaxRange; public float CommitTime; diff --git a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs index f2c5b45f7..61a733e3a 100644 --- a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs +++ b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs @@ -4,18 +4,23 @@ using System; using System.Linq; +using System.Reflection; using UniRx; using Xunit; using static CultMath.math; using float2 = CultMath.float2; +using float2x2 = CultMath.float2x2; using float3 = CultMath.float3; +using int2 = CultMath.int2; -// Ballistic flight: FireControl.RoundAt is the one statement of where a fired round is at a sim time, from the -// PendingShot's frozen origin, direction, speed and fire time, and FireControl.RoundEnd the one statement of when its -// drawn flight ends for what the sim has published of its outcome. Unity draws rounds through DrawAhead's round statics -// and integrates nothing, so these tests pin the line, the end and the drawing against first principles (the shooter's -// position, the aim, the authored velocity and range) rather than against the code that computes them. Shares the -// Cut 12.4 engagement fixture. +// Ballistic flight: a round's flight is presentation (ruling projectile-flight-is-presentation). The simulation publishes +// the facts (the PendingShot's frozen origin, direction, speed, fire time and arrival, the outcome with its commit tick, +// impact cell and burst point) and DrawAhead's statics draw the round from them: Round the one statement of where a round +// is at a sim time, Impact where a hit lands on the target's drawn pose, RoundEnd and RoundOver when its drawn flight +// ends. FireControl owns none of it (ruling direct-arrival-keep-timing: damage keeps landing at the arrival time, the +// hit is drawn at its cell). Unity's Projectile calls Round and integrates nothing, so these tests pin the drawing +// against first principles (the shooter's position, the aim, the authored velocity and range, the hull's own frame) +// rather than against the code that computes it. Shares the Cut 12.4 engagement fixture. public sealed partial class FireControlCut124Tests { private const float BallisticSpeed = 20f; @@ -32,57 +37,77 @@ public sealed partial class FireControlCut124Tests return e; } + // What the sim publishes for a shot, as a presenter holds it: only the result (no impact, no burst point). + private static ShotOutcome Known(in PendingShot shot, ShotResult result) => + new ShotOutcome { ShotId = shot.ShotId, Result = result, ArrivalIn = max(0f, shot.ArrivalTime - shot.CommitTime) }; + + // A hit on a cell of the target, committed at `commit`: ArrivalIn is how long after the commit damage lands. + private static ShotOutcome Hit(in PendingShot shot, Entity target, int2 cell, float commit) => + new ShotOutcome { ShotId = shot.ShotId, Result = ShotResult.Hit, Target = target, Cell = cell, ArrivalIn = shot.ArrivalTime - commit }; + + // The hull's occupied coordinate farthest from its centre of mass: a cell well off the target's position. + private static int2 FarCell(Engagement e) => + e.HullData.Shape.Coordinates.OrderByDescending(c => length((float2) c - e.HullData.Shape.CenterOfMass)).First(); + [Fact] - public void RoundAtIsTheFiredLine() + public void ARoundIsDrawnOnItsFiredLine() { // Burst round: leaves the shooter, is where its own speed puts it a second in, and ends on the burst point. var e = FireBurstRound(out var origin); - var burst = SafeAssert.OnlyShot(e.Zone); + ShotOutcome outcome = null; + PendingShot burst = default; + using var c = e.Zone.ShotCommitted.Subscribe(o => + { + outcome = o; + e.Zone.TryGetShot(o.ShotId, out burst); + }); + e.Zone.Update(.01f); + Assert.NotNull(outcome); + Assert.True(outcome.HasBurstPoint); var aim = normalize(float2(2, 1)); // The shooter moves on after the round left: the line is the one frozen at Fire, not one drawn from where it is now. e.Shooter.Position += float3(7, 0, 3); - var start = FireControl.RoundAt(burst, null, burst.FireTime); + var start = DrawAhead.Round(burst, null, burst.FireOrigin, 0f, burst.FireTime, 0f); Assert.Equal(origin.x, start.x, 3); - Assert.Equal(origin.z, start.y, 3); + Assert.Equal(origin.z, start.z, 3); - var second = FireControl.RoundAt(burst, null, burst.FireTime + 1f); + var second = DrawAhead.Round(burst, null, burst.FireOrigin, 0f, burst.FireTime + 1f, 0f); Assert.Equal(origin.x + aim.x * BallisticSpeed, second.x, 3); - Assert.Equal(origin.z + aim.y * BallisticSpeed, second.y, 3); + Assert.Equal(origin.z + aim.y * BallisticSpeed, second.z, 3); - foreach (ShotResult? known in new ShotResult?[] { null, ShotResult.Burst }) + foreach (var known in new[] { null, outcome }) foreach (var later in new[] { 0f, 1f, 50f }) { - var arrival = FireControl.RoundAt(burst, known, burst.ArrivalTime + later); + var arrival = DrawAhead.Round(burst, known, burst.FireOrigin, 0f, burst.ArrivalTime + later, 0f); Assert.Equal(burst.BurstPosition.x, arrival.x, 3); - Assert.Equal(burst.BurstPosition.z, arrival.y, 3); + Assert.Equal(burst.BurstPosition.z, arrival.z, 3); } // Past the weapon's range it is held at the range point, however late it is asked. - var held = FireControl.RoundAt(burst, ShotResult.Miss, burst.FireTime + NoLockRange / BallisticSpeed + 50f); + var held = DrawAhead.Round(burst, Known(burst, ShotResult.Miss), burst.FireOrigin, 0f, burst.FireTime + NoLockRange / BallisticSpeed + 50f, 0f); Assert.Equal(origin.x + aim.x * NoLockRange, held.x, 3); - Assert.Equal(origin.z + aim.y * NoLockRange, held.y, 3); + Assert.Equal(origin.z + aim.y * NoLockRange, held.z, 3); // Before it was fired it is still at the origin, not behind it. - var before = FireControl.RoundAt(burst, null, burst.FireTime - 5f); + var before = DrawAhead.Round(burst, null, burst.FireOrigin, 0f, burst.FireTime - 5f, 0f); Assert.Equal(origin.x, before.x, 3); - Assert.Equal(origin.z, before.y, 3); + Assert.Equal(origin.z, before.z, 3); // Direct round at a stationary target 100 ahead (+z): arrives on the target, and a miss would fly on to the weapon's range. var d = Build(TestSettings(), SolidShape(5, 4), velocity: BallisticSpeed, weaponRange: 1000f); var dOrigin = d.Shooter.Position; FireControl.Fire(d.Weapon, d.WeaponItem, d.Shooter); var direct = SafeAssert.OnlyShot(d.Zone); - foreach (ShotResult? known in new ShotResult?[] { null, ShotResult.Hit }) - // A round not known to miss waits at its arrival point however late it is asked. - foreach (var at in new[] { direct.ArrivalTime, direct.FireTime + 99f }) - { - var hit = FireControl.RoundAt(direct, known, at); - Assert.Equal(d.Target.Position.x, hit.x, 2); - Assert.Equal(d.Target.Position.z, hit.y, 2); - } - Assert.Equal(dOrigin.z + 100f / 2f, FireControl.RoundAt(direct, null, direct.FireTime + 2.5f).y, 2); - var heldDirect = FireControl.RoundAt(direct, ShotResult.Miss, direct.FireTime + 1000f / BallisticSpeed + 99f); - Assert.Equal(dOrigin.z + 1000f, heldDirect.y, 2); + // A round not known to miss waits at its arrival point however late it is asked. + foreach (var at in new[] { direct.ArrivalTime, direct.FireTime + 99f }) + { + var waiting = DrawAhead.Round(direct, null, direct.FireOrigin, 0f, at, 0f); + Assert.Equal(d.Target.Position.x, waiting.x, 2); + Assert.Equal(d.Target.Position.z, waiting.z, 2); + } + Assert.Equal(dOrigin.z + 100f / 2f, DrawAhead.Round(direct, null, direct.FireOrigin, 0f, direct.FireTime + 2.5f, 0f).z, 2); + var heldDirect = DrawAhead.Round(direct, Known(direct, ShotResult.Miss), direct.FireOrigin, 0f, direct.FireTime + 1000f / BallisticSpeed + 99f, 0f); + Assert.Equal(dOrigin.z + 1000f, heldDirect.z, 2); } // A round with no speed (the weapon authors Velocity 0, or effectively 0) does not fly: it is at its origin at any @@ -95,9 +120,9 @@ public sealed partial class FireControlCut124Tests FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); var shot = SafeAssert.OnlyShot(e.Zone); - var late = FireControl.RoundAt(shot, ShotResult.Miss, shot.FireTime + 5000f); + var late = DrawAhead.Round(shot, Known(shot, ShotResult.Miss), shot.FireOrigin, 0f, shot.FireTime + 5000f, 0f); Assert.Equal(origin.x, late.x, 3); - Assert.Equal(origin.z, late.y, 3); + Assert.Equal(origin.z, late.z, 3); } [Fact] @@ -115,39 +140,44 @@ public sealed partial class FireControlCut124Tests var after = SafeAssert.OnlyShot(e.Zone); Assert.Equal(BallisticSpeed, after.Speed, 4); var aim = normalize(float2(2, 1)); - var at = FireControl.RoundAt(after, null, after.FireTime + 1f); + var at = DrawAhead.Round(after, null, after.FireOrigin, 0f, after.FireTime + 1f, 0f); Assert.Equal(origin.x + aim.x * BallisticSpeed, at.x, 3); - Assert.Equal(origin.z + aim.y * BallisticSpeed, at.y, 3); + Assert.Equal(origin.z + aim.y * BallisticSpeed, at.z, 3); } [Fact] - public void RoundAtIsPure() + public void ARoundIsPure() { - var e = FireBurstRound(out _); + var e = FireDirectRound(20f, 1000f); var shot = SafeAssert.OnlyShot(e.Zone); + e.Target.Direction = normalize(float2(1, 2)); + e.Target.TurnRate = 3f; var time = e.Zone.Time; var entities = e.Zone.Entities.ToList(); var positions = entities.Select(x => x.Position).ToList(); var velocities = entities.Select(x => x.Velocity).ToList(); + var directions = entities.Select(x => x.Direction).ToList(); + var turnRates = entities.Select(x => x.TurnRate).ToList(); var barrel = shot.FireOrigin + float3(1, .5f, -2); - for (var i = 0; i < 25; i++) - { - var at = shot.FireTime + i * .37f; - foreach (ShotResult? known in new ShotResult?[] { null, ShotResult.Miss }) + var cell = FarCell(e); + foreach (var known in new[] { null, Known(shot, ShotResult.Miss), Hit(shot, e.Target, cell, shot.CommitTime) }) + for (var i = 0; i < 25; i++) { - FireControl.RoundAt(shot, known, at); - FireControl.RoundEnd(shot, known); - DrawAhead.Round(shot, known, barrel, .1f, at); - DrawAhead.RoundOver(shot, known, at); + var at = shot.FireTime + i * .37f; + DrawAhead.Round(shot, known, barrel, .1f, at, .01f); + DrawAhead.RoundOver(shot, known, at, .01f); + DrawAhead.RoundEnd(shot, known?.Result); + DrawAhead.Impact(e.Target, cell, .01f * i); } - } Assert.Equal(time, e.Zone.Time); Assert.True(e.Zone.PendingShots.Count == 1 && e.Zone.PendingShots[0].Equals(shot), "the pending shot changed"); Assert.True(e.Zone.Entities.SequenceEqual(entities), "the zone's entities changed"); Assert.True(entities.Select(x => x.Position).SequenceEqual(positions), "an entity moved"); Assert.True(entities.Select(x => x.Velocity).SequenceEqual(velocities), "an entity's velocity changed"); + Assert.True(entities.Select(x => x.Direction).SequenceEqual(directions), "an entity turned"); + Assert.True(entities.Select(x => x.TurnRate).SequenceEqual(turnRates), "an entity's turn rate changed"); } // The weapon's Range is frozen with the rest: a round that misses flies on to the range it left with, whatever the stat @@ -158,7 +188,7 @@ public sealed partial class FireControlCut124Tests var e = FireBurstRound(out var origin); var shot = SafeAssert.OnlyShot(e.Zone); Assert.Equal(NoLockRange, shot.MaxRange, 4); - var endBefore = FireControl.RoundEnd(shot, ShotResult.Miss); + var endBefore = DrawAhead.RoundEnd(shot, ShotResult.Miss); // The stat changes after the round left, and the weapon re-reads it. ((InstantWeaponData) e.Weapon.WeaponData).Range = Constant(NoLockRange * 5f); @@ -167,11 +197,11 @@ public sealed partial class FireControlCut124Tests var after = SafeAssert.OnlyShot(e.Zone); Assert.Equal(NoLockRange, after.MaxRange, 4); - Assert.Equal(endBefore, FireControl.RoundEnd(after, ShotResult.Miss), 4); + Assert.Equal(endBefore, DrawAhead.RoundEnd(after, ShotResult.Miss), 4); var aim = normalize(float2(2, 1)); - var held = FireControl.RoundAt(after, ShotResult.Miss, after.FireTime + 1000f); + var held = DrawAhead.Round(after, Known(after, ShotResult.Miss), after.FireOrigin, 0f, after.FireTime + 1000f, 0f); Assert.Equal(origin.x + aim.x * NoLockRange, held.x, 3); - Assert.Equal(origin.z + aim.y * NoLockRange, held.y, 3); + Assert.Equal(origin.z + aim.y * NoLockRange, held.z, 3); } // A direct round at a stationary target `targetRange` ahead (+z) of the shooter, weapon Range `range`, at `speed`; @@ -198,29 +228,30 @@ public sealed partial class FireControlCut124Tests var d = FireDirectRound(speed, 1000f); var direct = SafeAssert.OnlyShot(d.Zone); Assert.Equal(direct.FireTime + 100f / speed, direct.ArrivalTime, 4); - Assert.Equal(direct.FireTime + 100f / speed, FireControl.RoundEnd(direct, null), 4); - Assert.Equal(direct.FireTime + 100f / speed, FireControl.RoundEnd(direct, ShotResult.Hit), 4); - Assert.Equal(direct.FireTime + 100f / speed, FireControl.RoundEnd(direct, ShotResult.Burst), 4); - Assert.Equal(direct.FireTime + 1000f / speed, FireControl.RoundEnd(direct, ShotResult.Miss), 4); + Assert.Equal(direct.FireTime + 100f / speed, DrawAhead.RoundEnd(direct, null), 4); + Assert.Equal(direct.FireTime + 100f / speed, DrawAhead.RoundEnd(direct, ShotResult.Hit), 4); + Assert.Equal(direct.FireTime + 100f / speed, DrawAhead.RoundEnd(direct, ShotResult.Burst), 4); + Assert.Equal(direct.FireTime + 1000f / speed, DrawAhead.RoundEnd(direct, ShotResult.Miss), 4); - foreach (ShotResult? known in new ShotResult?[] { null, ShotResult.Hit, ShotResult.Burst }) - Assert.False(DrawAhead.RoundOver(direct, known, direct.FireTime + 10000f)); + Assert.False(DrawAhead.RoundOver(direct, null, direct.FireTime + 10000f, 0f)); + foreach (var result in new[] { ShotResult.Hit, ShotResult.Burst }) + Assert.False(DrawAhead.RoundOver(direct, Known(direct, result), direct.FireTime + 10000f, 0f)); var missEnd = direct.FireTime + 1000f / speed; - Assert.False(DrawAhead.RoundOver(direct, ShotResult.Miss, missEnd - .01f)); - Assert.True(DrawAhead.RoundOver(direct, ShotResult.Miss, missEnd)); + Assert.False(DrawAhead.RoundOver(direct, Known(direct, ShotResult.Miss), missEnd - .01f, 0f)); + Assert.True(DrawAhead.RoundOver(direct, Known(direct, ShotResult.Miss), missEnd, 0f)); // A target beyond the weapon's range: a miss never ends short of its own arrival. var far = FireDirectRound(speed, 100f, targetRange: 150f); var beyond = SafeAssert.OnlyShot(far.Zone); Assert.Equal(beyond.FireTime + 150f / speed, beyond.ArrivalTime, 4); - Assert.Equal(beyond.FireTime + 150f / speed, FireControl.RoundEnd(beyond, ShotResult.Miss), 4); + Assert.Equal(beyond.FireTime + 150f / speed, DrawAhead.RoundEnd(beyond, ShotResult.Miss), 4); // A round fired at nothing arrives where it left, and a miss flies on to the range. var none = FireDirectRound(speed, 100f, atNothing: true); var nothing = SafeAssert.OnlyShot(none.Zone); Assert.Equal(nothing.FireTime, nothing.ArrivalTime, 4); - Assert.Equal(nothing.FireTime, FireControl.RoundEnd(nothing, null), 4); - Assert.Equal(nothing.FireTime + 100f / speed, FireControl.RoundEnd(nothing, ShotResult.Miss), 4); + Assert.Equal(nothing.FireTime, DrawAhead.RoundEnd(nothing, null), 4); + Assert.Equal(nothing.FireTime + 100f / speed, DrawAhead.RoundEnd(nothing, ShotResult.Miss), 4); } } @@ -239,21 +270,17 @@ public sealed partial class FireControlCut124Tests foreach (var lead in new[] { 0f, step / 2f, .999f * step }) { - var time = shot.ArrivalTime + lead; - foreach (ShotResult? known in new ShotResult?[] { null, ShotResult.Hit, ShotResult.Burst }) - { - var drawn = DrawAhead.Round(shot, known, shot.FireOrigin, 0f, time); - Assert.Equal(origin.x, drawn.x, 3); - Assert.Equal(origin.z + 100f, drawn.z, 3); - } - var missed = DrawAhead.Round(shot, ShotResult.Miss, shot.FireOrigin, 0f, time); + var waiting = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, shot.ArrivalTime, lead); + Assert.Equal(origin.x, waiting.x, 3); + Assert.Equal(origin.z + 100f, waiting.z, 3); + var missed = DrawAhead.Round(shot, Known(shot, ShotResult.Miss), shot.FireOrigin, 0f, shot.ArrivalTime, lead); Assert.Equal(origin.x, missed.x, 3); Assert.Equal(origin.z + 100f + lead * speed, missed.z, 3); } for (var time = shot.FireTime; time <= shot.ArrivalTime + 1f; time += 1f / 144f) { - var drawn = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, time); + var drawn = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, time, 0f); Assert.True(drawn.z - origin.z <= 100f + 1e-3f, "the round was drawn past its arrival point"); } } @@ -289,7 +316,7 @@ public sealed partial class FireControlCut124Tests Assert.Equal(fired.FireTime + 1.05f, record.ArrivalTime, 2); Assert.True(record.ArrivalTime < fired.ArrivalTime); - var stop = DrawAhead.RoundStop(record, outcome, record.FireOrigin, 0f); + var stop = DrawAhead.Round(record, outcome, record.FireOrigin, 0f, record.ArrivalTime, 0f); var expected = outcome.HasBurstPoint ? outcome.BurstPoint : record.FireOrigin.xz + record.TravelDirection * (20f * (record.ArrivalTime - record.FireTime)); @@ -297,7 +324,7 @@ public sealed partial class FireControlCut124Tests Assert.Equal(expected.y, stop.z, 3); foreach (var later in new[] { 0f, .5f, 5f }) { - var drawn = DrawAhead.Round(record, ShotResult.Burst, record.FireOrigin, 0f, record.ArrivalTime + later); + var drawn = DrawAhead.Round(record, outcome, record.FireOrigin, 0f, record.ArrivalTime + later, 0f); Assert.Equal(expected.x, drawn.x, 3); Assert.Equal(expected.y, drawn.z, 3); } @@ -324,11 +351,11 @@ public sealed partial class FireControlCut124Tests Assert.Equal(want.z, got.z, 3); } - Close(barrel, DrawAhead.Round(shot, null, barrel, blend, shot.FireTime)); - Close(Line(blend / 2f) + offset * .5f, DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + blend / 2f)); - Close(Line(blend), DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + blend)); - Close(Line(2f * blend), DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + 2f * blend)); - Close(Line(0f), DrawAhead.Round(shot, null, barrel, 0f, shot.FireTime)); + Close(barrel, DrawAhead.Round(shot, null, barrel, blend, shot.FireTime, 0f)); + Close(Line(blend / 2f) + offset * .5f, DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + blend / 2f, 0f)); + Close(Line(blend), DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + blend, 0f)); + Close(Line(2f * blend), DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + 2f * blend, 0f)); + Close(Line(0f), DrawAhead.Round(shot, null, barrel, 0f, shot.FireTime, 0f)); } // Placement depends on sim time alone: any history of frames walked to a checkpoint draws what a fresh call draws, which @@ -342,6 +369,7 @@ public sealed partial class FireControlCut124Tests var shot = SafeAssert.OnlyShot(e.Zone); var checkpoints = new[] { .05f, .3f, 1f, 3f }; var histories = new[] { new[] { 1f / 144f }, new[] { 1f / 30f }, new[] { 1f / 144f, 1f / 30f, 1f / 90f } }; + var miss = Known(shot, ShotResult.Miss); foreach (var frames in histories) { @@ -352,10 +380,18 @@ public sealed partial class FireControlCut124Tests while (elapsed + 1e-4f < checkpoint) { elapsed = min(checkpoint, elapsed + frames[frame++ % frames.Length]); - DrawAhead.Round(shot, ShotResult.Miss, shot.FireOrigin, .1f, shot.FireTime + elapsed); + DrawAhead.Round(shot, miss, shot.FireOrigin, .1f, shot.FireTime + elapsed, 0f); + } + var drawn = DrawAhead.Round(shot, miss, shot.FireOrigin, .1f, shot.FireTime + checkpoint, 0f); + var fresh = DrawAhead.Round(shot, miss, shot.FireOrigin, .1f, shot.FireTime + checkpoint, 0f); + // A frame that falls `lead` after the step draws what the sim time lead later would. + foreach (var lead in new[] { 1f / 60f, 1f / 90f }) + { + var ahead = DrawAhead.Round(shot, miss, shot.FireOrigin, .1f, shot.FireTime + checkpoint - lead, lead); + Assert.Equal(fresh.x, ahead.x, 3); + Assert.Equal(fresh.y, ahead.y, 3); + Assert.Equal(fresh.z, ahead.z, 3); } - var drawn = DrawAhead.Round(shot, ShotResult.Miss, shot.FireOrigin, .1f, shot.FireTime + checkpoint); - var fresh = DrawAhead.Round(shot, ShotResult.Miss, shot.FireOrigin, .1f, shot.FireTime + checkpoint); Assert.Equal(fresh.z, drawn.z, 3); Assert.Equal(shot.FireOrigin.z + speed * min(checkpoint, 1000f / speed), drawn.z, 3); Assert.Equal(shot.FireOrigin.x, drawn.x, 3); @@ -364,31 +400,118 @@ public sealed partial class FireControlCut124Tests } } - // Today's sim geometry (a target crossing at 10 against a speed of 20, so an intercept exists): the round flies the line frozen at Fire and stops at the arrival point on it, whatever the - // target does afterwards. (Where a leading round's arrival should lie is the sim's question, never Projectile's.) - [Fact] - public void AHitOnAMovingTargetStopsOnItsFrozenLine() + // A hit ends at its impact cell on the target's drawn pose when damage lands, not at the arrival point of the line + // frozen at Fire (ruling direct-arrival-keep-timing): the target has moved and turned since, and is drawn ahead by its + // last step's velocity and turn. The expected point is computed apart from DrawAhead, by moving the entity itself and + // asking its own schematic frame (Entity.ToWorldPoint) where the cell is. + [Theory] + [InlineData(20f)] + [InlineData(200f)] + public void AHitEndsAtItsImpactCellOnTheTargetsDrawnPose(float speed) { - var e = Build(TestSettings(), SolidShape(5, 4), velocity: BallisticSpeed, weaponRange: 1000f); + var e = Build(TestSettings(), SolidShape(5, 4), velocity: speed, weaponRange: 1000f); e.Target.Velocity = float2(10, 0); FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); var shot = SafeAssert.OnlyShot(e.Zone); Assert.True(shot.TravelDirection.x > 0f, "the round did not lead the target"); + const float step = 1f / 30f; e.Target.Position += float3(40, 0, 0); - var stop = DrawAhead.RoundStop(shot, new ShotOutcome { ShotId = shot.ShotId, Result = ShotResult.Hit }, shot.FireOrigin, 0f); - var line = shot.FireOrigin.xz + shot.TravelDirection * (BallisticSpeed * (shot.ArrivalTime - shot.FireTime)); - Assert.Equal(line.x, stop.x, 3); - Assert.Equal(shot.FireOrigin.y, stop.y, 3); - Assert.Equal(line.y, stop.z, 3); + e.Target.Direction = normalize(float2(1, 2)); + e.Target.TurnRate = 3f; + var cell = FarCell(e); + var hit = Hit(shot, e.Target, cell, shot.CommitTime); + var arrivalOnTheLine = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, shot.ArrivalTime, 0f); + var cellSize = e.Items.GameplaySettings.SchematicCellSize; + + foreach (var lead in new[] { 0f, step / 2f, .999f * step }) + { + var position = e.Target.Position; + var direction = e.Target.Direction; + e.Target.Position += float3(e.Target.Velocity.x, 0, e.Target.Velocity.y) * lead; + e.Target.Direction = mul(e.Target.Direction, float2x2.Rotate(e.Target.TurnRate * lead)); + var cellPoint = e.Target.ToWorldPoint((float2) cell); + var expected = float3(cellPoint.x, e.Target.Position.y, cellPoint.y); + e.Target.Position = position; + e.Target.Direction = direction; + + Assert.True(length((expected - arrivalOnTheLine).xz) > cellSize, "the fixture put the cell on the frozen line's arrival point"); + var impact = DrawAhead.Impact(e.Target, cell, lead); + Assert.Equal(expected.x, impact.x, 3); + Assert.Equal(expected.y, impact.y, 3); + Assert.Equal(expected.z, impact.z, 3); + foreach (var simTime in new[] { shot.ArrivalTime, shot.ArrivalTime + .5f }) + { + var drawn = DrawAhead.Round(shot, hit, shot.FireOrigin, .1f, simTime, lead); + Assert.Equal(expected.x, drawn.x, 3); + Assert.Equal(expected.y, drawn.y, 3); + Assert.Equal(expected.z, drawn.z, 3); + } + } + } + + // A hit leaves its frozen line at the commit tick and steers onto its end by the time damage lands: before the commit it + // is on the line, half way through the steer it is half way to the impact cell, and a frame drawn ahead by the clock's + // lead places the round where the sim time that much later would. + [Fact] + public void ASteeredRoundLeavesItsLineAtTheCommit() + { + var e = FireDirectRound(20f, 1000f); + var shot = SafeAssert.OnlyShot(e.Zone); + Assert.Equal(shot.FireTime + 5f, shot.ArrivalTime, 3); + var cell = FarCell(e); + var commit = shot.ArrivalTime - 2f; + var hit = Hit(shot, e.Target, cell, commit); + + foreach (var t in new[] { commit - 1f, commit }) + { + var steered = DrawAhead.Round(shot, hit, shot.FireOrigin, 0f, t, 0f); + var line = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, t, 0f); + Assert.Equal(line.x, steered.x, 3); + Assert.Equal(line.y, steered.y, 3); + Assert.Equal(line.z, steered.z, 3); + } + + var m = commit + 1f; + var onTheLine = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, m, 0f); + var end = DrawAhead.Impact(e.Target, cell, 0f); + Assert.True(length((end - onTheLine).xz) > 1f, "the fixture put the impact cell on the line"); + var halfway = DrawAhead.Round(shot, hit, shot.FireOrigin, 0f, m, 0f); + Assert.Equal(onTheLine.x + (end.x - onTheLine.x) * .5f, halfway.x, 3); + Assert.Equal(onTheLine.y + (end.y - onTheLine.y) * .5f, halfway.y, 3); + Assert.Equal(onTheLine.z + (end.z - onTheLine.z) * .5f, halfway.z, 3); + + var ahead = DrawAhead.Round(shot, hit, shot.FireOrigin, 0f, m - .02f, .02f); + Assert.Equal(halfway.x, ahead.x, 3); + Assert.Equal(halfway.y, ahead.y, 3); + Assert.Equal(halfway.z, ahead.z, 3); + } + + // The sim states facts and draws nothing: no drawing function, and no lead, blend or barrel, lives in FireControl; + // those belong to DrawAhead, the presentation statics. + [Fact] + public void FireControlHasNoDrawingFunctions() + { + const BindingFlags all = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly; + var methods = typeof(FireControl).GetMethods(all).Cast().Concat(typeof(FireControl).GetConstructors(all)).ToList(); + Assert.NotEmpty(methods); + foreach (var method in methods) + { + Assert.False(method.Name.StartsWith("Round") || method.Name.StartsWith("Draw"), "FireControl has a drawing function: " + method.Name); + foreach (var parameter in method.GetParameters()) + Assert.False(new[] { "lead", "blendTime", "barrel" }.Contains(parameter.Name), "FireControl takes a drawing parameter: " + parameter.Name); + } + + foreach (var name in new[] { "Round", "Impact", "RoundOver", "RoundEnd" }) + Assert.NotNull(typeof(DrawAhead).GetMethod(name, BindingFlags.Public | BindingFlags.Static)); } // A proximity round at a target that recedes faster than the round closes bursts at the weapon's range, pulled back // along its line, while it arrives when it has flown the distance the target was fired at: its burst point is not - // the line at its arrival. The stop is the sim's burst point, so a presenter that stopped at the arrival line would + // the line at its arrival. The end is the sim's burst point, so a presenter that ended at the arrival line would // end the tracer short of where the round burst. [Fact] - public void ABurstRoundStopsAtItsBurstPointNotItsArrivalLine() + public void ABurstRoundEndsAtItsBurstPointNotItsArrivalLine() { var e = Build(TestSettings(), SolidShape(5, 4), velocity: BallisticSpeed, fuse: WeaponFuse.Proximity, blastRadius: 4f, damage: 100f, weaponRange: NoLockRange, targetRange: 50f); @@ -404,8 +527,8 @@ public sealed partial class FireControlCut124Tests Assert.Equal(origin.x, outcome.BurstPoint.x, 3); Assert.Equal(origin.z + NoLockRange, outcome.BurstPoint.y, 3); - Assert.Equal(origin.z + 50f, FireControl.RoundAt(record, outcome.Result, record.ArrivalTime).y, 3); - var stop = DrawAhead.RoundStop(record, outcome, record.FireOrigin, 0f); + Assert.Equal(origin.z + 50f, DrawAhead.Round(record, null, record.FireOrigin, 0f, record.ArrivalTime, 0f).z, 3); + var stop = DrawAhead.Round(record, outcome, record.FireOrigin, 0f, record.ArrivalTime, 0f); Assert.Equal(outcome.BurstPoint.x, stop.x, 3); Assert.Equal(outcome.BurstPoint.y, stop.z, 3); } From 6b36835953780ff7c2b3ddd67d9d5e2f1c590665 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 23:29:32 +0200 Subject: [PATCH 109/120] Ballistic flight r3: name CultMath.float2x2.Rotate in full (the math.float2x2 method shadows the type) The previous commit (2dcb7b20) does not build: DrawAhead.cs CS0119. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/DrawAhead.cs | 2 +- tests/Aetheria.Shared.Tests/BallisticFlightTests.cs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Assets/Scripts/ServerShared/DrawAhead.cs b/Assets/Scripts/ServerShared/DrawAhead.cs index 98b70656e..13da8aae9 100644 --- a/Assets/Scripts/ServerShared/DrawAhead.cs +++ b/Assets/Scripts/ServerShared/DrawAhead.cs @@ -63,7 +63,7 @@ public static class DrawAhead { var at = Position(target, lead); var offset = target.ToWorldPoint((float2) cell) - target.Position.xz; - var turned = mul(offset, float2x2.Rotate(Turn(target, lead))); + var turned = mul(offset, CultMath.float2x2.Rotate(Turn(target, lead))); return float3(at.x + turned.x, at.y, at.z + turned.y); } diff --git a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs index 61a733e3a..79c787a74 100644 --- a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs +++ b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs @@ -9,7 +9,6 @@ using UniRx; using Xunit; using static CultMath.math; using float2 = CultMath.float2; -using float2x2 = CultMath.float2x2; using float3 = CultMath.float3; using int2 = CultMath.int2; @@ -429,7 +428,7 @@ public sealed partial class FireControlCut124Tests var position = e.Target.Position; var direction = e.Target.Direction; e.Target.Position += float3(e.Target.Velocity.x, 0, e.Target.Velocity.y) * lead; - e.Target.Direction = mul(e.Target.Direction, float2x2.Rotate(e.Target.TurnRate * lead)); + e.Target.Direction = mul(e.Target.Direction, CultMath.float2x2.Rotate(e.Target.TurnRate * lead)); var cellPoint = e.Target.ToWorldPoint((float2) cell); var expected = float3(cellPoint.x, e.Target.Position.y, cellPoint.y); e.Target.Position = position; From afe44b09d3ae0f28a5006f9341c36241ec5e6762 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 23:38:26 +0200 Subject: [PATCH 110/120] ConsumableTests: the overlap test attaches both modifiers before the first expires The first draft activated the second effect so late it was not yet attached when the first detached, so the shared-entry guard mutants survived it. Builds; tests green status is in the next verification run. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/ConsumableTests.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/ConsumableTests.cs b/tests/Aetheria.Shared.Tests/ConsumableTests.cs index 843b0f221..a94024c12 100644 --- a/tests/Aetheria.Shared.Tests/ConsumableTests.cs +++ b/tests/Aetheria.Shared.Tests/ConsumableTests.cs @@ -186,8 +186,8 @@ public sealed class ConsumableTests : IDisposable Assert.Equal(40f, rig.Engine.Evaluate(rig.Thrust), 3); } - // Two modifiers share one resolver entry. The first expires while the second still runs: the second's - // boost stays, and when it expires too nothing is left behind. + // Two modifiers share one resolver entry, both attached. The first expires while the second still runs: the + // second's boost stays, and when it expires too nothing is left behind. [Fact] public void AnExpiringConsumableLeavesAnOverlappingOnesBoost() { @@ -199,13 +199,16 @@ public sealed class ConsumableTests : IDisposable Assert.True(rig.Hold.TryStore(rig.Mint())); Assert.True(rig.Ship.TryActivateConsumable(rig.Consumable)); - Tick(rig, 2, .3f); + Tick(rig, 1, .3f); Assert.True(rig.Ship.TryActivateConsumable(rig.Consumable)); - Tick(rig, 2, .3f); // the first ran out on the fourth tick overall; the second has 0.4 s left + Tick(rig, 2, .3f); // both attached: the first has 0.1 s left, the second 0.4 s + Assert.Equal(40f, rig.Engine.Evaluate(rig.Thrust), 3); + + Tick(rig, 1, .3f); // the first ran out; the second's boost is still on the shared entry Assert.Equal(20f, rig.Engine.Evaluate(rig.Thrust), 3); Assert.True(resolver.ModifierEntryCount > baseline); - Tick(rig, 2, .3f); + Tick(rig, 1, .3f); // and now the second Assert.Equal(10f, rig.Engine.Evaluate(rig.Thrust), 3); Assert.Equal(baseline, resolver.ModifierEntryCount); } From a74065dbeeda05feb938c00bf7ec8cef3b4703e3 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 23:46:33 +0200 Subject: [PATCH 111/120] ThrottleLockTests: pin stick length, counter-clockwise turn, non-lock consumables and the lock-end boundary Four tests for Soul's survivors lock-intent-length, lock-ignores-ccw-turn, lock-any-consumable and lock-ends-at-half-duration. Tests only; not yet run on Yggdrasil. Co-Authored-By: Claude Opus 5.5 --- .../ThrottleLockTests.cs | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs b/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs index 03f7b1060..032e74d0f 100644 --- a/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs +++ b/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs @@ -180,4 +180,92 @@ public sealed class ThrottleLockTests : IDisposable Assert.Equal(1f, f.Forward.Axis); Assert.Equal(0f, f.Reverse.Axis); } + + // Finding zero-stick-unpinned. Mutation: float2(0, length(MovementDirection)) as the locked intent. The lock + // supplies full forward whatever the stick's magnitude, including none and a short reverse push. + [Fact] + public void ALockedShipGetsFullForwardWhateverTheStickLength() + { + var f = Build(100f); + using var _ = f.Cache; + f.ActivateLock(); + foreach (var intent in new[] { float2(0, 0), float2(.3f, .2f), float2(-.5f, -.5f) }) + { + f.Ship.MovementDirection = intent; + f.Ship.Turn = 0; + f.Ship.Update(0.01f); + Assert.Equal(1f, f.Forward.Axis); + Assert.Equal(0f, f.Reverse.Axis); + Assert.All(f.Right, t => Assert.Equal(0f, t.Axis)); + Assert.All(f.Left, t => Assert.Equal(0f, t.Axis)); + } + } + + // Finding ccw-turn-unpinned. Mutation: drop the counter-clockwise Turn. Turn in either sign reaches only the + // thrusters with torque of that sign, at full, while the lock holds forward. + [Fact] + public void ALockedShipTurnsBothWays() + { + var f = Build(100f); + using var _ = f.Cache; + f.ActivateLock(); + var all = f.Ship.GetBehaviors().ToArray(); + var cw = all.Where(t => t.Torque > 0 && t.Item.EquippableItem.Rotation == ItemRotation.None).ToArray(); + var ccw = all.Where(t => t.Torque < 0 && t.Item.EquippableItem.Rotation == ItemRotation.None).ToArray(); + Assert.NotEmpty(cw); + Assert.NotEmpty(ccw); + f.Ship.MovementDirection = float2(1, -1); + foreach (var turn in new[] { 1f, -1f }) + { + f.Ship.Turn = turn; + f.Ship.Update(0.01f); + Assert.All(turn > 0 ? cw : ccw, t => Assert.Equal(1f, t.Axis)); + Assert.All(turn > 0 ? ccw : cw, t => Assert.Equal(0f, t.Axis)); + Assert.Equal(1f, f.Forward.Axis); + } + } + + // Finding non-lock-consumable-unpinned. Mutation: any active consumable locks. A consumable without + // ThrottleLockData leaves the pilot's intent in force. + [Fact] + public void AConsumableWithoutTheLockDoesNotLock() + { + var f = Build(100f); + using var _ = f.Cache; + var plain = new ConsumableItemData { Name = "Plain", Duration = 100f }; + f.Cache.Upsert(plain); + f.Ship.ActivateConsumable(new ConsumableItem + { + Data = f.Cache.RefOf(plain), + Lot = f.Items.Lots.Add(new Lot { Design = f.Cache.RefOf(plain), Origin = new Attributed(), Quality = .5f, Roles = new List() }) + }); + Assert.False(f.Ship.ThrottleLocked); + f.Ship.MovementDirection = float2(0, -1); + f.Ship.Update(0.01f); + Assert.Equal(1f, f.Reverse.Axis); + Assert.Equal(0f, f.Forward.Axis); + } + + // Finding lock-end-boundary-unpinned. Mutation: the lock ends at half Duration. Stepped in quarter-second + // ticks over a 1 s Duration, the lock holds through 0.75 s and is gone by 2 s. The exact tick of expiry is + // consumable-host's rule and is not pinned here. + [Fact] + public void TheLockHoldsThroughMostOfItsDurationAndEndsAfter() + { + var f = Build(1f); + using var _ = f.Cache; + f.ActivateLock(); + f.Ship.MovementDirection = float2(0, -1); + for (var i = 0; i < 3; i++) + { + f.Ship.Update(.25f); + Assert.True(f.Ship.ThrottleLocked); + } + f.Ship.Update(.25f); + Assert.Equal(0f, f.Reverse.Axis); + for (var i = 0; i < 4; i++) f.Ship.Update(.25f); + Assert.False(f.Ship.ThrottleLocked); + f.Ship.Update(.25f); + Assert.Equal(1f, f.Reverse.Axis); + } } From d519d99b1e11f9e3a2379b59ddd24446b046ab1a Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 23:48:02 +0200 Subject: [PATCH 112/120] Consumable host: an effect lives exactly its Duration, not Duration plus one tick Expiry fires when the remaining duration reaches zero (<= 0), so behaviours execute on Duration/dt ticks and not one more. AConsumableRunsForExactlyItsDurationInTicks pins it with 0.25 s ticks. Builds; verified in the next run. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/Entity.cs | 2 +- tests/Aetheria.Shared.Tests/ConsumableTests.cs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Assets/Scripts/ServerShared/Entity.cs b/Assets/Scripts/ServerShared/Entity.cs index d7152b172..7d9dfb868 100644 --- a/Assets/Scripts/ServerShared/Entity.cs +++ b/Assets/Scripts/ServerShared/Entity.cs @@ -1457,7 +1457,7 @@ public abstract class Entity for (var i = 0; i < _activeConsumables.Count; i++) { _activeConsumables[i].Update(delta); - if (_activeConsumables[i].RemainingDuration < 0) + if (_activeConsumables[i].RemainingDuration <= 0) { // Cut 2 Gate 1 fix (docs/stats-and-power-cut.md): an expired consumable dropped out of this // list without ever telling the resolver, leaving its generation/cache/modifier entries diff --git a/tests/Aetheria.Shared.Tests/ConsumableTests.cs b/tests/Aetheria.Shared.Tests/ConsumableTests.cs index a94024c12..3e2d89db7 100644 --- a/tests/Aetheria.Shared.Tests/ConsumableTests.cs +++ b/tests/Aetheria.Shared.Tests/ConsumableTests.cs @@ -169,6 +169,22 @@ public sealed class ConsumableTests : IDisposable Assert.Equal(10f, rig.Engine.Evaluate(rig.Thrust), 3); } + // An effect runs for exactly its Duration: Duration 1 with 0.25 s ticks (exact in floats) executes its + // behaviours on four ticks and is gone when the fourth ends, not on a fifth. + [Fact] + public void AConsumableRunsForExactlyItsDurationInTicks() + { + var rig = Build(); + using var _ = rig.Cache; + rig.Ship.ActivateConsumable(rig.Mint()); + Tick(rig, 3, .25f); + Assert.NotNull(rig.Ship.FindActiveConsumable(rig.Consumable)); + Assert.Equal(20f, rig.Engine.Evaluate(rig.Thrust), 3); + Tick(rig, 1, .25f); // the fourth tick spends the last of the Duration + Assert.Null(rig.Ship.FindActiveConsumable(rig.Consumable)); + Assert.Equal(10f, rig.Engine.Evaluate(rig.Thrust), 3); + } + // CS-R1, the stackable side: a stackable design activates again while its first effect runs, and every // multiplier on the stat multiplies. [Fact] From 840dfe6460b7a0ac424d2a0bb9c0e1af5e61160c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 00:26:21 +0200 Subject: [PATCH 113/120] PBR materials: 12 ambientCG CC0 sets for Unity (GlowFade) under Assets/Materials/PBR 2K-JPG Color/Roughness/NormalGL (+Metalness, AO where the set has them), generated MetallicSmoothness.png (R metallic, A 1-roughness), texture and material .meta files, SOURCES.md. Unity side not opened yet. Co-Authored-By: Claude Opus 5.5 --- Assets/Materials/PBR.meta | 8 ++ Assets/Materials/PBR/Carbon_Fibre.meta | 8 ++ .../PBR/Carbon_Fibre/Carbon_Fibre.mat | 86 +++++++++++++++ .../PBR/Carbon_Fibre/Carbon_Fibre.mat.meta | 8 ++ .../PBR/Carbon_Fibre/Carbon_Fibre_Color.jpg | 3 + .../Carbon_Fibre/Carbon_Fibre_Color.jpg.meta | 104 ++++++++++++++++++ .../Carbon_Fibre_MetallicSmoothness.png | 3 + .../Carbon_Fibre_MetallicSmoothness.png.meta | 104 ++++++++++++++++++ .../Carbon_Fibre/Carbon_Fibre_Metalness.jpg | 3 + .../Carbon_Fibre_Metalness.jpg.meta | 104 ++++++++++++++++++ .../Carbon_Fibre/Carbon_Fibre_NormalGL.jpg | 3 + .../Carbon_Fibre_NormalGL.jpg.meta | 104 ++++++++++++++++++ .../Carbon_Fibre/Carbon_Fibre_Roughness.jpg | 3 + .../Carbon_Fibre_Roughness.jpg.meta | 104 ++++++++++++++++++ Assets/Materials/PBR/Gunmetal_Dark.meta | 8 ++ .../PBR/Gunmetal_Dark/Gunmetal_Dark.mat | 86 +++++++++++++++ .../PBR/Gunmetal_Dark/Gunmetal_Dark.mat.meta | 8 ++ .../PBR/Gunmetal_Dark/Gunmetal_Dark_Color.jpg | 3 + .../Gunmetal_Dark_Color.jpg.meta | 104 ++++++++++++++++++ .../Gunmetal_Dark_MetallicSmoothness.png | 3 + .../Gunmetal_Dark_MetallicSmoothness.png.meta | 104 ++++++++++++++++++ .../Gunmetal_Dark/Gunmetal_Dark_Metalness.jpg | 3 + .../Gunmetal_Dark_Metalness.jpg.meta | 104 ++++++++++++++++++ .../Gunmetal_Dark/Gunmetal_Dark_NormalGL.jpg | 3 + .../Gunmetal_Dark_NormalGL.jpg.meta | 104 ++++++++++++++++++ .../Gunmetal_Dark/Gunmetal_Dark_Roughness.jpg | 3 + .../Gunmetal_Dark_Roughness.jpg.meta | 104 ++++++++++++++++++ Assets/Materials/PBR/Metal_Scratched.meta | 8 ++ .../PBR/Metal_Scratched/Metal_Scratched.mat | 86 +++++++++++++++ .../Metal_Scratched/Metal_Scratched.mat.meta | 8 ++ .../Metal_Scratched/Metal_Scratched_Color.jpg | 3 + .../Metal_Scratched_Color.jpg.meta | 104 ++++++++++++++++++ .../Metal_Scratched_MetallicSmoothness.png | 3 + ...etal_Scratched_MetallicSmoothness.png.meta | 104 ++++++++++++++++++ .../Metal_Scratched_Metalness.jpg | 3 + .../Metal_Scratched_Metalness.jpg.meta | 104 ++++++++++++++++++ .../Metal_Scratched_NormalGL.jpg | 3 + .../Metal_Scratched_NormalGL.jpg.meta | 104 ++++++++++++++++++ .../Metal_Scratched_Roughness.jpg | 3 + .../Metal_Scratched_Roughness.jpg.meta | 104 ++++++++++++++++++ Assets/Materials/PBR/Paint_Clean_Light.meta | 8 ++ .../Paint_Clean_Light/Paint_Clean_Light.mat | 86 +++++++++++++++ .../Paint_Clean_Light.mat.meta | 8 ++ .../Paint_Clean_Light_Color.jpg | 3 + .../Paint_Clean_Light_Color.jpg.meta | 104 ++++++++++++++++++ .../Paint_Clean_Light_MetallicSmoothness.png | 3 + ...nt_Clean_Light_MetallicSmoothness.png.meta | 104 ++++++++++++++++++ .../Paint_Clean_Light_NormalGL.jpg | 3 + .../Paint_Clean_Light_NormalGL.jpg.meta | 104 ++++++++++++++++++ .../Paint_Clean_Light_Roughness.jpg | 3 + .../Paint_Clean_Light_Roughness.jpg.meta | 104 ++++++++++++++++++ Assets/Materials/PBR/Paint_Worn_Dark.meta | 8 ++ .../PBR/Paint_Worn_Dark/Paint_Worn_Dark.mat | 86 +++++++++++++++ .../Paint_Worn_Dark/Paint_Worn_Dark.mat.meta | 8 ++ .../Paint_Worn_Dark/Paint_Worn_Dark_AO.jpg | 3 + .../Paint_Worn_Dark_AO.jpg.meta | 104 ++++++++++++++++++ .../Paint_Worn_Dark/Paint_Worn_Dark_Color.jpg | 3 + .../Paint_Worn_Dark_Color.jpg.meta | 104 ++++++++++++++++++ .../Paint_Worn_Dark_MetallicSmoothness.png | 3 + ...aint_Worn_Dark_MetallicSmoothness.png.meta | 104 ++++++++++++++++++ .../Paint_Worn_Dark_Metalness.jpg | 3 + .../Paint_Worn_Dark_Metalness.jpg.meta | 104 ++++++++++++++++++ .../Paint_Worn_Dark_NormalGL.jpg | 3 + .../Paint_Worn_Dark_NormalGL.jpg.meta | 104 ++++++++++++++++++ .../Paint_Worn_Dark_Roughness.jpg | 3 + .../Paint_Worn_Dark_Roughness.jpg.meta | 104 ++++++++++++++++++ Assets/Materials/PBR/Paint_Worn_Light.meta | 8 ++ .../PBR/Paint_Worn_Light/Paint_Worn_Light.mat | 86 +++++++++++++++ .../Paint_Worn_Light.mat.meta | 8 ++ .../Paint_Worn_Light/Paint_Worn_Light_AO.jpg | 3 + .../Paint_Worn_Light_AO.jpg.meta | 104 ++++++++++++++++++ .../Paint_Worn_Light_Color.jpg | 3 + .../Paint_Worn_Light_Color.jpg.meta | 104 ++++++++++++++++++ .../Paint_Worn_Light_MetallicSmoothness.png | 3 + ...int_Worn_Light_MetallicSmoothness.png.meta | 104 ++++++++++++++++++ .../Paint_Worn_Light_Metalness.jpg | 3 + .../Paint_Worn_Light_Metalness.jpg.meta | 104 ++++++++++++++++++ .../Paint_Worn_Light_NormalGL.jpg | 3 + .../Paint_Worn_Light_NormalGL.jpg.meta | 104 ++++++++++++++++++ .../Paint_Worn_Light_Roughness.jpg | 3 + .../Paint_Worn_Light_Roughness.jpg.meta | 104 ++++++++++++++++++ Assets/Materials/PBR/Plastic_Matte.meta | 8 ++ .../PBR/Plastic_Matte/Plastic_Matte.mat | 86 +++++++++++++++ .../PBR/Plastic_Matte/Plastic_Matte.mat.meta | 8 ++ .../PBR/Plastic_Matte/Plastic_Matte_Color.jpg | 3 + .../Plastic_Matte_Color.jpg.meta | 104 ++++++++++++++++++ .../Plastic_Matte_MetallicSmoothness.png | 3 + .../Plastic_Matte_MetallicSmoothness.png.meta | 104 ++++++++++++++++++ .../Plastic_Matte/Plastic_Matte_NormalGL.jpg | 3 + .../Plastic_Matte_NormalGL.jpg.meta | 104 ++++++++++++++++++ .../Plastic_Matte/Plastic_Matte_Roughness.jpg | 3 + .../Plastic_Matte_Roughness.jpg.meta | 104 ++++++++++++++++++ Assets/Materials/PBR/Plates_Panels.meta | 8 ++ .../PBR/Plates_Panels/Plates_Panels.mat | 86 +++++++++++++++ .../PBR/Plates_Panels/Plates_Panels.mat.meta | 8 ++ .../PBR/Plates_Panels/Plates_Panels_Color.jpg | 3 + .../Plates_Panels_Color.jpg.meta | 104 ++++++++++++++++++ .../Plates_Panels_MetallicSmoothness.png | 3 + .../Plates_Panels_MetallicSmoothness.png.meta | 104 ++++++++++++++++++ .../Plates_Panels/Plates_Panels_Metalness.jpg | 3 + .../Plates_Panels_Metalness.jpg.meta | 104 ++++++++++++++++++ .../Plates_Panels/Plates_Panels_NormalGL.jpg | 3 + .../Plates_Panels_NormalGL.jpg.meta | 104 ++++++++++++++++++ .../Plates_Panels/Plates_Panels_Roughness.jpg | 3 + .../Plates_Panels_Roughness.jpg.meta | 104 ++++++++++++++++++ Assets/Materials/PBR/Plates_Riveted.meta | 8 ++ .../PBR/Plates_Riveted/Plates_Riveted.mat | 86 +++++++++++++++ .../Plates_Riveted/Plates_Riveted.mat.meta | 8 ++ .../Plates_Riveted/Plates_Riveted_Color.jpg | 3 + .../Plates_Riveted_Color.jpg.meta | 104 ++++++++++++++++++ .../Plates_Riveted_MetallicSmoothness.png | 3 + ...Plates_Riveted_MetallicSmoothness.png.meta | 104 ++++++++++++++++++ .../Plates_Riveted_Metalness.jpg | 3 + .../Plates_Riveted_Metalness.jpg.meta | 104 ++++++++++++++++++ .../Plates_Riveted_NormalGL.jpg | 3 + .../Plates_Riveted_NormalGL.jpg.meta | 104 ++++++++++++++++++ .../Plates_Riveted_Roughness.jpg | 3 + .../Plates_Riveted_Roughness.jpg.meta | 104 ++++++++++++++++++ Assets/Materials/PBR/Rubber.meta | 8 ++ Assets/Materials/PBR/Rubber/Rubber.mat | 86 +++++++++++++++ Assets/Materials/PBR/Rubber/Rubber.mat.meta | 8 ++ Assets/Materials/PBR/Rubber/Rubber_Color.jpg | 3 + .../PBR/Rubber/Rubber_Color.jpg.meta | 104 ++++++++++++++++++ .../PBR/Rubber/Rubber_MetallicSmoothness.png | 3 + .../Rubber/Rubber_MetallicSmoothness.png.meta | 104 ++++++++++++++++++ .../Materials/PBR/Rubber/Rubber_NormalGL.jpg | 3 + .../PBR/Rubber/Rubber_NormalGL.jpg.meta | 104 ++++++++++++++++++ .../Materials/PBR/Rubber/Rubber_Roughness.jpg | 3 + .../PBR/Rubber/Rubber_Roughness.jpg.meta | 104 ++++++++++++++++++ Assets/Materials/PBR/SOURCES.md | 22 ++++ Assets/Materials/PBR/SOURCES.md.meta | 7 ++ Assets/Materials/PBR/Steel_Brushed.meta | 8 ++ .../PBR/Steel_Brushed/Steel_Brushed.mat | 86 +++++++++++++++ .../PBR/Steel_Brushed/Steel_Brushed.mat.meta | 8 ++ .../PBR/Steel_Brushed/Steel_Brushed_Color.jpg | 3 + .../Steel_Brushed_Color.jpg.meta | 104 ++++++++++++++++++ .../Steel_Brushed_MetallicSmoothness.png | 3 + .../Steel_Brushed_MetallicSmoothness.png.meta | 104 ++++++++++++++++++ .../Steel_Brushed/Steel_Brushed_Metalness.jpg | 3 + .../Steel_Brushed_Metalness.jpg.meta | 104 ++++++++++++++++++ .../Steel_Brushed/Steel_Brushed_NormalGL.jpg | 3 + .../Steel_Brushed_NormalGL.jpg.meta | 104 ++++++++++++++++++ .../Steel_Brushed/Steel_Brushed_Roughness.jpg | 3 + .../Steel_Brushed_Roughness.jpg.meta | 104 ++++++++++++++++++ Assets/Materials/PBR/Tread_Diamond.meta | 8 ++ .../PBR/Tread_Diamond/Tread_Diamond.mat | 86 +++++++++++++++ .../PBR/Tread_Diamond/Tread_Diamond.mat.meta | 8 ++ .../PBR/Tread_Diamond/Tread_Diamond_Color.jpg | 3 + .../Tread_Diamond_Color.jpg.meta | 104 ++++++++++++++++++ .../Tread_Diamond_MetallicSmoothness.png | 3 + .../Tread_Diamond_MetallicSmoothness.png.meta | 104 ++++++++++++++++++ .../Tread_Diamond/Tread_Diamond_Metalness.jpg | 3 + .../Tread_Diamond_Metalness.jpg.meta | 104 ++++++++++++++++++ .../Tread_Diamond/Tread_Diamond_NormalGL.jpg | 3 + .../Tread_Diamond_NormalGL.jpg.meta | 104 ++++++++++++++++++ .../Tread_Diamond/Tread_Diamond_Roughness.jpg | 3 + .../Tread_Diamond_Roughness.jpg.meta | 104 ++++++++++++++++++ 157 files changed, 7574 insertions(+) create mode 100644 Assets/Materials/PBR.meta create mode 100644 Assets/Materials/PBR/Carbon_Fibre.meta create mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre.mat create mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre.mat.meta create mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Color.jpg create mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Color.jpg.meta create mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_MetallicSmoothness.png create mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_MetallicSmoothness.png.meta create mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg create mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg.meta create mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_NormalGL.jpg create mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_NormalGL.jpg.meta create mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Roughness.jpg create mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Roughness.jpg.meta create mode 100644 Assets/Materials/PBR/Gunmetal_Dark.meta create mode 100644 Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark.mat create mode 100644 Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark.mat.meta create mode 100644 Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Color.jpg create mode 100644 Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Color.jpg.meta create mode 100644 Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_MetallicSmoothness.png create mode 100644 Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_MetallicSmoothness.png.meta create mode 100644 Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Metalness.jpg create mode 100644 Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Metalness.jpg.meta create mode 100644 Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_NormalGL.jpg create mode 100644 Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_NormalGL.jpg.meta create mode 100644 Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Roughness.jpg create mode 100644 Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Roughness.jpg.meta create mode 100644 Assets/Materials/PBR/Metal_Scratched.meta create mode 100644 Assets/Materials/PBR/Metal_Scratched/Metal_Scratched.mat create mode 100644 Assets/Materials/PBR/Metal_Scratched/Metal_Scratched.mat.meta create mode 100644 Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Color.jpg create mode 100644 Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Color.jpg.meta create mode 100644 Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_MetallicSmoothness.png create mode 100644 Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_MetallicSmoothness.png.meta create mode 100644 Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Metalness.jpg create mode 100644 Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Metalness.jpg.meta create mode 100644 Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_NormalGL.jpg create mode 100644 Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_NormalGL.jpg.meta create mode 100644 Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Roughness.jpg create mode 100644 Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Roughness.jpg.meta create mode 100644 Assets/Materials/PBR/Paint_Clean_Light.meta create mode 100644 Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light.mat create mode 100644 Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light.mat.meta create mode 100644 Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Color.jpg create mode 100644 Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Color.jpg.meta create mode 100644 Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_MetallicSmoothness.png create mode 100644 Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_MetallicSmoothness.png.meta create mode 100644 Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_NormalGL.jpg create mode 100644 Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_NormalGL.jpg.meta create mode 100644 Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Roughness.jpg create mode 100644 Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Roughness.jpg.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark.mat create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark.mat.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_AO.jpg create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_AO.jpg.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Color.jpg create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Color.jpg.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_MetallicSmoothness.png create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_MetallicSmoothness.png.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Metalness.jpg create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Metalness.jpg.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_NormalGL.jpg create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_NormalGL.jpg.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Roughness.jpg create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Roughness.jpg.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Light.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light.mat create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light.mat.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_AO.jpg create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_AO.jpg.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Color.jpg create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Color.jpg.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_MetallicSmoothness.png create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_MetallicSmoothness.png.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Metalness.jpg create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Metalness.jpg.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_NormalGL.jpg create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_NormalGL.jpg.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Roughness.jpg create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Roughness.jpg.meta create mode 100644 Assets/Materials/PBR/Plastic_Matte.meta create mode 100644 Assets/Materials/PBR/Plastic_Matte/Plastic_Matte.mat create mode 100644 Assets/Materials/PBR/Plastic_Matte/Plastic_Matte.mat.meta create mode 100644 Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Color.jpg create mode 100644 Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Color.jpg.meta create mode 100644 Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_MetallicSmoothness.png create mode 100644 Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_MetallicSmoothness.png.meta create mode 100644 Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_NormalGL.jpg create mode 100644 Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_NormalGL.jpg.meta create mode 100644 Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Roughness.jpg create mode 100644 Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Roughness.jpg.meta create mode 100644 Assets/Materials/PBR/Plates_Panels.meta create mode 100644 Assets/Materials/PBR/Plates_Panels/Plates_Panels.mat create mode 100644 Assets/Materials/PBR/Plates_Panels/Plates_Panels.mat.meta create mode 100644 Assets/Materials/PBR/Plates_Panels/Plates_Panels_Color.jpg create mode 100644 Assets/Materials/PBR/Plates_Panels/Plates_Panels_Color.jpg.meta create mode 100644 Assets/Materials/PBR/Plates_Panels/Plates_Panels_MetallicSmoothness.png create mode 100644 Assets/Materials/PBR/Plates_Panels/Plates_Panels_MetallicSmoothness.png.meta create mode 100644 Assets/Materials/PBR/Plates_Panels/Plates_Panels_Metalness.jpg create mode 100644 Assets/Materials/PBR/Plates_Panels/Plates_Panels_Metalness.jpg.meta create mode 100644 Assets/Materials/PBR/Plates_Panels/Plates_Panels_NormalGL.jpg create mode 100644 Assets/Materials/PBR/Plates_Panels/Plates_Panels_NormalGL.jpg.meta create mode 100644 Assets/Materials/PBR/Plates_Panels/Plates_Panels_Roughness.jpg create mode 100644 Assets/Materials/PBR/Plates_Panels/Plates_Panels_Roughness.jpg.meta create mode 100644 Assets/Materials/PBR/Plates_Riveted.meta create mode 100644 Assets/Materials/PBR/Plates_Riveted/Plates_Riveted.mat create mode 100644 Assets/Materials/PBR/Plates_Riveted/Plates_Riveted.mat.meta create mode 100644 Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Color.jpg create mode 100644 Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Color.jpg.meta create mode 100644 Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_MetallicSmoothness.png create mode 100644 Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_MetallicSmoothness.png.meta create mode 100644 Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Metalness.jpg create mode 100644 Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Metalness.jpg.meta create mode 100644 Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_NormalGL.jpg create mode 100644 Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_NormalGL.jpg.meta create mode 100644 Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Roughness.jpg create mode 100644 Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Roughness.jpg.meta create mode 100644 Assets/Materials/PBR/Rubber.meta create mode 100644 Assets/Materials/PBR/Rubber/Rubber.mat create mode 100644 Assets/Materials/PBR/Rubber/Rubber.mat.meta create mode 100644 Assets/Materials/PBR/Rubber/Rubber_Color.jpg create mode 100644 Assets/Materials/PBR/Rubber/Rubber_Color.jpg.meta create mode 100644 Assets/Materials/PBR/Rubber/Rubber_MetallicSmoothness.png create mode 100644 Assets/Materials/PBR/Rubber/Rubber_MetallicSmoothness.png.meta create mode 100644 Assets/Materials/PBR/Rubber/Rubber_NormalGL.jpg create mode 100644 Assets/Materials/PBR/Rubber/Rubber_NormalGL.jpg.meta create mode 100644 Assets/Materials/PBR/Rubber/Rubber_Roughness.jpg create mode 100644 Assets/Materials/PBR/Rubber/Rubber_Roughness.jpg.meta create mode 100644 Assets/Materials/PBR/SOURCES.md create mode 100644 Assets/Materials/PBR/SOURCES.md.meta create mode 100644 Assets/Materials/PBR/Steel_Brushed.meta create mode 100644 Assets/Materials/PBR/Steel_Brushed/Steel_Brushed.mat create mode 100644 Assets/Materials/PBR/Steel_Brushed/Steel_Brushed.mat.meta create mode 100644 Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Color.jpg create mode 100644 Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Color.jpg.meta create mode 100644 Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_MetallicSmoothness.png create mode 100644 Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_MetallicSmoothness.png.meta create mode 100644 Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Metalness.jpg create mode 100644 Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Metalness.jpg.meta create mode 100644 Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_NormalGL.jpg create mode 100644 Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_NormalGL.jpg.meta create mode 100644 Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Roughness.jpg create mode 100644 Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Roughness.jpg.meta create mode 100644 Assets/Materials/PBR/Tread_Diamond.meta create mode 100644 Assets/Materials/PBR/Tread_Diamond/Tread_Diamond.mat create mode 100644 Assets/Materials/PBR/Tread_Diamond/Tread_Diamond.mat.meta create mode 100644 Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Color.jpg create mode 100644 Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Color.jpg.meta create mode 100644 Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_MetallicSmoothness.png create mode 100644 Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_MetallicSmoothness.png.meta create mode 100644 Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Metalness.jpg create mode 100644 Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Metalness.jpg.meta create mode 100644 Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_NormalGL.jpg create mode 100644 Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_NormalGL.jpg.meta create mode 100644 Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Roughness.jpg create mode 100644 Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Roughness.jpg.meta diff --git a/Assets/Materials/PBR.meta b/Assets/Materials/PBR.meta new file mode 100644 index 000000000..a534b0d5a --- /dev/null +++ b/Assets/Materials/PBR.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5b293e272c024dc999b28fe49925e1d5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Carbon_Fibre.meta b/Assets/Materials/PBR/Carbon_Fibre.meta new file mode 100644 index 000000000..c4c6f51e4 --- /dev/null +++ b/Assets/Materials/PBR/Carbon_Fibre.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 636104c9ca48436da05c3f6003fd3eae +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre.mat b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre.mat new file mode 100644 index 000000000..b7f244b60 --- /dev/null +++ b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre.mat @@ -0,0 +1,86 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Carbon_Fibre + m_Shader: {fileID: 4800000, guid: ddff0b48b92c991499817d2a0f5d5059, type: 3} + m_ShaderKeywords: _MAINTEX _METALLICGLOSSMAP _NORMALMAP + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 2800000, guid: 5723eb533deb4d73a2b015bb75b523b4, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _FadeTex: + m_Texture: {fileID: 2800000, guid: df0632a08c5f1a1498fa325c755dc459, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 2800000, guid: 32fc9d1c4304495da5350f8a8b52e14b, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 2800000, guid: 15a9bba7a82740718ad2d3913f7297af, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _Fade: 0 + - _FadeEdge: 0.05 + - _FadeSharpness: 16 + - _GlossMapScale: 1 + - _Glossiness: 1 + - _GlossyReflections: 1 + - _Metallic: 1 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EdgeColor: {r: 3.0117648, g: 3.0117648, b: 11.984314, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre.mat.meta b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre.mat.meta new file mode 100644 index 000000000..b0f1c3097 --- /dev/null +++ b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8e5a779dec614c80a12925ba063bb99f +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Color.jpg b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Color.jpg new file mode 100644 index 000000000..4e3c4a374 --- /dev/null +++ b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Color.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:327e42b2dfc859efd9b4f122cf1adbdb611c75e80dd3cb8a18bc69e83eef9585 +size 566952 diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Color.jpg.meta b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Color.jpg.meta new file mode 100644 index 000000000..614f43088 --- /dev/null +++ b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Color.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 32fc9d1c4304495da5350f8a8b52e14b +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_MetallicSmoothness.png b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_MetallicSmoothness.png new file mode 100644 index 000000000..49cf03f2e --- /dev/null +++ b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_MetallicSmoothness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a053fa899ea1950563d133010b55a52eee89be8efb3f703e270789a9ad7f75c2 +size 1944565 diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_MetallicSmoothness.png.meta b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_MetallicSmoothness.png.meta new file mode 100644 index 000000000..19a8fae68 --- /dev/null +++ b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_MetallicSmoothness.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 15a9bba7a82740718ad2d3913f7297af +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg new file mode 100644 index 000000000..afd0fe3a6 --- /dev/null +++ b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b65d6b9091747bd81ee58adedcb6209d8ee21b2bde9298d7216989ada85ef622 +size 16545 diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg.meta b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg.meta new file mode 100644 index 000000000..2089e25b6 --- /dev/null +++ b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: b4012db38dfe4d4b8ecea91919903a8f +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_NormalGL.jpg b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_NormalGL.jpg new file mode 100644 index 000000000..c0e1c9aa8 --- /dev/null +++ b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_NormalGL.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e76a53fce87086a2346756ef1ef25f6db6153bda3433611a192ac71b32b2ebdc +size 1356999 diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_NormalGL.jpg.meta b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_NormalGL.jpg.meta new file mode 100644 index 000000000..072cf13c3 --- /dev/null +++ b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_NormalGL.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 5723eb533deb4d73a2b015bb75b523b4 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 1 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Roughness.jpg b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Roughness.jpg new file mode 100644 index 000000000..fd7a1f7f1 --- /dev/null +++ b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Roughness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8005027078e974df48351781ce90885bd6325ac63d0a16b41938fca8fd35a634 +size 1189710 diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Roughness.jpg.meta b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Roughness.jpg.meta new file mode 100644 index 000000000..503d1d154 --- /dev/null +++ b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Roughness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: b8e5cd91353441ebb42296a830fef845 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Gunmetal_Dark.meta b/Assets/Materials/PBR/Gunmetal_Dark.meta new file mode 100644 index 000000000..7680bfe76 --- /dev/null +++ b/Assets/Materials/PBR/Gunmetal_Dark.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 22e47659e8e14482b3466711751b01b8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark.mat b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark.mat new file mode 100644 index 000000000..150f0f72c --- /dev/null +++ b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark.mat @@ -0,0 +1,86 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Gunmetal_Dark + m_Shader: {fileID: 4800000, guid: ddff0b48b92c991499817d2a0f5d5059, type: 3} + m_ShaderKeywords: _MAINTEX _METALLICGLOSSMAP _NORMALMAP + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 2800000, guid: dc71e34b4a1f4de7aa826d18b67b5852, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _FadeTex: + m_Texture: {fileID: 2800000, guid: df0632a08c5f1a1498fa325c755dc459, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 2800000, guid: 1ed06708491744fe944217d19e450157, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 2800000, guid: 31e7d642ff8a49a28390c82626c4b6b9, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _Fade: 0 + - _FadeEdge: 0.05 + - _FadeSharpness: 16 + - _GlossMapScale: 1 + - _Glossiness: 1 + - _GlossyReflections: 1 + - _Metallic: 1 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EdgeColor: {r: 3.0117648, g: 3.0117648, b: 11.984314, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark.mat.meta b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark.mat.meta new file mode 100644 index 000000000..24a52eab3 --- /dev/null +++ b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 32199fedf9ce473191d8a734a74f3d5f +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Color.jpg b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Color.jpg new file mode 100644 index 000000000..4a0118922 --- /dev/null +++ b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Color.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28845bf0cc6a1edeed7e9f10f5db4034cae4ba7377bc53633f413de0dbcae5b7 +size 1451112 diff --git a/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Color.jpg.meta b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Color.jpg.meta new file mode 100644 index 000000000..848791f40 --- /dev/null +++ b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Color.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 1ed06708491744fe944217d19e450157 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_MetallicSmoothness.png b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_MetallicSmoothness.png new file mode 100644 index 000000000..f3cca5c7f --- /dev/null +++ b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_MetallicSmoothness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c101b3b9d7b3ff0636b6539cc65908dc5cac76e31e951a6a375918bb681076c +size 2899231 diff --git a/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_MetallicSmoothness.png.meta b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_MetallicSmoothness.png.meta new file mode 100644 index 000000000..598a3c189 --- /dev/null +++ b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_MetallicSmoothness.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 31e7d642ff8a49a28390c82626c4b6b9 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Metalness.jpg b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Metalness.jpg new file mode 100644 index 000000000..afd0fe3a6 --- /dev/null +++ b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Metalness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b65d6b9091747bd81ee58adedcb6209d8ee21b2bde9298d7216989ada85ef622 +size 16545 diff --git a/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Metalness.jpg.meta b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Metalness.jpg.meta new file mode 100644 index 000000000..2eb33ad5e --- /dev/null +++ b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Metalness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 51e45f19716b43128ce5d2aac5b5aed4 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_NormalGL.jpg b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_NormalGL.jpg new file mode 100644 index 000000000..37f403c32 --- /dev/null +++ b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_NormalGL.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e7b4437dca1fe52addbb21e145d3ccc43e8c1780b7930cc9dad1e2b73cb7455b +size 2260227 diff --git a/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_NormalGL.jpg.meta b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_NormalGL.jpg.meta new file mode 100644 index 000000000..e3fed3ebc --- /dev/null +++ b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_NormalGL.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: dc71e34b4a1f4de7aa826d18b67b5852 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 1 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Roughness.jpg b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Roughness.jpg new file mode 100644 index 000000000..1e6edb81f --- /dev/null +++ b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Roughness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f466bcf997694d56cb7bc93526a881a195e2ee63b1e0c8a19da88289596b9f4 +size 1228992 diff --git a/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Roughness.jpg.meta b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Roughness.jpg.meta new file mode 100644 index 000000000..d09d43872 --- /dev/null +++ b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Roughness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 5677a8acff8f49358627841b9be8e2dd +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Metal_Scratched.meta b/Assets/Materials/PBR/Metal_Scratched.meta new file mode 100644 index 000000000..a53ff5c65 --- /dev/null +++ b/Assets/Materials/PBR/Metal_Scratched.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3e9d00e0b2454cc08632c972d2ee499b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched.mat b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched.mat new file mode 100644 index 000000000..fe6463397 --- /dev/null +++ b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched.mat @@ -0,0 +1,86 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Metal_Scratched + m_Shader: {fileID: 4800000, guid: ddff0b48b92c991499817d2a0f5d5059, type: 3} + m_ShaderKeywords: _MAINTEX _METALLICGLOSSMAP _NORMALMAP + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 2800000, guid: 3a10044baac4438f9fc0930783eccdf7, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _FadeTex: + m_Texture: {fileID: 2800000, guid: df0632a08c5f1a1498fa325c755dc459, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 2800000, guid: 5a9cee903a2d48718aaf6b224126f279, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 2800000, guid: 8b24d7ea11c341408a146fa39f59b7b0, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _Fade: 0 + - _FadeEdge: 0.05 + - _FadeSharpness: 16 + - _GlossMapScale: 1 + - _Glossiness: 1 + - _GlossyReflections: 1 + - _Metallic: 1 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EdgeColor: {r: 3.0117648, g: 3.0117648, b: 11.984314, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched.mat.meta b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched.mat.meta new file mode 100644 index 000000000..1d5895d52 --- /dev/null +++ b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a5874fba9e2242c79aea6b21ddb63af0 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Color.jpg b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Color.jpg new file mode 100644 index 000000000..cb09761f3 --- /dev/null +++ b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Color.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:934e3f0e21b71cb534442f398b287f70158ba7cc3e11ab829f873ddce9cea7fb +size 4314757 diff --git a/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Color.jpg.meta b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Color.jpg.meta new file mode 100644 index 000000000..7b34b1c8f --- /dev/null +++ b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Color.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 5a9cee903a2d48718aaf6b224126f279 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_MetallicSmoothness.png b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_MetallicSmoothness.png new file mode 100644 index 000000000..7b1578437 --- /dev/null +++ b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_MetallicSmoothness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ce0400bc23c44f54aa018b195472b99b7c7007bf93166017bfaf05d68bca38d +size 4647897 diff --git a/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_MetallicSmoothness.png.meta b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_MetallicSmoothness.png.meta new file mode 100644 index 000000000..b6b4c4b0e --- /dev/null +++ b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_MetallicSmoothness.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 8b24d7ea11c341408a146fa39f59b7b0 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Metalness.jpg b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Metalness.jpg new file mode 100644 index 000000000..687e06af5 --- /dev/null +++ b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Metalness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a4f1124670270e69c5fae2537f2493ab55d6f6264310fa344414eb75026cfde +size 564910 diff --git a/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Metalness.jpg.meta b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Metalness.jpg.meta new file mode 100644 index 000000000..697296e8c --- /dev/null +++ b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Metalness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 2eace05436f24d15944266ed3b841f21 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_NormalGL.jpg b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_NormalGL.jpg new file mode 100644 index 000000000..e772289b2 --- /dev/null +++ b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_NormalGL.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb3d7953fb7867f7948b5d7a0dff6044f92a6bba56e6f4a48ad30a94c6096d98 +size 5038957 diff --git a/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_NormalGL.jpg.meta b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_NormalGL.jpg.meta new file mode 100644 index 000000000..932d93b51 --- /dev/null +++ b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_NormalGL.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 3a10044baac4438f9fc0930783eccdf7 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 1 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Roughness.jpg b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Roughness.jpg new file mode 100644 index 000000000..9dab37dc6 --- /dev/null +++ b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Roughness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8fbd2281a995ab1185715dae4d7a657c10b338dfcb57a2f2245eac530d5bd7ed +size 2140434 diff --git a/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Roughness.jpg.meta b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Roughness.jpg.meta new file mode 100644 index 000000000..393e3bd91 --- /dev/null +++ b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Roughness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 986a2475c1e248d7be5208d88ee17a15 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Clean_Light.meta b/Assets/Materials/PBR/Paint_Clean_Light.meta new file mode 100644 index 000000000..c72ff31ca --- /dev/null +++ b/Assets/Materials/PBR/Paint_Clean_Light.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c2497e59922c471b935a06c396acb73e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light.mat b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light.mat new file mode 100644 index 000000000..f76052e2d --- /dev/null +++ b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light.mat @@ -0,0 +1,86 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Paint_Clean_Light + m_Shader: {fileID: 4800000, guid: ddff0b48b92c991499817d2a0f5d5059, type: 3} + m_ShaderKeywords: _MAINTEX _METALLICGLOSSMAP _NORMALMAP + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 2800000, guid: f548cd63b5734e04a284018fefc81b30, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _FadeTex: + m_Texture: {fileID: 2800000, guid: df0632a08c5f1a1498fa325c755dc459, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 2800000, guid: 2c82a94e33b64fd481cbfeb7df9516bd, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 2800000, guid: ad8a50784ce04f5d9b3bab2115eeea34, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _Fade: 0 + - _FadeEdge: 0.05 + - _FadeSharpness: 16 + - _GlossMapScale: 1 + - _Glossiness: 1 + - _GlossyReflections: 1 + - _Metallic: 1 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EdgeColor: {r: 3.0117648, g: 3.0117648, b: 11.984314, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light.mat.meta b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light.mat.meta new file mode 100644 index 000000000..519f702b0 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 132cb2f99d9f42fe9b05db1c0d653897 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Color.jpg b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Color.jpg new file mode 100644 index 000000000..222b441ba --- /dev/null +++ b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Color.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e966978debc85c05e1e4834e1ac8406d863f9267c396d1580ec2d94fdce9738 +size 638778 diff --git a/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Color.jpg.meta b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Color.jpg.meta new file mode 100644 index 000000000..029f71505 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Color.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 2c82a94e33b64fd481cbfeb7df9516bd +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_MetallicSmoothness.png b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_MetallicSmoothness.png new file mode 100644 index 000000000..a61363526 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_MetallicSmoothness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:412547b698a0968ba6f7c4f31e888bd88afb9f24e049f5811ca51c7490510998 +size 3692433 diff --git a/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_MetallicSmoothness.png.meta b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_MetallicSmoothness.png.meta new file mode 100644 index 000000000..41ba9a3be --- /dev/null +++ b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_MetallicSmoothness.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: ad8a50784ce04f5d9b3bab2115eeea34 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_NormalGL.jpg b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_NormalGL.jpg new file mode 100644 index 000000000..0c74319d3 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_NormalGL.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70e36c645d2d6bc9ddc79b86d1275147c52947613fed8ed75dc6aec5d9282187 +size 3220864 diff --git a/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_NormalGL.jpg.meta b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_NormalGL.jpg.meta new file mode 100644 index 000000000..d0c0e745e --- /dev/null +++ b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_NormalGL.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: f548cd63b5734e04a284018fefc81b30 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 1 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Roughness.jpg b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Roughness.jpg new file mode 100644 index 000000000..499f33718 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Roughness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44a1a3eb43252db6c242cd3e777a2e41819e872058e032e3cf8285cc578f35bb +size 2011305 diff --git a/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Roughness.jpg.meta b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Roughness.jpg.meta new file mode 100644 index 000000000..117171aad --- /dev/null +++ b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Roughness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 21721bf50f1f407d8e0f2b5e06bf4ef5 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Dark.meta b/Assets/Materials/PBR/Paint_Worn_Dark.meta new file mode 100644 index 000000000..f6af35502 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 22c55f60ad784e7495f2ddbae1e2799e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark.mat b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark.mat new file mode 100644 index 000000000..11deee75d --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark.mat @@ -0,0 +1,86 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Paint_Worn_Dark + m_Shader: {fileID: 4800000, guid: ddff0b48b92c991499817d2a0f5d5059, type: 3} + m_ShaderKeywords: _MAINTEX _METALLICGLOSSMAP _NORMALMAP _OCCLUSIONMAP + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 2800000, guid: b37cce6dfe8a45d0a7ba4e9a844c68b9, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _FadeTex: + m_Texture: {fileID: 2800000, guid: df0632a08c5f1a1498fa325c755dc459, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 2800000, guid: 8f9bc4377cc4451aae71a877219e700f, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 2800000, guid: b5158bc316ef42bead7fa70b7bc08a7a, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 2800000, guid: e8713adbe3e74ac4a4f431581bb31a4b, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _Fade: 0 + - _FadeEdge: 0.05 + - _FadeSharpness: 16 + - _GlossMapScale: 1 + - _Glossiness: 1 + - _GlossyReflections: 1 + - _Metallic: 1 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EdgeColor: {r: 3.0117648, g: 3.0117648, b: 11.984314, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark.mat.meta b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark.mat.meta new file mode 100644 index 000000000..c70f30afd --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e081b7d98e6d424fa42a6876a8e0bab3 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_AO.jpg b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_AO.jpg new file mode 100644 index 000000000..7c4f50094 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_AO.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2e4af5867078208c00332af23bded29a4ac325b040f5980c2730ff8e551bc9d +size 2924087 diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_AO.jpg.meta b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_AO.jpg.meta new file mode 100644 index 000000000..e3c6ca27e --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_AO.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: e8713adbe3e74ac4a4f431581bb31a4b +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Color.jpg b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Color.jpg new file mode 100644 index 000000000..de88b6108 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Color.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c57d484d39db6704fbcf32184f73472e6a78cfc63027422426fa170d87bdfc4a +size 5177435 diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Color.jpg.meta b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Color.jpg.meta new file mode 100644 index 000000000..5cdcca076 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Color.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 8f9bc4377cc4451aae71a877219e700f +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_MetallicSmoothness.png b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_MetallicSmoothness.png new file mode 100644 index 000000000..ca9d86708 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_MetallicSmoothness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98cc6442c3952acba81f86ae0210e9e0b171522dec1a80d9177eb942dd295d73 +size 5117887 diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_MetallicSmoothness.png.meta b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_MetallicSmoothness.png.meta new file mode 100644 index 000000000..28b873752 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_MetallicSmoothness.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: b5158bc316ef42bead7fa70b7bc08a7a +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Metalness.jpg b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Metalness.jpg new file mode 100644 index 000000000..90096ce14 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Metalness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3cdb47576c1085bea1bd3e90d055c7f45b66bda53d3ad03f825e5dc9665b93f +size 1073864 diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Metalness.jpg.meta b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Metalness.jpg.meta new file mode 100644 index 000000000..fb46b8a1a --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Metalness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: c6d0917aab0d4442ad102a6088628f53 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_NormalGL.jpg b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_NormalGL.jpg new file mode 100644 index 000000000..9c151b688 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_NormalGL.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60e7d12d4eefed9f9c763b3095b6db35542ee00c7f076b72bb2718a3ccd649c9 +size 7526970 diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_NormalGL.jpg.meta b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_NormalGL.jpg.meta new file mode 100644 index 000000000..bcfcf9af3 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_NormalGL.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: b37cce6dfe8a45d0a7ba4e9a844c68b9 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 1 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Roughness.jpg b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Roughness.jpg new file mode 100644 index 000000000..918bb48c5 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Roughness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:648bf332066975dad0fc618eda260571e8077a7d418587b8b55a68c7da797541 +size 2083327 diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Roughness.jpg.meta b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Roughness.jpg.meta new file mode 100644 index 000000000..3607f105d --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Roughness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 46fe06d220b044d1a6bb1a716c6e8705 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Light.meta b/Assets/Materials/PBR/Paint_Worn_Light.meta new file mode 100644 index 000000000..34d26b097 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b660a03d95714359ac68aa01a52064b9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light.mat b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light.mat new file mode 100644 index 000000000..9f6a4ff51 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light.mat @@ -0,0 +1,86 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Paint_Worn_Light + m_Shader: {fileID: 4800000, guid: ddff0b48b92c991499817d2a0f5d5059, type: 3} + m_ShaderKeywords: _MAINTEX _METALLICGLOSSMAP _NORMALMAP _OCCLUSIONMAP + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 2800000, guid: 4ce3c2cc69304068b4a2ddc833b748b9, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _FadeTex: + m_Texture: {fileID: 2800000, guid: df0632a08c5f1a1498fa325c755dc459, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 2800000, guid: 73568a0c02bd4184bcb398c9e3dd7ab6, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 2800000, guid: d9cd336372c244e398532512e3f8f3be, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 2800000, guid: 73e117729c3447e5aa0eb1a30fad7c75, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _Fade: 0 + - _FadeEdge: 0.05 + - _FadeSharpness: 16 + - _GlossMapScale: 1 + - _Glossiness: 1 + - _GlossyReflections: 1 + - _Metallic: 1 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EdgeColor: {r: 3.0117648, g: 3.0117648, b: 11.984314, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light.mat.meta b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light.mat.meta new file mode 100644 index 000000000..1441ced17 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5eb890ad19d44af98f2710f3af280e3c +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_AO.jpg b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_AO.jpg new file mode 100644 index 000000000..a9989d282 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_AO.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2560f1aa453ffd2f4374c9ba4d184e1c43456726d8971460062515acef83b76 +size 2764255 diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_AO.jpg.meta b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_AO.jpg.meta new file mode 100644 index 000000000..be48781f6 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_AO.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 73e117729c3447e5aa0eb1a30fad7c75 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Color.jpg b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Color.jpg new file mode 100644 index 000000000..d05a21ab2 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Color.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:401b7ee4394595110564bcb12a40c0b87ea50acfc3632b7e7237e2d0165d1ded +size 4364255 diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Color.jpg.meta b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Color.jpg.meta new file mode 100644 index 000000000..cc5b0e17b --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Color.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 73568a0c02bd4184bcb398c9e3dd7ab6 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_MetallicSmoothness.png b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_MetallicSmoothness.png new file mode 100644 index 000000000..08375c3a3 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_MetallicSmoothness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c59489103f8a5ef438f0f74484cdf5af90b1ee2985a180d690772d1325816bf +size 6990606 diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_MetallicSmoothness.png.meta b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_MetallicSmoothness.png.meta new file mode 100644 index 000000000..13b040de5 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_MetallicSmoothness.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: d9cd336372c244e398532512e3f8f3be +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Metalness.jpg b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Metalness.jpg new file mode 100644 index 000000000..10a0cd502 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Metalness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8be2891eeff9e521ac8d672edb106830e878ee0b73852a5763c803a8088014f2 +size 2705735 diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Metalness.jpg.meta b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Metalness.jpg.meta new file mode 100644 index 000000000..402c2f3f3 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Metalness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 464aecd85593406aa2426215e07b988a +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_NormalGL.jpg b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_NormalGL.jpg new file mode 100644 index 000000000..549164ad2 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_NormalGL.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2ae51f315549a7d63c7f0290e9f6d35dfcd1bc42df422579ee407c30692599a +size 6857447 diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_NormalGL.jpg.meta b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_NormalGL.jpg.meta new file mode 100644 index 000000000..a9aa1c988 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_NormalGL.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 4ce3c2cc69304068b4a2ddc833b748b9 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 1 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Roughness.jpg b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Roughness.jpg new file mode 100644 index 000000000..d0ab06978 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Roughness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1207c0c4716229b36290416e4af37ccaf5351a65c8d93237a7802a95f45dfbcf +size 1929057 diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Roughness.jpg.meta b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Roughness.jpg.meta new file mode 100644 index 000000000..2b1eeabdb --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Roughness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 58cb097056b5431e819ca45954ee3b47 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plastic_Matte.meta b/Assets/Materials/PBR/Plastic_Matte.meta new file mode 100644 index 000000000..91fd17d97 --- /dev/null +++ b/Assets/Materials/PBR/Plastic_Matte.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1126ae7b5bcb4d6b8403e31233638b8d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte.mat b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte.mat new file mode 100644 index 000000000..f5845363d --- /dev/null +++ b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte.mat @@ -0,0 +1,86 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Plastic_Matte + m_Shader: {fileID: 4800000, guid: ddff0b48b92c991499817d2a0f5d5059, type: 3} + m_ShaderKeywords: _MAINTEX _METALLICGLOSSMAP _NORMALMAP + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 2800000, guid: d9e9439b6e9f43ed9c47015cafdcd5d3, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _FadeTex: + m_Texture: {fileID: 2800000, guid: df0632a08c5f1a1498fa325c755dc459, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 2800000, guid: 053c9b821bf649bab9c8f7b234803809, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 2800000, guid: f405c1ca89914831a4366ac1e4d850ee, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _Fade: 0 + - _FadeEdge: 0.05 + - _FadeSharpness: 16 + - _GlossMapScale: 1 + - _Glossiness: 1 + - _GlossyReflections: 1 + - _Metallic: 1 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EdgeColor: {r: 3.0117648, g: 3.0117648, b: 11.984314, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte.mat.meta b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte.mat.meta new file mode 100644 index 000000000..f92b559c4 --- /dev/null +++ b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 636667df941046e79d2417c6b062d995 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Color.jpg b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Color.jpg new file mode 100644 index 000000000..00c2a444a --- /dev/null +++ b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Color.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5cb3a446755c2f868957807195d1e51ff989384da7878e8c0d9f93f391a5b755 +size 905715 diff --git a/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Color.jpg.meta b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Color.jpg.meta new file mode 100644 index 000000000..e0947dee4 --- /dev/null +++ b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Color.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 053c9b821bf649bab9c8f7b234803809 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_MetallicSmoothness.png b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_MetallicSmoothness.png new file mode 100644 index 000000000..45e075a5b --- /dev/null +++ b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_MetallicSmoothness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:811b05a5b78048c711f5129d14073bfa1983443553a2b75453f15e46ad6da998 +size 3782071 diff --git a/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_MetallicSmoothness.png.meta b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_MetallicSmoothness.png.meta new file mode 100644 index 000000000..c641103a4 --- /dev/null +++ b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_MetallicSmoothness.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: f405c1ca89914831a4366ac1e4d850ee +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_NormalGL.jpg b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_NormalGL.jpg new file mode 100644 index 000000000..12b6ad1ff --- /dev/null +++ b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_NormalGL.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e177082ed5027b4bb533e39b0556c7f3ca4701d6ffa3761910dff678d7f43d07 +size 9096346 diff --git a/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_NormalGL.jpg.meta b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_NormalGL.jpg.meta new file mode 100644 index 000000000..357ce2800 --- /dev/null +++ b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_NormalGL.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: d9e9439b6e9f43ed9c47015cafdcd5d3 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 1 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Roughness.jpg b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Roughness.jpg new file mode 100644 index 000000000..c1eb5f150 --- /dev/null +++ b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Roughness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:629c695383835080794b90174ae19a422cb7234870752642afa81f36a28cd9e7 +size 2095946 diff --git a/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Roughness.jpg.meta b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Roughness.jpg.meta new file mode 100644 index 000000000..575bf2428 --- /dev/null +++ b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Roughness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 142ed10762d242a9a4ecf63ee0e38b71 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Panels.meta b/Assets/Materials/PBR/Plates_Panels.meta new file mode 100644 index 000000000..99eb09f70 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Panels.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 086928e6583747e1b14c6767ced082b4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Panels/Plates_Panels.mat b/Assets/Materials/PBR/Plates_Panels/Plates_Panels.mat new file mode 100644 index 000000000..babe13ca0 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Panels/Plates_Panels.mat @@ -0,0 +1,86 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Plates_Panels + m_Shader: {fileID: 4800000, guid: ddff0b48b92c991499817d2a0f5d5059, type: 3} + m_ShaderKeywords: _MAINTEX _METALLICGLOSSMAP _NORMALMAP + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 2800000, guid: 8bc10cfce8a64defaf9f67d156003f09, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _FadeTex: + m_Texture: {fileID: 2800000, guid: df0632a08c5f1a1498fa325c755dc459, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 2800000, guid: 2195885db9e846999fd10aa7f5c629b1, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 2800000, guid: d91bff887b764847a484d0a9bedce9e9, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _Fade: 0 + - _FadeEdge: 0.05 + - _FadeSharpness: 16 + - _GlossMapScale: 1 + - _Glossiness: 1 + - _GlossyReflections: 1 + - _Metallic: 1 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EdgeColor: {r: 3.0117648, g: 3.0117648, b: 11.984314, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Materials/PBR/Plates_Panels/Plates_Panels.mat.meta b/Assets/Materials/PBR/Plates_Panels/Plates_Panels.mat.meta new file mode 100644 index 000000000..a8a703cf0 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Panels/Plates_Panels.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c508beb10bbe4c7f88fc9a7c003e51d5 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Color.jpg b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Color.jpg new file mode 100644 index 000000000..9945f16ae --- /dev/null +++ b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Color.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e69514467f58898537ecc6b3c47e618537406a00b7eb24099d3835ca4ae4c9a +size 2132182 diff --git a/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Color.jpg.meta b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Color.jpg.meta new file mode 100644 index 000000000..960e6e0e3 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Color.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 2195885db9e846999fd10aa7f5c629b1 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Panels/Plates_Panels_MetallicSmoothness.png b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_MetallicSmoothness.png new file mode 100644 index 000000000..de028ea73 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_MetallicSmoothness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b5ea65058b4792f97c7f9affd89930342b2306ab5dbd21f930ef153d4f4acf1 +size 3414650 diff --git a/Assets/Materials/PBR/Plates_Panels/Plates_Panels_MetallicSmoothness.png.meta b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_MetallicSmoothness.png.meta new file mode 100644 index 000000000..6b036cb95 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_MetallicSmoothness.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: d91bff887b764847a484d0a9bedce9e9 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Metalness.jpg b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Metalness.jpg new file mode 100644 index 000000000..9edfab941 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Metalness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:736a312c106a791672e1115df51ba0a08be7927f6cfc4aff97fcfb60311e002f +size 171958 diff --git a/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Metalness.jpg.meta b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Metalness.jpg.meta new file mode 100644 index 000000000..69399ee42 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Metalness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 5df39678e5734f51a29940f2f6a43dda +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Panels/Plates_Panels_NormalGL.jpg b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_NormalGL.jpg new file mode 100644 index 000000000..127c7888d --- /dev/null +++ b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_NormalGL.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f916040ca388073191224a515def08761a3e136a74f559feb5774ef50527d54 +size 3505435 diff --git a/Assets/Materials/PBR/Plates_Panels/Plates_Panels_NormalGL.jpg.meta b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_NormalGL.jpg.meta new file mode 100644 index 000000000..da4e2137e --- /dev/null +++ b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_NormalGL.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 8bc10cfce8a64defaf9f67d156003f09 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 1 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Roughness.jpg b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Roughness.jpg new file mode 100644 index 000000000..cf7a6709b --- /dev/null +++ b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Roughness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9fc8550cdb559c16cbd2d43196dcc271653e78c802519bc446684224048ff69 +size 1742286 diff --git a/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Roughness.jpg.meta b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Roughness.jpg.meta new file mode 100644 index 000000000..9d51593f6 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Roughness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: e2f6e4f21fd14d6eb65e100b4f012349 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Riveted.meta b/Assets/Materials/PBR/Plates_Riveted.meta new file mode 100644 index 000000000..870fc938a --- /dev/null +++ b/Assets/Materials/PBR/Plates_Riveted.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 79fbea05234f4b53a53eab2f090d2005 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted.mat b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted.mat new file mode 100644 index 000000000..e8088be9b --- /dev/null +++ b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted.mat @@ -0,0 +1,86 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Plates_Riveted + m_Shader: {fileID: 4800000, guid: ddff0b48b92c991499817d2a0f5d5059, type: 3} + m_ShaderKeywords: _MAINTEX _METALLICGLOSSMAP _NORMALMAP + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 2800000, guid: 6fb688a276034675b895c2edeb1b538b, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _FadeTex: + m_Texture: {fileID: 2800000, guid: df0632a08c5f1a1498fa325c755dc459, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 2800000, guid: f07bacf217654b269eecfabbcc651b2b, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 2800000, guid: 08b45a54ea8c4521b7c4e7b3974f0b42, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _Fade: 0 + - _FadeEdge: 0.05 + - _FadeSharpness: 16 + - _GlossMapScale: 1 + - _Glossiness: 1 + - _GlossyReflections: 1 + - _Metallic: 1 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EdgeColor: {r: 3.0117648, g: 3.0117648, b: 11.984314, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted.mat.meta b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted.mat.meta new file mode 100644 index 000000000..62c10b5e6 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 538d82fdc2de48f4bcf4e93b17407f90 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Color.jpg b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Color.jpg new file mode 100644 index 000000000..fe22d7428 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Color.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a72ee2d4e5e6c3d4154e9244ad57f7c6b644b40b356db162c9f2b01e80547eb +size 1602784 diff --git a/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Color.jpg.meta b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Color.jpg.meta new file mode 100644 index 000000000..dc74b1c54 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Color.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: f07bacf217654b269eecfabbcc651b2b +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_MetallicSmoothness.png b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_MetallicSmoothness.png new file mode 100644 index 000000000..527fe9f5f --- /dev/null +++ b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_MetallicSmoothness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d678e8bff66b2d3bc703c5d355c47644f135544ac33afd517f1a542195b7e43d +size 5021835 diff --git a/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_MetallicSmoothness.png.meta b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_MetallicSmoothness.png.meta new file mode 100644 index 000000000..778892894 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_MetallicSmoothness.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 08b45a54ea8c4521b7c4e7b3974f0b42 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Metalness.jpg b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Metalness.jpg new file mode 100644 index 000000000..05c05f99a --- /dev/null +++ b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Metalness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:212258a43acddd1cb67203b3b263721022ae5f22bbf2213842a608fb09dc2b2f +size 1396144 diff --git a/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Metalness.jpg.meta b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Metalness.jpg.meta new file mode 100644 index 000000000..86f483ff4 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Metalness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 24e3f6b76da9420592e2f21931defae7 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_NormalGL.jpg b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_NormalGL.jpg new file mode 100644 index 000000000..4c272122c --- /dev/null +++ b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_NormalGL.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e49f90c003cf728de4892eec7d71f08abec1b2f9cb4780c875d005cb313498ac +size 3616644 diff --git a/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_NormalGL.jpg.meta b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_NormalGL.jpg.meta new file mode 100644 index 000000000..aa8d94dbe --- /dev/null +++ b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_NormalGL.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 6fb688a276034675b895c2edeb1b538b +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 1 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Roughness.jpg b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Roughness.jpg new file mode 100644 index 000000000..b96f7460a --- /dev/null +++ b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Roughness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d48b683b55fd5e89c91a0c4bfde3a539fc892abc958387045e7d0786fe5c1fbd +size 1889081 diff --git a/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Roughness.jpg.meta b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Roughness.jpg.meta new file mode 100644 index 000000000..232a09cf3 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Roughness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 7b50b58679684fc8991f112974a62120 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Rubber.meta b/Assets/Materials/PBR/Rubber.meta new file mode 100644 index 000000000..f4d39fafb --- /dev/null +++ b/Assets/Materials/PBR/Rubber.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 180d9a65c5784b618970824077421002 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Rubber/Rubber.mat b/Assets/Materials/PBR/Rubber/Rubber.mat new file mode 100644 index 000000000..2c10c3488 --- /dev/null +++ b/Assets/Materials/PBR/Rubber/Rubber.mat @@ -0,0 +1,86 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Rubber + m_Shader: {fileID: 4800000, guid: ddff0b48b92c991499817d2a0f5d5059, type: 3} + m_ShaderKeywords: _MAINTEX _METALLICGLOSSMAP _NORMALMAP + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 2800000, guid: 42826f488ae243178be306ada5ee9f41, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _FadeTex: + m_Texture: {fileID: 2800000, guid: df0632a08c5f1a1498fa325c755dc459, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 2800000, guid: 83e6c1768e014acfb494fbf007c40c80, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 2800000, guid: f1d4409638e4452cb1412503ed8526de, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _Fade: 0 + - _FadeEdge: 0.05 + - _FadeSharpness: 16 + - _GlossMapScale: 1 + - _Glossiness: 1 + - _GlossyReflections: 1 + - _Metallic: 1 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EdgeColor: {r: 3.0117648, g: 3.0117648, b: 11.984314, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Materials/PBR/Rubber/Rubber.mat.meta b/Assets/Materials/PBR/Rubber/Rubber.mat.meta new file mode 100644 index 000000000..989c2cd22 --- /dev/null +++ b/Assets/Materials/PBR/Rubber/Rubber.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 257d49e9af154d6c9b5ea50a288db1fa +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Rubber/Rubber_Color.jpg b/Assets/Materials/PBR/Rubber/Rubber_Color.jpg new file mode 100644 index 000000000..3a0e09062 --- /dev/null +++ b/Assets/Materials/PBR/Rubber/Rubber_Color.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d712c25987d3e6f40e3f37fae1ce1a4a4155d5e27cff64dadf2bbe140979dd98 +size 4479634 diff --git a/Assets/Materials/PBR/Rubber/Rubber_Color.jpg.meta b/Assets/Materials/PBR/Rubber/Rubber_Color.jpg.meta new file mode 100644 index 000000000..ff443d7b4 --- /dev/null +++ b/Assets/Materials/PBR/Rubber/Rubber_Color.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 83e6c1768e014acfb494fbf007c40c80 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Rubber/Rubber_MetallicSmoothness.png b/Assets/Materials/PBR/Rubber/Rubber_MetallicSmoothness.png new file mode 100644 index 000000000..d194ddc8f --- /dev/null +++ b/Assets/Materials/PBR/Rubber/Rubber_MetallicSmoothness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a5d2c567edceeeec37de50166d325a0b3d29bc6cd90ad891635bf4cf8334376 +size 5044494 diff --git a/Assets/Materials/PBR/Rubber/Rubber_MetallicSmoothness.png.meta b/Assets/Materials/PBR/Rubber/Rubber_MetallicSmoothness.png.meta new file mode 100644 index 000000000..fe4234492 --- /dev/null +++ b/Assets/Materials/PBR/Rubber/Rubber_MetallicSmoothness.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: f1d4409638e4452cb1412503ed8526de +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Rubber/Rubber_NormalGL.jpg b/Assets/Materials/PBR/Rubber/Rubber_NormalGL.jpg new file mode 100644 index 000000000..461b856ff --- /dev/null +++ b/Assets/Materials/PBR/Rubber/Rubber_NormalGL.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:61bf1b38eae723913275a1de5eb08d4ac543f11e28f4ac6f596a0644169010fc +size 9238212 diff --git a/Assets/Materials/PBR/Rubber/Rubber_NormalGL.jpg.meta b/Assets/Materials/PBR/Rubber/Rubber_NormalGL.jpg.meta new file mode 100644 index 000000000..16144739a --- /dev/null +++ b/Assets/Materials/PBR/Rubber/Rubber_NormalGL.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 42826f488ae243178be306ada5ee9f41 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 1 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Rubber/Rubber_Roughness.jpg b/Assets/Materials/PBR/Rubber/Rubber_Roughness.jpg new file mode 100644 index 000000000..7c59cbb40 --- /dev/null +++ b/Assets/Materials/PBR/Rubber/Rubber_Roughness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc137f95179a4ae42d67819750d78c3fe878d438b789cf96b33947618305a275 +size 3042709 diff --git a/Assets/Materials/PBR/Rubber/Rubber_Roughness.jpg.meta b/Assets/Materials/PBR/Rubber/Rubber_Roughness.jpg.meta new file mode 100644 index 000000000..feca8c0c2 --- /dev/null +++ b/Assets/Materials/PBR/Rubber/Rubber_Roughness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 834b5a5e1e1a49559f9fdeffd947e0a0 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/SOURCES.md b/Assets/Materials/PBR/SOURCES.md new file mode 100644 index 000000000..d0f2c96cb --- /dev/null +++ b/Assets/Materials/PBR/SOURCES.md @@ -0,0 +1,22 @@ +# PBR material sources + +All texture sets are from ambientCG (https://ambientcg.com), licensed CC0 1.0 Universal (public domain, https://creativecommons.org/publicdomain/zero/1.0/). 2K-JPG downloads; displacement and NormalDX dropped; NormalGL kept (OpenGL convention, used by Blender and Unity). + +| Material | ambientCG id | URL | Metallic source | +|---|---|---|---| +| Paint_Clean_Light | Plastic010 | https://ambientcg.com/view?id=Plastic010 | none in set: constant 0 | +| Paint_Worn_Light | PaintedMetal012 | https://ambientcg.com/view?id=PaintedMetal012 | ambientCG Metalness map | +| Paint_Worn_Dark | PaintedMetal007 | https://ambientcg.com/view?id=PaintedMetal007 | ambientCG Metalness map | +| Steel_Brushed | Metal009 | https://ambientcg.com/view?id=Metal009 | ambientCG Metalness map | +| Metal_Scratched | Metal002 | https://ambientcg.com/view?id=Metal002 | ambientCG Metalness map | +| Gunmetal_Dark | Metal030 | https://ambientcg.com/view?id=Metal030 | ambientCG Metalness map | +| Plates_Riveted | MetalPlates002 | https://ambientcg.com/view?id=MetalPlates002 | ambientCG Metalness map | +| Plates_Panels | MetalPlates006 | https://ambientcg.com/view?id=MetalPlates006 | ambientCG Metalness map | +| Tread_Diamond | DiamondPlate001 | https://ambientcg.com/view?id=DiamondPlate001 | ambientCG Metalness map | +| Rubber | Rubber001 | https://ambientcg.com/view?id=Rubber001 | none in set: constant 0 | +| Plastic_Matte | Plastic004 | https://ambientcg.com/view?id=Plastic004 | none in set: constant 0 | +| Carbon_Fibre | Fabric004 | https://ambientcg.com/view?id=Fabric004 | ambientCG Metalness map | + +Files per material: `_Color`, `_Roughness`, `_NormalGL`, `_Metalness` and `_AO` (where the set has them), plus `_MetallicSmoothness.png` generated here for Unity (Metallic in R, Smoothness = 1 - Roughness in A). AO exists only for Paint_Worn_Light and Paint_Worn_Dark. + +Blender library: `Asset Sources/PBR Materials.blend` (catalog Aetheria/PBR, images linked by relative path to this folder). Unity materials use Aetheria/GlowFade. diff --git a/Assets/Materials/PBR/SOURCES.md.meta b/Assets/Materials/PBR/SOURCES.md.meta new file mode 100644 index 000000000..4c16a4a62 --- /dev/null +++ b/Assets/Materials/PBR/SOURCES.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a29568b9a0cf46babc43fe6045d6e10a +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Steel_Brushed.meta b/Assets/Materials/PBR/Steel_Brushed.meta new file mode 100644 index 000000000..0a76b67a2 --- /dev/null +++ b/Assets/Materials/PBR/Steel_Brushed.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 76edddc4bbd74a8d80e7cc29729b334e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed.mat b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed.mat new file mode 100644 index 000000000..7147c9aa2 --- /dev/null +++ b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed.mat @@ -0,0 +1,86 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Steel_Brushed + m_Shader: {fileID: 4800000, guid: ddff0b48b92c991499817d2a0f5d5059, type: 3} + m_ShaderKeywords: _MAINTEX _METALLICGLOSSMAP _NORMALMAP + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 2800000, guid: c3ef09438e3e46d18a4c267810aeb285, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _FadeTex: + m_Texture: {fileID: 2800000, guid: df0632a08c5f1a1498fa325c755dc459, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 2800000, guid: f70b6df0395b4752b4ec80cb7bfd8f42, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 2800000, guid: 40f5e915d2734d43bc01f984772e9d29, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _Fade: 0 + - _FadeEdge: 0.05 + - _FadeSharpness: 16 + - _GlossMapScale: 1 + - _Glossiness: 1 + - _GlossyReflections: 1 + - _Metallic: 1 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EdgeColor: {r: 3.0117648, g: 3.0117648, b: 11.984314, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed.mat.meta b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed.mat.meta new file mode 100644 index 000000000..6a17f81b3 --- /dev/null +++ b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 65c607f056d64708bb493dd31b39e79e +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Color.jpg b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Color.jpg new file mode 100644 index 000000000..c3406774a --- /dev/null +++ b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Color.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:209d3bb050be9f58c5af179d9712fa7799567a4cd50afd338d37674bdcff765b +size 2439272 diff --git a/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Color.jpg.meta b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Color.jpg.meta new file mode 100644 index 000000000..d6973f061 --- /dev/null +++ b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Color.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: f70b6df0395b4752b4ec80cb7bfd8f42 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_MetallicSmoothness.png b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_MetallicSmoothness.png new file mode 100644 index 000000000..8142b06f2 --- /dev/null +++ b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_MetallicSmoothness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2b84e6acb67eb71fb263546ba5fe2ddd5f6e00517addb3c96c0f226becab3bb +size 3309438 diff --git a/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_MetallicSmoothness.png.meta b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_MetallicSmoothness.png.meta new file mode 100644 index 000000000..130146909 --- /dev/null +++ b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_MetallicSmoothness.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 40f5e915d2734d43bc01f984772e9d29 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Metalness.jpg b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Metalness.jpg new file mode 100644 index 000000000..afd0fe3a6 --- /dev/null +++ b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Metalness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b65d6b9091747bd81ee58adedcb6209d8ee21b2bde9298d7216989ada85ef622 +size 16545 diff --git a/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Metalness.jpg.meta b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Metalness.jpg.meta new file mode 100644 index 000000000..1aa819912 --- /dev/null +++ b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Metalness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 415d2498454d4d419948b2aa11eac78c +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_NormalGL.jpg b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_NormalGL.jpg new file mode 100644 index 000000000..2c9942542 --- /dev/null +++ b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_NormalGL.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8773d9e97a4b057ced3e538d8ecc6069d27c8948191d61daf79b9ad93d93e3b3 +size 4120356 diff --git a/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_NormalGL.jpg.meta b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_NormalGL.jpg.meta new file mode 100644 index 000000000..e223398fa --- /dev/null +++ b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_NormalGL.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: c3ef09438e3e46d18a4c267810aeb285 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 1 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Roughness.jpg b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Roughness.jpg new file mode 100644 index 000000000..a1d7edd1f --- /dev/null +++ b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Roughness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e802f013b93cb1bd4b55cd91f02a1863233475a7f8bd0e6755cfaa447a729674 +size 1891607 diff --git a/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Roughness.jpg.meta b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Roughness.jpg.meta new file mode 100644 index 000000000..726ff5549 --- /dev/null +++ b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Roughness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 158c7794c28740d0a16fa2dc22e93d8c +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Tread_Diamond.meta b/Assets/Materials/PBR/Tread_Diamond.meta new file mode 100644 index 000000000..6a3445907 --- /dev/null +++ b/Assets/Materials/PBR/Tread_Diamond.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 78b78bf7d2f84a8d85daaf75d21fa56f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond.mat b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond.mat new file mode 100644 index 000000000..5b9542d0f --- /dev/null +++ b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond.mat @@ -0,0 +1,86 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Tread_Diamond + m_Shader: {fileID: 4800000, guid: ddff0b48b92c991499817d2a0f5d5059, type: 3} + m_ShaderKeywords: _MAINTEX _METALLICGLOSSMAP _NORMALMAP + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 2800000, guid: 043636050ee1492697e5bebb7603bf12, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _FadeTex: + m_Texture: {fileID: 2800000, guid: df0632a08c5f1a1498fa325c755dc459, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 2800000, guid: f04f0c584e824d81b7d7a91b969efef8, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 2800000, guid: 28456bc0a3c045e394e0937b6fe0231c, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _Fade: 0 + - _FadeEdge: 0.05 + - _FadeSharpness: 16 + - _GlossMapScale: 1 + - _Glossiness: 1 + - _GlossyReflections: 1 + - _Metallic: 1 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EdgeColor: {r: 3.0117648, g: 3.0117648, b: 11.984314, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond.mat.meta b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond.mat.meta new file mode 100644 index 000000000..dd3e8ee22 --- /dev/null +++ b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bd8a08731d8c4a4d8961c92fe318f1bb +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Color.jpg b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Color.jpg new file mode 100644 index 000000000..c4aae91a2 --- /dev/null +++ b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Color.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a1de0119ef8592c3054949b6a9608a5a814156f661ed54541b390feb6fb87cd +size 2675454 diff --git a/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Color.jpg.meta b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Color.jpg.meta new file mode 100644 index 000000000..f02278e36 --- /dev/null +++ b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Color.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: f04f0c584e824d81b7d7a91b969efef8 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_MetallicSmoothness.png b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_MetallicSmoothness.png new file mode 100644 index 000000000..8c48d3028 --- /dev/null +++ b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_MetallicSmoothness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6131476bdbc8b6e910a5537a0aaadc977584f22c2a76d97f4cab4aa2b2b7ec0 +size 7151369 diff --git a/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_MetallicSmoothness.png.meta b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_MetallicSmoothness.png.meta new file mode 100644 index 000000000..8804c2d58 --- /dev/null +++ b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_MetallicSmoothness.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 28456bc0a3c045e394e0937b6fe0231c +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Metalness.jpg b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Metalness.jpg new file mode 100644 index 000000000..d65027465 --- /dev/null +++ b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Metalness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6925202668aae7828b0a4765ca8854c539d94c4cd0f945b5195b5576272cdc7a +size 2420905 diff --git a/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Metalness.jpg.meta b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Metalness.jpg.meta new file mode 100644 index 000000000..c786edbd3 --- /dev/null +++ b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Metalness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 5086225cf1bf485597fe191f1b9ca912 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_NormalGL.jpg b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_NormalGL.jpg new file mode 100644 index 000000000..7411d45d0 --- /dev/null +++ b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_NormalGL.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:317627265f2587cdfb00e06ea80415333b0ff2597321c79a77c2fd55448e5ec5 +size 7454546 diff --git a/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_NormalGL.jpg.meta b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_NormalGL.jpg.meta new file mode 100644 index 000000000..747097254 --- /dev/null +++ b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_NormalGL.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 043636050ee1492697e5bebb7603bf12 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 1 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Roughness.jpg b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Roughness.jpg new file mode 100644 index 000000000..2a2b7c7f8 --- /dev/null +++ b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Roughness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6aa2c8bd6044829215771cb1c3d82fcd3845565ff1a9416f4a10403dc2dfbc1b +size 2395648 diff --git a/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Roughness.jpg.meta b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Roughness.jpg.meta new file mode 100644 index 000000000..cc15cc441 --- /dev/null +++ b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Roughness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 1d942e92edc64885ae82ceb88af83104 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: From 492a8333fb4aeb07648c78e7eb31d0e2d1043dfd Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 00:26:22 +0200 Subject: [PATCH 114/120] Blender asset library: PBR Materials.blend (catalog Aetheria/PBR) 12 asset-marked materials, UV mapping with a scalar Tiling node, images linked by relative path to Assets/Materials/PBR, previews generated. Co-Authored-By: Claude Opus 5.5 --- Asset Sources/PBR Materials.blend | 3 +++ Asset Sources/blender_assets.cats.txt | 10 ++++++++++ 2 files changed, 13 insertions(+) create mode 100644 Asset Sources/PBR Materials.blend create mode 100644 Asset Sources/blender_assets.cats.txt diff --git a/Asset Sources/PBR Materials.blend b/Asset Sources/PBR Materials.blend new file mode 100644 index 000000000..445390b60 --- /dev/null +++ b/Asset Sources/PBR Materials.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f86c6a34ead2264030e7a54f276229e0d84d90f9c09db25fe31c1c086cc933c7 +size 356054 diff --git a/Asset Sources/blender_assets.cats.txt b/Asset Sources/blender_assets.cats.txt new file mode 100644 index 000000000..0dc908483 --- /dev/null +++ b/Asset Sources/blender_assets.cats.txt @@ -0,0 +1,10 @@ +# This is an Asset Catalog Definition file for Blender. +# +# Empty lines and lines starting with `#` will be ignored. +# The first non-ignored line should be the version indicator. +# Other lines are of the format "UUID:catalog/path/for/assets:simple catalog name" + +VERSION 1 + +6a165bb4-35a8-42c5-904e-cea20c966e8e:Aetheria:Aetheria +815f70ff-5f5c-4582-b612-a6ca360fcc59:Aetheria/PBR:Aetheria-PBR From 80842ba2fc362d1066e715a988a1b2e87bd7abcb Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 00:27:53 +0200 Subject: [PATCH 115/120] Carbon_Fibre: dielectric, metallic 0 (drop unused Metalness map) Co-Authored-By: Claude Opus 5.5 --- Asset Sources/PBR Materials.blend | 4 +- Asset Sources/blender_assets.cats.txt | 4 +- .../Carbon_Fibre_MetallicSmoothness.png | 4 +- .../Carbon_Fibre/Carbon_Fibre_Metalness.jpg | 3 - .../Carbon_Fibre_Metalness.jpg.meta | 104 ------------------ Assets/Materials/PBR/SOURCES.md | 2 +- 6 files changed, 7 insertions(+), 114 deletions(-) delete mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg delete mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg.meta diff --git a/Asset Sources/PBR Materials.blend b/Asset Sources/PBR Materials.blend index 445390b60..cbe7a475b 100644 --- a/Asset Sources/PBR Materials.blend +++ b/Asset Sources/PBR Materials.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f86c6a34ead2264030e7a54f276229e0d84d90f9c09db25fe31c1c086cc933c7 -size 356054 +oid sha256:7c88c4c5380c3553182a545e04ffcc0776a54f7cbde4fa80de95f5e569b931de +size 359499 diff --git a/Asset Sources/blender_assets.cats.txt b/Asset Sources/blender_assets.cats.txt index 0dc908483..7fc29fe08 100644 --- a/Asset Sources/blender_assets.cats.txt +++ b/Asset Sources/blender_assets.cats.txt @@ -6,5 +6,5 @@ VERSION 1 -6a165bb4-35a8-42c5-904e-cea20c966e8e:Aetheria:Aetheria -815f70ff-5f5c-4582-b612-a6ca360fcc59:Aetheria/PBR:Aetheria-PBR +2c70f9d2-3a69-4004-8f42-4712b3e6c439:Aetheria:Aetheria +a71be0dc-105e-4c66-8ff3-acb25522e9cd:Aetheria/PBR:Aetheria-PBR diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_MetallicSmoothness.png b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_MetallicSmoothness.png index 49cf03f2e..31131f1a7 100644 --- a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_MetallicSmoothness.png +++ b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_MetallicSmoothness.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a053fa899ea1950563d133010b55a52eee89be8efb3f703e270789a9ad7f75c2 -size 1944565 +oid sha256:fd50d0187fdf8c05eba5ef982da5217c6adf9f03720a2c8041ef4bebd5a2658d +size 1944562 diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg deleted file mode 100644 index afd0fe3a6..000000000 --- a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b65d6b9091747bd81ee58adedcb6209d8ee21b2bde9298d7216989ada85ef622 -size 16545 diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg.meta b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg.meta deleted file mode 100644 index 2089e25b6..000000000 --- a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg.meta +++ /dev/null @@ -1,104 +0,0 @@ -fileFormatVersion: 2 -guid: b4012db38dfe4d4b8ecea91919903a8f -TextureImporter: - internalIDToNameTable: [] - externalObjects: {} - serializedVersion: 13 - mipmaps: - mipMapMode: 0 - enableMipMap: 1 - sRGBTexture: 0 - linearTexture: 0 - fadeOut: 0 - borderMipMap: 0 - mipMapsPreserveCoverage: 0 - alphaTestReferenceValue: 0.5 - mipMapFadeDistanceStart: 1 - mipMapFadeDistanceEnd: 3 - bumpmap: - convertToNormalMap: 0 - externalNormalMap: 0 - heightScale: 0.25 - normalMapFilter: 0 - flipGreenChannel: 0 - isReadable: 0 - streamingMipmaps: 0 - streamingMipmapsPriority: 0 - vTOnly: 0 - ignoreMipmapLimit: 0 - grayScaleToAlpha: 0 - generateCubemap: 6 - cubemapConvolution: 0 - seamlessCubemap: 0 - textureFormat: 1 - maxTextureSize: 2048 - textureSettings: - serializedVersion: 2 - filterMode: 1 - aniso: 4 - mipBias: 0 - wrapU: 0 - wrapV: 0 - wrapW: 0 - nPOTScale: 1 - lightmap: 0 - compressionQuality: 50 - spriteMode: 0 - spriteExtrude: 1 - spriteMeshType: 1 - alignment: 0 - spritePivot: {x: 0.5, y: 0.5} - spritePixelsToUnits: 100 - spriteBorder: {x: 0, y: 0, z: 0, w: 0} - spriteGenerateFallbackPhysicsShape: 1 - alphaUsage: 1 - alphaIsTransparency: 0 - spriteTessellationDetail: -1 - textureType: 0 - textureShape: 1 - singleChannelComponent: 0 - flipbookRows: 1 - flipbookColumns: 1 - maxTextureSizeSet: 0 - compressionQualitySet: 0 - textureFormatSet: 0 - ignorePngGamma: 0 - applyGammaDecoding: 0 - swizzle: 50462976 - cookieLightType: 1 - platformSettings: - - serializedVersion: 4 - buildTarget: DefaultTexturePlatform - maxTextureSize: 2048 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - ignorePlatformSupport: 0 - androidETC2FallbackOverride: 0 - forceMaximumCompressionQuality_BC6H_BC7: 0 - spriteSheet: - serializedVersion: 2 - sprites: [] - outline: [] - customData: - physicsShape: [] - bones: [] - spriteID: - internalID: 0 - vertices: [] - indices: - edges: [] - weights: [] - secondaryTextures: [] - spriteCustomMetadata: - entries: [] - nameFileIdTable: {} - mipmapLimitGroupName: - pSDRemoveMatte: 0 - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Materials/PBR/SOURCES.md b/Assets/Materials/PBR/SOURCES.md index d0f2c96cb..baebd4022 100644 --- a/Assets/Materials/PBR/SOURCES.md +++ b/Assets/Materials/PBR/SOURCES.md @@ -15,7 +15,7 @@ All texture sets are from ambientCG (https://ambientcg.com), licensed CC0 1.0 Un | Tread_Diamond | DiamondPlate001 | https://ambientcg.com/view?id=DiamondPlate001 | ambientCG Metalness map | | Rubber | Rubber001 | https://ambientcg.com/view?id=Rubber001 | none in set: constant 0 | | Plastic_Matte | Plastic004 | https://ambientcg.com/view?id=Plastic004 | none in set: constant 0 | -| Carbon_Fibre | Fabric004 | https://ambientcg.com/view?id=Fabric004 | ambientCG Metalness map | +| Carbon_Fibre | Fabric004 | https://ambientcg.com/view?id=Fabric004 | set map is all 1.0; dielectric, so constant 0 | Files per material: `_Color`, `_Roughness`, `_NormalGL`, `_Metalness` and `_AO` (where the set has them), plus `_MetallicSmoothness.png` generated here for Unity (Metallic in R, Smoothness = 1 - Roughness in A). AO exists only for Paint_Worn_Light and Paint_Worn_Dark. From ce0023f1f8b47f8a60978702c35f2cc2f4e80c33 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 00:50:14 +0200 Subject: [PATCH 116/120] Research: prior art on hull decals, panel lines and wear layering Eyes pass before the hull shader fork: DECALmachine 2.17 export paths, forward-renderer decal practice, parallax for panel lines, layered wear on the built-in pipeline, and this project's rendering constraints. Co-Authored-By: Claude Opus 5.5 --- .../hull-decals-and-wear-prior-art.md | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 docs/research/hull-decals-and-wear-prior-art.md diff --git a/docs/research/hull-decals-and-wear-prior-art.md b/docs/research/hull-decals-and-wear-prior-art.md new file mode 100644 index 000000000..e8e62a16a --- /dev/null +++ b/docs/research/hull-decals-and-wear-prior-art.md @@ -0,0 +1,70 @@ +# Hull decals, panel lines and wear layering: prior art + +Eyes pass, 2026-10-10, before the hull shader design fork (operator: "I want tiling materials with global effects on top, plus decals ... Needs a new shader in unity to apply parallax decals for nice panel lines"). Related release-map follow-ups: hull-decals, hull-damage-wear. Facts only; tags mark FETCHED, SEARCH and KNOWLEDGE (unverified). + +# Hull decals and wear: prior-art facts (Eyes, 2026-10-10) + +Target (operator): tiling PBR materials + global per-ship wear/dirt masks + parallax panel-line decals, authored with DECALmachine 2.17 (Blender 5.2). +Tags: FETCHED = page read this session; SEARCH = search-result summary only; KNOWLEDGE = unverified recall; PROJECT = read from origin/master. +Placement: F:\Projects\Aetheria has no docs/research on origin/master, so this file sits in the scratchpad for Self to commit. + +## 5. Project / Unity constraints (PROJECT, origin/master; ProjectSettings are LFS pointers, read via `git lfs smudge`) +- Unity 6000.3.24f1 (ProjectVersion.txt). Built-in pipeline: GraphicsSettings m_CustomRenderPipeline {fileID: 0}. +- Colour space linear: ProjectSettings m_ActiveColorSpace: 1. +- GraphicsSettings: m_DefaultRenderingPath: 1, m_DefaultMobileRenderingPath: 1, tier settings (m_BuildTarget 1, Tier 2) renderingPath: 1, useHDR: 1, useDetailNormalMap: 1, standardShaderQuality 2. (Enum reading, KNOWLEDGE: 1 = Forward.) +- Per-camera override, counted in scenes/prefabs: m_RenderingPath: 1 on 21 cameras, -1 (use project setting) on 5, 3 (Deferred) on 4: Assets/Scenes/ARPG.unity:30582, FieldShieldTest.unity:1695, Main Menu.unity:1258, TestScene.unity:3781. Not checked: which camera ARPG:30582 is, or whether it is the gameplay camera. Effective path is forward for most cameras, deferred on those four. +- QualitySettings: single level "Amazing", pixelLightCount 4, shadows 0, shadowCascades 4, shadowDistance 150, anisotropicTextures 0, textureQuality 0. +- Graphics APIs set: Windows m_APIs 0200000012000000 (KNOWLEDGE decode: 2 = D3D11, 0x12 = Vulkan), Android 0b000000 (KNOWLEDGE: GLES3), iOS Metal; not decoded further. +- GlowFade (Assets/Shaders/GlowFade.shader, "Aetheria/GlowFade") is a CGPROGRAM surface shader: `#pragma surface surf Standard fullforwardshadows noambient vertex:vert`, `#pragma target 3.0`, Cull Off, RenderType Opaque, FallBack Diffuse. + - Samplers declared: _MainTex, _MetallicGlossMap, _OcclusionMap, _FadeTex, _BumpMap (5). All except _FadeTex sampled with uv_MainTex; _FadeTex uses uv_FadeTex. + - Includes "Dither Functions.cginc" and Volumetric.cginc (VolumeSampleColorSimple at vertex, ambientColor into Emission * 10). Sampler use inside Volumetric.cginc not inspected. + - Dither-clip fade via screenPos; multi_compile FLOW_GLOBAL / FLOW_SLOPE / NOISE_SLOPE; per-instance _Fade via UNITY_INSTANCING_BUFFER. Metallic = _Metallic * m.r, Smoothness = _Glossiness * m.a (packed metallic-smoothness). Normal = UnpackNormal(...) * _BumpScale. + - Input struct has no viewDir, worldPos, worldNormal or uv2; only uv_MainTex, uv_FadeTex, screenPos, ambientColor. +- docs/aetheria-release-map.md:358 lists `hull-decals` as a full-game follow-up ("Insignia and damage decals"); :1651 "With a mask texture, painted-in logos can live in the mask; only separate insignia and damage decals remain"; :2140 names `hull-damage-wear` as "the first real Ship parameter". No decal code or shader on origin/master (git grep decal over cs/shader/md hits only that doc). +- Assets/Materials/PBR holds 12 .mat folders (e.g. Carbon_Fibre, Gunmetal_Dark) each with Color, MetallicSmoothness, Normal(GL), Roughness maps. Shader used by those .mat files not determined (LFS pointer parse failed). +- Unity 6.5/6.6 manual banner (FETCHED, docs 6000.6 decals-birp page): Built-In RP "is deprecated and will be made obsolete in a future release", supported incl. bug fixes through the 6.7 LTS lifecycle. That page's only BiRP decal technique is the legacy Projector component. SEARCH: 6.3 LTS docs had no such banner. +- Sampler limits (SEARCH, Unity forum/issue tracker): D3D11 allows 16 active texture samplers per shader (issue tracker "maximum ps_4_0 sampler register index 16 exceeded"); surface shaders spend some internally (lightmaps, shadows); sampling the same texture twice reuses its sampler; a later Unity fix shares some internal samplers inline. No official Unity page stating the number found. Forum mentions GLES 3.0 as the min spec when more are needed. +- Unity Standard shader (FETCHED, StandardShaderMaterialParameterDetail): Secondary Maps = second Albedo and second Normal, usually tiled finer; Detail Mask limits where; "The Secondary normal map channel is more expensive than the primary one". Page does not name the UV set. SEARCH (old forum thread): only detail albedo and normal can use the secondary UV; occlusion cannot. +- Unity built-in has no decal system beyond Projector (FETCHED above). Deferred decals sample = Unity's "RenderingCommandBuffers" project (SEARCH, Unity manual GraphicsCommandBuffers older versions): "each decal is implemented as a box ... affects any geometry inside the box volume", "very similar to how lights are done in deferred shading, except instead of accumulating the lighting we modify the G-buffer textures". Command buffers attach via Camera.AddCommandBuffer at a CameraEvent; in deferred, depth is resolved at AfterGBuffer and shadows at BeforeLighting (SEARCH). The blog post body could not be fetched (404). + +## 1. DECALmachine export (machin3.io/DECALmachine/docs/...) +- Export intro (FETCHED, export_introduction): "DECALmachine 2.0 supports 3 approaches for exporting to Unity, Unreal Engine, etc.": Atlasing, Trim Sheets, Baking. +- Main page (FETCHED, machin3.io/DECALmachine): Atlases "for deferred rendering engines such as Unity HDRP and Unreal Engine"; Trim Sheets "anywhere, including forward renderers like Unity URP"; Baking = decals baked into the parent's UV space. +- Atlas (FETCHED, export_atlas): + - Output: texture set copied to a target folder plus `data.json` (each decal's position in the sheet). Atlas and trim sheet share the data format, so atlased decals appear as "Trims" in the file. Atlased decals get UVs remapped to atlas position; reversible. Channel packing is in the Atlas section of the Decal Export panel. Decal meshes export through the same panel or Blender's native exporter. + - Texture types named: Height, Opacity/masks (example packing: R = diffuse+emissive mask, G = normal mask, B = metallic+roughness mask, from Subset Occlusion / Alpha dropdowns), Base Color, Roughness/Metallic, Normal. + - Unity HDRP: deferred decals blending with the material underneath; Shader Graph decal shaders (Simple/Subset/Panel and Info) on GitHub; Amplify Shader Editor can build equivalents "and add parallax that matches Blender". Unity parallax: possible via ASE using the height map. + - Unity URP: forward, "decals are always opaque", no deferred projectors, trim sheets suggested; "a simple Lit shader with optional alpha mapping is possible, but matching materials is manual"; URP cannot reproduce HDRP material blending. + - Unreal 4.24: mesh decals (not decal actors) for performance; decal-domain material combining base colour/metal/rough/normal and the opacity map; per-channel opacity needs an engine patch; height map into a POM node on the UVs, Bump Offset as cheaper alternative. That section is by a third party. + - Caveats stated: no colour map is produced for normal-mapped decals or atlases (fake AO by blackening normal-only regions in base colour); UE 4.24 mesh decals lack AO. +- Trim sheet export (FETCHED, export_trim_sheet): models with trim detail export via native Blender export; textures via the Atlas export tools after adding one Trim Decal from the sheet and setting the sheet as its atlas; textures can be channel packed. No second-UV or parent-material description on the page. +- Bake (FETCHED, export_bake): "bakes decal textures onto the UV space of their parent objects"; works on objects with decals and at least one UV channel; one or several objects. Fetched page has an empty "Using Bake" heading and no texture list. The caveat "There is an open Blender issue/bug, that limits the usefulness of Baking Decals right now" is SEARCH (search summary of that page and the Superhive, Blender Market, Bazaar listings, which mark baking not recommended). The issue is not named; no source found saying it is fixed in 2.17. Listing: v2.17 supports Blender 4.3 to 5.2 (SEARCH). +- Material matching (FETCHED, match_material, shortcut DV): Simple, Subset, Panel decals, not Info. Source material must end in a Principled BSDF; nodes feeding it are ignored, "simple, parameter-based materials work best". Up to three material components per decal; Re-Apply and asset loaders match only Material and Material 2. Trim sheet materials matchable only if the sheet has no Color, Metallic or Roughness map. Blender-side only. +- Transfer UVs (FETCHED, transfer_uvs): creates a second UV channel on the decal mesh, copies the parent's UVs into it, copies the parent's image textures into the decal material (only UV-mapped image textures wired directly into a Principled BSDF; generated-coordinate parents use Init Generated Coords). Can use the parent's normal map and apply parallax to the transferred textures; works properly "only when the decal's and parent's UV spaces face the same way", which the page says is rarely the case. Page does not say whether this second UV channel is meant for export. +- Parallax (FETCHED, docs/assets): Simple, Subset, Panel decals (Panel inconsistent on the page) use parallax to offset UVs; view-angle dependent; depth into the surface works better than above; one depth level better than several; square decals better than elongated; mirrored decals need flipped UVs; Eevee flicker when a decal sits too close to the surface (fix: Displace modifier height or viewport clip start). Page does not name the height map or its channel. +- Decal types (FETCHED): Simple = normal + parallax, one material component; Subset = normal + parallax, two; Panel = normal, up to three; Info = colour. +- Not found: any documented export path of decal height into a forward hull shader; the atlas docs' only forward answer is URP trim sheets with manual material matching. + +## 2. Shipped-game practice +- Star Citizen (SEARCH, Polycount/Unreal forum threads; no primary talk found): community accounts of floating mesh decals blended over the base material; a secondary blend shader driven by vertex alpha for subtle wear in occluded areas (attributed to an SC developer post); POM on decals is hearsay; decals need an offset to avoid z-fighting; ships too large to bake from one high-poly. Whitepaper link on the Polycount thread not retrieved (polycount.com/discussion/comment/2585025 returned 403). Treat as unconfirmed. +- Unreal mesh decals (SEARCH, forum): POM works on decal actors but not mesh decals per one report; opacity tied to all channels so normal maps get affected; metalness/AO hard on mesh decals. +- Doom 2016 (SEARCH, DF interview, 80.lv, id Tech 6 wiki): opaque geometry and decals use clustered-forward shading; frustum grid of lights, probes and decals looked up in the forward pixel shader; 4000-decal in-view limit; depth prepass + equal test; decals are projected volumes in the cluster structure, not mesh decals. Doom Eternal course notes (advances.realtimerendering.com/s2020/RenderingDoomEternal.pdf, listed, not opened): fully forward, hybrid cluster+tile lists. +- Godot decal docs (SEARCH): a normal-only decal uses the albedo alpha to decide where the surface normal is overridden. Bevy clustered decals (SEARCH, commit): normal maps combined with Whiteout blending (SIGGRAPH 2007), chosen because it is commutative and associative unlike RNM. Both are projected decals, not mesh decals. +- Trim sheets vs decal sheets (SEARCH, Polycount forum answer): trim sheet = textures side by side in one bitmap; decal sheet = same layout overlaid on another texture via alpha edges or buffered rendering (deferred/stencil). Trim sheets suit repeated machinery/pipes. +- Insomniac/Ratchet trim-sheet talks, Halo, Destiny, Titanfall mesh-decal talks: not found (searches returned a members-only GDC lighting talk and a fan "ultimate trim" article on 80.lv). Polycount wiki decals / trim sheet pages: not retrieved. Destiny/Halo/Titanfall technique details: KNOWLEDGE only, unverified, not stated here. + +## 3. Parallax techniques +- SEARCH (Wikipedia, Tatarchuk SI3D06 PDF advances.realtimerendering.com/s2006/Tatarchuk-POM.pdf, listed not opened): POM first published 2005 (Brawley and Tatarchuk, ShaderX3); ray-marches a heightfield per fragment; "substantial GPU time", fragment-bound; changes apparent surface not the silhouette, so a flat outline shows at glancing angles; earlier methods aliased at grazing angles, Tatarchuk's adds adaptive sampling and refined intersection; prism POM (Dachsbacher and Tatarchuk 2007) addresses silhouettes; sensitive to fragment-vs-face normal variation; multi-pass use degrades from height mismatch. Step counts not surfaced. +- KNOWLEDGE (unverified): simple parallax mapping with offset limiting costs one extra height sample and a view-vector offset; steep/relief/POM variants take N linear steps (commonly 8-32) plus refinement; all need a tangent-space view vector; offset UVs can leave a decal's alpha/UV island at its edge. +- Alternatives named in sources: normal-map-only lines (Godot normal-only decal; DECALmachine Panel/Simple decals are normal-mapped); Unreal Bump Offset as cheaper than POM (FETCHED, DECALmachine atlas page); real geometry insets ("limited use of actual geometry" for silhouettes, SEARCH). + +## 4. Layering tiling material + global wear masks in built-in +- Unity Standard detail maps and detail mask: see section 5. +- Surface-shader extension pattern: GlowFade (PROJECT) shows surf function, extra samplers, Input struct fields. KNOWLEDGE (unverified): `uv2_Name` in Input reads UV1; `viewDir` and `worldPos` are available Input members. +- Height-blended material layers, per-ship curvature/AO mask packed RGB on UV1: no source consulted this session. KNOWLEDGE (unverified): mask-driven lerp of albedo/smoothness/normal with optional height-based contrast; mask on a unique non-overlapping UV channel because tiling UV0 repeats. +- Hand-retopologised hulls with deliberate seams: no source consulted on seam handling for tiling materials or triplanar alternatives. + +## Gaps for Self +- Which camera runs deferred in ARPG.unity:30582 and whether gameplay uses it. +- Shader assigned to the 12 PBR .mat files; sampler use in Volumetric.cginc. +- Primary sources not opened: Tatarchuk PDF, Doom Eternal PDF, Unity RenderingCommandBuffers project, SC whitepaper, identity of the DECALmachine Blender bug. From 8755da5ecb5f08f201a4c8c388f5e1bdb17c1e1c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 05:12:27 +0200 Subject: [PATCH 117/120] Release map: articulated mounts re-mapped to the hardpoint-arc ruling Imagination r2 re-map (session self-2026-10-10-ag): the sim already carries per-hardpoint arc and weapon traverse; links are rig only. Target doc moves articulated weapon mounts into scope under ruling mount-arc-gun-traverse. Co-Authored-By: Claude Opus 5.5 --- docs/aetheria-release-map.md | 253 +++++++++++++++++++++++--------- docs/aetheria-release-target.md | 4 +- 2 files changed, 183 insertions(+), 74 deletions(-) diff --git a/docs/aetheria-release-map.md b/docs/aetheria-release-map.md index fe79044d6..08528d188 100644 --- a/docs/aetheria-release-map.md +++ b/docs/aetheria-release-map.md @@ -7332,14 +7332,19 @@ ships-authoring-tool r2, ships-player r3. New: cut player-build r1, follow-up ## Articulated mounts Mapped 2026-10-09 by Imagination (`imagination-aeth-articulated`, session `self-2026-10-09-ag`) for follow-up -`articulated-mounts`, against `origin/master` `6d83597b`. Rulings: `arc-and-traverse-on-the-link`, -`rig-is-presentation`, `sim-hooks-presentation-agnostic`, `projectile-flight-is-presentation`, -`ship-roles-are-tags`, `thrusters-radiators-are-meshes`; follow-up `articulated-mount-traverse-min`. -Anchors are exact at `6d83597b`. +`articulated-mounts`, against `origin/master` `6d83597b`. Re-mapped 2026-10-10 by Imagination +(`imagination-aeth-articulated-r2`, session `self-2026-10-10-ag`) against `origin/master` `acea1aa7`, after the +operator's rulings `mount-arc-gun-traverse` (arc on the hardpoint, traverse on the weapon, the link presentation +only; it supersedes `arc-on-the-hardpoint`, which superseded `arc-and-traverse-on-the-link`) and +`solver-in-gamecult-animation`. Rulings in force: `mount-arc-gun-traverse`, `rig-is-presentation`, +`solver-in-gamecult-animation`, `sim-hooks-presentation-agnostic`, `projectile-flight-is-presentation`, +`ship-roles-are-tags`, `thrusters-radiators-are-meshes`. AM1-AM11 were probed at `6d83597b`; AM12-AM17 at +`acea1aa7`. Every anchor in the specs is exact at `acea1aa7`. Where AM1-AM11 speak of links, read them as the +2026-10-09 model; AM12-AM17 and everything after them are the live model. **Scope tension for Self.** Target r4's `not_in_scope` lists "articulated mounts". The operator asked for this work -now (her hermit-crab hull carries its big gun on a jointed arm). The cuts are admitted; the target needs a revision -that takes articulated mounts out of `not_in_scope` before Hands is briefed. +(her hermit-crab hull carries its big gun on a jointed arm). The target needs a revision that takes articulated +mounts out of `not_in_scope` before Hands is briefed. ### Body facts @@ -7427,92 +7432,194 @@ form of CultMath's existing `BoundedLeastSquares`. Every engine Eyes checked (Un Unreal Control Rig) keeps IK in the animation layer; none was found in a math library. Aim-an-axis leaves twist about the aim free, so a chain aims with two effective degrees of freedom. +**AM12. The sim already matches ruling `mount-arc-gun-traverse`; no sim cut is owed.** Probed at `acea1aa7`. +Arc: `HardpointData.FiringArc` (`ItemData.cs:567-569`, key 6) is the per-hardpoint, per-hull arc, and +`FireControl.ArcFor` (`FireControl.cs:34-41`) reads it, falling back to `GameplaySettings.FiringArc` (120) at 0. +Traverse: `WeaponData.Tracking` (`Behaviors/Weapon.cs:70-73`, key 33, degrees per second, +infinity when +unauthored; ruling `weapon-tracking-authored`) is a weapon stat, and `FireControl.TrackingRate` +(`FireControl.cs:239-245`) is that rate times the ship's targeting gear. Nothing reads a link, and no `LinkData`, +`HullLinks` or `HardpointData.Link` exists (`git grep` over Assets, tests and tools: 0). The only stale text is the +comment at `FireControl.cs:133-134`, which says traverse belongs to the link and cites the superseded ruling. +`ShipAuthoring.cs`, `ShipModCatalog.cs`, `ShipModPlan.cs`, `ItemData.cs`, `ShipModVisual.cs`, `ShipModShips.cs`, +`EntityInstance.cs`, `CultCacheDrawers.cs`, `tools/blender`, `docs/moddable-ship-authoring.md` and the ship tests +are byte-identical between `6d83597b` and `acea1aa7` (`git diff --stat`); `FireControl.cs` gained 9 lines, all +below line 755. + +**AM13. Shipped traverse falls with calibre already.** Probe `probe_track.py` (scratchpad +`imagination-aeth-articulated-r2`, cultcache-py over `GameData/Aetheria.cc` at `acea1aa7`), `WeaponData.Tracking` +per weapon item: Small guns 1.5 to 40 degrees per second (ClearPath 12, Earp 8, Spectra 10, ColdFire 6, the two +point-defence guns 40, RainbowLite 1.5); Medium guns 3 to 20 (Autocannon 4, ChargeBlast 3 and 3.5, Flak 20); the +one Large gun (plight) 2; launchers and the mine launcher +infinity. Twenty-two weapon items in all. + +**AM14. Shipped hardpoints and arcs.** Probe `probe_arcs.py` (same scratchpad): the catalog holds four hulls. Only +the Turret hull authors an arc: its two Ballistic hardpoints, 8 cells each (2x4, the ExtraLarge band), at 360. Ship +hulls carry weapon hardpoints of 2 cells (Energy, Ballistic) and 3 or 6 cells (Launcher), all at `FiringArc` 0, so +the default 120 applies. Cut `caliber-bands` (unlanded) states the bands Small 1-2 cells, Medium 3-4, Large 5-6, +ExtraLarge 7-9 only in its test (AM9). + +**AM15. What CultMath has for the rig today.** `F:\Projects\CultLib\packages\cultmath\src\CultMath`, read +2026-10-10: `quaternion` (`quaternion.cs`) is a plain struct with `identity`, `LookRotation(forward, up)` and float4 +conversions; `math.normalize(quaternion)` (`math.cs:225-235`) is the only quaternion operation. There is no +quaternion product, conjugate, axis-angle constructor, vector rotation by a quaternion, slerp, swing-twist split or +joint clamp. `BoundedLeastSquares.Solve(m, n, a, b, lo, hi, x, workspace, out iterations, ...)` minimises +`||A x - b||^2` in a box, dense and allocation-free, with `WorkspaceLength(n)`. That is the solve AM11 names. + +**AM16. A moving part needs no record of its own.** The joint tree (AM6: bones export as ordinary nodes with their +data-bone extras) already says which moving part a weapon mount rides: the nearest joint above the mount's node. +A tag on that bone adds nothing the hierarchy does not say, and the arc and traverse the link record carried now +live on the hardpoint and the weapon (AM12). + +**AM17. The add-on chain under these cuts is unlanded.** Specs `ships-addon-frame` r5, `ships-addon-mounts` r5 and +`ships-addon-mesh-mounts` r1 have no report; `tools/blender` at `acea1aa7` is the pre-r5 add-on (`_save_layout` +absent; Save Layout writes `FiringArc` from the panel's `firing_arc`, `__init__.py:43`, `:111`, `:174`, `:458`). +The add-on specs below are anchored at `acea1aa7` and name the region to re-find once that chain merges. + ### Model page | Kind | Identity | Lifecycle | Authority | |---|---|---|---| -| Link (`LinkData` in `HullData.Links`, key 33) | `Id`: the `aetheria.id` that Tag Link writes on the data bone or object (ruling `ship-roles-are-tags`). Renaming never changes it. For a shipped hull, the id the migration writes. | Edited in Blender's layout buffer and written by Save or Package. A package edited after a save changes the arc or traverse a run's guns use on Continue. No save names a link, because the sim holds no slew state (AM2). If a link is removed while a hardpoint names it, the judge fails the package, compose excludes it, and Continue refuses the run, naming the missing design (`RunSave.RequireDesigns`). It adds nothing to `mod-hull-changed-under-run`, because a link change moves no cell. | The author, through the add-on, for arc and traverse. `HullLinks.Validate` judges it. `FireControl` reads it, only through `ArcFor` and `TrackingRate`. | -| Hardpoint on a link (`HardpointData.Link`, key 7) | The hardpoint's mount id (`Transform`) plus the link id. Empty means a fixed mount: the default arc and the gun's own traverse. | Derived at Package from the mount object's ancestry (the nearest ancestor tagged link, through a bone parent), as mounted `Position` is derived from the object. A mount re-parented in Blender changes link at the next Package. For `ambiguous-hardpoint-origin`, a link is one more way two hardpoints at one origin differ (arc, as rotation already does), so it neither fixes nor worsens that defect. The fix there is to save the hardpoint's identity: C4's element id on `HardpointData`. | The mount's place in the Blender hierarchy; Package derives it. The panel and Studio show it read-only for a mounted row. The judge refuses a name that is not a link of the hull. | -| Rig chain (`ShipJoint` in `ShipAuthoring.Joints`, key 5, and the `link` anchors) | Each joint's `Id` is its bone's `aetheria.id`, assigned once by Package (`joint.`) and kept on the data bone. A link anchor's id is its link's id. | Re-rigging changes only the visual record, and every Package and compose judges it again. No save, sim fact or catalog record names a joint. The Unity rig components are cache-only and rebuilt each boot with the prototype. | The author's Blender bone IK settings (lock, limit, min, max), copied by Package. The judge checks the tree and that the yaw reach covers the link's arc. Unity reads and drives it. No joint feeds a sim fact (ruling `rig-is-presentation`). | +| Hardpoint arc (`HardpointData.FiringArc`, key 6, unchanged) | The hardpoint's mount id (`Transform`) on its hull. 0 is a fixed mount at `GameplaySettings.FiringArc`. | Authored per hull in the add-on's panel (or Studio for a shipped hull) and written by Save or Package. A package edited after a save changes the arc a run's guns use on Continue; it moves no cell, so it adds nothing to `mod-hull-changed-under-run`. | The ship author. `ShipAuthoringStore.Validate` judges it finite and non-negative and, for a mount on a joint, positive and within the joint chain's yaw reach. `FireControl.ArcFor` is its one reader. | +| Traverse (`WeaponData.Tracking`, key 33, unchanged) | A stat of the weapon item. | Authored per weapon design; any gun goes on any mount (ruling `mount-arc-gun-traverse`). | The weapon designer. `FireControl.TrackingRate` prices it; the rig presenter slews the picture at the same rate. | +| Joint (`ShipJoint` in `ShipAuthoring.Joints`, key 5) | Its bone's `aetheria.id`, which is its GLB node id. Package assigns `joint.` once to a data bone on a weapon mount's chain and keeps it there. | Re-rigging changes only the visual record; every Package and compose judges it again. No save, sim fact or catalog record names a joint. | The author's Blender bone IK settings (lock, limit, min, max), copied by Package. Validate judges the tree and the reach; Bind judges it against the GLB. No joint feeds a sim fact (ruling `rig-is-presentation`). | +| A mount's joint (`ShipAnchor.Joint`, key 5) | On a `weapon-mount` anchor only: the nearest joint above the mount's node; empty for a fixed mount. | Derived at Package from the mount's bone ancestry, as mounted `Position` is derived from the object. Re-parenting in Blender changes it at the next Package. | The Blender hierarchy; Package derives it; Bind refuses a GLB whose hierarchy disagrees. No panel or Studio edit writes it. | +| Rig (the Unity `MountRig`, one per driven joint) | The driven joint's id. | Cache-only: built with the prototype at boot from `ShipModPlan.Rigs` and the rest pose. | Writes only its joints' Transforms. Reads the lead weapon's `GunSolution.Direction` and `TrackingRate`. | -**C4 (follow-up `c4-ship-element-ids`).** Links are a fourth element kind for variants: `LinkData` rows keyed by -`Id`, as hardpoints are keyed by `Transform`. A heavy variant that narrows a link's arc is a tail-slot patch by link -id. `ship_cc` carries link rows' unknown slots by id, as it does for hardpoints and anchors; `ships-addon-links` -lands that. +A driven joint is a joint some weapon-mount anchor names. Its chain is that joint and its ancestors up to, not +including, the nearest ancestor that is itself driven, so a turret on a turret solves as two chains and the outer +chain never moves under the inner gun's solve. + +**C4 (follow-up `c4-ship-element-ids`).** No link element kind exists for variants: a variant that narrows a gun's +arc is a hardpoint tail-slot patch on `FiringArc`, by mount id, which C4 already plans. ### Authority map -- **Owner.** `LinkData`, authored per hull, owns arc width and traverse speed. The Blender hierarchy owns whether a - hardpoint is on a link, derived into `HardpointData.Link` at Package. The Blender bone IK settings own the joint - limits, packaged into `ShipAuthoring.Joints`. -- **Sim inputs.** `HullData.Links` and `HardpointData.Link`, nothing else. -- **Outputs.** `ArcFor`, `InArc`, `AimDirection` and `TrackingRate`, signatures unchanged. Presentation reads - `GunSolution.Direction` of a link's lead weapon and the link's traverse. -- **Derived.** `HardpointData.Link`, from the hierarchy. A fixed mount's arc, which is `GameplaySettings.FiringArc`. - An object link's joint (one yaw degree of freedom, plus or minus half the arc), at Package. The Unity link rigs - (cache-only). -- **Demoted, then cut.** `HardpointData.FiringArc` is no longer an owner; the link is. `links-record` refuses a - hardpoint that names a link while its `FiringArc` is set. `links-arc` stops reading it and refuses it on packages. - `retire-firing-arc` deletes it and retires key 6. `link-rig-record` replaces the `articulation` role with `link`. -- **Forbidden writers.** No joint limit, bone length, IK result or Unity transform feeds `ArcFor` or - `TrackingRate`. No panel or Studio edit writes a mounted row's `Link`. No helper invents an arc or a traverse. +- **Owner.** The hardpoint owns its arc (`FiringArc`), per hull. The weapon owns its traverse (`Tracking`). The + Blender hierarchy owns which joint a mount rides, derived into `ShipAnchor.Joint` at Package. The bone IK settings + own the joint limits, packaged into `ShipAuthoring.Joints`. FireControl owns the aim (`GunSolution.Direction`). +- **Sim inputs.** `HardpointData.FiringArc` and `WeaponData.Tracking`, as on `acea1aa7`. Nothing the rig adds. +- **Outputs.** `ArcFor`, `InArc`, `AimDirection`, `Solution` and `TrackingRate`, unchanged. Presentation reads the + lead weapon's `GunSolution.Direction` and `TrackingRate`. +- **Derived.** `ShipAnchor.Joint`, from the hierarchy. A driven joint's chain, from the joint tree. Yaw reach, from + the chain's degrees of freedom. The rigs and their `AimChain`s (cache-only). +- **No longer owners.** The link is not a record: `LinkData`, `HullData.Links`, `HardpointData.Link`, the Tag Link + operator and link rows are not built (cuts `links-record`, `links-arc`, `ships-addon-link-rows` withdrawn). + `FiringArc` is not retired (cut `retire-firing-arc` withdrawn). The `articulation` role is cut; a joint is not an + anchor. +- **Forbidden writers.** No joint limit, bone length, IK result or Unity transform feeds `ArcFor`, `TrackingRate` + or any sim fact. No panel or Studio edit writes `ShipAnchor.Joint`. No helper invents an arc. The add-on keeps no + copy of `GameplaySettings.FiringArc`. +- **Shared paths.** Validate's reach check and `ShipModPlan.Rigs` define a chain through one helper. Package and + the gizmo drag derive a mount's place through the same hull_grid functions. ### Rationale -**Links are flat in `HullData`; chains live in the visual record.** The sim reads only the link a hardpoint names -(ruling `rig-is-presentation`). A turret on a turret is a chain of joints, and a chain is presentation, so `LinkData` -has no parent: its arc is the whole coverage the author grants that part. Parenting lives in `ShipJoint.Parent`. A -link's chain runs from its joint up to the nearest ancestor that is a link, or to the root, and parent links solve -first. +**A moving part is a joint, not a record.** The operator ruled the link carries no sim stat (AM12). What remained +of it was a tag and a row holding nothing but an id. The joint tree already says which part a mount rides (AM16), +so the record keeps only what the presenter and the judge need: the joints with their limits, and on each weapon +mount the joint it rides. Moving parts come from armature bones only. A turret built from plain objects is a +one-bone armature; there is no object-link path, so joints have one source. -**One facing per link.** Weapon hardpoints on one link must share a `Rotation`. `MountDirection` stays derived from -the item's rotation (`FireControl.cs:28-33`), the link needs no facing field, and the presenter's lead weapon aims -for every gun on the part. +**A bone with no free axis is not a joint.** Package turns a chain bone into a joint only when its IK settings +leave at least one degree of freedom. A gun on a fully locked bone has no joint above it and is a fixed mount at +its own arc, the default included. -**Why `FiringArc` is retired rather than kept for fixed mounts.** Two places that state an arc are two owners. A -fixed mount is the degenerate case: no link, the default arc, the gun's own traverse. Any other arc is a link, -including the shipped Turret hull's, which migrates to one link with arc 360 and traverse +infinity, so its behaviour -is unchanged. The subtraction is its own cut and lands after the add-on cuts that edit the same panel, so no open -cut's hunks are stranded. +**A gun on a joint states its arc.** The judge checks the chain's yaw reach against the arc, and the judge has no +`GameplaySettings` (the default is a game setting, and the add-on keeps no copy of it either). So a weapon whose +mount rides a joint must author `FiringArc` above 0. Weapons on one joint share one `Rotation` and one `FiringArc`, +because the lead weapon's aim moves them all. -**Traverse +infinity means no mechanical limit.** It matches `Weapon.Tracking`'s convention ("an unauthored gun is -+infinity", `FireControl.cs:238-245`), so `min` needs no special case. The add-on shows it as a Free traverse -toggle; zero and NaN are refused. +**The reach check is yaw only, and conservative** (unchanged from 2026-10-09). The sim is planar. A chain covers the +arc when the summed ranges of its degrees of freedom whose axis lies within 5 degrees of Ship Root up reach the +arc, capped at 360. Serial parallel axes add, so the sum is exact for the arms the add-on makes. A chain whose yaw +comes only from tilted axes is refused with a message naming the joint. -**Joint limits come from Blender's bone IK settings, not from Limit Rotation constraints.** Blender's own IK solver -honours `lock_ik_*`, `use_ik_limit_*` and `ik_min_*`/`ik_max_*`, so an author previewing with an IK constraint sees -the limits the game uses. The semantics follow Blender's: an axis is free unless locked or limited. Package writes -each degree of freedom as a unit axis in Ship Root space at rest, converted to glTF axes, with a min and max in -degrees. This keeps the record independent of the exporter's bone-axis correction, which this map did not verify. +**Joint limits come from Blender's bone IK settings, not from Limit Rotation constraints** (unchanged). Blender's +own IK honours `lock_ik_*`, `use_ik_limit_*` and `ik_min_*`/`ik_max_*`; glTF carries no constraint (AM6). -**The reach check is yaw only, and conservative.** The sim is planar. A link's chain covers its arc when the summed -ranges of the chain's degrees of freedom whose axis lies within 5 degrees of Ship Root up reach the arc (an arc of -360 needs a summed 360). Serial joints about parallel axes add, so the sum is exact for the arms the add-on makes. A -chain whose yaw comes only from tilted axes is refused with a message naming the link. That is the price of a check -the judge can state in one line. +**The presenter slews at the weapon's traverse.** It turns its aim toward the lead weapon's +`GunSolution.Direction` (already arc-clamped by the sim) at most `FireControl.TrackingRate(lead, entity)` degrees +per second, the rate the sim prices the angular term with. +infinity is no slew. The sim stays stateless. -**The solver is behind a port.** Aetheria's presenter needs only this: a chain (joints root-first, their rest -offsets, their degrees of freedom with limits), an effector (the muzzle offset and its local aim axis), the current -angles and a target direction; it gets back new angles. `link-rig-presenter` codes against that port. Question -`solver-owner` decides who implements it. +**No Aetheria-side solver port.** Ruling `solver-in-gamecult-animation` puts the solve in a pure CultLib package. +The presenter calls that package's API; an Aetheria interface with one implementation would be a second surface +buying nothing a pure, headless-testable library does not already give. -**Thrusters on links stay a follow-up.** A gimballed thruster changes where thrust points. That is a sim fact, and -the thrust allocator (cuts `thrust-allocator-core`, `thrust-allocator` and `envelope-from-columns`, all in flight) -reads thrusters as fixed columns. Putting thrusters on links now would reopen those cuts. The judge refuses a -non-weapon hardpoint on a link and names follow-up `gimballed-thrusters`. +**Calibre and arc.** The operator's own examples put a heavy gun on a wide arc (the crab's claw) and a light gun in +a narrow port, and her ruling says calibre caps are enforced "if at all". The shipped Turret's ExtraLarge +hardpoints sweep 360 (AM14), and the weapon's traverse already falls with calibre (AM13), which prices a heavy gun +on a wide arc through `TrackingRate`. Question `hardpoint-arc-caps` puts the choice to her with no-cap recommended. +Under no-cap, follow-up `caliber-bands-one-table` is moot: the band table stays in `caliber-bands`' test, its only +reader. Under either cap option the judge reads the bands, so they become one table in ServerShared and that test +reads it. -**Cut order.** `links-record` (types and judge), then `links-arc` (FireControl reads the link; the Turret hull and -the test fixtures migrate), carry the sim. `link-rig-record` adds the joint record and the GLB checks. -`ships-addon-links` lands the Blender side after `ships-addon-mounts`. Mounts r5 needs no revision: it writes no -arc, and `ships-addon-links` extends its derivation. `ships-addon-gizmos` r4 replaces r3. `link-rig-presenter` -waits on `solver-owner`, `link-calibre-judge` waits on `calibre-arc-caps`, and `retire-firing-arc` goes last. +**Thrusters on joints stay a follow-up.** A gimballed thruster changes where thrust points, which is a sim fact the +thrust allocator does not model. Validate refuses `ShipAnchor.Joint` on any non-weapon anchor and Bind refuses a +thruster or radiator node under a joint, both naming follow-up `gimballed-thrusters`. + +### The rig's interface to GameCult.Animation and CultMath + +This is what `link-rig-presenter` calls; a later cultlib-gaps pass maps the cuts that provide it. Types and +operations only. Angles are radians at this API (the record's degrees convert once, when the presenter builds its +chain). + +CultMath (`quaternion`, `float3`; pure and allocation-free): +- `math.mul(quaternion a, quaternion b) -> quaternion`, composing in the Unity.Mathematics convention (`mul(a, b)` + applies `b` first). +- `math.rotate(quaternion q, float3 v) -> float3`. +- `math.conjugate(quaternion q) -> quaternion` and `math.inverse(quaternion q) -> quaternion`. +- `quaternion.AxisAngle(float3 unitAxis, float radians) -> quaternion`. +- `math.slerp(quaternion a, quaternion b, float t) -> quaternion` (shortest arc). +- Swing-twist and joint clamps, which the ruling also gives CultMath. The presenter does not call them (its degrees + of freedom are per-axis boxes); GameCult.Animation may: `math.swing_twist(quaternion q, float3 unitTwistAxis, out + quaternion swing, out quaternion twist)`, `math.clamp_hinge(quaternion q, float3 unitAxis, float min, float max) + -> quaternion`, `math.clamp_cone(quaternion swing, float3 unitAxis, float maxAngle) -> quaternion`, + `math.clamp_twist(quaternion twist, float3 unitAxis, float min, float max) -> quaternion`. + +GameCult.Animation (a new CultLib package, netstandard2.1, depending only on CultMath, and published as a UPM package +`org.gamecult.animation` beside `org.gamecult.cultmath` so Aetheria's `Packages/manifest.json` pins it the same way): +- `AimJoint { float3 RestPosition; quaternion RestRotation; }`: rest offset and rotation relative to the previous + joint (the first relative to the chain root's frame). +- `AimDof { int Joint; float3 Axis; float Min; float Max; }`: a unit axis in that joint's local frame and its bounds. +- `AimEffector { float3 Offset; float3 Axis; }`: the muzzle's offset and unit aim axis in the last joint's frame. +- `AimChain.Create(ReadOnlySpan joints, ReadOnlySpan dofs, AimEffector effector) -> AimChain`: + validates (finite values, unit axes within a tolerance, `Min <= Max`, `Joint` ascending and in range) and throws + `ArgumentException` naming the field, never the value. Exposes `JointCount` and `DofCount`. +- `AimChain.WorkspaceLength(in AimChain chain) -> int`. +- `AimChainSolver.Solve(in AimChain chain, ReadOnlySpan current, float3 targetDirection, Span next, + Span workspace) -> AimSolveStatus` (`Converged`, `IterationCap`, `InvalidInput`): turns the effector's aim + axis toward `targetDirection` (chain root frame) by damped least squares over `BoundedLeastSquares`, every angle + in `next` inside its `[Min, Max]`, twist about the aim left free; deterministic, allocation-free, warm-started from + `current`. +- `AimChain.Pose(in AimChain chain, ReadOnlySpan angles, Span localRotations)`: each joint's + local rotation, its rest rotation times the product of its degrees of freedom's axis-angle rotations in DOF order. +- `AimChain.EffectorDirection(in AimChain chain, ReadOnlySpan angles) -> float3`: the aim axis in the chain + root's frame (forward kinematics), for tests and the presenter's settled check. + +Aetheria keeps the glTF-to-Unity axis conversion (`JointAxes.ToLocal`, ServerShared, pure), the slew (planar angle +arithmetic over the existing `math.atan2` and `math.rotate(float2, radians)`), and the `UnityEngine.Quaternion` +conversion at the Transform write. + +### Cut order + +1. `link-rig-record` r3, no dependency: the joint record, the judge's rig rules, Bind's GLB checks, + `ShipModPlan.Rigs`, and the stale FireControl comment. **First.** +2. `ships-addon-gizmos` r6 after `ships-addon-mesh-mounts`: arc fans from the row's `FiringArc`. +3. `ships-addon-link-package` r3 after `link-rig-record`, `ships-addon-mounts` and `ships-addon-mesh-mounts`: + Package writes the joints and each weapon mount's joint. +4. `link-rig-presenter` r2 after `link-rig-record`, `ships-addon-link-package`, and the CultLib release carrying the + interface above (cultlib-gaps), pinned. +5. `link-calibre-judge` r2: blocked on question `hardpoint-arc-caps`; withdrawn if she rules no-cap. + +Withdrawn: `links-record` r1, `links-arc` r1, `retire-firing-arc` r1, `ships-addon-link-rows` r1, and question +`calibre-arc-caps`. ### Admitted -Receipts `mind-commit-b0c6b373...`, `mind-commit-c00a16b5...`, `mind-commit-08fce00f...` and `mind-commit-06e09a45...` -(2026-10-09 21:18-21:24 UTC). New cuts: links-record r1, links-arc r1, link-rig-record r2 (r1 superseded for one -out-of-range anchor), ships-addon-link-rows r1, ships-addon-link-package r1, link-rig-presenter r1, link-calibre-judge -r1, retire-firing-arc r1. Revised: ships-addon-gizmos r5 (r3 superseded by r4 and r4 by r5 for an untested negative -pattern). ships-addon-mounts r5 is unchanged. Questions: `solver-owner` (raised in link-rig-presenter) and -`calibre-arc-caps` (raised in link-calibre-judge). Follow-ups: `gimballed-thrusters`, `caliber-bands-one-table`. -context-pack read every anchor SAME at `6d83597b`. +2026-10-09 (`imagination-aeth-articulated`): receipts `mind-commit-b0c6b373...`, `mind-commit-c00a16b5...`, +`mind-commit-08fce00f...`, `mind-commit-06e09a45...`. + +2026-10-10 (`imagination-aeth-articulated-r2`): receipts `mind-commit-0b2e932e...` (withdrew links-record r1, links-arc r1, +retire-firing-arc r1, ships-addon-link-rows r1; link-rig-record r3; ships-addon-gizmos r6), +`mind-commit-0c23fa01...` (ships-addon-link-package r2, link-rig-presenter r2, link-calibre-judge r2, question +`hardpoint-arc-caps`, withdrew question `calibre-arc-caps`) and `mind-commit-40788d58...` (ships-addon-link-package r3, +r2's arm smoke pins fixed). context-pack read every anchor SAME at `acea1aa7`. diff --git a/docs/aetheria-release-target.md b/docs/aetheria-release-target.md index 9f8a6b53a..402a6fe88 100644 --- a/docs/aetheria-release-target.md +++ b/docs/aetheria-release-target.md @@ -146,7 +146,9 @@ The typed target carries these as labelled statements. The reasons: (ruling `targeting-sublinear-wanted-not-critical`), and belt rendering through the index. - The faction-play tender loop (`faction-play-4`), drones and munitions, the - Colosseum, articulated mounts. + Colosseum, gimballed thrusters. Articulated weapon mounts are in scope: the arc + is the hardpoint's, traverse is the weapon's, and the moving part is + presentation only (ruling `mount-arc-gun-traverse`). - Narrative: storylets, Ghostlight, Ink beats per boss. - Volumetrics and the nebula backport. - Asura and procedural planet content. From b623d519f77c30c21c45f7e915c0eaad422b42b2 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 05:26:51 +0200 Subject: [PATCH 118/120] ThrottleLock tests: pin the exact tick the lock ends, as a function of Duration Replaces TheLockHoldsThroughMostOfItsDurationAndEndsAfter with a theory over Duration 1 and 2 at 0.25 s ticks. Builds. Co-Authored-By: Claude Opus 5.5 --- .../ThrottleLockTests.cs | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs b/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs index 032e74d0f..3f6109155 100644 --- a/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs +++ b/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs @@ -246,26 +246,29 @@ public sealed class ThrottleLockTests : IDisposable Assert.Equal(0f, f.Forward.Axis); } - // Finding lock-end-boundary-unpinned. Mutation: the lock ends at half Duration. Stepped in quarter-second - // ticks over a 1 s Duration, the lock holds through 0.75 s and is gone by 2 s. The exact tick of expiry is - // consumable-host's rule and is not pinned here. - [Fact] - public void TheLockHoldsThroughMostOfItsDurationAndEndsAfter() + // Finding lock-end-boundary-unpinned, finding lock-end-at-90pct-unpinned. Mutations: the lock ends at half + // Duration, at 0.8 or 0.9 of Duration, or one tick late. Duration 1 stepped in 0.25 s ticks (exact in + // floats) locks the axes on exactly four ticks (reverse stays 0), the effect is gone when the fourth ends, + // and the fifth tick follows the stick. The count scales with Duration: 2 s locks eight ticks. + [Theory] + [InlineData(1f, 4)] + [InlineData(2f, 8)] + public void TheLockEndsAtTheExactTickOfItsDuration(float duration, int lockedTicks) { - var f = Build(1f); + var f = Build(duration); using var _ = f.Cache; f.ActivateLock(); f.Ship.MovementDirection = float2(0, -1); - for (var i = 0; i < 3; i++) + for (var i = 0; i < lockedTicks; i++) { - f.Ship.Update(.25f); Assert.True(f.Ship.ThrottleLocked); + f.Ship.Update(.25f); + Assert.Equal(0f, f.Reverse.Axis); + Assert.Equal(1f, f.Forward.Axis); } - f.Ship.Update(.25f); - Assert.Equal(0f, f.Reverse.Axis); - for (var i = 0; i < 4; i++) f.Ship.Update(.25f); Assert.False(f.Ship.ThrottleLocked); f.Ship.Update(.25f); Assert.Equal(1f, f.Reverse.Axis); + Assert.Equal(0f, f.Forward.Axis); } } From 11a7843c61856c76cd215a5aa986f70c9793b6d8 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 05:30:38 +0200 Subject: [PATCH 119/120] StatResolver tests: boosts on one stat multiply, constants add, gear and consumable share the rule Pins ruling boost-stacking-multiply through the production path (gear and consumable StatModifierData on a ship, read via EquippedItem.Evaluate): TwoScalesOnOneStatMultiply, AConsumableBoostMultipliesWithGear, TwoConstantsOnOneStatAdd. Tests only; no production change. Not yet run; the commit is unverified. Co-Authored-By: Claude Opus 5.5 --- .../StatResolverTests.cs | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/tests/Aetheria.Shared.Tests/StatResolverTests.cs b/tests/Aetheria.Shared.Tests/StatResolverTests.cs index 0c0283f20..a9ed2e28b 100644 --- a/tests/Aetheria.Shared.Tests/StatResolverTests.cs +++ b/tests/Aetheria.Shared.Tests/StatResolverTests.cs @@ -452,4 +452,125 @@ public sealed class StatResolverTests : IDisposable Assert.Equal(baselineCache, resolver.CacheEntryCount); Assert.Equal(baselineModifiers, resolver.ModifierEntryCount); } + + // Ruling boost-stacking-multiply: every multiplier on one stat multiplies, with no stacking penalty, and every + // constant adds. The rig is the production path end to end: gear carrying StatModifierData is equipped on a ship + // beside a thruster, a consumable is minted through ItemManager.CreateInstance and activated on the ship, and + // the resolved value is read through EquippedItem.Evaluate. Nothing here calls AttachModifier by hand. + private sealed class ThrustRig + { + public CultCache Cache; + public ItemManager Items; + public Ship Ship; + public EquippedItem Engine; + public PerformanceStat Thrust; + public Func Consumable; // null when the rig was built without one + + public float Resolved => Engine.Evaluate(Thrust); + // Two ticks: the first executes every modifier, the second applies it (StatModifier.Update). + public void Tick(float dt) { for (var i = 0; i < 2; i++) Ship.Update(dt); } + } + + private static StatModifierData ThrustModifier(StatModifierType type, float value) => new StatModifierData + { + Stat = new StatReference { Target = nameof(ThrusterData), Stat = nameof(ThrusterData.Thrust) }, + Modifier = new PerformanceStat { Min = value, Max = value }, + Type = type + }; + + // A Thrust-10 thruster and one gear per entry of `gear`, in that attach order, plus (optionally) a consumable + // whose single modifier is `consumable`. The hull has four hardpoints, so at most three gear modifiers. + private ThrustRig BuildThrustRig((StatModifierType Type, float Value)[] gear, (StatModifierType Type, float Value)? consumable = null) + { + var thrust = new PerformanceStat { Min = 10, Max = 10 }; + var cache = AetheriaStores.Open(Catalog, catalogWritable: true); + cache.Upsert(new TestCatalogGlobal { Name = "Temperament" }); + var hullShape = new Shape(3, 3); + foreach (var cell in hullShape.AllCoordinates) hullShape[cell] = true; + var cells = new[] { HardpointCell, SecondHardpointCell, FirstGunCell, SecondGunCell }; + cache.Upsert(new HullData + { + Name = "Skiff", HullType = HullType.Ship, Shape = hullShape, Durability = 10, + Hardpoints = cells.Select(c => new HardpointData { Type = HardpointType.Sensors, Position = c, Shape = new Shape() }).ToList() + }); + cache.Upsert(new GearData { Name = "Engine", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Durability = 10, Behaviors = { new ThrusterData { Thrust = thrust } } }); + for (var i = 0; i < gear.Length; i++) + cache.Upsert(new GearData { Name = "Boost" + i, Hardpoint = HardpointType.Sensors, Shape = new Shape(), Durability = 10, Behaviors = { ThrustModifier(gear[i].Type, gear[i].Value) } }); + if (consumable != null) + cache.Upsert(new ConsumableItemData { Name = "Stim", Duration = 1f, Shape = new Shape(), Behaviors = { ThrustModifier(consumable.Value.Type, consumable.Value.Value) } }); + cache.FlushAsync().Wait(); + + var items = new ItemManager(cache, new ProvenanceLedger(), Settings(), _ => { }); + var zone = new Zone(items, new PlanetSettings(), new ZonePack(), new GalaxyZone { Name = "Test Zone", Owner = null }, null); + var ship = new Ship(items, zone, Mint(cache, items, cache.GetByName("Skiff")), new EntitySettings()); + Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName("Engine")), cells[0])); + for (var i = 0; i < gear.Length; i++) + Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName("Boost" + i)), cells[i + 1])); + zone.Entities.Add(ship); + ship.Activate(); + + Func mint = null; + if (consumable != null) + { + var design = cache.GetByName("Stim"); + mint = () => Assert.IsType(items.CreateInstance(items.Lots.Add( + new Lot { Design = cache.RefOf(design), Origin = new Attributed(), Quality = .5f, Roles = new List() }))); + } + return new ThrustRig { Cache = cache, Items = items, Ship = ship, Engine = ship.Equipment.Single(e => e.Data.Name == "Engine"), Thrust = thrust, Consumable = mint }; + } + + // Two scales multiply: x1.2 and x1.25 on one stat resolve to base x1.5, and x1.1, x1.2, x1.25 to base x1.65, + // whatever order they attach in. Mutations at StatResolver.ScaleModifier: a sum of excesses reads 14.5 and + // 15.5, the strongest-only rule 12.5, a diminishing curve 14.67, and `result *= value * value` or + // `result *= value + 0.01f` (functions of the scale itself) read off 15. + [Theory] + [InlineData(new[] { 1.2f, 1.25f }, 15f)] + [InlineData(new[] { 1.25f, 1.2f }, 15f)] + [InlineData(new[] { 1.1f, 1.2f, 1.25f }, 16.5f)] + [InlineData(new[] { 1.25f, 1.1f, 1.2f }, 16.5f)] + [InlineData(new[] { 1.2f, 1.25f, 1.1f }, 16.5f)] + public void TwoScalesOnOneStatMultiply(float[] scales, float expected) + { + var rig = BuildThrustRig(scales.Select(s => (StatModifierType.Multiplier, s)).ToArray()); + using var _ = rig.Cache; + Assert.Equal(10f, rig.Resolved, 3); // modifiers not yet applied + rig.Tick(.1f); + Assert.Equal(expected, rig.Resolved, 3); + } + + // A gear modifier and a consumable modifier on one drive's Thrust share one entry and multiply: x1.2 gear and + // x1.5 consumable read x1.8, and when the consumable expires only the gear's x1.2 remains. + [Fact] + public void AConsumableBoostMultipliesWithGear() + { + var rig = BuildThrustRig(new[] { (StatModifierType.Multiplier, 1.2f) }, (StatModifierType.Multiplier, 1.5f)); + using var _ = rig.Cache; + rig.Tick(.1f); + Assert.Equal(12f, rig.Resolved, 3); // gear alone + + rig.Ship.ActivateConsumable(rig.Consumable()); + rig.Tick(.1f); // 0.8s of the 1s remain + Assert.Equal(18f, rig.Resolved, 3); + + rig.Tick(.5f); // the consumable's duration has run out + Assert.Equal(12f, rig.Resolved, 3); + } + + // Constants add, and compose with scales through PerformanceStat.Evaluate: (base x scale) + constant. +3 and +4 + // resolve as a constant of 7; with a x2 scale beside them the value is Evaluate's for scale 2 and constant 7, + // (10 x 2) + 7. Mutations at ConstantModifier: `result = Math.Max(result, value)` reads constant 4; + // `result += value * 2` reads constant 14. + [Theory] + [InlineData(new[] { 3f, 4f }, new float[0], 1f, 7f, 17f)] + [InlineData(new[] { 3f, 4f }, new[] { 2f }, 2f, 7f, 27f)] + public void TwoConstantsOnOneStatAdd(float[] constants, float[] scales, float expectedScale, float expectedConstant, float expected) + { + var rig = BuildThrustRig(constants.Select(c => (StatModifierType.Constant, c)) + .Concat(scales.Select(s => (StatModifierType.Multiplier, s))).ToArray()); + using var _ = rig.Cache; + rig.Tick(.1f); + Assert.Equal(expectedScale, rig.Ship.Resolver.ScaleModifier(rig.Engine, rig.Thrust), 3); + Assert.Equal(expectedConstant, rig.Ship.Resolver.ConstantModifier(rig.Engine, rig.Thrust), 3); + Assert.Equal(expected, rig.Resolved, 3); + } } From 77e441df31e49b36f1fc63ba4f507ca86eaf6353 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 05:32:45 +0200 Subject: [PATCH 120/120] StatResolver tests: keep the boost gear online with the entity's override so the modifiers execute The first commit's gear modifiers never executed (test gear has no heat response, so it read offline). Builds; the first commit's new tests fail without this. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/StatResolverTests.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/Aetheria.Shared.Tests/StatResolverTests.cs b/tests/Aetheria.Shared.Tests/StatResolverTests.cs index a9ed2e28b..b156412ab 100644 --- a/tests/Aetheria.Shared.Tests/StatResolverTests.cs +++ b/tests/Aetheria.Shared.Tests/StatResolverTests.cs @@ -505,7 +505,14 @@ public sealed class StatResolverTests : IDisposable var ship = new Ship(items, zone, Mint(cache, items, cache.GetByName("Skiff")), new EntitySettings()); Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName("Engine")), cells[0])); for (var i = 0; i < gear.Length; i++) - Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName("Boost" + i)), cells[i + 1])); + { + // A test gear has no authored heat response, so its thermal performance reads 0 and it would sit offline; + // the override is the entity's own switch for keeping a part running, not a stand-in for the rule. + var boost = Mint(cache, items, cache.GetByName("Boost" + i)); + boost.OverrideShutdown = true; + Assert.True(ship.TryEquip(boost, cells[i + 1])); + } + ship.OverrideShutdown = true; zone.Entities.Add(ship); ship.Activate();