Merge origin/master into eureka/aetheria-release-link-rig-record
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
577da4f75b
27 changed files with 1899 additions and 29 deletions
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using GameCult.Caching;
|
||||
using GameCult.Caching.MessagePack;
|
||||
|
|
@ -15,6 +16,9 @@ public static class AetheriaStores
|
|||
// no records at all, a seed store being authored from nothing; a populated catalog is held to it however it opens.
|
||||
public static CultCache Open(string catalogPath, string runPath = null, string playerPath = null, bool catalogWritable = false)
|
||||
{
|
||||
// A read-only open of an absent catalog would hydrate an empty game; a shipped player missing its catalog stops here.
|
||||
if (!catalogWritable && !File.Exists(catalogPath))
|
||||
throw new FileNotFoundException($"Catalog {catalogPath} does not exist; a read-only open never invents one.", catalogPath);
|
||||
var cache = new CultCache();
|
||||
try
|
||||
{
|
||||
|
|
|
|||
|
|
@ -187,6 +187,8 @@ 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)),
|
||||
Union(42, typeof(VapourDumpData)),
|
||||
JsonObject(MemberSerialization.OptIn)]
|
||||
public abstract class BehaviorData
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
35
Assets/Scripts/ServerShared/Behaviors/ThrottleLock.cs
Normal file
35
Assets/Scripts/ServerShared/Behaviors/ThrottleLock.cs
Normal file
|
|
@ -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;
|
||||
}
|
||||
61
Assets/Scripts/ServerShared/Behaviors/VapourDump.cs
Normal file
61
Assets/Scripts/ServerShared/Behaviors/VapourDump.cs
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/ServerShared/Behaviors/VapourDump.cs.meta
Normal file
11
Assets/Scripts/ServerShared/Behaviors/VapourDump.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 961a74683c574807a300eb26694f206d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
|
|
@ -860,9 +858,23 @@ 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.
|
||||
InitializeBehaviors(effect.Behaviors);
|
||||
_activeConsumables.Add(effect);
|
||||
}
|
||||
|
||||
private static void InitializeBehaviors(IEnumerable<Behavior> behaviors)
|
||||
{
|
||||
foreach (var behavior in behaviors.ToList())
|
||||
if (behavior is IInitializableBehavior initializable)
|
||||
initializable.Initialize();
|
||||
}
|
||||
|
||||
// Derived, never written: true iff an active consumable effect carries a ThrottleLock.
|
||||
public bool ThrottleLocked => _activeConsumables.Any(e => e.Behaviors.OfType<ThrottleLock>().Any());
|
||||
|
||||
public ConsumableItemEffect FindActiveConsumable(ConsumableItemData data)
|
||||
{
|
||||
return _activeConsumables.FirstOrDefault(ac => ac.Data == data);
|
||||
|
|
@ -1448,11 +1460,16 @@ 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
|
||||
// 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--);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
32
Assets/Scripts/ServerShared/VapourClouds.cs
Normal file
32
Assets/Scripts/ServerShared/VapourClouds.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
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;
|
||||
|
||||
// A cloud with no lifetime divides by zero; saturate maps the NaN and the infinity to nothing left.
|
||||
public float OpacityAt(double now) =>
|
||||
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;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/ServerShared/VapourClouds.cs.meta
Normal file
11
Assets/Scripts/ServerShared/VapourClouds.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ced45a9738974452b0065b57ee527e8a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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<Mine> Mines = new ReactiveCollection<Mine>();
|
||||
|
||||
// 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<VapourCloud> Clouds = new ReactiveCollection<VapourCloud>();
|
||||
public Dictionary<CultRecordKey, BodyData> Planets = new Dictionary<CultRecordKey, BodyData>();
|
||||
public Dictionary<CultRecordKey, Planet> PlanetInstances = new Dictionary<CultRecordKey, Planet>();
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue