Share dock parent sensor picture
This commit is contained in:
parent
4351226b41
commit
cadddb4c87
7 changed files with 208 additions and 9 deletions
|
|
@ -2274,6 +2274,96 @@ public class DaemonRuntimeDocumentTests
|
|||
Assert.AreEqual(0.8, arrayShip.Visibility, 0.0001);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DockedPilotConsumesStationContactPictureThroughEveAndSoa()
|
||||
{
|
||||
var pilot = new AetheriaRuntimeEntitySnapshotCommit
|
||||
{
|
||||
EntityIndex = 7, Kind = "ship", FactionKey = "player", IsActive = true,
|
||||
Contacts = new[]
|
||||
{
|
||||
new AetheriaRuntimeEntityContactCommit
|
||||
{
|
||||
TargetEntityIndex = 3, InfoGathered = 0.2, Hostile = true, Visible = false
|
||||
}
|
||||
}
|
||||
};
|
||||
var station = new AetheriaRuntimeEntitySnapshotCommit
|
||||
{
|
||||
EntityIndex = 2, Kind = "station", FactionKey = "player", IsActive = true,
|
||||
DockingBayAssignments = new[] { 7 },
|
||||
Contacts = new[]
|
||||
{
|
||||
new AetheriaRuntimeEntityContactCommit
|
||||
{
|
||||
TargetEntityIndex = 3, InfoGathered = 0.9, Visible = true
|
||||
},
|
||||
new AetheriaRuntimeEntityContactCommit
|
||||
{
|
||||
TargetEntityIndex = 4, InfoGathered = 0.7, Visible = true
|
||||
}
|
||||
}
|
||||
};
|
||||
var unrelatedObserver = new AetheriaRuntimeEntitySnapshotCommit
|
||||
{
|
||||
EntityIndex = 8, Kind = "ship", IsActive = true,
|
||||
Contacts = new[]
|
||||
{
|
||||
new AetheriaRuntimeEntityContactCommit
|
||||
{
|
||||
TargetEntityIndex = 5, InfoGathered = 1, Visible = true
|
||||
}
|
||||
}
|
||||
};
|
||||
var zone = new AetheriaRuntimeZoneSnapshotCommit
|
||||
{
|
||||
ZoneIndex = 2,
|
||||
Entities = new[]
|
||||
{
|
||||
station,
|
||||
pilot,
|
||||
unrelatedObserver,
|
||||
new AetheriaRuntimeEntitySnapshotCommit { EntityIndex = 3, IsActive = true },
|
||||
new AetheriaRuntimeEntitySnapshotCommit { EntityIndex = 4, IsActive = true },
|
||||
new AetheriaRuntimeEntitySnapshotCommit { EntityIndex = 5, IsActive = true }
|
||||
}
|
||||
};
|
||||
var frame = AetheriaRuntimeDaemonFrameDocument.Create(
|
||||
new AetheriaRuntimeRunCheckpointCommit
|
||||
{
|
||||
RunId = "docked-sensors",
|
||||
CurrentZoneIndex = 2,
|
||||
CurrentEntityKey = "global:aetheria.run_state.docked-sensors.zone.2.entity.7.v1",
|
||||
Zones = new[] { zone }
|
||||
},
|
||||
"aetheria-daemon",
|
||||
"session-docked-sensors",
|
||||
51,
|
||||
1,
|
||||
0.02);
|
||||
|
||||
var contacts = AetheriaRuntimeGameDocuments.ZoneContacts(frame);
|
||||
var pilotContacts = contacts.Contacts
|
||||
.Where(contact => contact.ObserverEntityIndex == 7)
|
||||
.OrderBy(contact => contact.TargetEntityIndex)
|
||||
.ToArray();
|
||||
Assert.AreEqual(2, pilotContacts.Length);
|
||||
Assert.AreEqual(3, pilotContacts[0].TargetEntityIndex);
|
||||
Assert.AreEqual(0.9, pilotContacts[0].InfoGathered, 0.0001);
|
||||
Assert.IsTrue(pilotContacts[0].Visible);
|
||||
Assert.IsTrue(pilotContacts[0].Hostile, "Contact facts from both sensor sources must merge.");
|
||||
Assert.AreEqual(2, pilotContacts[0].PrimarySensorSourceEntityIndex);
|
||||
Assert.AreEqual(4, pilotContacts[1].TargetEntityIndex);
|
||||
Assert.AreEqual(2, pilotContacts[1].PrimarySensorSourceEntityIndex);
|
||||
Assert.IsFalse(pilotContacts.Any(contact => contact.TargetEntityIndex == 5),
|
||||
"An unrelated observer's contacts must not enter the pilot picture.");
|
||||
|
||||
var statePath = Path.Combine(Path.GetTempPath(), "aetheria-docked-sensors", Path.GetRandomFileName(), "state.cc");
|
||||
var view = AetheriaRuntimeDaemonSoaFramePublisher.BuildCurrentZoneEntities(statePath, frame);
|
||||
CollectionAssert.AreEquivalent(new[] { 2, 3, 4, 7 },
|
||||
view.Identities.Select(identity => identity.EntityIndex).ToArray());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TerminusPilotTractorScoopsCargoFromTargetedWreck()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -676,8 +676,92 @@ namespace GameCult.Aetheria.State.Verse
|
|||
public double DepthRange { get; }
|
||||
}
|
||||
|
||||
public readonly struct AetheriaRuntimeEffectiveContact
|
||||
{
|
||||
public AetheriaRuntimeEffectiveContact(
|
||||
int observerEntityIndex,
|
||||
int primarySensorSourceEntityIndex,
|
||||
AetheriaRuntimeEntityContactCommit contact)
|
||||
{
|
||||
ObserverEntityIndex = observerEntityIndex;
|
||||
PrimarySensorSourceEntityIndex = primarySensorSourceEntityIndex;
|
||||
Contact = contact ?? throw new ArgumentNullException(nameof(contact));
|
||||
}
|
||||
|
||||
public int ObserverEntityIndex { get; }
|
||||
public int PrimarySensorSourceEntityIndex { get; }
|
||||
public AetheriaRuntimeEntityContactCommit Contact { get; }
|
||||
}
|
||||
|
||||
public static class AetheriaRuntimeDaemonRenderQueries
|
||||
{
|
||||
public static AetheriaRuntimeEffectiveContact[] QueryEffectiveContacts(
|
||||
AetheriaRuntimeZoneSnapshotCommit? zone,
|
||||
int observerEntityIndex)
|
||||
{
|
||||
if (zone == null || observerEntityIndex < 0)
|
||||
return Array.Empty<AetheriaRuntimeEffectiveContact>();
|
||||
|
||||
var entities = zone.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>();
|
||||
var observer = entities.FirstOrDefault(entity =>
|
||||
entity != null && entity.EntityIndex == observerEntityIndex);
|
||||
if (observer == null)
|
||||
return Array.Empty<AetheriaRuntimeEffectiveContact>();
|
||||
|
||||
var sources = new List<AetheriaRuntimeEntitySnapshotCommit> { observer };
|
||||
var dockParent = entities.FirstOrDefault(entity => entity != null &&
|
||||
(entity.DockingBayAssignments ?? Array.Empty<int>()).Contains(observerEntityIndex));
|
||||
if (dockParent != null && dockParent.EntityIndex != observerEntityIndex)
|
||||
sources.Add(dockParent);
|
||||
|
||||
var contacts = new Dictionary<int, AetheriaRuntimeEffectiveContact>();
|
||||
foreach (var source in sources)
|
||||
foreach (var contact in source.Contacts ?? Array.Empty<AetheriaRuntimeEntityContactCommit>())
|
||||
{
|
||||
if (contact == null || contact.TargetEntityIndex < 0)
|
||||
continue;
|
||||
|
||||
if (!contacts.TryGetValue(contact.TargetEntityIndex, out var previous))
|
||||
{
|
||||
contacts.Add(contact.TargetEntityIndex, Effective(observerEntityIndex, source.EntityIndex, contact));
|
||||
continue;
|
||||
}
|
||||
|
||||
var previousContact = previous.Contact;
|
||||
var sourceWins = contact.InfoGathered > previousContact.InfoGathered ||
|
||||
(contact.InfoGathered == previousContact.InfoGathered && contact.Visible && !previousContact.Visible);
|
||||
contacts[contact.TargetEntityIndex] = new AetheriaRuntimeEffectiveContact(
|
||||
observerEntityIndex,
|
||||
sourceWins ? source.EntityIndex : previous.PrimarySensorSourceEntityIndex,
|
||||
new AetheriaRuntimeEntityContactCommit
|
||||
{
|
||||
TargetEntityIndex = contact.TargetEntityIndex,
|
||||
InfoGathered = Math.Max(previousContact.InfoGathered, contact.InfoGathered),
|
||||
Hostile = previousContact.Hostile || contact.Hostile,
|
||||
Visible = previousContact.Visible || contact.Visible
|
||||
});
|
||||
}
|
||||
|
||||
return contacts.Values
|
||||
.OrderBy(contact => contact.Contact.TargetEntityIndex)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeEffectiveContact Effective(
|
||||
int observerEntityIndex,
|
||||
int primarySensorSourceEntityIndex,
|
||||
AetheriaRuntimeEntityContactCommit contact) =>
|
||||
new AetheriaRuntimeEffectiveContact(
|
||||
observerEntityIndex,
|
||||
primarySensorSourceEntityIndex,
|
||||
new AetheriaRuntimeEntityContactCommit
|
||||
{
|
||||
TargetEntityIndex = contact.TargetEntityIndex,
|
||||
InfoGathered = contact.InfoGathered,
|
||||
Hostile = contact.Hostile,
|
||||
Visible = contact.Visible
|
||||
});
|
||||
|
||||
public static double ResolveZoneRenderRadius(
|
||||
AetheriaRuntimeZoneSnapshotCommit? zone,
|
||||
double fallbackRadius)
|
||||
|
|
|
|||
|
|
@ -51,11 +51,10 @@ namespace GameCult.Aetheria.State.Verse
|
|||
var dockParent = (zone?.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
|
||||
.FirstOrDefault(entity => entity != null && controlled != null &&
|
||||
(entity.DockingBayAssignments ?? Array.Empty<int>()).Contains(controlled.EntityIndex));
|
||||
var visibleContacts = (controlled?.Contacts ?? Array.Empty<AetheriaRuntimeEntityContactCommit>())
|
||||
.Concat(dockParent?.Contacts ?? Array.Empty<AetheriaRuntimeEntityContactCommit>());
|
||||
var visibleEntityIndices = visibleContacts
|
||||
.Where(contact => contact != null && contact.Visible)
|
||||
.Select(contact => contact.TargetEntityIndex)
|
||||
var visibleEntityIndices = AetheriaRuntimeDaemonRenderQueries
|
||||
.QueryEffectiveContacts(zone, controlled?.EntityIndex ?? -1)
|
||||
.Where(contact => contact.Contact.Visible)
|
||||
.Select(contact => contact.Contact.TargetEntityIndex)
|
||||
.Append(controlled?.EntityIndex ?? -1)
|
||||
.ToHashSet();
|
||||
if (dockParent != null)
|
||||
|
|
|
|||
|
|
@ -481,6 +481,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
var entityMap = entities
|
||||
.Where(entity => entity != null && entity.EntityIndex >= 0)
|
||||
.ToDictionary(entity => entity.EntityIndex, entity => entity);
|
||||
var currentEntityIndex = TryParseEntityIndex(context.Run.CurrentEntityKey);
|
||||
|
||||
return new AetheriaRuntimeZoneContactsDocument
|
||||
{
|
||||
|
|
@ -497,12 +498,25 @@ namespace GameCult.Aetheria.State.Verse
|
|||
.Select(entity => ToZoneTargetRow(entity, entityMap[entity.TargetEntityIndex]))
|
||||
.ToArray(),
|
||||
Contacts = entities
|
||||
.Where(entity => entity != null)
|
||||
.Where(entity => entity != null && entity.EntityIndex != currentEntityIndex)
|
||||
.SelectMany(entity => (entity.Contacts ?? Array.Empty<AetheriaRuntimeEntityContactCommit>())
|
||||
.Where(contact => contact != null &&
|
||||
contact.TargetEntityIndex >= 0 &&
|
||||
entityMap.ContainsKey(contact.TargetEntityIndex))
|
||||
.Select(contact => ProjectZoneContactRow(entity, entityMap[contact.TargetEntityIndex], contact)))
|
||||
.Select(contact => ProjectZoneContactRow(
|
||||
entity.EntityIndex,
|
||||
entity.EntityIndex,
|
||||
entity,
|
||||
entityMap[contact.TargetEntityIndex],
|
||||
contact)))
|
||||
.Concat(AetheriaRuntimeDaemonRenderQueries.QueryEffectiveContacts(context.Zone, currentEntityIndex)
|
||||
.Where(effective => entityMap.ContainsKey(effective.Contact.TargetEntityIndex))
|
||||
.Select(effective => ProjectZoneContactRow(
|
||||
effective.ObserverEntityIndex,
|
||||
effective.PrimarySensorSourceEntityIndex,
|
||||
entityMap[currentEntityIndex],
|
||||
entityMap[effective.Contact.TargetEntityIndex],
|
||||
effective.Contact)))
|
||||
.ToArray()
|
||||
};
|
||||
}
|
||||
|
|
@ -588,6 +602,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
}
|
||||
|
||||
private static AetheriaRuntimeZoneContactRow ProjectZoneContactRow(
|
||||
int observerEntityIndex,
|
||||
int primarySensorSourceEntityIndex,
|
||||
AetheriaRuntimeEntitySnapshotCommit observer,
|
||||
AetheriaRuntimeEntitySnapshotCommit target,
|
||||
AetheriaRuntimeEntityContactCommit contact)
|
||||
|
|
@ -597,7 +613,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
var deltaZ = target.PositionZ - observer.PositionZ;
|
||||
return new AetheriaRuntimeZoneContactRow
|
||||
{
|
||||
ObserverEntityIndex = observer.EntityIndex,
|
||||
ObserverEntityIndex = observerEntityIndex,
|
||||
PrimarySensorSourceEntityIndex = primarySensorSourceEntityIndex,
|
||||
TargetEntityIndex = target.EntityIndex,
|
||||
InfoGathered = contact.InfoGathered,
|
||||
Hostile = contact.Hostile,
|
||||
|
|
|
|||
|
|
@ -461,6 +461,9 @@ namespace GameCult.Aetheria.State.Verse
|
|||
|
||||
[Key(11)]
|
||||
public double Distance { get; set; }
|
||||
|
||||
[Key(12)]
|
||||
public int PrimarySensorSourceEntityIndex { get; set; } = -1;
|
||||
}
|
||||
|
||||
[CultDocument("gamecult.aetheria.station_refit", "gamecult.aetheria.station_refit.v1")]
|
||||
|
|
|
|||
|
|
@ -279,6 +279,12 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted.
|
|||
installed equipment. Contact refresh consumes this derived reach but does
|
||||
not write `Visibility`; emitted signature remains owned by thermal,
|
||||
propulsion, weapons, reflectors, explicit visibility sources, and pings.
|
||||
- The daemon observation query owns docked sensor sharing. A docked controlled
|
||||
craft's effective contact picture merges its own contacts with the dock
|
||||
parent's contacts, retains the primary sensor source on every projected row,
|
||||
and resolves duplicate targets to the strongest gathered information.
|
||||
Eve contact documents and SoA render bodies consume this same query; runtime
|
||||
clients do not discover docking relationships or union sensor state.
|
||||
`EntityConstructionBlueprint` still exists for construction/loadout projection,
|
||||
but those live hull grids and session scalars are not blueprint fields and
|
||||
cannot be restored by the old blueprint projector path.
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity.
|
|||
| Terminus loop | original game composition | daemon/Ymir/Eve | pilot surface | Aetheria.Unity | live minimum-loop proof: generic Unity discovers the provider, lowers the pilot world/assets, single-steps dock and undock while paused, buys daemon-priced station stock, single-steps Ymir salvage collection, resumes, moves, targets, fires, and presents authoritative receipts/shot outcome | destruction-created loot pickup, richer trade content, docking transitions, and sustained combat encounters |
|
||||
| Starbridge | original/shared architecture intent | one daemon session | pilot + commander surfaces | Unity + Electron | commander task board and autonomous explore execution wired; Electron Eve lowering and split-target proof missing | commander orders plus four pilots and autonomous workers |
|
||||
| Behavior engine | `ServerShared/Behaviors` composition | daemon | executable action/status projection | action bar and feedback | tick reconciles equipped catalog payloads into persistent behavior state; shared evaluated-stat query proven; execution families remain partial | every behavior family, ordering, resource transaction and modifier lifecycle |
|
||||
| Sensors/visibility | installed sensor sensitivity, decaying signature sources, and observer information thresholds | daemon | contacts and ping events | indicators/maps | passive reach now derives from enabled installed Sensor behaviors with diminishing-return array composition regardless of entity kind; contact refresh no longer overwrites emitted visibility; the old station range survives only as obsolete snapshot compatibility | author station/ship sensor loadout budgets, then prove power/thermal shutdown, threshold decay, ping, and classification timelines |
|
||||
| Sensors/visibility | installed sensor sensitivity, decaying signature sources, and observer information thresholds | daemon | contacts and ping events | indicators/maps | passive reach derives from enabled installed Sensor behaviors with diminishing-return array composition regardless of entity kind; contact refresh no longer overwrites emitted visibility; docked craft consume one daemon-derived union of craft and dock-parent contacts through both Eve rows and SoA bodies, with sensor-source provenance; the old station range survives only as obsolete snapshot compatibility | replace station/ship sensor-count shortcuts with mass/volume/power/cooling/cost allocation, then prove shutdown, threshold decay, ping, and classification timelines |
|
||||
| AI/tasks | corporation task scheduler plus agent state machines | daemon | commander task board, corporation survey ledger, and shared semantic controls | Eve commander/pilot clients | point exploration, attack with optimum-range control, same-zone haul, mining/offload, resource surveying, physical station towing, persistent orbit patrol, route-aware assignment, and autonomous return home wired through shared commands; attack admission rejects missing/friendly targets and unavailable weapon groups before durable queueing; capability/priority assignment and rejection behavior proven; generic Electron commander lowering proves the worker roster | cross-zone logistics execution, cancellation, reconnect, and multi-client command transfer |
|
||||
| Consumables | cargo-backed timed behavior containers | daemon | dynamic actions, active effects, and lifecycle facts | action bar/schematic | daemon atomically resolves typed metadata and concrete cargo, preserves consumed-instance quality, enforces authored stacking, evaluates effectiveness/quality, and executes ordered `EnergyDraw`, fossil-compatible no-op `Heat`, and `ItemUsage` payloads after energy tick initialization; false and unsupported payloads stop later execution with a typed fact; timers retain the fossil's exact-zero final tick; Unity remains a projection | stable effect identity, catalog revision pinning, cooldown/always-update state, stat modifiers, and remaining behavior families |
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue