Compare commits

..
Author SHA1 Message Date
Meta Vens
9c983a5ee2 Remove stale generated CultMath project 2026-07-22 06:19:45 +01:00
Meta Vens
3ec6222f82 Pin released Geometry Unity stack 2026-07-22 05:51:18 +01:00
Meta Vens
f361125a3a Use coordinated CultLib Unity candidate 2026-07-22 05:21:10 +01:00
Meta Vens
251efa176c Enable Geometry Unity package tests 2026-07-22 05:17:49 +01:00
Meta Vens
6e561d336f Use precompiled CultMath Unity package 2026-07-22 04:52:55 +01:00
Meta Vens
7e4feed75b Fix sibling Unity package paths 2026-07-22 04:35:57 +01:00
Meta Vens
20cd4e8c80 Pin CultMath local Unity candidate 2026-07-22 04:26:22 +01:00
Meta Vens
e04b613cc5 Fix CultRect render query access 2026-07-22 04:23:48 +01:00
Meta Vens
557bc71f7a Remove stale geometry ownership guidance 2026-07-22 04:14:03 +01:00
Meta Vens
26948f81b6 Migrate Aetheria geometry ownership 2026-07-22 04:08:57 +01:00
4850 changed files with 422252 additions and 244504 deletions

4
.gitattributes vendored
View file

@ -53,10 +53,6 @@
*.engine filter=lfs diff=lfs merge=lfs -text
*.sbsar filter=lfs diff=lfs merge=lfs -text
*.msgpack filter=lfs diff=lfs merge=lfs -text
GameData/*.cc filter=lfs diff=lfs merge=lfs -text
GameData/Mods/**/*.cc filter=lfs diff=lfs merge=lfs -text
GameData/Mods/**/*.glb filter=lfs diff=lfs merge=lfs -text
tools/blender/tests/fixtures/*.glb filter=lfs diff=lfs merge=lfs -text
*.a filter=lfs diff=lfs merge=lfs -text
*AkSoundEngine filter=lfs diff=lfs merge=lfs -text
*AkWaapiClient filter=lfs diff=lfs merge=lfs -text

116
.gitignore vendored
View file

@ -32,6 +32,10 @@
# Visual Studio cache directory
.vs/
# Local agent/editor runtime state
.brokkr/
.codex/
# Gradle cache directory
.gradle/
@ -51,24 +55,31 @@ ExportedObj/
*.opendb
*.VC.db
# Unity/IDE generated solution and project files
/*.sln
/*.csproj
# CultCache single-file store locks
GameData/*.lock
# Per-user editor state
/[Uu]ser[Ss]ettings/
# .NET build output outside Unity
/Aetheria.Shared/bin/
/Aetheria.Shared/obj/
/tools/**/bin/
/tools/**/obj/
/ModTools/
/tests/**/bin/
/tests/**/obj/
# Unity3D generate project files
Aetheria-Economy.sln
Aetheria.Shared.Unity.csproj
CultBase.csproj
CultCache.csproj
CultCache.Editor.csproj
Assembly-CSharp.csproj
Assembly-CSharp-Editor.csproj
Assembly-CSharp-firstpass.csproj
Assembly-CSharp-Editor-firstpass.csproj
Substance.EditorHelper.csproj
Substance.Platform.csproj
RethinkDB.csproj
MessagePack.csproj
UniRx.csproj
Ink.csproj
Tests.csproj
AK.Wwise.Unity.Timeline.Editor.csproj
AK.Wwise.Unity.Timeline.csproj
AK.Wwise.Unity.MonoBehaviour.Editor.csproj
AK.Wwise.Unity.MonoBehaviour.csproj
AK.Wwise.Unity.API.WwiseTypes.csproj
AK.Wwise.Unity.API.Editor.csproj
AK.Wwise.Unity.API.csproj
Ak.Wwise.Api.WAAPI.csproj
# Unity3D generated meta files
*.pidb.meta
@ -85,6 +96,45 @@ sysinfo.txt
# Crashlytics generated file
crashlytics-build.properties
# Server generated files
Economy.Server/bin/
Economy.Server/obj/
Economy.Shared/bin/
Economy.Shared/obj/
Aetheria.State/bin/
Aetheria.State/obj/
Aetheria.State.Daemon/bin/
Aetheria.State.Daemon/obj/
Aetheria.State.Daemon.Smoke/bin/
Aetheria.State.Daemon.Smoke/obj/
Aetheria.State.Import/bin/
Aetheria.State.Import/obj/
Aetheria.State.Smoke/bin/
Aetheria.State.Smoke/obj/
Aetheria.State.Unity/bin/
Aetheria.State.Unity/obj/
Aetheria.State.Unity.Smoke/bin/
Aetheria.State.Unity.Smoke/obj/
Aetheria.State.Replica/bin/
Aetheria.State.Replica/obj/
Aetheria.State.Verify/bin/
Aetheria.State.Verify/obj/
Aetheria.Rts.Web/bin/
Aetheria.Rts.Web/obj/
Aetheria.Rts.Web/electron-dist/
Aetheria.Rts.Web/logs/
Aetheria.Rts.Web/node_modules/
Aetheria.Rts.Web/runtime/
Aetheria.Rts.Web/Aetheria.Rts.Web/logs/
Aetheria.Rts.Web/*.log
Aetheria.Rts.Web/wwwroot/*.js
Aetheria.State.AuthoritySmoke/bin/
Aetheria.State.AuthoritySmoke/obj/
Aetheria.State.Freeze/bin/
Aetheria.State.Freeze/obj/
Aetheria.State.Daemon/*.log
*.client-quic-*.pfx
# JetBrains Rider .idea folder
.idea/
@ -92,24 +142,12 @@ crashlytics-build.properties
*.blend1
Packages/packages-lock.json
GameData/run.cc
GameData/player.cc
GameData/RemovedMods/
# Agent scratch, sandboxed toolchain caches, and throwaway checkouts. These had
# grown to 47 untracked entries; the worktree must stay legible, so they never
# come back.
.scratch-*
scratch-*
tests/_scratch_*
.codex/
.codex-tmp/
.evepkg/
.npm-cache*/
.nuget-appdata/
.nuget-sandbox/
.dotnet-sandbox/
.appdata-sandbox/
# Stryker.NET mutation reports (regenerated per run)
StrykerOutput/
GameData/PlayerSettings.msgpack
GameData/*.cultmesh/
GameData/*.cc.records/
GameData/*.cc.records.*/
GameData/*.cc.eve.pending/
GameData/*.cc.before-*
GameData/*.cc.daemon.*.cc
/UserSettings/
Vault/Aetheria/.obsidian/plugins/copilot/data.json

View file

@ -1,2 +0,0 @@
[lfs]
url = https://eureka.gamecult.org/forge/GameCult/Aetheria.git/info/lfs

Binary file not shown.

81
AGENTS.md Normal file
View file

@ -0,0 +1,81 @@
# Aetheria Agent Instructions
This repo is mid-migration toward CultMesh-native gameplay state. Treat access
simplicity as an architectural requirement, not a nice-to-have.
## Canonical Typed State
- Default to one canonical shared typed document for each gameplay state concept.
The daemon may own authority over that document, but it must not imply a
separate private daemon truth plus a copied client-facing state document.
- The shared runtime assembly defines the document. The daemon mutates/publishes
it. Unity, Electron, tools, tests, and later runtimes read the same document
type through CultMesh.
- Client input should feel like grabbing a managed typed state handle and either
reading it for display or writing/submitting through it according to authority
policy. Prediction, debounce, routing, quorum, reconciliation, and smoothing
are CultMesh responsibilities.
Use projections only when the state shape is intentionally different:
- hidden-information filtering;
- expensive or shared derived aggregation;
- viewport/windowed selection;
- SoA/native render or physics layout;
- lossy UI summaries;
- Eve/CultUI surface documents;
- compatibility mirrors with a named removal stage.
"The client needs to see it" is not a projection reason.
## Access Simplicity
- A UI-only inline value should be derived from already accessible typed state.
- A shared UI read should usually be: define the typed document, then read it
with `client.State.Reactive<TDocument>()` or `client.State.Latest<TDocument>()`.
- A simulation feature should usually be: define the canonical typed document,
let daemon authority mutate/publish it, then read or modify that same managed
document from clients according to authority policy.
- Add named handles only when type alone cannot identify the state, such as
parameterized viewport/detail documents, multiple documents sharing one CLR
type, semantic query identity, operation policy, or native view ownership.
## Heretek Shape
The facade/projector/adapter/surface-builder chain is heretek now. Do not add a
stack of translation layers to recover one typed domain value.
One adapter at a true boundary is allowed:
- Unity GameObject presentation;
- Eve/CultUI lowering;
- legacy import;
- persistence;
- native view ownership.
Two or more translation layers in a row mean the code is missing a CultMesh
primitive, generated typed handle, canonical document, query surface, operation
handle, state pointer, or native view descriptor.
## Verifier Discipline
The verifier should encode the desired architecture, not the current accidental
shape. When removing old access paths, add checks that reject their return. Do
not write verifier rules that require wrapper/session/facade/projector chains as
proof of correctness.
Good verifier pressure:
- client code owns `CultMeshReactiveDocument<TDocument>`;
- client code reads `.Current`;
- client code uses generic typed document access where possible;
- hot render paths use named SoA/native views;
- operations are typed and authority-aware;
- legacy adapters are quarantined and named for deletion.
Bad verifier pressure:
- requiring `LatestFoo()`/`ReactiveFoo()` wrappers for single fixed documents;
- requiring one-document session wrappers;
- requiring facade/projector/adapter/surface-builder chains;
- treating renderer-local facade indexes as gameplay-state APIs.

BIN
Aetheria RTS.lnk Normal file

Binary file not shown.

View file

@ -0,0 +1,906 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{CA5C9672-2298-4659-B32D-52AE660403FF}" SchemaVersion="97">
<AudioObjects>
<WorkUnit Name="Ambience AM Unit" ID="{CA5C9672-2298-4659-B32D-52AE660403FF}" PersistMode="Standalone">
<ChildrenList>
<RandomSequenceContainer Name="Amb BaseDrone" ID="{E45BD333-61F1-48C2-A4C1-ED9A5105C648}" ShortID="341369367">
<PropertyList>
<Property Name="Highpass" Type="int16">
<ValueList>
<Value>19</Value>
</ValueList>
</Property>
<Property Name="NormalOrShuffle" Type="int16" Value="0"/>
<Property Name="Pitch" Type="int32">
<ModifierList>
<ModifierInfo>
<Modifier Name="" ID="{EBD48CEE-FAC0-458F-B99B-278ABE681AD9}">
<PropertyList>
<Property Name="Enabled" Type="bool" Value="True"/>
<Property Name="Max" Type="Real64" Value="150"/>
<Property Name="Min" Type="Real64" Value="-150"/>
</PropertyList>
</Modifier>
</ModifierInfo>
</ModifierList>
</Property>
<Property Name="PlayMechanismLoop" Type="bool" Value="True"/>
<Property Name="PlayMechanismSpecialTransitions" Type="bool" Value="True"/>
<Property Name="PlayMechanismSpecialTransitionsType" Type="int16" Value="4"/>
<Property Name="PlayMechanismSpecialTransitionsValue" Type="Real64" Value="3.5"/>
<Property Name="PlayMechanismStepOrContinuous" Type="int16" Value="0"/>
<Property Name="RandomAvoidRepeatingCount" Type="int32" Value="2"/>
<Property Name="UserAuxSendVolume0" Type="Real64">
<ValueList>
<Value>-7</Value>
</ValueList>
</Property>
<Property Name="Volume" Type="Real64">
<ValueList>
<Value>-2</Value>
</ValueList>
</Property>
</PropertyList>
<ReferenceList>
<Reference Name="Conversion">
<ObjectRef Name="Default Conversion Settings" ID="{6D1B890C-9826-4384-BF07-C15223E9FB56}" WorkUnitID="{499CA58A-C702-47D8-B39A-D2BA9F895A1F}"/>
</Reference>
<Reference Name="OutputBus">
<ObjectRef Name="Ambience Bus" ID="{32BF429D-C1DC-4D83-BB3E-074A4305E69E}" WorkUnitID="{A840C15E-F723-46FB-A6CF-9D8CDA869AEE}"/>
</Reference>
<Reference Name="UserAuxSend0">
<ObjectRef Name="Space_Reverb" ID="{BBC34EE4-5BCA-4840-A54A-7E82E4030CDA}" WorkUnitID="{A840C15E-F723-46FB-A6CF-9D8CDA869AEE}"/>
</Reference>
</ReferenceList>
<ChildrenList>
<Sound Name="Atmos_BaseDrone_1" ID="{5B9F85D6-AED8-4ACC-971D-D01D7A9D5947}" ShortID="273547428">
<ReferenceList>
<Reference Name="Conversion">
<ObjectRef Name="Default Conversion Settings" ID="{6D1B890C-9826-4384-BF07-C15223E9FB56}" WorkUnitID="{499CA58A-C702-47D8-B39A-D2BA9F895A1F}"/>
</Reference>
<Reference Name="OutputBus">
<ObjectRef Name="Master Audio Bus" ID="{1514A4D8-1DA6-412A-A17E-75CA0C2149F3}" WorkUnitID="{1F437DD1-6486-4464-8395-A927FAD94FB4}"/>
</Reference>
</ReferenceList>
<ChildrenList>
<AudioFileSource Name="Atmos_BaseDrone_1" ID="{0500E27A-D0DB-4BE6-BE95-1D41C246805A}">
<Language>SFX</Language>
<AudioFile>Atmos_BaseDrone_1.wav</AudioFile>
<MediaIDList>
<MediaID ID="815157338"/>
</MediaIDList>
</AudioFileSource>
</ChildrenList>
<ActiveSourceList>
<ActiveSource Name="Atmos_BaseDrone_1" ID="{0500E27A-D0DB-4BE6-BE95-1D41C246805A}" Platform="Linked"/>
</ActiveSourceList>
</Sound>
<Sound Name="Atmos_BaseDrone_2" ID="{0A13CE6B-B2BE-49A2-A84A-61C622F81E4F}" ShortID="361149343">
<ReferenceList>
<Reference Name="Conversion">
<ObjectRef Name="Default Conversion Settings" ID="{6D1B890C-9826-4384-BF07-C15223E9FB56}" WorkUnitID="{499CA58A-C702-47D8-B39A-D2BA9F895A1F}"/>
</Reference>
<Reference Name="OutputBus">
<ObjectRef Name="Master Audio Bus" ID="{1514A4D8-1DA6-412A-A17E-75CA0C2149F3}" WorkUnitID="{1F437DD1-6486-4464-8395-A927FAD94FB4}"/>
</Reference>
</ReferenceList>
<ChildrenList>
<AudioFileSource Name="Atmos_BaseDrone_2" ID="{C8F358CA-B6E6-443D-AC9F-F764AD1A859F}">
<Language>SFX</Language>
<AudioFile>Atmos_BaseDrone_2.wav</AudioFile>
<MediaIDList>
<MediaID ID="477872164"/>
</MediaIDList>
</AudioFileSource>
</ChildrenList>
<ActiveSourceList>
<ActiveSource Name="Atmos_BaseDrone_2" ID="{C8F358CA-B6E6-443D-AC9F-F764AD1A859F}" Platform="Linked"/>
</ActiveSourceList>
</Sound>
<Sound Name="Atmos_BaseDrone_3" ID="{FEA296DD-B4FC-4ED0-984A-3BCB8A7341BA}" ShortID="948314657">
<ReferenceList>
<Reference Name="Conversion">
<ObjectRef Name="Default Conversion Settings" ID="{6D1B890C-9826-4384-BF07-C15223E9FB56}" WorkUnitID="{499CA58A-C702-47D8-B39A-D2BA9F895A1F}"/>
</Reference>
<Reference Name="OutputBus">
<ObjectRef Name="Master Audio Bus" ID="{1514A4D8-1DA6-412A-A17E-75CA0C2149F3}" WorkUnitID="{1F437DD1-6486-4464-8395-A927FAD94FB4}"/>
</Reference>
</ReferenceList>
<ChildrenList>
<AudioFileSource Name="Atmos_BaseDrone_3" ID="{27C6A8C0-C2EC-4022-AA5B-6CAEFC653375}">
<Language>SFX</Language>
<AudioFile>Atmos_BaseDrone_3.wav</AudioFile>
<MediaIDList>
<MediaID ID="561717499"/>
</MediaIDList>
</AudioFileSource>
</ChildrenList>
<ActiveSourceList>
<ActiveSource Name="Atmos_BaseDrone_3" ID="{27C6A8C0-C2EC-4022-AA5B-6CAEFC653375}" Platform="Linked"/>
</ActiveSourceList>
</Sound>
<Sound Name="Atmos_BaseDrone_4" ID="{B2C7FE69-4E85-4258-9358-B35B09BBE244}" ShortID="826398956">
<ReferenceList>
<Reference Name="Conversion">
<ObjectRef Name="Default Conversion Settings" ID="{6D1B890C-9826-4384-BF07-C15223E9FB56}" WorkUnitID="{499CA58A-C702-47D8-B39A-D2BA9F895A1F}"/>
</Reference>
<Reference Name="OutputBus">
<ObjectRef Name="Master Audio Bus" ID="{1514A4D8-1DA6-412A-A17E-75CA0C2149F3}" WorkUnitID="{1F437DD1-6486-4464-8395-A927FAD94FB4}"/>
</Reference>
</ReferenceList>
<ChildrenList>
<AudioFileSource Name="Atmos_BaseDrone_4" ID="{EA58F35D-FFE5-4808-B0A5-C098A8E35020}">
<Language>SFX</Language>
<AudioFile>Atmos_BaseDrone_4.wav</AudioFile>
<MediaIDList>
<MediaID ID="511454619"/>
</MediaIDList>
</AudioFileSource>
</ChildrenList>
<ActiveSourceList>
<ActiveSource Name="Atmos_BaseDrone_4" ID="{EA58F35D-FFE5-4808-B0A5-C098A8E35020}" Platform="Linked"/>
</ActiveSourceList>
</Sound>
</ChildrenList>
</RandomSequenceContainer>
<RandomSequenceContainer Name="Amb Low Oneshots" ID="{F53FC63D-6B1A-46B0-B21C-4CC28B7F26E1}" ShortID="62478438">
<PropertyList>
<Property Name="EnableAttenuation" Type="bool" Value="False"/>
<Property Name="Highpass" Type="int16">
<ValueList>
<Value>23</Value>
</ValueList>
</Property>
<Property Name="Lowpass" Type="int16">
<ValueList>
<Value>30</Value>
</ValueList>
</Property>
<Property Name="NormalOrShuffle" Type="int16" Value="0"/>
<Property Name="Pitch" Type="int32">
<ModifierList>
<ModifierInfo>
<Modifier Name="" ID="{8EBDB7CB-B0B5-4BCE-BCAD-E245A592A763}">
<PropertyList>
<Property Name="Enabled" Type="bool" Value="True"/>
<Property Name="Max" Type="Real64" Value="100"/>
<Property Name="Min" Type="Real64" Value="-250"/>
</PropertyList>
</Modifier>
</ModifierInfo>
</ModifierList>
</Property>
<Property Name="PlayMechanismLoop" Type="bool" Value="True"/>
<Property Name="PlayMechanismSpecialTransitions" Type="bool" Value="True"/>
<Property Name="PlayMechanismSpecialTransitionsType" Type="int16" Value="1"/>
<Property Name="PlayMechanismSpecialTransitionsValue" Type="Real64" Value="7">
<ModifierList>
<ModifierInfo>
<Modifier Name="" ID="{3A5A4912-D249-4809-8DD2-83C5DCB20CD9}">
<PropertyList>
<Property Name="Enabled" Type="bool" Value="True"/>
<Property Name="Max" Type="Real64" Value="2"/>
<Property Name="Min" Type="Real64" Value="-3"/>
</PropertyList>
</Modifier>
</ModifierInfo>
</ModifierList>
</Property>
<Property Name="PlayMechanismStepOrContinuous" Type="int16" Value="0"/>
<Property Name="RandomAvoidRepeatingCount" Type="int32" Value="2"/>
<Property Name="SpeakerPanning" Type="int16" Value="1"/>
<Property Name="Volume" Type="Real64">
<ModifierList>
<ModifierInfo>
<Modifier Name="" ID="{E75B9DD9-DA24-4114-9975-A6E3D5C0BF55}">
<PropertyList>
<Property Name="Max" Type="Real64" Value="200"/>
<Property Name="Min" Type="Real64" Value="-200"/>
</PropertyList>
</Modifier>
</ModifierInfo>
</ModifierList>
</Property>
</PropertyList>
<ReferenceList>
<Reference Name="Conversion">
<ObjectRef Name="Default Conversion Settings" ID="{6D1B890C-9826-4384-BF07-C15223E9FB56}" WorkUnitID="{499CA58A-C702-47D8-B39A-D2BA9F895A1F}"/>
</Reference>
<Reference Name="OutputBus">
<ObjectRef Name="Depth Reverb" ID="{02A8082A-7457-40EE-9DA7-4D7BE13562EB}" WorkUnitID="{A840C15E-F723-46FB-A6CF-9D8CDA869AEE}"/>
</Reference>
</ReferenceList>
<ChildrenList>
<Sound Name="Atmos_Creak_1" ID="{5B6481D5-5DAB-4F5D-8C0B-0C82ACE88352}" ShortID="132095328">
<PropertyList>
<Property Name="EnableAttenuation" Type="bool" Value="False"/>
<Property Name="Highpass" Type="int16">
<ValueList>
<Value>10</Value>
</ValueList>
</Property>
<Property Name="Lowpass" Type="int16">
<ValueList>
<Value>18</Value>
</ValueList>
</Property>
<Property Name="OverridePositioning" Type="bool" Value="True"/>
<Property Name="Pitch" Type="int32">
<ValueList>
<Value>-1390</Value>
</ValueList>
</Property>
<Property Name="SpeakerPanning" Type="int16" Value="1"/>
<Property Name="Volume" Type="Real64">
<ValueList>
<Value>2</Value>
</ValueList>
</Property>
</PropertyList>
<ReferenceList>
<Reference Name="Conversion">
<ObjectRef Name="Default Conversion Settings" ID="{6D1B890C-9826-4384-BF07-C15223E9FB56}" WorkUnitID="{499CA58A-C702-47D8-B39A-D2BA9F895A1F}"/>
</Reference>
<Reference Name="OutputBus">
<ObjectRef Name="Master Audio Bus" ID="{1514A4D8-1DA6-412A-A17E-75CA0C2149F3}" WorkUnitID="{1F437DD1-6486-4464-8395-A927FAD94FB4}"/>
</Reference>
</ReferenceList>
<ChildrenList>
<AudioFileSource Name="Atmos_Creak_1" ID="{671B46F0-7C72-4488-9D77-725FB08B3F6C}">
<Language>SFX</Language>
<AudioFile>Atmos_Creak_1.wav</AudioFile>
<MediaIDList>
<MediaID ID="42363413"/>
</MediaIDList>
</AudioFileSource>
</ChildrenList>
<PositioningInfo>
<Panner Name="" ID="{D25553C9-D841-4B3C-B582-F51D9E2A562B}">
<PropertyList>
<Property Name="PanX" Type="Real64" Value="-100"/>
<Property Name="PanY" Type="Real64" Value="-1.27659574468085"/>
</PropertyList>
</Panner>
</PositioningInfo>
<ActiveSourceList>
<ActiveSource Name="Atmos_Creak_1" ID="{671B46F0-7C72-4488-9D77-725FB08B3F6C}" Platform="Linked"/>
</ActiveSourceList>
</Sound>
<Sound Name="Atmos_Creak_2" ID="{7E458943-33C2-4B79-ABD9-33AD5062FDFF}" ShortID="951254820">
<PropertyList>
<Property Name="Highpass" Type="int16">
<ValueList>
<Value>10</Value>
</ValueList>
</Property>
<Property Name="OverridePositioning" Type="bool" Value="True"/>
<Property Name="Pitch" Type="int32">
<ValueList>
<Value>-1240</Value>
</ValueList>
</Property>
<Property Name="SpeakerPanning" Type="int16" Value="1"/>
<Property Name="Volume" Type="Real64">
<ValueList>
<Value>2</Value>
</ValueList>
</Property>
</PropertyList>
<ReferenceList>
<Reference Name="Conversion">
<ObjectRef Name="Default Conversion Settings" ID="{6D1B890C-9826-4384-BF07-C15223E9FB56}" WorkUnitID="{499CA58A-C702-47D8-B39A-D2BA9F895A1F}"/>
</Reference>
<Reference Name="OutputBus">
<ObjectRef Name="Master Audio Bus" ID="{1514A4D8-1DA6-412A-A17E-75CA0C2149F3}" WorkUnitID="{1F437DD1-6486-4464-8395-A927FAD94FB4}"/>
</Reference>
</ReferenceList>
<ChildrenList>
<AudioFileSource Name="Atmos_Creak_2" ID="{057C2308-AB2E-4976-AD36-392E2763C886}">
<Language>SFX</Language>
<AudioFile>Atmos_Creak_2.wav</AudioFile>
<MediaIDList>
<MediaID ID="864546841"/>
</MediaIDList>
</AudioFileSource>
</ChildrenList>
<PositioningInfo>
<Panner Name="" ID="{F39470D9-C80E-4038-841A-033A68E155C8}">
<PropertyList>
<Property Name="PanX" Type="Real64" Value="100"/>
<Property Name="PanY" Type="Real64" Value="-5.48024982368162E-16"/>
</PropertyList>
</Panner>
</PositioningInfo>
<ActiveSourceList>
<ActiveSource Name="Atmos_Creak_2" ID="{057C2308-AB2E-4976-AD36-392E2763C886}" Platform="Linked"/>
</ActiveSourceList>
</Sound>
<Sound Name="Atmos_Boom_1" ID="{3A0A6E46-F5A4-49B1-B7D9-E67AE35EC7A2}" ShortID="173149066">
<PropertyList>
<Property Name="EnableAttenuation" Type="bool" Value="False"/>
<Property Name="Highpass" Type="int16">
<ValueList>
<Value>2</Value>
</ValueList>
</Property>
<Property Name="Lowpass" Type="int16">
<ValueList>
<Value>15</Value>
</ValueList>
</Property>
<Property Name="OverridePositioning" Type="bool" Value="True"/>
<Property Name="SpeakerPanning" Type="int16" Value="1"/>
<Property Name="Volume" Type="Real64">
<ValueList>
<Value>5</Value>
</ValueList>
</Property>
</PropertyList>
<ReferenceList>
<Reference Name="Conversion">
<ObjectRef Name="Default Conversion Settings" ID="{6D1B890C-9826-4384-BF07-C15223E9FB56}" WorkUnitID="{499CA58A-C702-47D8-B39A-D2BA9F895A1F}"/>
</Reference>
<Reference Name="OutputBus">
<ObjectRef Name="Master Audio Bus" ID="{1514A4D8-1DA6-412A-A17E-75CA0C2149F3}" WorkUnitID="{1F437DD1-6486-4464-8395-A927FAD94FB4}"/>
</Reference>
</ReferenceList>
<ChildrenList>
<AudioFileSource Name="Atmos_Boom_1" ID="{1505352F-70C5-41D0-B8F5-1A898FF63C16}">
<Language>SFX</Language>
<AudioFile>Atmos_Boom_1.wav</AudioFile>
<MediaIDList>
<MediaID ID="371326037"/>
</MediaIDList>
</AudioFileSource>
</ChildrenList>
<PositioningInfo>
<Panner Name="" ID="{423DBAAE-ADB2-4DAC-B96B-E30838AE00A7}">
<PropertyList>
<Property Name="PanX" Type="Real64" Value="-83.1325301204819"/>
<Property Name="PanY" Type="Real64" Value="-65.531914893617"/>
</PropertyList>
</Panner>
</PositioningInfo>
<ActiveSourceList>
<ActiveSource Name="Atmos_Boom_1" ID="{1505352F-70C5-41D0-B8F5-1A898FF63C16}" Platform="Linked"/>
</ActiveSourceList>
</Sound>
<Sound Name="Atmos_Boom_2" ID="{1AF51C57-EC81-40E5-8D49-89E6568AAC29}" ShortID="877269440">
<PropertyList>
<Property Name="EnableAttenuation" Type="bool" Value="False"/>
<Property Name="Highpass" Type="int16">
<ValueList>
<Value>2</Value>
</ValueList>
</Property>
<Property Name="Lowpass" Type="int16">
<ValueList>
<Value>15</Value>
</ValueList>
</Property>
<Property Name="OverridePositioning" Type="bool" Value="True"/>
<Property Name="SpeakerPanning" Type="int16" Value="1"/>
<Property Name="Volume" Type="Real64">
<ValueList>
<Value>3</Value>
</ValueList>
</Property>
</PropertyList>
<ReferenceList>
<Reference Name="Conversion">
<ObjectRef Name="Default Conversion Settings" ID="{6D1B890C-9826-4384-BF07-C15223E9FB56}" WorkUnitID="{499CA58A-C702-47D8-B39A-D2BA9F895A1F}"/>
</Reference>
<Reference Name="OutputBus">
<ObjectRef Name="Master Audio Bus" ID="{1514A4D8-1DA6-412A-A17E-75CA0C2149F3}" WorkUnitID="{1F437DD1-6486-4464-8395-A927FAD94FB4}"/>
</Reference>
</ReferenceList>
<ChildrenList>
<AudioFileSource Name="Atmos_Boom_2" ID="{975DFBAC-9571-414B-AC44-F97EDF6B02F7}">
<Language>SFX</Language>
<AudioFile>Atmos_Boom_2.wav</AudioFile>
<MediaIDList>
<MediaID ID="183218560"/>
</MediaIDList>
</AudioFileSource>
</ChildrenList>
<PositioningInfo>
<Panner Name="" ID="{C8AC9199-1F01-4821-BF6A-D752C1AE470A}">
<PropertyList>
<Property Name="PanX" Type="Real64" Value="73.0923694779116"/>
<Property Name="PanY" Type="Real64" Value="52.7659574468085"/>
</PropertyList>
</Panner>
</PositioningInfo>
<ActiveSourceList>
<ActiveSource Name="Atmos_Boom_2" ID="{975DFBAC-9571-414B-AC44-F97EDF6B02F7}" Platform="Linked"/>
</ActiveSourceList>
</Sound>
<Sound Name="Atmos_Boom_3" ID="{D42EC68F-0663-47DC-AC1D-F11594B99D95}" ShortID="500419615">
<PropertyList>
<Property Name="EnableAttenuation" Type="bool" Value="False"/>
<Property Name="Highpass" Type="int16">
<ValueList>
<Value>2</Value>
</ValueList>
</Property>
<Property Name="Lowpass" Type="int16">
<ValueList>
<Value>16</Value>
</ValueList>
</Property>
<Property Name="OverridePositioning" Type="bool" Value="True"/>
<Property Name="SpeakerPanning" Type="int16" Value="1"/>
<Property Name="Volume" Type="Real64">
<ValueList>
<Value>3</Value>
</ValueList>
</Property>
</PropertyList>
<ReferenceList>
<Reference Name="Conversion">
<ObjectRef Name="Default Conversion Settings" ID="{6D1B890C-9826-4384-BF07-C15223E9FB56}" WorkUnitID="{499CA58A-C702-47D8-B39A-D2BA9F895A1F}"/>
</Reference>
<Reference Name="OutputBus">
<ObjectRef Name="Master Audio Bus" ID="{1514A4D8-1DA6-412A-A17E-75CA0C2149F3}" WorkUnitID="{1F437DD1-6486-4464-8395-A927FAD94FB4}"/>
</Reference>
</ReferenceList>
<ChildrenList>
<AudioFileSource Name="Atmos_Boom_3" ID="{E764A171-C0E2-4563-A85B-9FFAAE5B09A8}">
<Language>SFX</Language>
<AudioFile>Atmos_Boom_3.wav</AudioFile>
<MediaIDList>
<MediaID ID="94837028"/>
</MediaIDList>
</AudioFileSource>
</ChildrenList>
<PositioningInfo>
<Panner Name="" ID="{CF8FC2E2-29F8-4CC3-8B01-1E884DE657CC}">
<PropertyList>
<Property Name="PanX" Type="Real64" Value="48.5943775100402"/>
<Property Name="PanY" Type="Real64" Value="-70.2127659574468"/>
</PropertyList>
</Panner>
</PositioningInfo>
<ActiveSourceList>
<ActiveSource Name="Atmos_Boom_3" ID="{E764A171-C0E2-4563-A85B-9FFAAE5B09A8}" Platform="Linked"/>
</ActiveSourceList>
</Sound>
</ChildrenList>
<PositioningInfo>
<Panner Name="" ID="{D792C12F-D27F-436A-9BCE-651102B65C51}">
<PropertyList>
<Property Name="PanX" Type="Real64" Value="-2.55351295663786E-15"/>
<Property Name="PanY" Type="Real64" Value="4.44089209850063E-16"/>
</PropertyList>
</Panner>
</PositioningInfo>
</RandomSequenceContainer>
<RandomSequenceContainer Name="Amb Mid and High Oneshots" ID="{B05F1CCC-B463-451A-ACD9-2101E5F13B5A}" ShortID="548418547">
<PropertyList>
<Property Name="Highpass" Type="int16">
<ValueList>
<Value>30</Value>
</ValueList>
</Property>
<Property Name="Lowpass" Type="int16">
<ValueList>
<Value>33</Value>
</ValueList>
<ModifierList>
<ModifierInfo>
<Modifier Name="" ID="{850776A6-10E3-4E0B-8D8B-A9A6E2E8B588}">
<PropertyList>
<Property Name="Enabled" Type="bool" Value="True"/>
<Property Name="Max" Type="Real64" Value="30"/>
</PropertyList>
</Modifier>
</ModifierInfo>
</ModifierList>
</Property>
<Property Name="NormalOrShuffle" Type="int16" Value="0"/>
<Property Name="Pitch" Type="int32">
<ModifierList>
<ModifierInfo>
<Modifier Name="" ID="{C9DAA261-356F-4BC9-BB64-F14A40B3C9DD}">
<PropertyList>
<Property Name="Enabled" Type="bool" Value="True"/>
<Property Name="Max" Type="Real64" Value="200"/>
<Property Name="Min" Type="Real64" Value="-200"/>
</PropertyList>
</Modifier>
</ModifierInfo>
</ModifierList>
</Property>
<Property Name="PlayMechanismLoop" Type="bool" Value="True"/>
<Property Name="PlayMechanismSpecialTransitions" Type="bool" Value="True"/>
<Property Name="PlayMechanismSpecialTransitionsType" Type="int16" Value="1"/>
<Property Name="PlayMechanismSpecialTransitionsValue" Type="Real64" Value="11">
<ModifierList>
<ModifierInfo>
<Modifier Name="" ID="{6EE57AB9-943F-49BE-BBB5-D73AD11CCB73}">
<PropertyList>
<Property Name="Enabled" Type="bool" Value="True"/>
<Property Name="Min" Type="Real64" Value="-2"/>
</PropertyList>
</Modifier>
</ModifierInfo>
</ModifierList>
</Property>
<Property Name="PlayMechanismStepOrContinuous" Type="int16" Value="0"/>
<Property Name="RandomAvoidRepeatingCount" Type="int32" Value="2"/>
<Property Name="Volume" Type="Real64">
<ValueList>
<Value>-2</Value>
</ValueList>
</Property>
</PropertyList>
<ReferenceList>
<Reference Name="Conversion">
<ObjectRef Name="Default Conversion Settings" ID="{6D1B890C-9826-4384-BF07-C15223E9FB56}" WorkUnitID="{499CA58A-C702-47D8-B39A-D2BA9F895A1F}"/>
</Reference>
<Reference Name="OutputBus">
<ObjectRef Name="Depth Reverb" ID="{02A8082A-7457-40EE-9DA7-4D7BE13562EB}" WorkUnitID="{A840C15E-F723-46FB-A6CF-9D8CDA869AEE}"/>
</Reference>
</ReferenceList>
<ChildrenList>
<Sound Name="Atmos_HiOmen_1" ID="{239D8593-1E2C-441E-8B45-0F27357ADF8D}" ShortID="533788911">
<ReferenceList>
<Reference Name="Conversion">
<ObjectRef Name="Default Conversion Settings" ID="{6D1B890C-9826-4384-BF07-C15223E9FB56}" WorkUnitID="{499CA58A-C702-47D8-B39A-D2BA9F895A1F}"/>
</Reference>
<Reference Name="OutputBus">
<ObjectRef Name="Master Audio Bus" ID="{1514A4D8-1DA6-412A-A17E-75CA0C2149F3}" WorkUnitID="{1F437DD1-6486-4464-8395-A927FAD94FB4}"/>
</Reference>
</ReferenceList>
<ChildrenList>
<AudioFileSource Name="Atmos_HiOmen_1" ID="{CC1E6D81-B554-4273-BB32-F4D7AE6FF814}">
<Language>SFX</Language>
<AudioFile>Atmos_HiOmen_1.wav</AudioFile>
<MediaIDList>
<MediaID ID="426617159"/>
</MediaIDList>
</AudioFileSource>
</ChildrenList>
<ActiveSourceList>
<ActiveSource Name="Atmos_HiOmen_1" ID="{CC1E6D81-B554-4273-BB32-F4D7AE6FF814}" Platform="Linked"/>
</ActiveSourceList>
</Sound>
<Sound Name="Atmos_HiOmen_2" ID="{A811E98A-581E-4757-9871-601E72B26D06}" ShortID="434935109">
<ReferenceList>
<Reference Name="Conversion">
<ObjectRef Name="Default Conversion Settings" ID="{6D1B890C-9826-4384-BF07-C15223E9FB56}" WorkUnitID="{499CA58A-C702-47D8-B39A-D2BA9F895A1F}"/>
</Reference>
<Reference Name="OutputBus">
<ObjectRef Name="Master Audio Bus" ID="{1514A4D8-1DA6-412A-A17E-75CA0C2149F3}" WorkUnitID="{1F437DD1-6486-4464-8395-A927FAD94FB4}"/>
</Reference>
</ReferenceList>
<ChildrenList>
<AudioFileSource Name="Atmos_HiOmen_2" ID="{B758FFC2-5697-4C56-A0B9-85EA858318A1}">
<Language>SFX</Language>
<AudioFile>Atmos_HiOmen_2.wav</AudioFile>
<MediaIDList>
<MediaID ID="832424248"/>
</MediaIDList>
</AudioFileSource>
</ChildrenList>
<ActiveSourceList>
<ActiveSource Name="Atmos_HiOmen_2" ID="{B758FFC2-5697-4C56-A0B9-85EA858318A1}" Platform="Linked"/>
</ActiveSourceList>
</Sound>
<Sound Name="Atmos_HiOmen_3" ID="{A1C7F3CB-C805-4199-A6B2-E867C06A8778}" ShortID="323608670">
<ReferenceList>
<Reference Name="Conversion">
<ObjectRef Name="Default Conversion Settings" ID="{6D1B890C-9826-4384-BF07-C15223E9FB56}" WorkUnitID="{499CA58A-C702-47D8-B39A-D2BA9F895A1F}"/>
</Reference>
<Reference Name="OutputBus">
<ObjectRef Name="Master Audio Bus" ID="{1514A4D8-1DA6-412A-A17E-75CA0C2149F3}" WorkUnitID="{1F437DD1-6486-4464-8395-A927FAD94FB4}"/>
</Reference>
</ReferenceList>
<ChildrenList>
<AudioFileSource Name="Atmos_HiOmen_3" ID="{B4709499-F5E6-422C-B535-999A0B954C5D}">
<Language>SFX</Language>
<AudioFile>Atmos_HiOmen_3.wav</AudioFile>
<MediaIDList>
<MediaID ID="574973554"/>
</MediaIDList>
</AudioFileSource>
</ChildrenList>
<ActiveSourceList>
<ActiveSource Name="Atmos_HiOmen_3" ID="{B4709499-F5E6-422C-B535-999A0B954C5D}" Platform="Linked"/>
</ActiveSourceList>
</Sound>
<Sound Name="Atmos_HiOmen_4" ID="{AA15D288-636E-4B96-9467-C52F89C27AFB}" ShortID="159812960">
<ReferenceList>
<Reference Name="Conversion">
<ObjectRef Name="Default Conversion Settings" ID="{6D1B890C-9826-4384-BF07-C15223E9FB56}" WorkUnitID="{499CA58A-C702-47D8-B39A-D2BA9F895A1F}"/>
</Reference>
<Reference Name="OutputBus">
<ObjectRef Name="Master Audio Bus" ID="{1514A4D8-1DA6-412A-A17E-75CA0C2149F3}" WorkUnitID="{1F437DD1-6486-4464-8395-A927FAD94FB4}"/>
</Reference>
</ReferenceList>
<ChildrenList>
<AudioFileSource Name="Atmos_HiOmen_4" ID="{82B9C5D9-5B98-42E9-88A3-66C5430046D4}">
<Language>SFX</Language>
<AudioFile>Atmos_HiOmen_4.wav</AudioFile>
<MediaIDList>
<MediaID ID="193744851"/>
</MediaIDList>
</AudioFileSource>
</ChildrenList>
<ActiveSourceList>
<ActiveSource Name="Atmos_HiOmen_4" ID="{82B9C5D9-5B98-42E9-88A3-66C5430046D4}" Platform="Linked"/>
</ActiveSourceList>
</Sound>
<Sound Name="Atmos_Hiss_1" ID="{65FEBA43-84CF-47B5-9CC9-9C1F856041C0}" ShortID="141170690">
<PropertyList>
<Property Name="Pitch" Type="int32">
<ModifierList>
<ModifierInfo>
<Modifier Name="" ID="{00855411-2DF0-428C-87F8-935A37642CB6}">
<PropertyList>
<Property Name="Max" Type="Real64" Value="100"/>
<Property Name="Min" Type="Real64" Value="-100"/>
</PropertyList>
</Modifier>
</ModifierInfo>
</ModifierList>
</Property>
<Property Name="Volume" Type="Real64">
<ValueList>
<Value>-2</Value>
</ValueList>
</Property>
</PropertyList>
<ReferenceList>
<Reference Name="Conversion">
<ObjectRef Name="Default Conversion Settings" ID="{6D1B890C-9826-4384-BF07-C15223E9FB56}" WorkUnitID="{499CA58A-C702-47D8-B39A-D2BA9F895A1F}"/>
</Reference>
<Reference Name="OutputBus">
<ObjectRef Name="Master Audio Bus" ID="{1514A4D8-1DA6-412A-A17E-75CA0C2149F3}" WorkUnitID="{1F437DD1-6486-4464-8395-A927FAD94FB4}"/>
</Reference>
</ReferenceList>
<ChildrenList>
<AudioFileSource Name="Atmos_Hiss_1" ID="{DB45FC78-AB6A-4DFC-A6F3-8B80F6F23662}">
<Language>SFX</Language>
<AudioFile>Atmos_Hiss_1.wav</AudioFile>
<MediaIDList>
<MediaID ID="41213335"/>
</MediaIDList>
</AudioFileSource>
</ChildrenList>
<ActiveSourceList>
<ActiveSource Name="Atmos_Hiss_1" ID="{DB45FC78-AB6A-4DFC-A6F3-8B80F6F23662}" Platform="Linked"/>
</ActiveSourceList>
</Sound>
<Sound Name="Atmos_Hiss_2" ID="{4598A238-3E94-406F-B3E7-94D5BCD80700}" ShortID="354641292">
<PropertyList>
<Property Name="Pitch" Type="int32">
<ModifierList>
<ModifierInfo>
<Modifier Name="" ID="{A04D5B3C-3436-4CB8-A8F8-994B94C16D0D}">
<PropertyList>
<Property Name="Min" Type="Real64" Value="-100"/>
</PropertyList>
</Modifier>
</ModifierInfo>
</ModifierList>
</Property>
<Property Name="Volume" Type="Real64">
<ValueList>
<Value>-2</Value>
</ValueList>
</Property>
</PropertyList>
<ReferenceList>
<Reference Name="Conversion">
<ObjectRef Name="Default Conversion Settings" ID="{6D1B890C-9826-4384-BF07-C15223E9FB56}" WorkUnitID="{499CA58A-C702-47D8-B39A-D2BA9F895A1F}"/>
</Reference>
<Reference Name="OutputBus">
<ObjectRef Name="Master Audio Bus" ID="{1514A4D8-1DA6-412A-A17E-75CA0C2149F3}" WorkUnitID="{1F437DD1-6486-4464-8395-A927FAD94FB4}"/>
</Reference>
</ReferenceList>
<ChildrenList>
<AudioFileSource Name="Atmos_Hiss_2" ID="{ED48A94A-AA63-4105-8A50-8D3D57103487}">
<Language>SFX</Language>
<AudioFile>Atmos_Hiss_2.wav</AudioFile>
<MediaIDList>
<MediaID ID="423419231"/>
</MediaIDList>
</AudioFileSource>
</ChildrenList>
<ActiveSourceList>
<ActiveSource Name="Atmos_Hiss_2" ID="{ED48A94A-AA63-4105-8A50-8D3D57103487}" Platform="Linked"/>
</ActiveSourceList>
</Sound>
<Sound Name="Atmos_Ping" ID="{15D1E039-5315-40F9-BE03-F332B9C6CFC6}" ShortID="232729606">
<PropertyList>
<Property Name="Volume" Type="Real64">
<ValueList>
<Value>4</Value>
</ValueList>
</Property>
</PropertyList>
<ReferenceList>
<Reference Name="Conversion">
<ObjectRef Name="Default Conversion Settings" ID="{6D1B890C-9826-4384-BF07-C15223E9FB56}" WorkUnitID="{499CA58A-C702-47D8-B39A-D2BA9F895A1F}"/>
</Reference>
<Reference Name="OutputBus">
<ObjectRef Name="Master Audio Bus" ID="{1514A4D8-1DA6-412A-A17E-75CA0C2149F3}" WorkUnitID="{1F437DD1-6486-4464-8395-A927FAD94FB4}"/>
</Reference>
</ReferenceList>
<ChildrenList>
<AudioFileSource Name="Atmos_Ping" ID="{42DAE4E4-E239-4170-A0A1-7C38547F3308}">
<Language>SFX</Language>
<AudioFile>Atmos_Ping.wav</AudioFile>
<MediaIDList>
<MediaID ID="613119089"/>
</MediaIDList>
</AudioFileSource>
</ChildrenList>
<ActiveSourceList>
<ActiveSource Name="Atmos_Ping" ID="{42DAE4E4-E239-4170-A0A1-7C38547F3308}" Platform="Linked"/>
</ActiveSourceList>
</Sound>
</ChildrenList>
</RandomSequenceContainer>
<RandomSequenceContainer Name="Amb Weatherlike Oneshots" ID="{9773EA84-27DC-45E0-8D4E-8D26137A7698}" ShortID="414024756">
<PropertyList>
<Property Name="Highpass" Type="int16">
<ValueList>
<Value>17</Value>
</ValueList>
</Property>
<Property Name="Lowpass" Type="int16">
<ValueList>
<Value>15</Value>
</ValueList>
</Property>
<Property Name="NormalOrShuffle" Type="int16" Value="0"/>
<Property Name="Pitch" Type="int32">
<ModifierList>
<ModifierInfo>
<Modifier Name="" ID="{635605AA-5AF4-4DE8-9E26-FADB28A55F64}">
<PropertyList>
<Property Name="Enabled" Type="bool" Value="True"/>
<Property Name="Max" Type="Real64" Value="200"/>
<Property Name="Min" Type="Real64" Value="-200"/>
</PropertyList>
</Modifier>
</ModifierInfo>
</ModifierList>
</Property>
<Property Name="PlayMechanismLoop" Type="bool" Value="True"/>
<Property Name="PlayMechanismSpecialTransitions" Type="bool" Value="True"/>
<Property Name="PlayMechanismSpecialTransitionsType" Type="int16" Value="1"/>
<Property Name="PlayMechanismSpecialTransitionsValue" Type="Real64" Value="13">
<ModifierList>
<ModifierInfo>
<Modifier Name="" ID="{5693FC9B-49B5-4D2D-B9B8-9EA73316091B}">
<PropertyList>
<Property Name="Enabled" Type="bool" Value="True"/>
<Property Name="Max" Type="Real64" Value="3"/>
<Property Name="Min" Type="Real64" Value="-2"/>
</PropertyList>
</Modifier>
</ModifierInfo>
</ModifierList>
</Property>
<Property Name="PlayMechanismStepOrContinuous" Type="int16" Value="0"/>
<Property Name="RandomAvoidRepeatingCount" Type="int32" Value="2"/>
<Property Name="UserAuxSendVolume0" Type="Real64">
<ValueList>
<Value>-3</Value>
</ValueList>
</Property>
<Property Name="Volume" Type="Real64">
<ValueList>
<Value>-9</Value>
</ValueList>
</Property>
</PropertyList>
<ReferenceList>
<Reference Name="Conversion">
<ObjectRef Name="Default Conversion Settings" ID="{6D1B890C-9826-4384-BF07-C15223E9FB56}" WorkUnitID="{499CA58A-C702-47D8-B39A-D2BA9F895A1F}"/>
</Reference>
<Reference Name="OutputBus">
<ObjectRef Name="Ambience Bus" ID="{32BF429D-C1DC-4D83-BB3E-074A4305E69E}" WorkUnitID="{A840C15E-F723-46FB-A6CF-9D8CDA869AEE}"/>
</Reference>
<Reference Name="UserAuxSend0">
<ObjectRef Name="Space_Reverb" ID="{BBC34EE4-5BCA-4840-A54A-7E82E4030CDA}" WorkUnitID="{A840C15E-F723-46FB-A6CF-9D8CDA869AEE}"/>
</Reference>
</ReferenceList>
<ChildrenList>
<Sound Name="Atmos_Thunder_1" ID="{0C6F403B-25CB-4BF4-A718-9B0EDE09649A}" ShortID="395095994">
<ReferenceList>
<Reference Name="Conversion">
<ObjectRef Name="Default Conversion Settings" ID="{6D1B890C-9826-4384-BF07-C15223E9FB56}" WorkUnitID="{499CA58A-C702-47D8-B39A-D2BA9F895A1F}"/>
</Reference>
<Reference Name="OutputBus">
<ObjectRef Name="Master Audio Bus" ID="{1514A4D8-1DA6-412A-A17E-75CA0C2149F3}" WorkUnitID="{1F437DD1-6486-4464-8395-A927FAD94FB4}"/>
</Reference>
</ReferenceList>
<ChildrenList>
<AudioFileSource Name="Atmos_Thunder_1" ID="{5087A3C0-7B35-4143-9773-8967BBC9F9AE}">
<Language>SFX</Language>
<AudioFile>Atmos_Thunder_1.wav</AudioFile>
<MediaIDList>
<MediaID ID="85851182"/>
</MediaIDList>
</AudioFileSource>
</ChildrenList>
<ActiveSourceList>
<ActiveSource Name="Atmos_Thunder_1" ID="{5087A3C0-7B35-4143-9773-8967BBC9F9AE}" Platform="Linked"/>
</ActiveSourceList>
</Sound>
<Sound Name="Atmos_Thunder_2" ID="{420A32C9-419D-45C1-8A27-C23C9016F328}" ShortID="756665464">
<ReferenceList>
<Reference Name="Conversion">
<ObjectRef Name="Default Conversion Settings" ID="{6D1B890C-9826-4384-BF07-C15223E9FB56}" WorkUnitID="{499CA58A-C702-47D8-B39A-D2BA9F895A1F}"/>
</Reference>
<Reference Name="OutputBus">
<ObjectRef Name="Master Audio Bus" ID="{1514A4D8-1DA6-412A-A17E-75CA0C2149F3}" WorkUnitID="{1F437DD1-6486-4464-8395-A927FAD94FB4}"/>
</Reference>
</ReferenceList>
<ChildrenList>
<AudioFileSource Name="Atmos_Thunder_2" ID="{4A1DB4AF-F2E9-4E42-ACF7-C988A7E1EAEC}">
<Language>SFX</Language>
<AudioFile>Atmos_Thunder_2.wav</AudioFile>
<MediaIDList>
<MediaID ID="463022251"/>
</MediaIDList>
</AudioFileSource>
</ChildrenList>
<ActiveSourceList>
<ActiveSource Name="Atmos_Thunder_2" ID="{4A1DB4AF-F2E9-4E42-ACF7-C988A7E1EAEC}" Platform="Linked"/>
</ActiveSourceList>
</Sound>
<Sound Name="Atmos_Thunder_3" ID="{6593B120-111D-4837-989F-02EB6238410F}" ShortID="843806875">
<ReferenceList>
<Reference Name="Conversion">
<ObjectRef Name="Default Conversion Settings" ID="{6D1B890C-9826-4384-BF07-C15223E9FB56}" WorkUnitID="{499CA58A-C702-47D8-B39A-D2BA9F895A1F}"/>
</Reference>
<Reference Name="OutputBus">
<ObjectRef Name="Master Audio Bus" ID="{1514A4D8-1DA6-412A-A17E-75CA0C2149F3}" WorkUnitID="{1F437DD1-6486-4464-8395-A927FAD94FB4}"/>
</Reference>
</ReferenceList>
<ChildrenList>
<AudioFileSource Name="Atmos_Thunder_3" ID="{75F1E5A7-CDBC-45A1-82D5-13C66FD24984}">
<Language>SFX</Language>
<AudioFile>Atmos_Thunder_3.wav</AudioFile>
<MediaIDList>
<MediaID ID="238104924"/>
</MediaIDList>
</AudioFileSource>
</ChildrenList>
<ActiveSourceList>
<ActiveSource Name="Atmos_Thunder_3" ID="{75F1E5A7-CDBC-45A1-82D5-13C66FD24984}" Platform="Linked"/>
</ActiveSourceList>
</Sound>
<Sound Name="Atmos_Thunder_4" ID="{8AF31E1D-50AC-49DA-B8BD-A165BEF0FC28}" ShortID="809845216">
<ReferenceList>
<Reference Name="Conversion">
<ObjectRef Name="Default Conversion Settings" ID="{6D1B890C-9826-4384-BF07-C15223E9FB56}" WorkUnitID="{499CA58A-C702-47D8-B39A-D2BA9F895A1F}"/>
</Reference>
<Reference Name="OutputBus">
<ObjectRef Name="Master Audio Bus" ID="{1514A4D8-1DA6-412A-A17E-75CA0C2149F3}" WorkUnitID="{1F437DD1-6486-4464-8395-A927FAD94FB4}"/>
</Reference>
</ReferenceList>
<ChildrenList>
<AudioFileSource Name="Atmos_Thunder_4" ID="{44DF5D07-50FF-4A6C-9389-2E78CD3C627E}">
<Language>SFX</Language>
<AudioFile>Atmos_Thunder_4.wav</AudioFile>
<MediaIDList>
<MediaID ID="842922880"/>
</MediaIDList>
</AudioFileSource>
</ChildrenList>
<ActiveSourceList>
<ActiveSource Name="Atmos_Thunder_4" ID="{44DF5D07-50FF-4A6C-9389-2E78CD3C627E}" Platform="Linked"/>
</ActiveSourceList>
</Sound>
</ChildrenList>
</RandomSequenceContainer>
</ChildrenList>
</WorkUnit>
</AudioObjects>
</WwiseDocument>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{57D7F7EA-A41D-4B60-BBFE-8E62F7C303A8}" SchemaVersion="97">
<AudioObjects>
<WorkUnit Name="copy_Unit" ID="{57D7F7EA-A41D-4B60-BBFE-8E62F7C303A8}" PersistMode="Standalone"/>
</AudioObjects>
</WwiseDocument>

View file

@ -0,0 +1,118 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="UserProjectSettings" SchemaVersion="97">
<UserProjectSettingsInfo>
<UserProjectSettings Name="" ID="{A8E9CFD1-183E-47CC-804B-0B8B4C92B529}">
<PropertyList>
<Property Name="Event Action List\{0673D570-EC52-4956-B47F-5BC475263F42}\x64_Expand States" Type="string" Value=""/>
<Property Name="Event Action List\{0F30DB3D-6A97-4659-BA04-13A267DC19EC}\x64_Expand States" Type="string" Value=""/>
<Property Name="Event Action List\{29C05807-68C5-4524-8E31-C48F854602E2}\x64_Expand States" Type="string" Value=""/>
<Property Name="Event Action List\{327B0775-AF04-45E9-B12C-6C0A23004D68}\x64_Expand States" Type="string" Value=""/>
<Property Name="Event Action List\{715CB4B5-1708-481E-92C4-BA55066D2E09}\x64_Expand States" Type="string" Value=""/>
<Property Name="Event Action List\{90C1B4E6-D445-425B-BA9B-CFB9BCB2663E}\x64_Expand States" Type="string" Value=""/>
<Property Name="Event Action List\{9CDB4529-2114-41EB-B4C4-0DDB7CF529F3}\x64_Expand States" Type="string" Value=""/>
<Property Name="Event Action List\{CC95135D-D17F-4088-B23C-BF8238FCA221}\x64_Expand States" Type="string" Value=""/>
<Property Name="EventView\FilterMode1" Type="int32" Value="2"/>
<Property Name="ExternalSourcesInputPath" Type="string">
<ValueList>
<Value Platform="Mac"></Value>
<Value Platform="Windows"></Value>
</ValueList>
</Property>
<Property Name="ExternalSourcesOutputPath" Type="string">
<ValueList>
<Value Platform="Mac">GeneratedSoundBanks\Mac\</Value>
<Value Platform="Windows">GeneratedSoundBanks\Windows\</Value>
</ValueList>
</Property>
<Property Name="Game Profiler\Voice Monitor\LastBus" Type="string" Value="{00000000-0000-0000-0000-000000000000}"/>
<Property Name="General\LatestLanguage" Type="int32" Value="684519430"/>
<Property Name="General\LatestPlatform" Type="string" Value="Mac"/>
<Property Name="General\SourceControlPluginID" Type="string" Value="{C403A906-0C19-4ED5-808E-748707065301}"/>
<Property Name="GenerateSoundBanks\ListMode" Type="string" Value="Tree"/>
<Property Name="GenerateSoundBanks\x64_SelectedLanguages" Type="string" Value=""/>
<Property Name="GenerateSoundBanks\x64_SelectedPlatforms" Type="string" Value="q2TZbD4hVEmzl8RAIDkpEg=="/>
<Property Name="GenerateSoundBanks\x64_SelectedSoundBanks" Type="string" Value="Za5mlmZVs0yOu7E3c3kEhV8VgxbcIJxLooznDdKtaGNQOo16Z7W/R4f5mBKZWA//jBqEB80Hl0asXeQrPmgLeF2AkYLVPe1OqjBJZNQ1lSlmiugEU1LaQ4hgo5ZUkq5kIQwTht2BuE2QKvFyzNeWZg=="/>
<Property Name="HardwareController\CurrentSession" Type="string" Value="{00000000-0000-0000-0000-000000000000}"/>
<Property Name="MeterView\LastObject1" Type="string" Value="{1514A4D8-1DA6-412A-A17E-75CA0C2149F3}"/>
<Property Name="MeterView\MeterMode1" Type="int32" Value="1"/>
<Property Name="MeterView\MeterMode1Viewpoint0" Type="int32" Value="1"/>
<Property Name="MeterView\MeterMode1Viewpoint1" Type="int32" Value="2"/>
<Property Name="MeterView\MeterMode1Viewpoint2" Type="int32" Value="3"/>
<Property Name="MeterView\MeterMode1Viewpoint3" Type="int32" Value="0"/>
<Property Name="Mixing Desk\LastSession" Type="string" Value="{00000000-0000-0000-0000-000000000000}"/>
<Property Name="MonitoringManager\GameObjectFilter\GameObjectFilterVersion" Type="int32" Value="1"/>
<Property Name="MonitoringManager\GameObjectFilter\GameObjectFiltersEnable" Type="bool" Value="True"/>
<Property Name="MonitoringManager\GameObjectFilter\x64_GameObjectFilters" Type="string" Value=""/>
<Property Name="PluginUserAcknowledge\ConvolutionReverb" Type="bool" Value="True"/>
<Property Name="PluginUserAcknowledge\iZotope Hybrid Reverb" Type="bool" Value="True"/>
<Property Name="PluginUserAcknowledge\iZotope Trash Distortion" Type="bool" Value="True"/>
<Property Name="ProjectExplorer\FirstVisible_10_1" Type="string" Value="{4C96B38B-3DB4-40C7-91C9-665510E38993}"/>
<Property Name="ProjectExplorer\FirstVisible_11_1" Type="string" Value="{23EC2DE3-3ED8-4B82-AC99-92E25F2E4C6B}"/>
<Property Name="ProjectExplorer\FirstVisible_1_1" Type="string" Value="{35564C2D-1BC9-40BB-B365-D9C4A47AE5F1}"/>
<Property Name="ProjectExplorer\FirstVisible_3_1" Type="string" Value="{A69BF0FC-D84A-487C-ABDD-56F71CB2A126}"/>
<Property Name="ProjectExplorer\FirstVisible_4_1" Type="string" Value="{CB6175F1-6080-4A23-92EF-0807E304D0C1}"/>
<Property Name="ProjectExplorer\FirstVisible_6_1" Type="string" Value="{C98E673F-A24C-48A8-960F-10565FFE70A0}"/>
<Property Name="ProjectExplorer\FirstVisible_8_1" Type="string" Value="{41093F77-829F-4D88-8601-E8B25F3D4781}"/>
<Property Name="ProjectExplorer\x64_Expanded_10_1" Type="string" Value="3PjXIkJoYEu0SFGHJbICNw=="/>
<Property Name="ProjectExplorer\x64_Expanded_11_1" Type="string" Value=""/>
<Property Name="ProjectExplorer\x64_Expanded_12_1" Type="string" Value=""/>
<Property Name="ProjectExplorer\x64_Expanded_13_1" Type="string" Value=""/>
<Property Name="ProjectExplorer\x64_Expanded_14_1" Type="string" Value=""/>
<Property Name="ProjectExplorer\x64_Expanded_15_1" Type="string" Value=""/>
<Property Name="ProjectExplorer\x64_Expanded_16_1" Type="string" Value=""/>
<Property Name="ProjectExplorer\x64_Expanded_17_1" Type="string" Value=""/>
<Property Name="ProjectExplorer\x64_Expanded_18_1" Type="string" Value=""/>
<Property Name="ProjectExplorer\x64_Expanded_19_1" Type="string" Value=""/>
<Property Name="ProjectExplorer\x64_Expanded_1_1" Type="string" Value=""/>
<Property Name="ProjectExplorer\x64_Expanded_20_1" Type="string" Value=""/>
<Property Name="ProjectExplorer\x64_Expanded_21_1" Type="string" Value=""/>
<Property Name="ProjectExplorer\x64_Expanded_2_1" Type="string" Value="H5tsB0ZV2kOKkuShybP9QswcX7BjtBpFrNkhAeXxO1qE6nOX3CfgRY1OjSYTenaYNrvpNJJ9MUaZb5LuRpaMcXKWXMqYIllGsy1SrmYEA/9KMlj1jt3QRrmn5BzObd7fOy3OxixQxUyBWrAdEcFX7g=="/>
<Property Name="ProjectExplorer\x64_Expanded_3_1" Type="string" Value="sxdyCsdSDEqZZvWvfsyY8D2NxuMXmQ1LnQrpBNe4/Pc="/>
<Property Name="ProjectExplorer\x64_Expanded_4_1" Type="string" Value=""/>
<Property Name="ProjectExplorer\x64_Expanded_5_1" Type="string" Value=""/>
<Property Name="ProjectExplorer\x64_Expanded_6_1" Type="string" Value="J9nMSvQJiEK4fHtELJz7CulvOLJI4fNHt1thMshjmy7KL2V8j32LSaExQ5extvRjXJ/teU4r1Uqc6ZkQGx1baKlLEyTAuoNLu15MttIwWh/HJ54PzOI6TaCpuzYYUCaBPdunN+1Z302GveAHIPhfI5CvlrV2eoxOi5zOrHOk+sI="/>
<Property Name="ProjectExplorer\x64_Expanded_7_1" Type="string" Value=""/>
<Property Name="ProjectExplorer\x64_Expanded_8_1" Type="string" Value=""/>
<Property Name="RTPC\GameParameterSimValue\{2A6D7A97-7D45-4B6C-96EE-CCD56FA83773}" Type="Real64" Value="1"/>
<Property Name="RTPC\GameParameterSimValue\{5540312A-E87E-4E09-91E8-71A5D349779A}" Type="Real64" Value="1"/>
<Property Name="RTPC\GameParameterSimValue\{778F5836-B82F-46F8-936A-E8581E9DA7A9}" Type="Real64" Value="0"/>
<Property Name="RTPC\GameParameterSimValue\{B907BE30-367C-4955-AF3B-AF6B2806F9D5}" Type="Real64" Value="0"/>
<Property Name="RTPC\GameParameterSimValue\{BFC68604-C477-4B9B-B488-F5B4A9ADEAA4}" Type="Real64" Value="1"/>
<Property Name="RTPC\GameParameterSimValue\{FC156F43-A577-4DCB-B6E8-84E4072A38C9}" Type="Real64" Value="1"/>
<Property Name="SoundBank Manager\x64_Expand States" Type="string" Value="xyeeD8ziOk2gqbs2GFAmgcovZXyPfYtJoTFDl7G29GNcn+15TivVSpzpmRAbHVto"/>
<Property Name="SoundBankHeaderFilePath" Type="string" Value="GeneratedSoundBanks\"/>
<Property Name="SoundBankPaths" Type="string">
<ValueList>
<Value Platform="Mac">GeneratedSoundBanks\Mac\</Value>
<Value Platform="Windows">GeneratedSoundBanks\Windows\</Value>
</ValueList>
</Property>
<Property Name="SoundBankPostGenerateCustomCmdDescription" Type="string">
<ValueList>
<Value Platform="Mac">Copy Streamed Files</Value>
<Value Platform="Windows">Copy Streamed Files</Value>
</ValueList>
</Property>
<Property Name="SoundBankPostGenerateCustomCmdLines" Type="string">
<ValueList>
<Value Platform="Mac">"$(CopyStreamedFilesExePath)" -info "$(InfoFilePath)" -outputpath "$(SoundBankPath)" -banks "$(SoundBankListAsTextFile)" -languages "$(LanguageList)"</Value>
<Value Platform="Windows">"$(CopyStreamedFilesExePath)" -info "$(InfoFilePath)" -outputpath "$(SoundBankPath)" -banks "$(SoundBankListAsTextFile)" -languages "$(LanguageList)"</Value>
</ValueList>
</Property>
<Property Name="SoundBankPreGenerateCustomCmdDescription" Type="string">
<ValueList>
<Value Platform="Mac"></Value>
<Value Platform="Windows"></Value>
</ValueList>
</Property>
<Property Name="SoundBankPreGenerateCustomCmdLines" Type="string">
<ValueList>
<Value Platform="Mac"></Value>
<Value Platform="Windows"></Value>
</ValueList>
</Property>
<Property Name="Soundcaster\PreviousSession" Type="string" Value="{E9D3086E-C326-478C-BE46-668AB254BFE6}"/>
</PropertyList>
</UserProjectSettings>
</UserProjectSettingsInfo>
</WwiseDocument>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{E4509B27-E9F5-476A-9C34-332C4DF09EF7}" SchemaVersion="97">
<Attenuations>
<WorkUnit Name="Default Work Unit" ID="{E4509B27-E9F5-476A-9C34-332C4DF09EF7}" PersistMode="Standalone"/>
</Attenuations>
</WwiseDocument>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{9A1CEC8E-BF0E-473B-970B-316CB723C123}" SchemaVersion="97">
<AudioDevices>
<WorkUnit Name="Default Work Unit" ID="{9A1CEC8E-BF0E-473B-970B-316CB723C123}" PersistMode="Standalone">
<ChildrenList>
<AudioDevice Name="System" ID="{D1B034C9-6382-4AB1-B1CD-DE028A3209A3}" PluginName="System" CompanyID="0" PluginID="174" PluginType="7"/>
<AudioDevice Name="No_Output" ID="{F00FD215-1B46-46CB-9CC7-862E6DD91142}" PluginName="No Output" CompanyID="0" PluginID="181" PluginType="7"/>
</ChildrenList>
</WorkUnit>
</AudioDevices>
</WwiseDocument>

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{2A24BAF6-8BA7-4D58-B762-DC05E3FF0321}" SchemaVersion="97">
<ControlSurfaceSessions>
<WorkUnit Name="Default Work Unit" ID="{2A24BAF6-8BA7-4D58-B762-DC05E3FF0321}" PersistMode="Standalone">
<ChildrenList>
<ControlSurfaceSession Name="Default Control Surface Session" ID="{2451DD2E-A7B2-45E6-B93B-6F49B9B887AD}">
<ChildrenList>
<ControlSurfaceBindingGroup Name="Global" ID="{3C6556FA-C534-449D-B0A8-BA53A4596DB9}"/>
<ControlSurfaceBindingGroup Name="Current Selection" ID="{8668A8B1-408A-452B-9113-85EF5CF605FD}">
<PropertyList>
<Property Name="GroupType" Type="int16" Value="4"/>
</PropertyList>
<ChildrenList>
<ControlSurfaceBinding Name="Binding" ID="{D871D7E7-630E-44F4-B9DA-215EC887D89E}">
<PropertyList>
<Property Name="TargetName" Type="string" Value="PassMidiNote"/>
<Property Name="TargetType" Type="int16" Value="1"/>
</PropertyList>
</ControlSurfaceBinding>
</ChildrenList>
</ControlSurfaceBindingGroup>
<ControlSurfaceBindingGroup Name="View Groups" ID="{D60A6E92-9849-4D41-BC17-83905D98E00D}">
<PropertyList>
<Property Name="GroupType" Type="int16" Value="3"/>
</PropertyList>
</ControlSurfaceBindingGroup>
</ChildrenList>
</ControlSurfaceSession>
</ChildrenList>
</WorkUnit>
</ControlSurfaceSessions>
</WwiseDocument>

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{499CA58A-C702-47D8-B39A-D2BA9F895A1F}" SchemaVersion="97">
<Conversions>
<WorkUnit Name="Default Work Unit" ID="{499CA58A-C702-47D8-B39A-D2BA9F895A1F}" PersistMode="Standalone">
<ChildrenList>
<Conversion Name="Default Conversion Settings" ID="{6D1B890C-9826-4384-BF07-C15223E9FB56}">
<PropertyList>
<Property Name="Channels" Type="int32">
<ValueList>
<Value Platform="Mac">4</Value>
<Value Platform="Windows">4</Value>
</ValueList>
</Property>
<Property Name="LRMix" Type="Real64">
<ValueList>
<Value Platform="Mac">0</Value>
<Value Platform="Windows">0</Value>
</ValueList>
</Property>
<Property Name="MaxSampleRate" Type="int32">
<ValueList>
<Value Platform="Mac">0</Value>
<Value Platform="Windows">0</Value>
</ValueList>
</Property>
<Property Name="MinSampleRate" Type="int32">
<ValueList>
<Value Platform="Mac">0</Value>
<Value Platform="Windows">0</Value>
</ValueList>
</Property>
<Property Name="SampleRate" Type="int32">
<ValueList>
<Value Platform="Mac">0</Value>
<Value Platform="Windows">0</Value>
</ValueList>
</Property>
</PropertyList>
<ConversionPluginInfoList>
<ConversionPluginInfo Platform="Mac">
<ConversionPlugin Name="" ID="{49628B85-68D9-4A83-BA83-F706DE75CD00}" PluginName="PCM" CompanyID="0" PluginID="1"/>
</ConversionPluginInfo>
<ConversionPluginInfo Platform="Windows">
<ConversionPlugin Name="" ID="{0423D32E-625D-4FDB-A858-177C72DB8C04}" PluginName="PCM" CompanyID="0" PluginID="1"/>
</ConversionPluginInfo>
</ConversionPluginInfoList>
</Conversion>
</ChildrenList>
</WorkUnit>
</Conversions>
</WwiseDocument>

View file

@ -0,0 +1,505 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{F6B2880C-85E5-47FA-A126-645B5DFD9ACC}" SchemaVersion="97">
<Conversions>
<WorkUnit Name="Factory Conversion Settings" ID="{F6B2880C-85E5-47FA-A126-645B5DFD9ACC}" PersistMode="Standalone">
<ChildrenList>
<Folder Name="Vorbis" ID="{09A6A685-7B03-4387-A7EE-705C2A234D14}" ShortID="784869707">
<ChildrenList>
<Conversion Name="Vorbis Auto Detect High" ID="{6F6A99BE-4FD1-49D3-89AF-54BE30CA1588}">
<PropertyList>
<Property Name="Channels" Type="int32">
<ValueList>
<Value>4</Value>
</ValueList>
</Property>
<Property Name="LRMix" Type="Real64">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MaxSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MinSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="SampleRate" Type="int32">
<ValueList>
<Value>3</Value>
</ValueList>
</Property>
</PropertyList>
<ConversionPluginInfoList>
<ConversionPluginInfo>
<ConversionPlugin Name="" ID="{583FA2A1-CDD5-4189-9023-94B4094D7DEC}" PluginName="Vorbis" CompanyID="0" PluginID="4"/>
</ConversionPluginInfo>
</ConversionPluginInfoList>
</Conversion>
<Conversion Name="Vorbis Auto Detect Low" ID="{1FEA84A1-764D-42EF-9C6A-5EA06BFD8AD4}">
<PropertyList>
<Property Name="Channels" Type="int32">
<ValueList>
<Value>4</Value>
</ValueList>
</Property>
<Property Name="LRMix" Type="Real64">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MaxSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MinSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="SampleRate" Type="int32">
<ValueList>
<Value>1</Value>
</ValueList>
</Property>
</PropertyList>
<ConversionPluginInfoList>
<ConversionPluginInfo>
<ConversionPlugin Name="" ID="{2A237DA2-A54B-4B75-A6B3-547C8A1DFEF4}" PluginName="Vorbis" CompanyID="0" PluginID="4"/>
</ConversionPluginInfo>
</ConversionPluginInfoList>
</Conversion>
<Conversion Name="Vorbis Auto Detect Medium" ID="{C2242190-EA66-4EC8-A876-8713CEEBAAC8}">
<PropertyList>
<Property Name="Channels" Type="int32">
<ValueList>
<Value>4</Value>
</ValueList>
</Property>
<Property Name="LRMix" Type="Real64">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MaxSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MinSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="SampleRate" Type="int32">
<ValueList>
<Value>2</Value>
</ValueList>
</Property>
</PropertyList>
<ConversionPluginInfoList>
<ConversionPluginInfo>
<ConversionPlugin Name="" ID="{CE3D6243-B1F0-4450-B580-E12770D53F0C}" PluginName="Vorbis" CompanyID="0" PluginID="4"/>
</ConversionPluginInfo>
</ConversionPluginInfoList>
</Conversion>
<Conversion Name="Vorbis Quality High" ID="{53A9DE0F-3F4F-4B59-8614-3F9E3C7358FC}">
<PropertyList>
<Property Name="Channels" Type="int32">
<ValueList>
<Value>4</Value>
</ValueList>
</Property>
<Property Name="LRMix" Type="Real64">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MaxSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MinSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="SampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
</PropertyList>
<ConversionPluginInfoList>
<ConversionPluginInfo>
<ConversionPlugin Name="" ID="{5F534328-DF07-49B0-887C-991084AA5E11}" PluginName="Vorbis" CompanyID="0" PluginID="4"/>
</ConversionPluginInfo>
</ConversionPluginInfoList>
</Conversion>
<Conversion Name="Vorbis Quality Low" ID="{AD876647-70E5-492F-86C9-FFDED7CB6C9E}">
<PropertyList>
<Property Name="Channels" Type="int32">
<ValueList>
<Value>4</Value>
</ValueList>
</Property>
<Property Name="LRMix" Type="Real64">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MaxSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MinSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="SampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
</PropertyList>
<ConversionPluginInfoList>
<ConversionPluginInfo>
<ConversionPlugin Name="" ID="{11DBC1C6-A78D-4340-A59D-B04ED52562C0}" PluginName="Vorbis" CompanyID="0" PluginID="4">
<PropertyList>
<Property Name="QualityFactor" Type="Real32" Value="0"/>
</PropertyList>
</ConversionPlugin>
</ConversionPluginInfo>
</ConversionPluginInfoList>
</Conversion>
<Conversion Name="Vorbis Quality Medium" ID="{01D20CF3-9A4C-4EEE-AE3E-2942B47543E9}">
<PropertyList>
<Property Name="Channels" Type="int32">
<ValueList>
<Value>4</Value>
</ValueList>
</Property>
<Property Name="LRMix" Type="Real64">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MaxSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MinSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="SampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
</PropertyList>
<ConversionPluginInfoList>
<ConversionPluginInfo>
<ConversionPlugin Name="" ID="{5D3B8B77-1CFD-4851-92C4-9B400886675D}" PluginName="Vorbis" CompanyID="0" PluginID="4">
<PropertyList>
<Property Name="QualityFactor" Type="Real32" Value="2"/>
</PropertyList>
</ConversionPlugin>
</ConversionPluginInfo>
</ConversionPluginInfoList>
</Conversion>
</ChildrenList>
</Folder>
<Folder Name="PCM" ID="{A6753C13-8ABB-4028-8D96-7483524C2618}" ShortID="764934186">
<ChildrenList>
<Conversion Name="PCM As Input" ID="{E712A671-9647-44EF-8B1B-857926D620F0}">
<PropertyList>
<Property Name="Channels" Type="int32">
<ValueList>
<Value>4</Value>
</ValueList>
</Property>
<Property Name="LRMix" Type="Real64">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MaxSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MinSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="SampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
</PropertyList>
<ConversionPluginInfoList>
<ConversionPluginInfo>
<ConversionPlugin Name="" ID="{2B70EAA4-7E9B-4E6F-BB75-85B7E1D6F15D}" PluginName="PCM" CompanyID="0" PluginID="1"/>
</ConversionPluginInfo>
</ConversionPluginInfoList>
</Conversion>
<Conversion Name="PCM Auto Detect Low" ID="{A55FD918-6235-455C-86B9-2C828B495C21}">
<PropertyList>
<Property Name="Channels" Type="int32">
<ValueList>
<Value>4</Value>
</ValueList>
</Property>
<Property Name="LRMix" Type="Real64">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MaxSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MinSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="SampleRate" Type="int32">
<ValueList>
<Value>1</Value>
</ValueList>
</Property>
</PropertyList>
<ConversionPluginInfoList>
<ConversionPluginInfo>
<ConversionPlugin Name="" ID="{A5F727CF-2EB8-4502-B32F-EEA11B9EF5F2}" PluginName="PCM" CompanyID="0" PluginID="1"/>
</ConversionPluginInfo>
</ConversionPluginInfoList>
</Conversion>
<Conversion Name="PCM Auto Detect Medium" ID="{E24008BC-4813-4D98-B020-6F66B2750A16}">
<PropertyList>
<Property Name="Channels" Type="int32">
<ValueList>
<Value>4</Value>
</ValueList>
</Property>
<Property Name="LRMix" Type="Real64">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MaxSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MinSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="SampleRate" Type="int32">
<ValueList>
<Value>2</Value>
</ValueList>
</Property>
</PropertyList>
<ConversionPluginInfoList>
<ConversionPluginInfo>
<ConversionPlugin Name="" ID="{E12A0D67-4EF6-4C93-BC15-E3EADC1694EF}" PluginName="PCM" CompanyID="0" PluginID="1"/>
</ConversionPluginInfo>
</ConversionPluginInfoList>
</Conversion>
<Conversion Name="PCM Auto Detect High" ID="{103058FA-B09E-42AF-A6BA-B711CD85E365}">
<PropertyList>
<Property Name="Channels" Type="int32">
<ValueList>
<Value>4</Value>
</ValueList>
</Property>
<Property Name="LRMix" Type="Real64">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MaxSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MinSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="SampleRate" Type="int32">
<ValueList>
<Value>3</Value>
</ValueList>
</Property>
</PropertyList>
<ConversionPluginInfoList>
<ConversionPluginInfo>
<ConversionPlugin Name="" ID="{24BCE860-04AB-430D-BBC0-6D29BB36FD36}" PluginName="PCM" CompanyID="0" PluginID="1"/>
</ConversionPluginInfo>
</ConversionPluginInfoList>
</Conversion>
</ChildrenList>
</Folder>
<Folder Name="ADPCM" ID="{C43B4493-F987-4828-ACDF-DAEAA7564E0A}" ShortID="637086534">
<ChildrenList>
<Conversion Name="ADPCM As Input" ID="{844B7EC4-A8FF-43AD-821E-25B9459D4413}">
<PropertyList>
<Property Name="Channels" Type="int32">
<ValueList>
<Value>4</Value>
</ValueList>
</Property>
<Property Name="LRMix" Type="Real64">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MaxSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MinSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="SampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
</PropertyList>
<ConversionPluginInfoList>
<ConversionPluginInfo>
<ConversionPlugin Name="" ID="{F957F696-06B2-4AAD-8094-A2A4A44CCB2B}" PluginName="ADPCM" CompanyID="0" PluginID="2"/>
</ConversionPluginInfo>
</ConversionPluginInfoList>
</Conversion>
<Conversion Name="ADPCM Auto Detect Low" ID="{638FA3E7-2C90-478F-AA99-4F8EC86F3356}">
<PropertyList>
<Property Name="Channels" Type="int32">
<ValueList>
<Value>4</Value>
</ValueList>
</Property>
<Property Name="LRMix" Type="Real64">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MaxSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MinSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="SampleRate" Type="int32">
<ValueList>
<Value>1</Value>
</ValueList>
</Property>
</PropertyList>
<ConversionPluginInfoList>
<ConversionPluginInfo>
<ConversionPlugin Name="" ID="{8EB43490-15BF-4285-B695-201E939203E2}" PluginName="ADPCM" CompanyID="0" PluginID="2"/>
</ConversionPluginInfo>
</ConversionPluginInfoList>
</Conversion>
<Conversion Name="ADPCM Auto Detect Medium" ID="{1ADE8C79-CF88-45CD-8D8B-13FFA25639DF}">
<PropertyList>
<Property Name="Channels" Type="int32">
<ValueList>
<Value>4</Value>
</ValueList>
</Property>
<Property Name="LRMix" Type="Real64">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MaxSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MinSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="SampleRate" Type="int32">
<ValueList>
<Value>2</Value>
</ValueList>
</Property>
</PropertyList>
<ConversionPluginInfoList>
<ConversionPluginInfo>
<ConversionPlugin Name="" ID="{9F292659-A320-4220-A063-F5327A1400DF}" PluginName="ADPCM" CompanyID="0" PluginID="2"/>
</ConversionPluginInfo>
</ConversionPluginInfoList>
</Conversion>
<Conversion Name="ADPCM Auto Detect High" ID="{6D51666E-9C7A-40E4-831D-C445512EBABD}">
<PropertyList>
<Property Name="Channels" Type="int32">
<ValueList>
<Value>4</Value>
</ValueList>
</Property>
<Property Name="LRMix" Type="Real64">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MaxSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="MinSampleRate" Type="int32">
<ValueList>
<Value>0</Value>
</ValueList>
</Property>
<Property Name="SampleRate" Type="int32">
<ValueList>
<Value>3</Value>
</ValueList>
</Property>
</PropertyList>
<ConversionPluginInfoList>
<ConversionPluginInfo>
<ConversionPlugin Name="" ID="{35F3A4A0-4263-45FE-B829-7AB849D3403C}" PluginName="ADPCM" CompanyID="0" PluginID="2"/>
</ConversionPluginInfo>
</ConversionPluginInfoList>
</Conversion>
</ChildrenList>
</Folder>
</ChildrenList>
</WorkUnit>
</Conversions>
</WwiseDocument>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{6E3DFC07-EBF2-4CE7-92DC-9339D30A48C1}" SchemaVersion="97">
<DynamicDialogue>
<WorkUnit Name="Default Work Unit" ID="{6E3DFC07-EBF2-4CE7-92DC-9339D30A48C1}" PersistMode="Standalone"/>
</DynamicDialogue>
</WwiseDocument>

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{BF6DB01C-A79D-4B99-89E5-E47246AD6F26}" SchemaVersion="97">
<Effects>
<WorkUnit Name="Default Work Unit" ID="{BF6DB01C-A79D-4B99-89E5-E47246AD6F26}" PersistMode="Standalone">
<ChildrenList>
<Effect Name="New_Wwise_Parametric_EQ_temp" ID="{C1731515-6D82-4599-90BB-03722BD2D338}" PluginName="Wwise Parametric EQ" CompanyID="0" PluginID="105" PluginType="3"/>
<Effect Name="New_Wwise_Parametric_EQ_reactor" ID="{B4E9590D-FCB4-4DFC-8FEC-25C46C0B4350}" PluginName="Wwise Parametric EQ" CompanyID="0" PluginID="105" PluginType="3">
<PropertyList>
<Property Name="FrequencyBand1" Type="Real32" Value="230"/>
<Property Name="FrequencyBand3" Type="Real32" Value="2639"/>
<Property Name="GainBand1" Type="Real32" Value="-4"/>
<Property Name="GainBand2" Type="Real32" Value="-3.9"/>
<Property Name="GainBand3" Type="Real32" Value="-1.1"/>
</PropertyList>
</Effect>
<Effect Name="New_Wwise_Parametric_EQ_reactor_low" ID="{AC0ED82A-51E5-48E0-B537-EE39864BAC5F}" PluginName="Wwise Parametric EQ" CompanyID="0" PluginID="105" PluginType="3">
<PropertyList>
<Property Name="FrequencyBand1" Type="Real32" Value="169"/>
<Property Name="FrequencyBand3" Type="Real32" Value="2507"/>
<Property Name="GainBand1" Type="Real32" Value="-9.2"/>
<Property Name="GainBand3" Type="Real32" Value="-3"/>
</PropertyList>
</Effect>
</ChildrenList>
</WorkUnit>
</Effects>
</WwiseDocument>

View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{E3C68D3D-9917-4B0D-9D0A-E904D7B8FCF7}" SchemaVersion="97">
<Events>
<WorkUnit Name="Ambience" ID="{E3C68D3D-9917-4B0D-9D0A-E904D7B8FCF7}" PersistMode="Standalone">
<ChildrenList>
<Event Name="Amb_BaseDrone" ID="{715CB4B5-1708-481E-92C4-BA55066D2E09}">
<ChildrenList>
<Action Name="" ID="{BFE8B933-FB72-4200-871B-9748AFCEC0C3}" ShortID="559828886">
<PropertyList>
<Property Name="FadeInCurve" Type="int16" Value="2"/>
<Property Name="FadeTime" Type="Real64" Value="2"/>
</PropertyList>
<ReferenceList>
<Reference Name="Target">
<ObjectRef Name="Amb BaseDrone" ID="{E45BD333-61F1-48C2-A4C1-ED9A5105C648}" WorkUnitID="{CA5C9672-2298-4659-B32D-52AE660403FF}"/>
</Reference>
</ReferenceList>
</Action>
</ChildrenList>
</Event>
<Event Name="Amb_Low_Oneshots" ID="{9CDB4529-2114-41EB-B4C4-0DDB7CF529F3}">
<ChildrenList>
<Action Name="" ID="{C132231B-E456-4682-AD75-527BB7543814}" ShortID="436691486">
<ReferenceList>
<Reference Name="Target">
<ObjectRef Name="Amb Low Oneshots" ID="{F53FC63D-6B1A-46B0-B21C-4CC28B7F26E1}" WorkUnitID="{CA5C9672-2298-4659-B32D-52AE660403FF}"/>
</Reference>
</ReferenceList>
</Action>
</ChildrenList>
</Event>
<Event Name="Amb_Mid_and_High_Oneshots" ID="{90C1B4E6-D445-425B-BA9B-CFB9BCB2663E}">
<ChildrenList>
<Action Name="" ID="{44905170-3FD7-428C-8953-91EF59DC3BF8}" ShortID="738693320">
<ReferenceList>
<Reference Name="Target">
<ObjectRef Name="Amb Mid and High Oneshots" ID="{B05F1CCC-B463-451A-ACD9-2101E5F13B5A}" WorkUnitID="{CA5C9672-2298-4659-B32D-52AE660403FF}"/>
</Reference>
</ReferenceList>
</Action>
</ChildrenList>
</Event>
<Event Name="Amb_Weatherlike_Oneshots" ID="{29C05807-68C5-4524-8E31-C48F854602E2}">
<ChildrenList>
<Action Name="" ID="{0263A190-C222-4295-A8A4-90CD4ECA1149}" ShortID="994530492">
<ReferenceList>
<Reference Name="Target">
<ObjectRef Name="Amb Weatherlike Oneshots" ID="{9773EA84-27DC-45E0-8D4E-8D26137A7698}" WorkUnitID="{CA5C9672-2298-4659-B32D-52AE660403FF}"/>
</Reference>
</ReferenceList>
</Action>
</ChildrenList>
</Event>
</ChildrenList>
</WorkUnit>
</Events>
</WwiseDocument>

View file

@ -0,0 +1,141 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{1140EE06-2469-4795-9250-0C72BFFAEF1B}" SchemaVersion="97">
<Events>
<WorkUnit Name="Default Work Unit" ID="{1140EE06-2469-4795-9250-0C72BFFAEF1B}" PersistMode="Standalone">
<ChildrenList>
<Event Name="Equip" ID="{44FA1280-D160-4DB5-B7EB-AA6E4CFBB17A}">
<ChildrenList>
<Action Name="" ID="{8C57B571-8CF2-444C-B746-D1C77FC96D79}" ShortID="862807673">
<ReferenceList>
<Reference Name="Target">
<ObjectRef Name="equip" ID="{BD74AFA5-47B1-41A7-B0BE-7677521DA18B}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}"/>
</Reference>
</ReferenceList>
</Action>
</ChildrenList>
</Event>
<Event Name="Unequip" ID="{3125BD51-8FFC-4E24-BD2E-D102896B8A0B}">
<ChildrenList>
<Action Name="" ID="{FDED5AD3-C08C-4900-B1E7-B1C2994580E2}" ShortID="47705295">
<ReferenceList>
<Reference Name="Target">
<ObjectRef Name="unequip" ID="{0D9341D7-18F3-491F-80E7-D6F5932B87CB}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}"/>
</Reference>
</ReferenceList>
</Action>
</ChildrenList>
</Event>
<Event Name="Drop" ID="{507ECF50-2D22-4222-A984-367EF0411E8F}">
<ChildrenList>
<Action Name="" ID="{BD2153D0-4236-49AB-B10E-385AC4BEF940}" ShortID="991952028">
<ReferenceList>
<Reference Name="Target">
<ObjectRef Name="putdown" ID="{1A9860A7-4977-4EAA-8A81-437C77FEF187}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}"/>
</Reference>
</ReferenceList>
</Action>
</ChildrenList>
</Event>
<Event Name="Pickup" ID="{A64914D4-7F8A-4586-BB48-8FA3137E2F1F}">
<ChildrenList>
<Action Name="" ID="{6EFACB7F-BB15-4A0A-B8C5-7C8865AEB847}" ShortID="114453170">
<ReferenceList>
<Reference Name="Target">
<ObjectRef Name="pickup" ID="{0324A4A7-54F5-43FB-ABFC-5396A50D2665}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}"/>
</Reference>
</ReferenceList>
</Action>
</ChildrenList>
</Event>
<Event Name="UI_Success" ID="{0E497EDC-A42D-4478-AF76-5114508A5B8B}">
<ChildrenList>
<Action Name="" ID="{7CD852A7-C2C5-41E1-98AC-56B3078D2746}" ShortID="994295438">
<ReferenceList>
<Reference Name="Target">
<ObjectRef Name="blip1" ID="{A0D4AB7B-D2D9-45FA-8ADF-638A67BCE5C0}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}"/>
</Reference>
</ReferenceList>
</Action>
</ChildrenList>
</Event>
<Event Name="UI_Fail" ID="{7DB8939E-DD1B-4B8C-B787-8C4440712523}">
<ChildrenList>
<Action Name="" ID="{F53B7E15-FACF-4FDB-B92F-2A31530CB37F}" ShortID="728082083">
<ReferenceList>
<Reference Name="Target">
<ObjectRef Name="blip2" ID="{B86B7D38-A9E2-4F1D-AF93-9256FEBA8749}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}"/>
</Reference>
</ReferenceList>
</Action>
</ChildrenList>
</Event>
<Event Name="Dock" ID="{CC95135D-D17F-4088-B23C-BF8238FCA221}">
<ChildrenList>
<Action Name="" ID="{8484998A-D398-4F8E-9028-6F4986F8C9BB}" ShortID="1067480388">
<ReferenceList>
<Reference Name="Target">
<ObjectRef Name="Aetheria_docking_spaceship_V2_1" ID="{032A1D4F-3BD6-4AC7-B619-799C3E968EC3}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}"/>
</Reference>
</ReferenceList>
</Action>
</ChildrenList>
</Event>
<Event Name="Undock" ID="{89374ED6-A730-48E4-AAF7-0E1C575CE1C2}">
<ChildrenList>
<Action Name="" ID="{5209E7B6-5FBB-483C-B04D-EE58DA12145A}" ShortID="968319449">
<ReferenceList>
<Reference Name="Target">
<ObjectRef Name="Aetheria_undocking_spaceship_V2_1" ID="{8545BD8D-4232-45EC-B3BF-9FD8B4AE3598}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}"/>
</Reference>
</ReferenceList>
</Action>
</ChildrenList>
</Event>
<Event Name="Dock_Fail" ID="{BE28B971-A0E1-4D96-B576-1B68C4CA5AA9}">
<ChildrenList>
<Action Name="" ID="{FDB56FA3-430B-4AA0-A91C-973CDCC6111F}" ShortID="70313087">
<ReferenceList>
<Reference Name="Target">
<ObjectRef Name="blip2" ID="{B86B7D38-A9E2-4F1D-AF93-9256FEBA8749}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}"/>
</Reference>
</ReferenceList>
</Action>
</ChildrenList>
</Event>
<Event Name="Wormhole_jump_in" ID="{A0DC84D8-679B-4055-834E-C4AB1E6D9323}">
<ChildrenList>
<Action Name="" ID="{A93333F4-A9D9-42C2-8F38-702FA6442D25}" ShortID="462209697">
<ReferenceList>
<Reference Name="Target">
<ObjectRef Name="Aetheria_Wormhole_travel_jumpin" ID="{393D96D3-4D2B-42BB-827D-10984647051F}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}"/>
</Reference>
</ReferenceList>
</Action>
</ChildrenList>
</Event>
<Event Name="wormhole_travel_loop" ID="{327B0775-AF04-45E9-B12C-6C0A23004D68}">
<ChildrenList>
<Action Name="" ID="{43AC117A-3D8E-4883-91AC-E56D9AD24801}" ShortID="344520343">
<ReferenceList>
<Reference Name="Target">
<ObjectRef Name="Aetheria_Wormhole_travel" ID="{0EE417AB-A0FE-4E79-8031-FBE3B604C3AB}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}"/>
</Reference>
</ReferenceList>
</Action>
</ChildrenList>
</Event>
<Event Name="wormhole_jump_out" ID="{0673D570-EC52-4956-B47F-5BC475263F42}">
<ChildrenList>
<Action Name="" ID="{C50E241F-D904-47E2-B748-516EF8D7E79B}" ShortID="1060804557">
<ReferenceList>
<Reference Name="Target">
<ObjectRef Name="Aetheria_Wormhole_travel_jumpout" ID="{290B97E7-B057-42A4-9BCE-33220B7473A8}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}"/>
</Reference>
</ReferenceList>
</Action>
</ChildrenList>
</Event>
</ChildrenList>
</WorkUnit>
</Events>
</WwiseDocument>

View file

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{0A7217B3-52C7-4A0C-9966-F5AF7ECC98F0}" SchemaVersion="97">
<Events>
<WorkUnit Name="Ship Audio" ID="{0A7217B3-52C7-4A0C-9966-F5AF7ECC98F0}" PersistMode="Standalone">
<ChildrenList>
<Event Name="test_spaship_full_unit" ID="{5E7D0CDC-6E2B-49AE-89E9-B39335CE8CAF}">
<ChildrenList>
<Action Name="" ID="{D6F2996A-8361-4D78-88C6-D60D15DD2F0E}" ShortID="692204141">
<ReferenceList>
<Reference Name="Target">
<ObjectRef Name="thruster_large" ID="{35564C2D-1BC9-40BB-B365-D9C4A47AE5F1}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}"/>
</Reference>
</ReferenceList>
</Action>
<Action Name="" ID="{B3F56B03-9140-4EF5-899C-4752BAFA8ADC}" ShortID="26652537">
<ReferenceList>
<Reference Name="Target">
<ObjectRef Name="reactor_type1" ID="{3AA3BAA8-A42D-4EAE-B9BD-BC64D22B2753}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}"/>
</Reference>
</ReferenceList>
</Action>
<Action Name="" ID="{F165B595-D783-4B56-A5AF-5DD85C00DEF8}" ShortID="97635930">
<ReferenceList>
<Reference Name="Target">
<ObjectRef Name="spaceship_reactor_overheat_alarm" ID="{C9F5C84F-7414-4607-B8F0-8734CCB51061}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}"/>
</Reference>
</ReferenceList>
</Action>
</ChildrenList>
</Event>
<Event Name="thruster_medium" ID="{0F30DB3D-6A97-4659-BA04-13A267DC19EC}">
<ChildrenList>
<Action Name="" ID="{9A7AD34B-FB31-46CE-8C31-D47E3DEF198A}" ShortID="644831442">
<ReferenceList>
<Reference Name="Target">
<ObjectRef Name="thruster_medium" ID="{4261D6D4-BDC3-4423-BA6B-A92DE2181A21}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}"/>
</Reference>
</ReferenceList>
</Action>
</ChildrenList>
</Event>
<Event Name="thruster_large" ID="{99504764-B63E-48E8-A9A0-717489DD3466}">
<ChildrenList>
<Action Name="" ID="{AE15152D-2BC4-4BE2-8CC1-CE46A85F2139}" ShortID="131458033">
<ReferenceList>
<Reference Name="Target">
<ObjectRef Name="thruster_large" ID="{35564C2D-1BC9-40BB-B365-D9C4A47AE5F1}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}"/>
</Reference>
</ReferenceList>
</Action>
</ChildrenList>
</Event>
<Event Name="thruster_small" ID="{63E66AEC-CFE1-4362-B99D-280AC043C538}">
<ChildrenList>
<Action Name="" ID="{E83A8A35-D933-48D9-BE5B-F2895CD0D141}" ShortID="13039791">
<ReferenceList>
<Reference Name="Target">
<ObjectRef Name="thruster_small" ID="{D355F5AE-732C-4638-988F-F1E01D1FC547}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}"/>
</Reference>
</ReferenceList>
</Action>
</ChildrenList>
</Event>
<Event Name="reactor_type01" ID="{8A0DAB86-FAC0-4D8A-91E6-DD9E0C93259C}">
<ChildrenList>
<Action Name="" ID="{1C3C46F3-1B59-4901-9585-6DF5F12B205B}" ShortID="218777135">
<ReferenceList>
<Reference Name="Target">
<ObjectRef Name="reactor_type1" ID="{3AA3BAA8-A42D-4EAE-B9BD-BC64D22B2753}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}"/>
</Reference>
</ReferenceList>
</Action>
<Action Name="" ID="{A69EAE78-9686-46D0-B4A1-8887A6544FD9}" ShortID="606916448">
<ReferenceList>
<Reference Name="Target">
<ObjectRef Name="spaceship_reactor_overheat_alarm" ID="{C9F5C84F-7414-4607-B8F0-8734CCB51061}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}"/>
</Reference>
</ReferenceList>
</Action>
</ChildrenList>
</Event>
</ChildrenList>
</WorkUnit>
</Events>
</WwiseDocument>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{C8D9A06E-7120-4CF8-A17F-E8CFFAB05ED0}" SchemaVersion="97">
<GameParameters>
<WorkUnit Name="Default Work Unit" ID="{C8D9A06E-7120-4CF8-A17F-E8CFFAB05ED0}" PersistMode="Standalone"/>
</GameParameters>
</WwiseDocument>

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{02152354-45CF-4389-B435-5367FA80C881}" SchemaVersion="97">
<GameParameters>
<WorkUnit Name="item_properties" ID="{02152354-45CF-4389-B435-5367FA80C881}" PersistMode="Standalone">
<ChildrenList>
<GameParameter Name="ship_velocity" ID="{B907BE30-367C-4955-AF3B-AF6B2806F9D5}">
<PropertyList>
<Property Name="InitialValue" Type="Real64" Value="0"/>
</PropertyList>
</GameParameter>
<GameParameter Name="performance_quality" ID="{BFC68604-C477-4B9B-B488-F5B4A9ADEAA4}">
<PropertyList>
<Property Name="InitialValue" Type="Real64" Value="1"/>
<Property Name="Max" Type="Real64" Value="1"/>
</PropertyList>
</GameParameter>
<GameParameter Name="performance_durability" ID="{5540312A-E87E-4E09-91E8-71A5D349779A}">
<PropertyList>
<Property Name="InitialValue" Type="Real64" Value="1"/>
<Property Name="Max" Type="Real64" Value="1"/>
</PropertyList>
</GameParameter>
<GameParameter Name="performance_temperature" ID="{778F5836-B82F-46F8-936A-E8581E9DA7A9}">
<PropertyList>
<Property Name="InitialValue" Type="Real64" Value="0"/>
<Property Name="Max" Type="Real64" Value="1"/>
</PropertyList>
</GameParameter>
<GameParameter Name="reactor_load" ID="{FC156F43-A577-4DCB-B6E8-84E4072A38C9}">
<PropertyList>
<Property Name="InitialValue" Type="Real64" Value="1"/>
<Property Name="Max" Type="Real64" Value="1"/>
</PropertyList>
</GameParameter>
<GameParameter Name="thruster_throttle" ID="{2A6D7A97-7D45-4B6C-96EE-CCD56FA83773}">
<PropertyList>
<Property Name="InitialValue" Type="Real64" Value="1"/>
<Property Name="Max" Type="Real64" Value="1"/>
</PropertyList>
</GameParameter>
</ChildrenList>
</WorkUnit>
</GameParameters>
</WwiseDocument>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{C19571A5-0138-450D-A632-8F0C50E7826B}" SchemaVersion="97">
<InteractiveMusic>
<WorkUnit Name="Default Work Unit" ID="{C19571A5-0138-450D-A632-8F0C50E7826B}" PersistMode="Standalone"/>
</InteractiveMusic>
</WwiseDocument>

View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{A840C15E-F723-46FB-A6CF-9D8CDA869AEE}" SchemaVersion="97">
<Busses>
<WorkUnit Name="Ambience MM Unit" ID="{A840C15E-F723-46FB-A6CF-9D8CDA869AEE}" PersistMode="Standalone">
<ChildrenList>
<Bus Name="Ambience Bus" ID="{32BF429D-C1DC-4D83-BB3E-074A4305E69E}">
<ReferenceList>
<Reference Name="AudioDevice">
<ObjectRef Name="System" ID="{D1B034C9-6382-4AB1-B1CD-DE028A3209A3}" WorkUnitID="{9A1CEC8E-BF0E-473B-970B-316CB723C123}"/>
</Reference>
</ReferenceList>
<ChildrenList>
<Bus Name="Depth Reverb" ID="{02A8082A-7457-40EE-9DA7-4D7BE13562EB}">
<PropertyList>
<Property Name="OutputBusLowpass" Type="int16">
<ValueList>
<Value>20</Value>
</ValueList>
</Property>
</PropertyList>
<ReferenceList>
<Reference Name="Effect0" PluginName="Wwise RoomVerb" CompanyID="0" PluginID="118" PluginType="3">
<ObjectRef Name="Plate_Large" ID="{62CB7384-421D-4F9A-9CF1-E90F7336A07B}" WorkUnitID="{E8613F7D-BAD3-45CD-A3ED-505576F31277}"/>
</Reference>
<Reference Name="UserAuxSend0">
<ObjectRef Name="Space_Reverb" ID="{BBC34EE4-5BCA-4840-A54A-7E82E4030CDA}" WorkUnitID="{A840C15E-F723-46FB-A6CF-9D8CDA869AEE}"/>
</Reference>
</ReferenceList>
</Bus>
<AuxBus Name="Space_Reverb" ID="{BBC34EE4-5BCA-4840-A54A-7E82E4030CDA}">
<PropertyList>
<Property Name="OutputBusLowpass" Type="int16">
<ValueList>
<Value>40</Value>
</ValueList>
</Property>
</PropertyList>
<ReferenceList>
<Reference Name="Effect0" PluginName="Wwise RoomVerb" CompanyID="0" PluginID="118" PluginType="3">
<ObjectRef Name="Space_Garage" ID="{1AF92D84-49DD-4BBC-8F29-6C6D10082B6E}" WorkUnitID="{E8613F7D-BAD3-45CD-A3ED-505576F31277}"/>
</Reference>
</ReferenceList>
</AuxBus>
</ChildrenList>
</Bus>
</ChildrenList>
</WorkUnit>
</Busses>
</WwiseDocument>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{1F437DD1-6486-4464-8395-A927FAD94FB4}" SchemaVersion="97">
<Busses>
<WorkUnit Name="Default Work Unit" ID="{1F437DD1-6486-4464-8395-A927FAD94FB4}" PersistMode="Standalone">
<ChildrenList>
<Bus Name="Master Audio Bus" ID="{1514A4D8-1DA6-412A-A17E-75CA0C2149F3}">
<ReferenceList>
<Reference Name="AudioDevice">
<ObjectRef Name="System" ID="{D1B034C9-6382-4AB1-B1CD-DE028A3209A3}" WorkUnitID="{9A1CEC8E-BF0E-473B-970B-316CB723C123}"/>
</Reference>
</ReferenceList>
</Bus>
</ChildrenList>
</WorkUnit>
</Busses>
</WwiseDocument>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{BE73D363-43A2-442B-B02C-B9B4B2C13A8A}" SchemaVersion="97">
<MixingSessions>
<WorkUnit Name="Default Work Unit" ID="{BE73D363-43A2-442B-B02C-B9B4B2C13A8A}" PersistMode="Standalone"/>
</MixingSessions>
</WwiseDocument>

View file

@ -0,0 +1,130 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{C82BF7F2-D931-4045-B4AA-C026BEE07171}" SchemaVersion="97">
<Modulators>
<WorkUnit Name="Default Work Unit" ID="{C82BF7F2-D931-4045-B4AA-C026BEE07171}" PersistMode="Standalone">
<ChildrenList>
<ModulatorLfo Name="New Modulator LFO_random" ID="{2451769E-2FDE-4872-B2C6-2FB7AE0E8DBE}" ShortID="858176646">
<PropertyList>
<Property Name="LfoDepth" Type="Real64" Value="57.9"/>
<Property Name="LfoFrequency" Type="Real64" Value="1.78"/>
<Property Name="LfoWaveform" Type="int32" Value="5"/>
</PropertyList>
</ModulatorLfo>
<ModulatorLfo Name="LFO_durability" ID="{67C35EF9-BCE2-4304-95F2-8027121F6D2B}" ShortID="995081500">
<PropertyList>
<Property Name="LfoDepth" Type="Real64" Value="0">
<ModifierList>
<ModifierInfo>
<Modifier Name="" ID="{3EFC0859-6D14-4DD6-9C99-6B88300F3699}">
<PropertyList>
<Property Name="Enabled" Type="bool" Value="True"/>
</PropertyList>
</Modifier>
</ModifierInfo>
</ModifierList>
<RTPCList>
<RTPC Name="" ID="{D4C001EF-8257-4BF6-A84C-07E881969789}" ShortID="974228159">
<ReferenceList>
<Reference Name="ControlInput">
<ObjectRef Name="performance_durability" ID="{5540312A-E87E-4E09-91E8-71A5D349779A}" WorkUnitID="{02152354-45CF-4389-B435-5367FA80C881}"/>
</Reference>
</ReferenceList>
<Curve Name="" ID="{D6A01942-6B11-4E11-BD65-BC257D3B5AE8}">
<PropertyList>
<Property Name="Flags" Type="int32" Value="1"/>
</PropertyList>
<PointList>
<Point>
<XPos>0</XPos>
<YPos>86.87259</YPos>
<Flags>5</Flags>
</Point>
<Point>
<XPos>0.153703703703704</XPos>
<YPos>79.9560225925926</YPos>
<Flags>0</Flags>
</Point>
<Point>
<XPos>0.45</XPos>
<YPos>41.31703</YPos>
<Flags>0</Flags>
</Point>
<Point>
<XPos>1</XPos>
<YPos>0</YPos>
<Flags>37</Flags>
</Point>
</PointList>
</Curve>
</RTPC>
</RTPCList>
</Property>
<Property Name="LfoFrequency" Type="Real64" Value="7.32"/>
<Property Name="LfoWaveform" Type="int32" Value="5"/>
</PropertyList>
</ModulatorLfo>
<ModulatorLfo Name="New Modulator LFO_reactor_dur" ID="{C69F4ABE-3072-4370-AB85-36C0ADE1DD5C}" ShortID="1022088505">
<PropertyList>
<Property Name="LfoDepth" Type="Real64" Value="0">
<RTPCList>
<RTPC Name="" ID="{E8956056-D7D4-4D89-8619-3AD841EB45A6}" ShortID="1023085858">
<ReferenceList>
<Reference Name="ControlInput">
<ObjectRef Name="performance_durability" ID="{5540312A-E87E-4E09-91E8-71A5D349779A}" WorkUnitID="{02152354-45CF-4389-B435-5367FA80C881}"/>
</Reference>
</ReferenceList>
<Curve Name="" ID="{20BF2639-7255-47A5-980B-C248BE64AE89}">
<PropertyList>
<Property Name="Flags" Type="int32" Value="1"/>
</PropertyList>
<PointList>
<Point>
<XPos>0</XPos>
<YPos>20</YPos>
<Flags>5</Flags>
</Point>
<Point>
<XPos>1</XPos>
<YPos>0</YPos>
<Flags>37</Flags>
</Point>
</PointList>
</Curve>
</RTPC>
</RTPCList>
</Property>
<Property Name="LfoFrequency" Type="Real64" Value="1.46">
<RTPCList>
<RTPC Name="" ID="{CB7A6950-C043-4BE8-986D-8D3897D1BD68}" ShortID="1061394024">
<ReferenceList>
<Reference Name="ControlInput">
<ObjectRef Name="performance_durability" ID="{5540312A-E87E-4E09-91E8-71A5D349779A}" WorkUnitID="{02152354-45CF-4389-B435-5367FA80C881}"/>
</Reference>
</ReferenceList>
<Curve Name="" ID="{EDAA33F0-8E90-46D3-9E8E-85AE2CAE8EC9}">
<PropertyList>
<Property Name="Flags" Type="int32" Value="5"/>
</PropertyList>
<PointList>
<Point>
<XPos>0</XPos>
<YPos>6</YPos>
<Flags>5</Flags>
</Point>
<Point>
<XPos>1</XPos>
<YPos>1</YPos>
<Flags>37</Flags>
</Point>
</PointList>
</Curve>
</RTPC>
</RTPCList>
</Property>
<Property Name="LfoWaveform" Type="int32" Value="5"/>
</PropertyList>
</ModulatorLfo>
</ChildrenList>
</WorkUnit>
</Modulators>
</WwiseDocument>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{EBA9925B-9470-4A48-81BE-34A670A046A4}" SchemaVersion="97">
<Presets>
<WorkUnit Name="Default Work Unit" ID="{EBA9925B-9470-4A48-81BE-34A670A046A4}" PersistMode="Standalone"/>
</Presets>
</WwiseDocument>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{F7AD8DCD-60E6-4873-9A71-65A083815696}" SchemaVersion="97">
<Presets>
<WorkUnit Name="Factory Spatial Audio" ID="{F7AD8DCD-60E6-4873-9A71-65A083815696}" PersistMode="Standalone">
<ChildrenList>
<AuxBus Name="Room Auxiliary Bus" ID="{C5616E96-F524-4E03-8F50-909FE35BE12D}">
<PropertyList>
<Property Name="3DSpatialization" Type="int16" Value="2"/>
<Property Name="EnableDiffraction" Type="bool" Value="True"/>
<Property Name="ListenerRelativeRouting" Type="bool" Value="True"/>
<Property Name="UseGameAuxSends" Type="bool" Value="True"/>
</PropertyList>
<ReferenceList>
<Reference Name="Effect0">
<Custom>
<Effect Name="Wwise_RoomVerb_(Custom)" ID="{4AF92620-9CD6-4410-BD1D-ADEF5D6FE936}" PluginName="Wwise RoomVerb" CompanyID="0" PluginID="118" PluginType="3">
<PropertyList>
<Property Name="DryLevel" Type="Real32" Value="-96.3"/>
</PropertyList>
</Effect>
</Custom>
</Reference>
</ReferenceList>
</AuxBus>
</ChildrenList>
</WorkUnit>
</Presets>
</WwiseDocument>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{355DF24E-9097-416D-8A70-C4BD993AE6DB}" SchemaVersion="97">
<Queries>
<WorkUnit Name="Default Work Unit" ID="{355DF24E-9097-416D-8A70-C4BD993AE6DB}" PersistMode="Standalone"/>
</Queries>
</WwiseDocument>

View file

@ -0,0 +1,623 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{7FBA6390-4CF1-44FA-9DA5-FC65F66E8FC0}" SchemaVersion="97">
<Queries>
<WorkUnit Name="Factory Queries" ID="{7FBA6390-4CF1-44FA-9DA5-FC65F66E8FC0}" PersistMode="Standalone">
<ChildrenList>
<Folder Name="Audio Source" ID="{E1FBD3A7-59FE-4064-8B8A-98F1BC63E8AF}" ShortID="26745238">
<ChildrenList>
<Query Name="Audio Source - Original Channels = Stereo" ID="{FA817DBC-3FE4-4F8B-83B7-35978BBF1A26}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="300"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="SourceChannels" ID="{363B34C4-905B-4DC4-9270-01BAE3CB3658}" SearchOperationID="{43981FC3-FF8B-4EE7-87FA-1C2A4BDD04C8}">
<PropertyList>
<Property Name="SourceChannelsLFEOption" Type="int32" Value="0"/>
<Property Name="SourceChannelsOthersCount" Type="int32" Value="2"/>
<Property Name="SourceChannelsOthersOption" Type="int32" Value="2"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Audio Source - Original Channels = Mono" ID="{88F6355E-73E7-4514-B343-528D930F2DB1}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="300"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="SourceChannels" ID="{C8526861-682F-471E-A397-83B2103F1886}" SearchOperationID="{43981FC3-FF8B-4EE7-87FA-1C2A4BDD04C8}">
<PropertyList>
<Property Name="SourceChannelsLFEOption" Type="int32" Value="0"/>
<Property Name="SourceChannelsOthersCount" Type="int32" Value="1"/>
<Property Name="SourceChannelsOthersOption" Type="int32" Value="2"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Audio Source - Format = Vorbis" ID="{4D1B2AA5-19D8-44D3-AAE5-94024469CE7C}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="300"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="SourceAudio Format" ID="{5F9BEE06-63F2-4A58-BA88-15F44E527016}" SearchOperationID="{38F10907-3DED-472A-BFAC-3EF68EFD3B37}">
<PropertyList>
<Property Name="SourceAudioFormat" Type="string" Value="Vorbis"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Audio Source - Original Channels = More than Stereo" ID="{E663EB68-BC59-4BF1-9FD0-B04C6AC48E72}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="300"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="SourceChannels" ID="{F496016D-A6BF-4F21-99A1-4B9951C22A5F}" SearchOperationID="{43981FC3-FF8B-4EE7-87FA-1C2A4BDD04C8}">
<PropertyList>
<Property Name="SourceChannelsOthersCount" Type="int32" Value="2"/>
<Property Name="SourceChannelsOthersOption" Type="int32" Value="4"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Audio Source - Original has LFE" ID="{388B7919-4AF9-48FF-8EFB-CD103A79B159}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="300"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="SourceChannels" ID="{F06890FE-31FA-49F2-8A38-B1E61F6364A8}" SearchOperationID="{43981FC3-FF8B-4EE7-87FA-1C2A4BDD04C8}">
<PropertyList>
<Property Name="SourceChannelsLFEOption" Type="int32" Value="1"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Audio Source - Format = PCM" ID="{D89F68E4-02ED-4640-B2CA-D2A080329DEA}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="300"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="SourceAudio Format" ID="{1605D55E-37BA-4DE3-9EA9-1B6229A3141A}" SearchOperationID="{38F10907-3DED-472A-BFAC-3EF68EFD3B37}">
<PropertyList>
<Property Name="SourceAudioFormat" Type="string" Value="PCM"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Audio Source - Converted Channels = Mono" ID="{B6FED2A6-D3EF-4B0E-AB7F-4F34F14D2BBF}">
<ChildrenList>
<SearchCriteria Name="SourceChannels (Converted)" ID="{85EEDF41-1586-44F1-B405-EBBBEBAB448E}" SearchOperationID="{26302C20-EEA2-4275-A9BA-7B504BA3C2E1}">
<PropertyList>
<Property Name="SourceChannelsLFEOption" Type="int32" Value="0"/>
<Property Name="SourceChannelsOthersCount" Type="int32" Value="1"/>
<Property Name="SourceChannelsOthersOption" Type="int32" Value="2"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Audio Source - Converted Channels = More than Stereo" ID="{2F6E84B3-CFC2-4CAB-AAEE-0E314E6AF2B0}">
<ChildrenList>
<SearchCriteria Name="SourceChannels (Converted)" ID="{71209BCA-C468-4163-8DB2-8FCBD1561BD0}" SearchOperationID="{26302C20-EEA2-4275-A9BA-7B504BA3C2E1}">
<PropertyList>
<Property Name="SourceChannelsOthersCount" Type="int32" Value="2"/>
<Property Name="SourceChannelsOthersOption" Type="int32" Value="4"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Audio Source - Converted Channels = Stereo" ID="{5951EBD5-5FEF-4769-A39F-B1B842DF5E6A}">
<ChildrenList>
<SearchCriteria Name="SourceChannels (Converted)" ID="{E6FD95BA-004F-4FEE-9E57-8C9120DC1AA7}" SearchOperationID="{26302C20-EEA2-4275-A9BA-7B504BA3C2E1}">
<PropertyList>
<Property Name="SourceChannelsLFEOption" Type="int32" Value="0"/>
<Property Name="SourceChannelsOthersCount" Type="int32" Value="2"/>
<Property Name="SourceChannelsOthersOption" Type="int32" Value="2"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Audio Source - Converted has LFE" ID="{E14078DB-003F-4A6D-A088-7FC9F1090E79}">
<ChildrenList>
<SearchCriteria Name="SourceChannels (Converted)" ID="{C1170BC6-B9C4-457E-B5BC-F97EC2BA85F2}" SearchOperationID="{26302C20-EEA2-4275-A9BA-7B504BA3C2E1}">
<PropertyList>
<Property Name="SourceChannelsLFEOption" Type="int32" Value="1"/>
<Property Name="SourceChannelsOthersCount" Type="int32" Value="1"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Audio Source - Original Sample Rate = 48000" ID="{ACEBA78A-108C-4514-8C41-F8B54A2FD3DA}">
<ChildrenList>
<SearchCriteria Name="SourceSample Rate (Original)" ID="{6DB1C72D-3F11-4991-B1F4-336C7C23FAD6}" SearchOperationID="{188E89CA-2CC1-4C71-ABB4-07CAD38C5F3C}">
<PropertyList>
<Property Name="LeftValue" Type="Real64" Value="48000"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Audio Source - Original Sample Rate Less than 48000" ID="{1B2E2199-193E-4163-AA57-5196D3EF5A74}">
<ChildrenList>
<SearchCriteria Name="SourceSample Rate (Original)" ID="{DA060722-287F-4941-91BE-540E9C5E7DA2}" SearchOperationID="{188E89CA-2CC1-4C71-ABB4-07CAD38C5F3C}">
<PropertyList>
<Property Name="LeftValue" Type="Real64" Value="48000"/>
<Property Name="NumericOperator" Type="int16" Value="1"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Audio Source - Converted Sample Rate = 48000" ID="{E3781E04-B989-49B0-B267-9F9D5750264C}">
<ChildrenList>
<SearchCriteria Name="SourceSample Rate (Converted)" ID="{9F8C60CE-2B1A-4EB5-B95C-2048D08785A7}" SearchOperationID="{35B827A2-F7AA-461A-BE22-6A7350B6F432}">
<PropertyList>
<Property Name="LeftValue" Type="Real64" Value="48000"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Audio Source - Converted Sample Rate Less than 48000" ID="{591347E7-B03F-430F-A507-41E1EA843ACB}">
<ChildrenList>
<SearchCriteria Name="SourceSample Rate (Converted)" ID="{67A519B8-8D09-4AB3-B74D-8B5131291D3E}" SearchOperationID="{35B827A2-F7AA-461A-BE22-6A7350B6F432}">
<PropertyList>
<Property Name="LeftValue" Type="Real64" Value="48000"/>
<Property Name="NumericOperator" Type="int16" Value="1"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
</ChildrenList>
</Folder>
<Folder Name="Sound" ID="{C1F1E09A-134D-477F-B23D-A77A9C88A6C7}" ShortID="354356955">
<ChildrenList>
<Query Name="Sound = Voice" ID="{DB864841-3338-4AFF-9D3D-DA261AC25994}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="800"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="GeneralSound Type" ID="{E99D2BF1-3D69-4BE7-BF4C-B9DBB64638E7}" SearchOperationID="{75FFAA5C-EAF4-4940-ACF2-FCE491BCDB5F}">
<PropertyList>
<Property Name="SoundType" Type="int16" Value="1"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Sound = SFX" ID="{52F6FBC8-499A-411D-A790-CB4FACBE6F6D}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="800"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="GeneralSound Type" ID="{2B842D69-30FC-431F-9A17-4DB9AF5C7B8D}" SearchOperationID="{75FFAA5C-EAF4-4940-ACF2-FCE491BCDB5F}"/>
</ChildrenList>
</Query>
<Query Name="Looping Sound" ID="{621EED10-FEF3-4BF4-8BF1-3E11D8077C02}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="800"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="PropertyValue" ID="{6BB0892D-391C-48C3-9EBB-DCA569BFEB8F}" SearchOperationID="{560736B7-4F5C-4EF7-9354-8D8DC6BBD70A}">
<PropertyList>
<Property Name="IsChecked" Type="bool" Value="True"/>
<Property Name="PropertyName" Type="string" Value="IsLoopingEnabled"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Streaming Sound" ID="{29D3A435-66B5-43BF-AB79-685BE6533DCF}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="800"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="PropertyValue" ID="{7634C9C3-0622-4D87-9EA2-609B7A685BF8}" SearchOperationID="{560736B7-4F5C-4EF7-9354-8D8DC6BBD70A}">
<PropertyList>
<Property Name="IsChecked" Type="bool" Value="True"/>
<Property Name="PropertyName" Type="string" Value="IsStreamingEnabled"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Sound - LFE higher than -96,3 dB" ID="{D0F78690-2DAE-4259-9241-994FD855C1C3}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="800"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="PropertyValue" ID="{C1CBCE65-2E3B-4D7C-B578-F2C68CBF2467}" SearchOperationID="{560736B7-4F5C-4EF7-9354-8D8DC6BBD70A}">
<PropertyList>
<Property Name="LeftValue" Type="Real64" Value="-96.3"/>
<Property Name="NumericOperator" Type="int16" Value="2"/>
<Property Name="PropertyName" Type="string" Value="LFEVolume"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Zero Latency" ID="{4416E3DF-24CC-474B-8ABF-A3C3CB6808E7}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="800"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="PropertyValue" ID="{E52E4DCE-B9AA-48E9-BD39-5455123C8DFE}" SearchOperationID="{560736B7-4F5C-4EF7-9354-8D8DC6BBD70A}">
<PropertyList>
<Property Name="IsChecked" Type="bool" Value="True"/>
<Property Name="PropertyName" Type="string" Value="IsZeroLantency"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
</ChildrenList>
</Folder>
<Folder Name="Interactive Music" ID="{0EA68A78-EBCC-4711-AAA7-7D6C890983BE}" ShortID="783787459">
<ChildrenList>
<Query Name="Streaming Tracks" ID="{9782FAF2-BE6D-4799-AED8-94AAFE6AB099}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="665"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="PropertyValue" ID="{D0672E41-9CCB-433C-85A7-84F04EECEF38}" SearchOperationID="{560736B7-4F5C-4EF7-9354-8D8DC6BBD70A}">
<PropertyList>
<Property Name="IsChecked" Type="bool" Value="True"/>
<Property Name="PropertyName" Type="string" Value="IsStreamingEnabled"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Direct Speaker Assignment" ID="{CB2F5C2D-6B65-4EA0-94A1-7F03B3E08C42}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="620"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="PropertyValue" ID="{FE756298-8E32-4C80-B372-7FE54E3FA60B}" SearchOperationID="{560736B7-4F5C-4EF7-9354-8D8DC6BBD70A}">
<PropertyList>
<Property Name="PropertyName" Type="string" Value="PanningType"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Balance-Fade Speaker Panning" ID="{5B5FADD1-0B8B-48F7-A1A0-7794AC707DC4}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="620"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="PropertyValue" ID="{A73FB12B-AE1A-499B-9EE3-55BDBDCDCE0F}" SearchOperationID="{560736B7-4F5C-4EF7-9354-8D8DC6BBD70A}">
<PropertyList>
<Property Name="LeftValue" Type="Real64" Value="1"/>
<Property Name="PropertyName" Type="string" Value="PanningType"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Time Signature - Switch = 4-4" ID="{EB5596D4-842B-4910-9844-5B230A3DB637}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="660"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="PropertyValue" ID="{642D48B9-7A50-42F2-B1D5-2510EB13CF50}" SearchOperationID="{560736B7-4F5C-4EF7-9354-8D8DC6BBD70A}">
<PropertyList>
<Property Name="LeftValue" Type="Real64" Value="4"/>
<Property Name="PropertyName" Type="string" Value="TimeSignatureUpper"/>
</PropertyList>
</SearchCriteria>
<SearchCriteria Name="PropertyValue" ID="{FBFEA0F5-F8EC-4AAF-9B07-65420F185B32}" SearchOperationID="{560736B7-4F5C-4EF7-9354-8D8DC6BBD70A}">
<PropertyList>
<Property Name="LeftValue" Type="Real64" Value="4"/>
<Property Name="PropertyName" Type="string" Value="TimeSignatureLower"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Time Signature - Segment = 4-4" ID="{BCA46BF9-70E6-408F-9BC9-2C05FCA2C897}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="650"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="PropertyValue" ID="{EFC611E9-5E12-4EE0-88B7-5AEEAA89F473}" SearchOperationID="{560736B7-4F5C-4EF7-9354-8D8DC6BBD70A}">
<PropertyList>
<Property Name="LeftValue" Type="Real64" Value="4"/>
<Property Name="PropertyName" Type="string" Value="TimeSignatureUpper"/>
</PropertyList>
</SearchCriteria>
<SearchCriteria Name="PropertyValue" ID="{DDB3F316-BF4F-4F6F-8694-2D9BE37D3027}" SearchOperationID="{560736B7-4F5C-4EF7-9354-8D8DC6BBD70A}">
<PropertyList>
<Property Name="LeftValue" Type="Real64" Value="4"/>
<Property Name="PropertyName" Type="string" Value="TimeSignatureLower"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Tempo - Switch = 120 BPM" ID="{FA142BFF-9A04-4417-B9C7-1B03ADB5A389}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="660"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="PropertyValue" ID="{2C6104EE-1C59-4D3C-A312-ADD299C841B4}" SearchOperationID="{560736B7-4F5C-4EF7-9354-8D8DC6BBD70A}">
<PropertyList>
<Property Name="LeftValue" Type="Real64" Value="120"/>
<Property Name="PropertyName" Type="string" Value="Tempo"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Tempo - Playlist = 120 BPM" ID="{593E5494-8214-43CE-90A0-FA194FBBA09D}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="645"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="PropertyValue" ID="{70760C28-5C8D-4639-96E3-FD4C7BAD4E51}" SearchOperationID="{560736B7-4F5C-4EF7-9354-8D8DC6BBD70A}">
<PropertyList>
<Property Name="LeftValue" Type="Real64" Value="120"/>
<Property Name="PropertyName" Type="string" Value="Tempo"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Tempo - Segment = 120 BPM" ID="{F8F3B78C-3D96-4049-B2C8-00B3D47259FC}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="650"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="PropertyValue" ID="{778F8AF6-4124-45DB-9C1C-23DA98A2BC2D}" SearchOperationID="{560736B7-4F5C-4EF7-9354-8D8DC6BBD70A}">
<PropertyList>
<Property Name="LeftValue" Type="Real64" Value="120"/>
<Property Name="PropertyName" Type="string" Value="Tempo"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Time Signature - Playlist = 4-4" ID="{D9337177-5234-47E0-90FF-B281E49A7ED9}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="645"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="PropertyValue" ID="{492347F2-B398-48F4-9466-F41EC1D336DE}" SearchOperationID="{560736B7-4F5C-4EF7-9354-8D8DC6BBD70A}">
<PropertyList>
<Property Name="LeftValue" Type="Real64" Value="4"/>
<Property Name="PropertyName" Type="string" Value="TimeSignatureUpper"/>
</PropertyList>
</SearchCriteria>
<SearchCriteria Name="PropertyValue" ID="{74BDE5D4-BBF6-4CD8-BC26-092839466E77}" SearchOperationID="{560736B7-4F5C-4EF7-9354-8D8DC6BBD70A}">
<PropertyList>
<Property Name="LeftValue" Type="Real64" Value="4"/>
<Property Name="PropertyName" Type="string" Value="TimeSignatureLower"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Look-ahead time - Higher than 100 ms" ID="{CB48D46B-0A50-4716-B9D8-1B05116029A3}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="665"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="PropertyValue" ID="{63F08487-A1E5-4950-BC6D-931C24D888ED}" SearchOperationID="{560736B7-4F5C-4EF7-9354-8D8DC6BBD70A}">
<PropertyList>
<Property Name="LeftValue" Type="Real64" Value="100"/>
<Property Name="NumericOperator" Type="int16" Value="2"/>
<Property Name="PropertyName" Type="string" Value="LookAheadTime"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Empty Switch Assignation" ID="{C0336C2C-5853-4B32-99A0-26BA7D991106}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="660"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="SwitchAssignation" ID="{40709D4A-7D69-4DC8-B6F3-03D1C0D3CEBE}" SearchOperationID="{907F121C-AF62-4291-B2E6-7C58C936571B}"/>
</ChildrenList>
</Query>
<Query Name="Objects with Unlinked Properties" ID="{B5845F67-3AD8-4C02-B671-013761947F7D}">
<PropertyList>
<Property Name="LogicalOperator" Type="int16" Value="1"/>
<Property Name="ObjectType" Type="int32" Value="620"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="PropertyLinkUnlink" ID="{6AF8EA3F-198A-43A7-8E5E-81596DA800F9}" SearchOperationID="{75897DFE-D859-416C-8EDE-30BA4A8369AF}">
<PropertyList>
<Property Name="IsLinked" Type="bool" Value="False"/>
<Property Name="PropertyName" Type="string" Value="Volume"/>
</PropertyList>
</SearchCriteria>
<SearchCriteria Name="PropertyLinkUnlink" ID="{F7743CB5-EAC2-4D1C-863C-E545E469CF13}" SearchOperationID="{75897DFE-D859-416C-8EDE-30BA4A8369AF}">
<PropertyList>
<Property Name="IsLinked" Type="bool" Value="False"/>
<Property Name="PropertyName" Type="string" Value="LFEVolume"/>
</PropertyList>
</SearchCriteria>
<SearchCriteria Name="PropertyLinkUnlink" ID="{883BBBE1-34AD-4239-8DBD-C0489AF38E27}" SearchOperationID="{75897DFE-D859-416C-8EDE-30BA4A8369AF}">
<PropertyList>
<Property Name="IsLinked" Type="bool" Value="False"/>
<Property Name="PropertyName" Type="string" Value="Lowpass"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
</ChildrenList>
</Folder>
<Folder Name="Actor-Mixer Hierarchy" ID="{75E26115-FC3D-4E25-99F9-448BC918921E}" ShortID="657523609">
<ChildrenList>
<Query Name="Direct Speaker Assignment" ID="{93BB05B6-6653-4842-9DE8-07BF927ED117}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="200"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="PropertyValue" ID="{57AD2421-B502-42DB-AD05-10F7968B4718}" SearchOperationID="{560736B7-4F5C-4EF7-9354-8D8DC6BBD70A}">
<PropertyList>
<Property Name="PropertyName" Type="string" Value="PanningType"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Balance-Fade Speaker Panning" ID="{834DE5CA-8DD6-4BC1-B8B2-EFE7E4FD8319}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="200"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="PropertyValue" ID="{6B72112E-B63A-4807-8BCB-86CFD81DF3B0}" SearchOperationID="{560736B7-4F5C-4EF7-9354-8D8DC6BBD70A}">
<PropertyList>
<Property Name="LeftValue" Type="Real64" Value="1"/>
<Property Name="PropertyName" Type="string" Value="PanningType"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="3D Automation" ID="{CE928DF7-C351-449E-AC18-A2C782D06E02}">
<PropertyList>
<Property Name="LogicalOperator" Type="int16" Value="1"/>
<Property Name="ObjectType" Type="int32" Value="200"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="PropertyValue" ID="{F584549F-0CD1-4B62-874A-C477EF188173}" SearchOperationID="{560736B7-4F5C-4EF7-9354-8D8DC6BBD70A}">
<PropertyList>
<Property Name="LeftValue" Type="Real64" Value="2"/>
<Property Name="PropertyName" Type="string" Value="3DPositionSource"/>
</PropertyList>
</SearchCriteria>
<SearchCriteria Name="PropertyValue" ID="{D65DDE6B-1E28-427C-A989-6FFDCEB08D67}" SearchOperationID="{560736B7-4F5C-4EF7-9354-8D8DC6BBD70A}">
<PropertyList>
<Property Name="LeftValue" Type="Real64" Value="1"/>
<Property Name="PropertyName" Type="string" Value="3DPositionSource"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="3D Spatialization" ID="{4CD5A13D-3ECB-4359-BD07-B6B79530D180}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="200"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="PropertyValue" ID="{0FE6C467-ACBB-40BD-B180-B6F1E4D2BCC7}" SearchOperationID="{560736B7-4F5C-4EF7-9354-8D8DC6BBD70A}">
<PropertyList>
<Property Name="LeftValue" Type="Real64" Value="1"/>
<Property Name="PropertyName" Type="string" Value="Spatialization"/>
</PropertyList>
</SearchCriteria>
<SearchCriteria Name="PropertyValue" ID="{7E2ECB28-E3BF-4C61-91B0-9BE73D8D04EE}" SearchOperationID="{560736B7-4F5C-4EF7-9354-8D8DC6BBD70A}">
<PropertyList>
<Property Name="LeftValue" Type="Real64" Value="2"/>
<Property Name="PropertyName" Type="string" Value="Spatialization"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Objects using Effect" ID="{9ABAF5B6-3430-42DB-B78C-058CAFD5A3BC}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="200"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="EffectGeneral Usage" ID="{96BA059A-EED0-44F7-8457-3C842687C6A8}" SearchOperationID="{E7F802D4-CD12-4B63-A0FE-0BBE545C59E1}">
<PropertyList>
<Property Name="IsUsed" Type="bool" Value="True"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Objects with Unlinked Properties" ID="{A64278F6-DC90-4FC6-8D04-74721BD71C65}">
<PropertyList>
<Property Name="LogicalOperator" Type="int16" Value="1"/>
<Property Name="ObjectType" Type="int32" Value="200"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="PropertyLinkUnlink" ID="{7C0ACBCA-F05A-4E08-945C-0F2A2D54000A}" SearchOperationID="{75897DFE-D859-416C-8EDE-30BA4A8369AF}">
<PropertyList>
<Property Name="IsLinked" Type="bool" Value="False"/>
<Property Name="PropertyName" Type="string" Value="Volume"/>
</PropertyList>
</SearchCriteria>
<SearchCriteria Name="PropertyLinkUnlink" ID="{9AE4E840-0489-4EC2-ACD4-11C2DB78C933}" SearchOperationID="{75897DFE-D859-416C-8EDE-30BA4A8369AF}">
<PropertyList>
<Property Name="IsLinked" Type="bool" Value="False"/>
<Property Name="PropertyName" Type="string" Value="LFEVolume"/>
</PropertyList>
</SearchCriteria>
<SearchCriteria Name="PropertyLinkUnlink" ID="{C6C01C8E-89BE-462E-ABBA-BE98C3BF4FA7}" SearchOperationID="{75897DFE-D859-416C-8EDE-30BA4A8369AF}">
<PropertyList>
<Property Name="IsLinked" Type="bool" Value="False"/>
<Property Name="PropertyName" Type="string" Value="Pitch"/>
</PropertyList>
</SearchCriteria>
<SearchCriteria Name="PropertyLinkUnlink" ID="{209F1501-BBC5-4CA2-8F8B-29A50751A976}" SearchOperationID="{75897DFE-D859-416C-8EDE-30BA4A8369AF}">
<PropertyList>
<Property Name="IsLinked" Type="bool" Value="False"/>
<Property Name="PropertyName" Type="string" Value="Lowpass"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Empty Switch Assignation" ID="{48CCE634-1384-4870-804F-D3E9A51194DD}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="1000"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="SwitchAssignation" ID="{D85B8F5E-DEC8-4F1A-8590-CA266F4E5FCA}" SearchOperationID="{907F121C-AF62-4291-B2E6-7C58C936571B}"/>
</ChildrenList>
</Query>
</ChildrenList>
</Folder>
<Folder Name="Master-Mixer Hierarchy" ID="{F23408F1-FA3E-4006-A7B4-2288E6885FC7}" ShortID="654516827">
<ChildrenList>
<Query Name="Busses using Effect" ID="{1CD96671-0175-4AE2-A9EE-AC8CF681D49F}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="500"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="EffectGeneral Usage" ID="{AFC12871-4325-4DAD-BF10-7F7CE79E9562}" SearchOperationID="{E7F802D4-CD12-4B63-A0FE-0BBE545C59E1}">
<PropertyList>
<Property Name="IsUsed" Type="bool" Value="True"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Environmental bus" ID="{F1B54F4B-4F9E-40C7-B862-6806135D18C8}">
<PropertyList>
<Property Name="ObjectType" Type="int32" Value="500"/>
</PropertyList>
<ChildrenList>
<SearchCriteria Name="PropertyValue" ID="{7B40ACE8-C756-4201-9B43-6A1B396D4E56}" SearchOperationID="{560736B7-4F5C-4EF7-9354-8D8DC6BBD70A}">
<PropertyList>
<Property Name="IsChecked" Type="bool" Value="True"/>
<Property Name="PropertyName" Type="string" Value="IsEnvironmental"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
</ChildrenList>
</Folder>
<Query Name="Notes - Contains" ID="{7E8B1952-A130-4754-B9C9-651A5DC68767}">
<ChildrenList>
<SearchCriteria Name="GeneralNote" ID="{0872061F-022C-44A9-B2C7-14B9BF00D8C7}" SearchOperationID="{3DF06B4A-2E31-463A-A42B-F4B576557320}">
<PropertyList>
<Property Name="UserString" Type="string" Value="*Note text here*"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Notes - Exact name" ID="{C4B2F216-5F5C-43FF-873A-94680392DCFF}">
<ChildrenList>
<SearchCriteria Name="GeneralNote" ID="{436744D5-C620-4539-B54E-E9A53B9E469E}" SearchOperationID="{3DF06B4A-2E31-463A-A42B-F4B576557320}">
<PropertyList>
<Property Name="UserString" Type="string" Value="Note text here"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Name - Contains" ID="{0C2BEF06-3ABD-40A6-BFE9-A5A656A52571}">
<ChildrenList>
<SearchCriteria Name="GeneralName" ID="{2E521352-0345-47A7-9939-53653A654F49}" SearchOperationID="{82CCCD47-591B-4BD8-B3CE-89A8172A1802}">
<PropertyList>
<Property Name="UserString" Type="string" Value="*Object name here*"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Name - Exact name" ID="{F9FCEB15-854D-406F-ADEE-37FBA2FEE74F}">
<ChildrenList>
<SearchCriteria Name="GeneralName" ID="{FD3B26D7-89AC-4785-AAB8-B882592CD2AD}" SearchOperationID="{82CCCD47-591B-4BD8-B3CE-89A8172A1802}">
<PropertyList>
<Property Name="UserString" Type="string" Value="Object name here"/>
</PropertyList>
</SearchCriteria>
</ChildrenList>
</Query>
<Query Name="Platform Excluded Objects" ID="{8E0F649F-7443-401D-A23F-A55AEA4AE5BC}">
<ChildrenList>
<SearchCriteria Name="GeneralPlatform Inclusion" ID="{361BBA96-A1DF-420E-A614-FFE411556F1E}" SearchOperationID="{76702F0A-B29D-4D2E-8E16-425DA225844E}"/>
</ChildrenList>
</Query>
</ChildrenList>
</WorkUnit>
</Queries>
</WwiseDocument>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{4ACCD927-09F4-4288-B87C-7B442C9CFB0A}" SchemaVersion="97">
<SoundBanks>
<WorkUnit Name="Ambience SB Unit" ID="{4ACCD927-09F4-4288-B87C-7B442C9CFB0A}" PersistMode="Standalone">
<ChildrenList>
<SoundBank Name="Ambience" ID="{9666AE65-5566-4CB3-8EBB-B13773790485}">
<ObjectInclusionList>
<ObjectRef Name="Ambience AM Unit" ID="{CA5C9672-2298-4659-B32D-52AE660403FF}" WorkUnitID="{CA5C9672-2298-4659-B32D-52AE660403FF}" Origin="Manual" Filter="7"/>
</ObjectInclusionList>
<ObjectExclusionList/>
<GameSyncExclusionList/>
</SoundBank>
</ChildrenList>
</WorkUnit>
</SoundBanks>
</WwiseDocument>

View file

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{37A7DB3D-59ED-4DDF-86BD-E00720F85F23}" SchemaVersion="97">
<SoundBanks>
<WorkUnit Name="Default Work Unit" ID="{37A7DB3D-59ED-4DDF-86BD-E00720F85F23}" PersistMode="Standalone">
<ChildrenList>
<SoundBank Name="UI" ID="{86130C21-81DD-4DB8-902A-F172CCD79666}">
<ObjectInclusionList>
<ObjectRef Name="Dock" ID="{CC95135D-D17F-4088-B23C-BF8238FCA221}" WorkUnitID="{1140EE06-2469-4795-9250-0C72BFFAEF1B}" Origin="Manual" Filter="7"/>
<ObjectRef Name="Drop" ID="{507ECF50-2D22-4222-A984-367EF0411E8F}" WorkUnitID="{1140EE06-2469-4795-9250-0C72BFFAEF1B}" Origin="Manual" Filter="7"/>
<ObjectRef Name="UI_Success" ID="{0E497EDC-A42D-4478-AF76-5114508A5B8B}" WorkUnitID="{1140EE06-2469-4795-9250-0C72BFFAEF1B}" Origin="Manual" Filter="7"/>
<ObjectRef Name="Pickup" ID="{A64914D4-7F8A-4586-BB48-8FA3137E2F1F}" WorkUnitID="{1140EE06-2469-4795-9250-0C72BFFAEF1B}" Origin="Manual" Filter="7"/>
<ObjectRef Name="Undock" ID="{89374ED6-A730-48E4-AAF7-0E1C575CE1C2}" WorkUnitID="{1140EE06-2469-4795-9250-0C72BFFAEF1B}" Origin="Manual" Filter="7"/>
<ObjectRef Name="UI_Fail" ID="{7DB8939E-DD1B-4B8C-B787-8C4440712523}" WorkUnitID="{1140EE06-2469-4795-9250-0C72BFFAEF1B}" Origin="Manual" Filter="7"/>
<ObjectRef Name="Dock_Fail" ID="{BE28B971-A0E1-4D96-B576-1B68C4CA5AA9}" WorkUnitID="{1140EE06-2469-4795-9250-0C72BFFAEF1B}" Origin="Manual" Filter="7"/>
<ObjectRef Name="Equip" ID="{44FA1280-D160-4DB5-B7EB-AA6E4CFBB17A}" WorkUnitID="{1140EE06-2469-4795-9250-0C72BFFAEF1B}" Origin="Manual" Filter="7"/>
<ObjectRef Name="Unequip" ID="{3125BD51-8FFC-4E24-BD2E-D102896B8A0B}" WorkUnitID="{1140EE06-2469-4795-9250-0C72BFFAEF1B}" Origin="Manual" Filter="7"/>
</ObjectInclusionList>
<ObjectExclusionList/>
<GameSyncExclusionList/>
</SoundBank>
<Folder Name="Ship" ID="{0F9E27C7-E2CC-4D3A-A0A9-BB3618502681}" ShortID="465647461">
<ChildrenList>
<Folder Name="Thruster_type" ID="{B2386FE9-E148-47F3-B75B-6132C8639B2E}" ShortID="981236698">
<ChildrenList>
<Folder Name="Type_01" ID="{B596AF90-7A76-4E8C-8B9C-CEAC73A4FAC2}" ShortID="930908810">
<ChildrenList>
<SoundBank Name="thruster_large" ID="{7A8D3A50-B567-47BF-87F9-981299580FFF}">
<ObjectInclusionList>
<ObjectRef Name="thruster_large" ID="{35564C2D-1BC9-40BB-B365-D9C4A47AE5F1}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}" Origin="Manual" Filter="7"/>
</ObjectInclusionList>
<ObjectExclusionList/>
<GameSyncExclusionList/>
</SoundBank>
<SoundBank Name="thruster_medium" ID="{07841A8C-07CD-4697-AC5D-E42B3E680B78}">
<ObjectInclusionList>
<ObjectRef Name="thruster_medium" ID="{4261D6D4-BDC3-4423-BA6B-A92DE2181A21}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}" Origin="Manual" Filter="7"/>
</ObjectInclusionList>
<ObjectExclusionList/>
<GameSyncExclusionList/>
</SoundBank>
<SoundBank Name="thruster_small" ID="{8291805D-3DD5-4EED-AA30-4964D4359529}">
<ObjectInclusionList>
<ObjectRef Name="thruster_small" ID="{D355F5AE-732C-4638-988F-F1E01D1FC547}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}" Origin="Manual" Filter="7"/>
</ObjectInclusionList>
<ObjectExclusionList/>
<GameSyncExclusionList/>
</SoundBank>
</ChildrenList>
</Folder>
</ChildrenList>
</Folder>
<Folder Name="Reactor_type" ID="{7C652FCA-7D8F-498B-A131-4397B1B6F463}" ShortID="1068431759">
<ChildrenList>
<Folder Name="Type_01" ID="{79ED9F5C-2B4E-4AD5-9CE9-99101B1D5B68}" ShortID="577434280">
<ChildrenList>
<SoundBank Name="reactor_type01" ID="{1683155F-20DC-4B9C-A28C-E70DD2AD6863}">
<ObjectInclusionList>
<ObjectRef Name="spaceship_reactor_overheat_alarm" ID="{C9F5C84F-7414-4607-B8F0-8734CCB51061}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}" Origin="Manual" Filter="7"/>
<ObjectRef Name="reactor_type1" ID="{3AA3BAA8-A42D-4EAE-B9BD-BC64D22B2753}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}" Origin="Manual" Filter="7"/>
</ObjectInclusionList>
<ObjectExclusionList/>
<GameSyncExclusionList/>
</SoundBank>
</ChildrenList>
</Folder>
</ChildrenList>
</Folder>
</ChildrenList>
</Folder>
<Folder Name="wormhole" ID="{24134BA9-BAC0-4B83-BB5E-4CB6D2305A1F}" ShortID="315933305">
<ChildrenList>
<SoundBank Name="wormhole" ID="{04E88A66-5253-43DA-8860-A3965492AE64}">
<ObjectInclusionList>
<ObjectRef Name="Aetheria_Wormhole_travel_jumpout" ID="{290B97E7-B057-42A4-9BCE-33220B7473A8}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}" Origin="Manual" Filter="7"/>
<ObjectRef Name="Aetheria_Wormhole_travel_jumpin" ID="{393D96D3-4D2B-42BB-827D-10984647051F}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}" Origin="Manual" Filter="7"/>
<ObjectRef Name="Aetheria_Wormhole_travel" ID="{0EE417AB-A0FE-4E79-8031-FBE3B604C3AB}" WorkUnitID="{076C9B1F-5546-43DA-8A92-E4A1C9B3FD42}" Origin="Manual" Filter="7"/>
</ObjectInclusionList>
<ObjectExclusionList/>
<GameSyncExclusionList/>
</SoundBank>
</ChildrenList>
</Folder>
</ChildrenList>
</WorkUnit>
</SoundBanks>
</WwiseDocument>

View file

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{22D7F8DC-6842-4B60-B448-518725B20237}" SchemaVersion="97">
<SoundcasterSessions>
<WorkUnit Name="Default Work Unit" ID="{22D7F8DC-6842-4B60-B448-518725B20237}" PersistMode="Standalone">
<ChildrenList>
<SoundcasterSession Name="New Soundcaster Session" ID="{DF759178-B639-4CA4-A6F0-D0185A29D18D}">
<RTPCValueList ShowAll="true">
<RTPCValue>
<GameParameterRef Name="performance_temperature" ID="{778F5836-B82F-46F8-936A-E8581E9DA7A9}"/>
<Value>0</Value>
</RTPCValue>
<RTPCValue>
<GameParameterRef Name="ship_velocity" ID="{B907BE30-367C-4955-AF3B-AF6B2806F9D5}"/>
<Value>0</Value>
</RTPCValue>
<RTPCValue>
<GameParameterRef Name="thruster_throttle" ID="{2A6D7A97-7D45-4B6C-96EE-CCD56FA83773}"/>
<Value>1</Value>
</RTPCValue>
<RTPCValue>
<GameParameterRef Name="performance_quality" ID="{BFC68604-C477-4B9B-B488-F5B4A9ADEAA4}"/>
<Value>1</Value>
</RTPCValue>
<RTPCValue>
<GameParameterRef Name="reactor_load" ID="{FC156F43-A577-4DCB-B6E8-84E4072A38C9}"/>
<Value>1</Value>
</RTPCValue>
<RTPCValue>
<GameParameterRef Name="performance_durability" ID="{5540312A-E87E-4E09-91E8-71A5D349779A}"/>
<Value>1</Value>
</RTPCValue>
</RTPCValueList>
<CurrentStateList ShowAll="true"/>
<CurrentSwitchList ShowAll="true"/>
</SoundcasterSession>
<SoundcasterSession Name="Ambience_Test" ID="{E9D3086E-C326-478C-BE46-668AB254BFE6}">
<ModuleList>
<Module>
<ObjectRef Name="Amb_BaseDrone" ID="{715CB4B5-1708-481E-92C4-BA55066D2E09}" WorkUnitID="{E3C68D3D-9917-4B0D-9D0A-E904D7B8FCF7}"/>
<Position X="0" Y="0"/>
</Module>
<Module>
<ObjectRef Name="Amb_Low_Oneshots" ID="{9CDB4529-2114-41EB-B4C4-0DDB7CF529F3}" WorkUnitID="{E3C68D3D-9917-4B0D-9D0A-E904D7B8FCF7}"/>
<Position X="0" Y="1"/>
</Module>
<Module>
<ObjectRef Name="Amb_Mid_and_High_Oneshots" ID="{90C1B4E6-D445-425B-BA9B-CFB9BCB2663E}" WorkUnitID="{E3C68D3D-9917-4B0D-9D0A-E904D7B8FCF7}"/>
<Position X="0" Y="2"/>
</Module>
<Module>
<ObjectRef Name="Amb_Weatherlike_Oneshots" ID="{29C05807-68C5-4524-8E31-C48F854602E2}" WorkUnitID="{E3C68D3D-9917-4B0D-9D0A-E904D7B8FCF7}"/>
<Position X="0" Y="3"/>
</Module>
</ModuleList>
<RTPCValueList ShowAll="true">
<RTPCValue>
<GameParameterRef Name="performance_temperature" ID="{778F5836-B82F-46F8-936A-E8581E9DA7A9}"/>
<Value>0</Value>
</RTPCValue>
<RTPCValue>
<GameParameterRef Name="ship_velocity" ID="{B907BE30-367C-4955-AF3B-AF6B2806F9D5}"/>
<Value>0</Value>
</RTPCValue>
<RTPCValue>
<GameParameterRef Name="thruster_throttle" ID="{2A6D7A97-7D45-4B6C-96EE-CCD56FA83773}"/>
<Value>1</Value>
</RTPCValue>
<RTPCValue>
<GameParameterRef Name="performance_quality" ID="{BFC68604-C477-4B9B-B488-F5B4A9ADEAA4}"/>
<Value>1</Value>
</RTPCValue>
<RTPCValue>
<GameParameterRef Name="reactor_load" ID="{FC156F43-A577-4DCB-B6E8-84E4072A38C9}"/>
<Value>1</Value>
</RTPCValue>
<RTPCValue>
<GameParameterRef Name="performance_durability" ID="{5540312A-E87E-4E09-91E8-71A5D349779A}"/>
<Value>1</Value>
</RTPCValue>
</RTPCValueList>
<CurrentStateList ShowAll="true"/>
<CurrentSwitchList ShowAll="true"/>
</SoundcasterSession>
</ChildrenList>
</WorkUnit>
</SoundcasterSessions>
</WwiseDocument>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{C69EEC73-93DE-44DF-BA3C-141DF535CF1F}" SchemaVersion="97">
<States>
<WorkUnit Name="Default Work Unit" ID="{C69EEC73-93DE-44DF-BA3C-141DF535CF1F}" PersistMode="Standalone"/>
</States>
</WwiseDocument>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{4DA45702-4508-4AEC-8591-074957EEB14E}" SchemaVersion="97">
<Switches>
<WorkUnit Name="Default Work Unit" ID="{4DA45702-4508-4AEC-8591-074957EEB14E}" PersistMode="Standalone"/>
</Switches>
</WwiseDocument>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{F54748F4-A530-4C55-A28C-043D155D0796}" SchemaVersion="97">
<Triggers>
<WorkUnit Name="Default Work Unit" ID="{F54748F4-A530-4C55-A28C-043D155D0796}" PersistMode="Standalone"/>
</Triggers>
</WwiseDocument>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<WwiseDocument Type="WorkUnit" ID="{C6CB2C59-D38D-4C70-808E-CF5418DD0107}" SchemaVersion="97">
<VirtualAcoustics>
<WorkUnit Name="Default Work Unit" ID="{C6CB2C59-D38D-4C70-808E-CF5418DD0107}" PersistMode="Standalone"/>
</VirtualAcoustics>
</WwiseDocument>

View file

@ -0,0 +1,17 @@
Project migration to XML Schema (XSD) version 98 is completed.
Project migration to XML Schema (XSD) version 99 is completed.
Project migration to XML Schema (XSD) version 100 is completed.
Project migration to XML Schema (XSD) version 101 is completed.
Project migration to XML Schema (XSD) version 102 is completed.
Project migration to XML Schema (XSD) version 103 is completed.
Work Unit '\Actor-Mixer Hierarchy\UI' has a unique ID conflicting with the already loaded Work Unit '\Actor-Mixer Hierarchy\copy_Unit'. It has been assigned a new unique ID.
Event '\Events\Ship Audio\thruster_large' has a unique ID conflicting with the already loaded Event '\Events\SFX\Thruster\thruster_play'. It has been assigned a new unique ID.
Event Action '\Events\Ship Audio\thruster_large\[Play - Thruster]' has a unique ID conflicting with the already loaded Event Action '\Events\SFX\Thruster\thruster_play\[Play - Thruster]'. It has been assigned a new unique ID.
Event Action '\Events\Ship Audio\thruster_large\[Play - Thruster]' has a short ID that conflicts with another object. A new short ID has been given to the object. IMPORTANT: Soundbanks must be regenerated.
Event '\Events\Ship Audio\thruster_large_stop' has a unique ID conflicting with the already loaded Event '\Events\SFX\Thruster\thruster_stop'. It has been assigned a new unique ID.
Event Action '\Events\Ship Audio\thruster_large_stop\[Stop - Thruster]' has a unique ID conflicting with the already loaded Event Action '\Events\SFX\Thruster\thruster_stop\[Stop - Thruster]'. It has been assigned a new unique ID.
Event Action '\Events\Ship Audio\thruster_large_stop\[Stop - Thruster]' has a short ID that conflicts with another object. A new short ID has been given to the object. IMPORTANT: Soundbanks must be regenerated.
Work Unit '\Game Parameters\SFX' has a unique ID conflicting with the already loaded Work Unit '\Game Parameters\item_properties'. It has been assigned a new unique ID.
Game Parameter '\Game Parameters\SFX\Reactor\sfx_reactor_alarm' has a unique ID conflicting with the already loaded Game Parameter '\Game Parameters\item_properties\performance_thermal'. It has been assigned a new unique ID.
Game Parameter '\Game Parameters\SFX\Shared\ship_velocity' has a unique ID conflicting with the already loaded Game Parameter '\Game Parameters\item_properties\ship_velocity_01'. It has been assigned a new unique ID.
The Game Parameter with the name 'ship_velocity' conflicts with an existing object or contains invalid characters. The object has been renamed to '\Game Parameters\item_properties\ship_velocity_01'.

View file

@ -0,0 +1,4 @@
import { EveBrowserProviderHost } from "../node_modules/@gamecult/eve-browser-lowering/dist/index.js";
import { mountEveElectronProvider } from "../node_modules/@gamecult/eve-electron/src/eve-electron-renderer.mjs";
void mountEveElectronProvider({ EveBrowserProviderHost });

View file

@ -0,0 +1,797 @@
import { encode } from "@msgpack/msgpack";
import { performance } from "node:perf_hooks";
import { CultMesh } from "cultmesh-ts";
import type {
CultMeshDocumentCatalog,
CultMeshDocumentPublicationSource,
CultMeshPublicationDocumentBinding,
CultMeshOperationContext,
CultMeshQueryContext,
CultMeshSurfaceCatalogIndexDiagnostic,
CultMeshVerse,
} from "cultmesh-ts";
import type {
CultNetDocumentPutRawMessage,
CultNetPeer,
} from "cultnet-ts";
import {
AetheriaRtsSchemas,
createAetheriaRuntimeGameDocuments,
createAetheriaRuntimeGameOperationHandles,
createAetheriaRuntimeGameVerseHandles,
createAetheriaRuntimeGameQueryHandles,
describeAetheriaRuntimeGameLiveFeedSurface,
describeAetheriaRuntimeGameQueryHandles,
describeAetheriaRuntimeGameSurfaceCatalog,
type AetheriaRuntimeDaemonCommandReceipt,
type AssetManifestDocument,
type AuthorityStatusDocument,
type DaemonHealthDocument,
type GravityViewportResponse,
type InventoryDocument,
type ObjectsViewportResponse,
type RenderSplatsViewportResponse,
type AetheriaRuntimeGameLiveFeedDiagnostic,
type AetheriaRuntimeGameQueryDiagnostic,
type AetheriaRuntimeGameSurfaceCatalogDiagnostic,
type AetheriaRuntimeGameOperationHandles,
type AetheriaRuntimeGameVerseHandles,
type AetheriaRuntimeGameDocuments,
type AetheriaRuntimeViewportFeedRequest,
type AetheriaRuntimeViewportFeedSnapshot,
type RtsSetMoveVectorRequest,
type RtsSetTargetRequest,
type SelectedObjectDocument,
type SelectedObjectRequest,
type StarbridgeSessionDocument,
type ViewportRequest,
type ViewportResponse,
} from "./aetheria-rts-bindings.js";
import {
readAuthorityStatusDocument,
readAssetManifestDocument,
readDaemonHealthDocument,
readGravityViewportDocument,
readInventoryDocument,
readObjectsViewportDocument,
readRenderSplatsViewportDocument,
readSelectedObjectDocument,
readStarbridgeSessionSummaryDocument,
readViewportDocument,
} from "./aetheria-rts-local-documents.js";
const connectionId = 0x43554c54;
type AetheriaPublicationDocumentSpec = CultMeshPublicationDocumentBinding & {
readonly localPath: string;
readonly remoteRecordKey?: string;
};
type AetheriaRuntimeGameQueryExecutors = {
mapViewport: (request: ViewportRequest) => Promise<ViewportResponse>;
objectsViewport: (request: ViewportRequest) => Promise<ObjectsViewportResponse>;
gravityViewport: (request: ViewportRequest) => Promise<GravityViewportResponse>;
renderSplatsViewport: (request: ViewportRequest) => Promise<RenderSplatsViewportResponse>;
selectedObject: (request: SelectedObjectRequest) => Promise<SelectedObjectDocument>;
inventory: (request: SelectedObjectRequest) => Promise<InventoryDocument>;
daemonHealth: () => Promise<DaemonHealthDocument>;
authorityStatus: () => Promise<AuthorityStatusDocument>;
starbridgeSession: () => Promise<StarbridgeSessionDocument>;
assetManifest: () => Promise<AssetManifestDocument>;
};
export type AetheriaCultMeshClientOptions = {
publicationMode?: "local" | "remote";
snapshotTimeoutMs?: number;
};
export type AetheriaCultMeshDaemonTarget = {
readonly uri: string;
readonly peerId?: string;
readonly verseId?: string;
readonly role?: string;
readonly endpoints?: readonly string[];
};
export type {
BodyView,
EntityStatus,
GravityViewportResponse,
GravityInfluence,
InventoryItem,
ObjectsViewportResponse,
AetheriaRuntimeViewportFeedRequest,
AetheriaRuntimeViewportFeedSnapshot,
RtsSetMoveVectorRequest,
RtsSetTargetRequest,
SelectedObjectDocument,
SelectedObjectRequest,
StarbridgeSessionDocument,
ViewObject,
ViewportRequest,
ViewportResponse,
} from "./aetheria-rts-bindings.js";
export class AetheriaCultMeshClient {
#peer: CultNetPeer | null = null;
private readonly verse: CultMeshVerse;
private readonly queryVerse: CultMeshVerse;
private readonly commandVerse: CultMeshVerse;
private readonly daemonTarget: Required<AetheriaCultMeshDaemonTarget>;
public constructor(
daemonTarget: string | AetheriaCultMeshDaemonTarget,
statePath: string,
private readonly runtimeId = "aetheria-electron-client",
options: AetheriaCultMeshClientOptions = {},
) {
this.daemonTarget = normalizeDaemonTarget(daemonTarget);
const publicationMode = options.publicationMode ?? "local";
this.publicationDescription = publicationMode === "remote" ? this.daemonTarget.uri : statePath;
this.verse = CultMesh.verse("aetheria.local", this.runtimeId);
this.queryVerse = publicationMode === "remote"
? this.verse.withRoute("network", this.publicationDescription)
: this.verse.withRoute("shared-memory", this.publicationDescription);
this.commandVerse = this.verse
.withRoute("network", this.daemonTarget.uri)
.withClaim("commander-control", { shardId: "aetheria.local" });
this.publications = CultMesh.documentsFromPublication(
this.createPublicationSource(publicationMode),
createAetheriaPublicationDocuments(statePath, publicationMode),
{
routeHint: this.queryVerse.context.routeHint,
timeoutMs: options.snapshotTimeoutMs,
pollMs: 50,
messageIdPrefix: `${this.runtimeId}:snapshot`,
},
);
const executors = {
mapViewport: async (request: ViewportRequest) =>
readViewportDocument(await this.fetchViewportDocument(AetheriaRtsSchemas.gameViewport, "aetheria.viewport.map", request)),
objectsViewport: async (request: ViewportRequest) =>
readObjectsViewportDocument(await this.fetchViewportDocument(AetheriaRtsSchemas.objectsViewport, "aetheria.viewport.objects", request)),
gravityViewport: async (request: ViewportRequest) =>
readGravityViewportDocument(await this.fetchViewportDocument(AetheriaRtsSchemas.gravityViewport, "aetheria.viewport.gravity", request)),
renderSplatsViewport: async (request: ViewportRequest) =>
readRenderSplatsViewportDocument(await this.fetchViewportDocument(AetheriaRtsSchemas.renderSplatsViewport, "aetheria.viewport.render_splats", request)),
selectedObject: async (request: SelectedObjectRequest) =>
readSelectedObjectDocument(await this.fetchIndexedDocument(AetheriaRtsSchemas.selectedObject, "aetheria.object.selected", request.entityIndex)),
inventory: async (request: SelectedObjectRequest) =>
readInventoryDocument(await this.fetchIndexedDocument(AetheriaRtsSchemas.inventory, "aetheria.inventory", request.entityIndex)),
daemonHealth: async () => readDaemonHealthDocument(await this.fetchDaemonHealthDocument()),
authorityStatus: async () => readAuthorityStatusDocument(await this.fetchAuthorityPolicyDocument()),
starbridgeSession: async () => readStarbridgeSessionSummaryDocument(await this.fetchStarbridgeSessionSummaryDocument()),
assetManifest: async () => readAssetManifestDocument(await this.fetchAssetManifestDocument()),
};
this.queryExecutors = executors;
this.queries = createAetheriaRuntimeGameQueryHandles(
executors,
this.queryVerse.context.routeHint,
{
objectsViewport: CultMesh.pollingQueryWatcher(executors.objectsViewport, { intervalMs: 50 }),
gravityViewport: CultMesh.pollingQueryWatcher(executors.gravityViewport, { intervalMs: 50 }),
renderSplatsViewport: CultMesh.pollingQueryWatcher(executors.renderSplatsViewport, { intervalMs: 50 }),
selectedObject: CultMesh.pollingQueryWatcher(executors.selectedObject, { intervalMs: 50 }),
inventory: CultMesh.pollingQueryWatcher(executors.inventory, { intervalMs: 50 }),
daemonHealth: CultMesh.pollingQueryWatcher(executors.daemonHealth, { intervalMs: 250 }),
authorityStatus: CultMesh.pollingQueryWatcher(executors.authorityStatus, { intervalMs: 250 }),
starbridgeSession: CultMesh.pollingQueryWatcher(executors.starbridgeSession, { intervalMs: 250 }),
assetManifest: CultMesh.pollingQueryWatcher(executors.assetManifest, { intervalMs: 1000 }),
},
);
this.documents = createAetheriaRuntimeGameDocuments(
this.queryVerse.context.routeHint,
{
daemonFrame: async () => this.fetchLatestFrameDocument(),
daemonHealth: async () => this.fetchDaemonHealthDocument(),
authorityPolicy: async () => this.fetchAuthorityPolicyDocument(),
starbridgeSession: async () => this.fetchStarbridgeSessionSummaryDocument(),
assetManifest: async () => this.fetchAssetManifestDocument(),
},
);
this.operations = createAetheriaRuntimeGameOperationHandles(
(commandId, issuedAtUtc, command, context) =>
this.sendCommandDocument(commandId, issuedAtUtc, command, context),
);
this.aetheria = createAetheriaRuntimeGameVerseHandles(
this.queryVerse.context,
this.commandVerse.context,
this.queries,
this.operations,
this.documents,
);
}
private readonly publicationDescription: string;
private readonly publications: CultMeshDocumentCatalog;
private readonly queryExecutors: AetheriaRuntimeGameQueryExecutors;
private readonly queries: ReturnType<typeof createAetheriaRuntimeGameQueryHandles>;
private readonly documents: AetheriaRuntimeGameDocuments;
private readonly operations: AetheriaRuntimeGameOperationHandles;
private readonly aetheria: AetheriaRuntimeGameVerseHandles;
public async close(): Promise<void> {
this.#peer?.close();
this.#peer = null;
}
public async waitForFrame(timeoutMs: number): Promise<void> {
const started = Date.now();
let lastError = "";
while (Date.now() - started < timeoutMs) {
try {
await this.mapViewport(CultMesh.viewportRequest(CultMesh.rectFromBounds(-1000, -1000, 1000, 1000)));
return;
} catch (error) {
lastError = error instanceof Error ? error.message : String(error);
await delay(250);
}
}
throw new Error(`Timed out waiting for Aetheria CultMesh frame at ${this.daemonTarget.uri}. ${lastError}`);
}
public async mapViewport(request: ViewportRequest): Promise<ViewportResponse> {
return this.aetheria.zone().viewport.within(request);
}
public async objectsViewport(request: ViewportRequest): Promise<ObjectsViewportResponse> {
return this.aetheria.zone().objects.visibleWithin(request);
}
public async gravityViewport(request: ViewportRequest): Promise<GravityViewportResponse> {
return this.aetheria.zone().gravity.within(request);
}
public async renderSplatsViewport(request: ViewportRequest): Promise<RenderSplatsViewportResponse> {
return this.aetheria.zone().renderSplats.within(request);
}
public async selectedObject(request: SelectedObjectRequest): Promise<SelectedObjectDocument> {
return this.aetheria.selectedObject(request.entityIndex);
}
public async inventory(request: SelectedObjectRequest): Promise<InventoryDocument> {
return this.aetheria.inventory(request.entityIndex);
}
public async daemonHealth(): Promise<DaemonHealthDocument> {
return this.aetheria.daemon.health();
}
public async authorityStatus(): Promise<AuthorityStatusDocument> {
return this.aetheria.daemon.authorityStatus();
}
public async starbridgeSession(): Promise<StarbridgeSessionDocument> {
return this.aetheria.daemon.starbridgeSession();
}
public async assetManifest(): Promise<AssetManifestDocument> {
return this.aetheria.daemon.assetManifest();
}
public queryDiagnostics(): Readonly<Record<string, AetheriaRuntimeGameQueryDiagnostic>> {
return describeAetheriaRuntimeGameQueryHandles(this.queries);
}
public liveFeedDiagnostics(): Readonly<Record<string, AetheriaRuntimeGameLiveFeedDiagnostic>> {
return {
viewportFeed: describeAetheriaRuntimeGameLiveFeedSurface(this.createViewportFeed()),
};
}
public surfaceCatalogDiagnostics(): AetheriaRuntimeGameSurfaceCatalogDiagnostic {
const catalog = describeAetheriaRuntimeGameSurfaceCatalog(this.queries, this.operations, this.documents);
return CultMesh.describeSurfaceCatalog(catalog.catalogId, [
...catalog.surfaces,
CultMesh.describeSurface(this.createViewportFeed()),
]);
}
public surfaceCatalogIndexDiagnostics(): CultMeshSurfaceCatalogIndexDiagnostic {
return CultMesh.surfaceCatalogIndex(this.surfaceCatalogDiagnostics());
}
public watchViewportFeed(
request: AetheriaRuntimeViewportFeedRequest,
callback: (snapshot: AetheriaRuntimeViewportFeedSnapshot) => void,
): () => void {
return this.createViewportFeed().watch(request, this.queryContext(), callback);
}
public async setMoveVector(request: RtsSetMoveVectorRequest): Promise<AetheriaRuntimeDaemonCommandReceipt> {
return this.aetheria.entity(request.actorEntityKey).pilot.move(
CultMesh.vec2(request.directionX, request.directionY),
{
scalar: request.scalar,
observedFrameId: request.observedFrameId,
},
);
}
public async setTarget(request: RtsSetTargetRequest): Promise<AetheriaRuntimeDaemonCommandReceipt> {
return this.aetheria.entity(request.actorEntityKey).pilot.target(request.targetEntityKey, {
observedFrameId: request.observedFrameId,
});
}
private async fetchLatestFrameDocument(): Promise<unknown> {
return this.fetchPublicationDocument(AetheriaRtsSchemas.daemonFrame);
}
private async fetchDaemonHealthDocument(): Promise<unknown> {
return this.fetchPublicationDocument(AetheriaRtsSchemas.daemonHealth);
}
private async fetchAuthorityPolicyDocument(): Promise<unknown> {
return this.fetchPublicationDocument(AetheriaRtsSchemas.verseAuthorityPolicy);
}
private async fetchStarbridgeSessionSummaryDocument(): Promise<unknown> {
return this.fetchPublicationDocument(AetheriaRtsSchemas.starbridgeSessionSummary);
}
private async fetchAssetManifestDocument(): Promise<unknown> {
return this.fetchPublicationDocument(AetheriaRtsSchemas.assetManifest);
}
private async fetchViewportDocument(
schemaId: string,
recordPrefix: string,
request: ViewportRequest,
): Promise<unknown> {
const recordKey = managedViewportRecordKey(recordPrefix, request);
const document = CultMesh.documentFromPublication(
{
kind: "peer-snapshot",
peer: () => this.peer(),
endpoint: this.resolvedRudpEndpoint(),
},
schemaId,
recordKey,
{
documentId: recordKey,
routeHint: this.queryVerse.context.routeHint,
sourceId: recordKey,
timeoutMs: 1500,
pollMs: 50,
messageIdPrefix: `${this.runtimeId}:viewport`,
},
);
return retryTransientPublicationRead(() => document.latest(this.queryContext()));
}
private async fetchIndexedDocument(
schemaId: string,
recordPrefix: string,
entityIndex: number,
): Promise<unknown> {
const recordKey = `${recordPrefix}.${Math.trunc(entityIndex)}`;
const document = CultMesh.documentFromPublication(
{
kind: "peer-snapshot",
peer: () => this.peer(),
endpoint: this.resolvedRudpEndpoint(),
},
schemaId,
recordKey,
{
documentId: recordKey,
routeHint: this.queryVerse.context.routeHint,
sourceId: recordKey,
timeoutMs: 1500,
pollMs: 50,
messageIdPrefix: `${this.runtimeId}:indexed-document`,
},
);
return retryTransientPublicationRead(() => document.latest(this.queryContext()));
}
private fetchPublicationDocument(schemaId: string): Promise<unknown> {
return retryTransientPublicationRead(() => this.publications.latest({ schemaId }, this.queryContext()));
}
private createPublicationSource(
mode: "local" | "remote",
): (binding: CultMeshPublicationDocumentBinding) => CultMeshDocumentPublicationSource {
if (mode === "remote") {
return () => ({
kind: "peer-snapshot",
peer: () => this.peer(),
endpoint: this.resolvedRudpEndpoint(),
});
}
return binding => ({
kind: "single-file",
path: (binding as AetheriaPublicationDocumentSpec).localPath,
});
}
private createViewportFeed() {
return CultMesh.liveFeed<AetheriaRuntimeViewportFeedRequest, AetheriaRuntimeViewportFeedSnapshot>(
"gamecult.aetheria.viewport_feed.v1",
async (request, context) => {
const objects = await this.queries.objectsViewport.execute(request.viewport, context);
return this.createViewportFeedSnapshot(request, objects);
},
{
sources: [
...this.queries.objectsViewport.sources,
...this.queries.gravityViewport.sources,
...this.queries.daemonHealth.sources,
...this.queries.authorityStatus.sources,
...this.queries.starbridgeSession.sources,
...this.queries.assetManifest.sources,
],
routeHint: this.queryVerse.context.routeHint,
watchFeed: (request, context, callback) => {
let disposed = false;
let sampling = false;
const unsubscribe = this.queries.objectsViewport.watch(request.viewport, context, objects => {
if (disposed || sampling) {
return;
}
sampling = true;
void this.createViewportFeedSnapshot(request, objects)
.then(snapshot => {
if (!disposed) {
callback(snapshot);
}
})
.finally(() => {
sampling = false;
});
});
return () => {
disposed = true;
unsubscribe();
};
},
},
);
}
private async createViewportFeedSnapshot(
request: AetheriaRuntimeViewportFeedRequest,
objects: ObjectsViewportResponse,
): Promise<AetheriaRuntimeViewportFeedSnapshot> {
const startedAt = performance.now();
const selectedEntityIndex = request.selectedEntityIndex ?? -1;
const [
gravity,
daemonHealth,
authorityStatus,
starbridgeSession,
assetManifest,
selectedObject,
inventory,
] = await Promise.all([
this.queryExecutors.gravityViewport(request.viewport),
this.queryExecutors.daemonHealth(),
this.queryExecutors.authorityStatus(),
this.queryExecutors.starbridgeSession(),
this.queryExecutors.assetManifest(),
selectedEntityIndex >= 0
? this.queryExecutors.selectedObject({ entityIndex: selectedEntityIndex })
: Promise.resolve(null),
selectedEntityIndex >= 0
? this.queryExecutors.inventory({ entityIndex: selectedEntityIndex })
: Promise.resolve(null),
]);
return {
viewport: composeViewport(objects, gravity),
selectedObject,
inventory,
daemonHealth,
authorityStatus,
starbridgeSession,
assetManifest,
receivedAtUtc: new Date().toISOString(),
sampleMs: performance.now() - startedAt,
};
}
private queryContext(): CultMeshQueryContext {
return this.queryVerse.queryContext();
}
private async sendCommandDocument(
commandId: string,
issuedAtUtc: string,
command: unknown[],
context: CultMeshOperationContext,
): Promise<void> {
const message: CultNetDocumentPutRawMessage = {
schemaVersion: "cultnet.document_put_raw.v0",
messageId: commandId,
document: {
schemaId: AetheriaRtsSchemas.daemonCommand,
recordKey: `daemon:commands:${stableToken(commandId)}:${AetheriaRtsSchemas.daemonCommand}`,
storedAt: issuedAtUtc,
payloadEncoding: "messagepack",
payload: encode(command),
sourceRuntimeId: context.runtimeId,
sourceRole: "aetheria-client",
tags: ["aetheria"],
},
};
const peer = await this.peer();
peer.send(message);
await delay(80);
}
private async peer(): Promise<CultNetPeer> {
if (this.#peer)
return this.#peer;
const peers = CultMesh.createPeerCatalog();
const leases = CultMesh.createAuthorityLeaseCatalog();
const leaseId = `${this.daemonTarget.peerId}:authority`;
peers.upsert({
peerId: this.daemonTarget.peerId,
verseId: this.daemonTarget.verseId,
endpoints: this.daemonTarget.endpoints,
roles: [this.daemonTarget.role],
authorityLeaseId: leaseId,
});
leases.upsert({
leaseId,
verseId: this.daemonTarget.verseId,
peerId: this.daemonTarget.peerId,
roles: [this.daemonTarget.role],
validFrom: new Date(Date.now() - 60_000),
expiresAt: new Date(Date.now() + 60_000),
});
this.#peer = await CultMesh.createRudpPeerForAuthorizedPeer(
this.runtimeId,
connectionId,
peers,
leases,
this.daemonTarget.verseId,
this.daemonTarget.role,
{
connectTimeoutMs: 2000,
maxFragmentBytes: 1200,
maxPendingReliablePackets: 512,
},
);
this.#peer.on("close", () => {
this.#peer = null;
});
return this.#peer;
}
private resolvedRudpEndpoint(): string {
const endpoint = this.daemonTarget.endpoints.find(value => value.toLowerCase().startsWith("rudp://"));
if (!endpoint) {
throw new Error(`Daemon ${this.daemonTarget.uri} has no resolved RUDP transport endpoint. Resolve it through Odin/CultMesh before opening a transport peer.`);
}
return endpoint;
}
}
function normalizeDaemonTarget(target: string | AetheriaCultMeshDaemonTarget): Required<AetheriaCultMeshDaemonTarget> {
const value = typeof target === "string"
? { uri: target }
: target;
if (!value.uri.trim()) {
throw new Error("Aetheria CultMesh daemon target URI is required.");
}
if (value.uri.toLowerCase().startsWith("rudp://")) {
throw new Error("Aetheria daemon targets must be CultMesh URIs; raw RUDP endpoints belong in resolved peer-card endpoints.");
}
return {
uri: value.uri,
peerId: value.peerId?.trim() || "aetheria-daemon",
verseId: value.verseId?.trim() || "aetheria.local",
role: value.role?.trim() || "aetheria-daemon",
endpoints: value.endpoints ?? [],
};
}
function managedViewportRecordKey(prefix: string, request: ViewportRequest): string {
const minX = Math.min(finiteNumber(request.minX), finiteNumber(request.maxX));
const minY = Math.min(finiteNumber(request.minY), finiteNumber(request.maxY));
const maxX = Math.max(finiteNumber(request.minX), finiteNumber(request.maxX));
const maxY = Math.max(finiteNumber(request.minY), finiteNumber(request.maxY));
return `${prefix}.${viewportToken(minX)}.${viewportToken(minY)}.${viewportToken(maxX)}.${viewportToken(maxY)}`;
}
function viewportToken(value: number): string {
const rounded = Math.round(value * 1000) / 1000;
return rounded
.toString()
.replace("-", "n")
.replace(".", "p");
}
function finiteNumber(value: unknown): number {
return typeof value === "number" && Number.isFinite(value) ? value : 0;
}
async function retryTransientPublicationRead<T>(read: () => Promise<T>): Promise<T> {
let lastError: unknown;
for (let attempt = 0; attempt < 40; attempt += 1) {
try {
return await read();
} catch (error) {
lastError = error;
if (!isTransientPublicationReadError(error))
throw error;
await delay(25 + attempt * 8);
}
}
throw lastError;
}
function isTransientPublicationReadError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error);
return message.includes("EBUSY") ||
message.includes("EPERM") ||
message.includes("ENOENT") ||
message.includes("did not contain schema");
}
function stableToken(value: string): string {
return value
.replace(/[^a-zA-Z0-9]+/gu, "-")
.replace(/^-+|-+$/gu, "")
.replace(/--+/gu, "-")
.toLowerCase() || "empty";
}
function composeViewport(
objects: ObjectsViewportResponse,
gravity: GravityViewportResponse,
): ViewportResponse {
return {
schema: "gamecult.aetheria.game_viewport.v1",
frameId: objects.frameId,
publishedAtUtc: objects.publishedAtUtc,
simulationTimeSeconds: objects.simulationTimeSeconds,
runId: objects.runId,
zoneIndex: objects.zoneIndex,
zoneName: objects.zoneName,
currentEntityKey: objects.currentEntityKey,
viewport: objects.viewport,
controlledEntityIndices: objects.controlledEntityIndices,
objects: objects.objects,
gravityInfluences: gravity.gravityInfluences,
bodies: gravity.bodies,
};
}
function createAetheriaPublicationDocuments(
statePath: string,
mode: "local" | "remote",
): readonly AetheriaPublicationDocumentSpec[] {
const documents: readonly AetheriaPublicationDocumentSpec[] = [
{
...CultMesh.publicationDocument(
AetheriaRtsSchemas.daemonFrame,
"daemon:aetheria.frame.latest.v1",
{
documentId: "daemon:aetheria.frame.latest.v1",
sourceId: "daemon:aetheria.frame.latest.v1",
},
),
localPath: statePath,
},
{
...CultMesh.publicationDocument(
AetheriaRtsSchemas.daemonHealth,
"daemon:aetheria.health.latest.v1",
{
documentId: "daemon:aetheria.health.latest.v1",
sourceId: "daemon:aetheria.health.latest.v1",
},
),
localPath: statePath,
remoteRecordKey: "daemon:aetheria.health.v1",
},
{
...CultMesh.publicationDocument(
AetheriaRtsSchemas.verseAuthorityPolicy,
"daemon:aetheria.authority.policy.latest.v1",
{
documentId: "daemon:aetheria.authority.policy.latest.v1",
sourceId: "daemon:aetheria.authority.policy.latest.v1",
},
),
localPath: statePath,
remoteRecordKey: "global:aetheria.verse_authority_policy.v1",
},
{
...CultMesh.publicationDocument(
AetheriaRtsSchemas.starbridgeSessionSummary,
"daemon:aetheria.starbridge.session.latest.v1",
{
documentId: "daemon:aetheria.starbridge.session.latest.v1",
sourceId: "daemon:aetheria.starbridge.session.latest.v1",
},
),
localPath: statePath,
},
{
...CultMesh.publicationDocument(
AetheriaRtsSchemas.assetManifest,
"daemon:aetheria.asset_manifest.latest.v1",
{
documentId: "daemon:aetheria.asset_manifest.latest.v1",
sourceId: "daemon:aetheria.asset_manifest.latest.v1",
},
),
localPath: statePath,
},
];
return documents.map(document => mode === "remote" && document.remoteRecordKey
? {
...document,
recordKey: document.remoteRecordKey,
}
: document);
}
function readField(value: unknown, property: string, index: number): unknown {
if (Array.isArray(value))
return value[index];
if (value && typeof value === "object") {
const record = value as Record<string, unknown>;
if (property in record)
return record[property];
const pascal = property.charAt(0).toUpperCase() + property.slice(1);
if (pascal in record)
return record[pascal];
}
return undefined;
}
function arrayValue(value: unknown): unknown[] {
return Array.isArray(value) ? value : [];
}
function normalizeCommandPayload(payload: Record<string, unknown> | undefined): Record<string, string> {
const result: Record<string, string> = {};
for (const [key, value] of Object.entries(payload ?? {})) {
if (value == null)
continue;
result[key] = typeof value === "string" ? value : String(value);
}
return result;
}
function objectValue(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? value as Record<string, unknown>
: {};
}
function stringOr(value: unknown, fallback: string): string {
return typeof value === "string" ? value : fallback;
}
function numberOr(value: unknown, fallback: number): number {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}
function stringRecord(value: unknown): Record<string, string> {
const source = objectValue(value);
const result: Record<string, string> = {};
for (const [key, entry] of Object.entries(source)) {
if (entry == null)
continue;
result[key] = typeof entry === "string" ? entry : String(entry);
}
return result;
}
function delay(milliseconds: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}

View file

@ -0,0 +1,58 @@
import type { CultMeshViewportRequest } from "cultmesh-ts";
export {
AetheriaRtsIpcChannels,
AetheriaRtsSchemas,
AetheriaRuntimeDaemonCommandKinds,
aetheriaRuntimeEveCommandDocumentSlots,
createAetheriaRuntimeGameDocuments,
createAetheriaRuntimeGameOperationHandles,
createAetheriaRuntimeGameQueryHandles,
createAetheriaRuntimeGameStatePointers,
createAetheriaRuntimeGameVerseHandles,
describeAetheriaRuntimeGameLiveFeedSurface,
describeAetheriaRuntimeGameQueryHandles,
describeAetheriaRuntimeGameSurfaceCatalog,
} from "./aetheria-rts-generated-bindings.js";
export type {
AetheriaRuntimeDaemonCommandReceipt,
AetheriaRuntimeGameLiveFeedDiagnostic,
AetheriaRuntimeGameDocumentResolvers,
AetheriaRuntimeGameDocuments,
AetheriaRuntimeGameOperationHandles,
AetheriaRuntimeGameQueryDiagnostic,
AetheriaRuntimeGameStatePointerResolvers,
AetheriaRuntimeGameStatePointers,
AetheriaRuntimeGameSurfaceCatalogDiagnostic,
AetheriaRuntimeGameVerseHandles,
AetheriaRuntimeSelectedObjectRequest as SelectedObjectRequest,
AetheriaRuntimeSetMoveVectorRequest as RtsSetMoveVectorRequest,
AetheriaRuntimeSetTargetRequest as RtsSetTargetRequest,
AetheriaRuntimeViewportFeedRequest,
AetheriaRuntimeViewportFeedSnapshot,
AssetManifestEntry,
AssetManifestDocument,
AssetRef,
AuthorityRuleDocument,
AuthorityStatusDocument,
BodyView,
DaemonHealthDocument,
EntityStatus,
GravityInfluence,
GravityViewportResponse,
InventoryItem,
InventoryDocument,
ObjectsViewportResponse,
RenderSplatsViewportResponse,
SelectedObjectDocument,
StarbridgeBaseStatusDocument,
StarbridgeRuntimeRoleDocument,
StarbridgeSessionDocument,
StarbridgeStationStockItemDocument,
StarbridgeWaveForecastDocument,
ViewObject,
ViewportResponse,
} from "./aetheria-rts-generated-bindings.js";
export type ViewportRequest = CultMeshViewportRequest;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,495 @@
import {
AetheriaRtsSchemas,
aetheriaRuntimeAssetManifestDocumentSlots as assetManifestSlots,
aetheriaRuntimeAssetManifestEntrySlots as assetManifestEntrySlots,
aetheriaRuntimeAssetRefSlots as assetRefSlots,
aetheriaRuntimeAuthorityRuleSlots as authorityRuleSlots,
aetheriaRuntimeDaemonHealthDocumentSlots as healthSlots,
aetheriaRuntimeGameViewportBoundsSlots as viewportBoundsSlots,
aetheriaRuntimeGameViewportDocumentSlots as gameViewportSlots,
aetheriaRuntimeGameViewportObjectSlots as viewportObjectSlots,
aetheriaRuntimeGravityViewportDocumentSlots as gravityViewportSlots,
aetheriaRuntimeInventoryDocumentSlots as inventoryDocumentSlots,
aetheriaRuntimeObjectsViewportDocumentSlots as objectsViewportSlots,
aetheriaRuntimeRenderSplatLayerDefinitionSlots as renderSplatLayerSlots,
aetheriaRuntimeRenderSplatsViewportDocumentSlots as renderSplatsViewportSlots,
aetheriaRuntimeRenderSplatSoaSlots as renderSplatSoaSlots,
aetheriaRuntimeRtsBodyViewSlots as bodyViewSlots,
aetheriaRuntimeRtsEntityStatusSlots as viewportStatusSlots,
aetheriaRuntimeRtsGravityInfluenceSlots as gravityInfluenceSlots,
aetheriaRuntimeRtsInventoryItemSlots as viewportInventoryItemSlots,
aetheriaRuntimeSelectedObjectDocumentSlots as selectedObjectDocumentSlots,
aetheriaRuntimeStarbridgeBaseStatusSlots as starbridgeBaseSlots,
aetheriaRuntimeStarbridgeRuntimeRoleSlots as starbridgeRoleSlots,
aetheriaRuntimeStarbridgeSessionSummaryDocumentSlots as starbridgeSummarySlots,
aetheriaRuntimeStarbridgeStationStockItemSlots as starbridgeStockSlots,
aetheriaRuntimeStarbridgeWaveForecastSlots as starbridgeWaveForecastSlots,
aetheriaRuntimeVerseAuthorityPolicyDocumentSlots as authorityPolicySlots,
} from "./aetheria-rts-generated-bindings.js";
import { cultMeshRectFromBounds, cultMeshViewportRequest } from "cultmesh-ts";
import type {
AssetManifestDocument,
AssetRef,
AuthorityStatusDocument,
BodyView,
DaemonHealthDocument,
EntityStatus,
GravityViewportResponse,
GravityInfluence,
InventoryDocument,
InventoryItem,
ObjectsViewportResponse,
RenderSplatsViewportResponse,
SelectedObjectDocument,
StarbridgeSessionDocument,
ViewObject,
ViewportRequest,
ViewportResponse,
} from "./aetheria-rts-bindings.js";
const missingDaemonRunId = "aetheria.run.unknown";
export function readSelectedObjectDocument(selectedObjectDocument: unknown): SelectedObjectDocument {
const document = arr(selectedObjectDocument);
return {
schema: str(document[selectedObjectDocumentSlots.schema]) || AetheriaRtsSchemas.selectedObject,
frameId: num(document[selectedObjectDocumentSlots.frameId]),
runId: str(document[selectedObjectDocumentSlots.runId]) || missingDaemonRunId,
zoneIndex: num(document[selectedObjectDocumentSlots.zoneIndex]),
entityIndex: num(document[selectedObjectDocumentSlots.entityIndex], -1),
selected: readNullableViewportObject(document[selectedObjectDocumentSlots.selected]),
};
}
export function readInventoryDocument(inventoryDocument: unknown): InventoryDocument {
const document = arr(inventoryDocument);
return {
schema: str(document[inventoryDocumentSlots.schema]) || AetheriaRtsSchemas.inventory,
frameId: num(document[inventoryDocumentSlots.frameId]),
runId: str(document[inventoryDocumentSlots.runId]) || missingDaemonRunId,
zoneIndex: num(document[inventoryDocumentSlots.zoneIndex]),
entityIndex: num(document[inventoryDocumentSlots.entityIndex], -1),
entityKey: str(document[inventoryDocumentSlots.entityKey]),
items: list<unknown[]>(document[inventoryDocumentSlots.items]).map(readViewportInventoryItem),
equipment: list<unknown[]>(document[inventoryDocumentSlots.equipment]).map(readViewportInventoryItem),
cargo: list<unknown[]>(document[inventoryDocumentSlots.cargo]).map(readViewportInventoryItem),
};
}
export function readViewportDocument(viewportDocument: unknown): ViewportResponse {
const document = arr(viewportDocument);
const objects = readViewportObjects(list<unknown[]>(document[gameViewportSlots.objects]));
const gravityInfluences = readGravityInfluences(list<unknown[]>(document[gameViewportSlots.gravityInfluences]));
const bodies = readBodyViews(list<unknown[]>(document[gameViewportSlots.bodies]));
return {
schema: str(document[gameViewportSlots.schema]) || AetheriaRtsSchemas.gameViewport,
frameId: num(document[gameViewportSlots.frameId]),
publishedAtUtc: str(document[gameViewportSlots.publishedAtUtc]),
simulationTimeSeconds: num(document[gameViewportSlots.simulationTimeSeconds]),
runId: str(document[gameViewportSlots.runId]) || missingDaemonRunId,
zoneIndex: num(document[gameViewportSlots.zoneIndex]),
zoneName: str(document[gameViewportSlots.zoneName]),
currentEntityKey: str(document[gameViewportSlots.currentEntityKey]),
viewport: readViewportBounds(document[gameViewportSlots.viewport]),
controlledEntityIndices: numberList(document[gameViewportSlots.controlledEntityIndices]),
objects,
gravityInfluences,
bodies,
};
}
export function readObjectsViewportDocument(viewportDocument: unknown): ObjectsViewportResponse {
const document = arr(viewportDocument);
return {
schema: str(document[objectsViewportSlots.schema]) || AetheriaRtsSchemas.objectsViewport,
frameId: num(document[objectsViewportSlots.frameId]),
publishedAtUtc: str(document[objectsViewportSlots.publishedAtUtc]),
simulationTimeSeconds: num(document[objectsViewportSlots.simulationTimeSeconds]),
runId: str(document[objectsViewportSlots.runId]) || missingDaemonRunId,
zoneIndex: num(document[objectsViewportSlots.zoneIndex]),
zoneName: str(document[objectsViewportSlots.zoneName]),
currentEntityKey: str(document[objectsViewportSlots.currentEntityKey]),
viewport: readViewportBounds(document[objectsViewportSlots.viewport]),
controlledEntityIndices: numberList(document[objectsViewportSlots.controlledEntityIndices]),
objects: readViewportObjects(list<unknown[]>(document[objectsViewportSlots.objects])),
};
}
export function readGravityViewportDocument(viewportDocument: unknown): GravityViewportResponse {
const document = arr(viewportDocument);
return {
schema: str(document[gravityViewportSlots.schema]) || AetheriaRtsSchemas.gravityViewport,
frameId: num(document[gravityViewportSlots.frameId]),
publishedAtUtc: str(document[gravityViewportSlots.publishedAtUtc]),
simulationTimeSeconds: num(document[gravityViewportSlots.simulationTimeSeconds]),
runId: str(document[gravityViewportSlots.runId]) || missingDaemonRunId,
zoneIndex: num(document[gravityViewportSlots.zoneIndex]),
zoneName: str(document[gravityViewportSlots.zoneName]),
viewport: readViewportBounds(document[gravityViewportSlots.viewport]),
gravityInfluences: readGravityInfluences(list<unknown[]>(document[gravityViewportSlots.gravityInfluences])),
bodies: readBodyViews(list<unknown[]>(document[gravityViewportSlots.bodies])),
};
}
export function readRenderSplatsViewportDocument(viewportDocument: unknown): RenderSplatsViewportResponse {
const document = arr(viewportDocument);
return {
schema: str(document[renderSplatsViewportSlots.schema]) || AetheriaRtsSchemas.renderSplatsViewport,
frameId: num(document[renderSplatsViewportSlots.frameId]),
publishedAtUtc: str(document[renderSplatsViewportSlots.publishedAtUtc]),
simulationTimeSeconds: num(document[renderSplatsViewportSlots.simulationTimeSeconds]),
runId: str(document[renderSplatsViewportSlots.runId]) || missingDaemonRunId,
zoneIndex: num(document[renderSplatsViewportSlots.zoneIndex]),
zoneName: str(document[renderSplatsViewportSlots.zoneName]),
viewport: readViewportBounds(document[renderSplatsViewportSlots.viewport]),
layers: list<unknown[]>(document[renderSplatsViewportSlots.layers]).map(readRenderSplatLayer),
splats: readRenderSplatSoa(document[renderSplatsViewportSlots.splats]),
};
}
export function readDaemonHealthDocument(healthDocument: unknown): DaemonHealthDocument {
const health = arr(healthDocument);
return {
schema: str(health[healthSlots.schema]) || AetheriaRtsSchemas.daemonHealth,
daemonId: str(health[healthSlots.daemonId]),
verseId: str(health[healthSlots.verseId]),
publishedAtUtc: str(health[healthSlots.publishedAtUtc]),
statePath: str(health[healthSlots.statePath]),
frameId: num(health[healthSlots.frameId]),
observedCommandCount: num(health[healthSlots.observedCommandCount]),
appliedCommandCount: num(health[healthSlots.appliedCommandCount]),
rejectedCommandCount: num(health[healthSlots.rejectedCommandCount]),
status: str(health[healthSlots.status]) || "unknown",
publicationSource: str(health[healthSlots.publicationSource]),
transport: str(health[healthSlots.transport]),
commandBoundaryPath: str(health[healthSlots.commandBoundaryPath]),
};
}
export function readAuthorityStatusDocument(policyDocument: unknown): AuthorityStatusDocument {
const policy = arr(policyDocument);
return {
schema: str(policy[authorityPolicySlots.schema]) || AetheriaRtsSchemas.verseAuthorityPolicy,
verseId: str(policy[authorityPolicySlots.verseId]),
policyId: str(policy[authorityPolicySlots.policyId]),
ruleVersion: str(policy[authorityPolicySlots.ruleVersion]),
hostRuntimeId: str(policy[authorityPolicySlots.hostRuntimeId]),
defaultMode: str(policy[authorityPolicySlots.defaultMode]),
updatedAtUtc: str(policy[authorityPolicySlots.updatedAtUtc]),
rules: list<unknown[]>(policy[authorityPolicySlots.rules]).map(rule => ({
ruleId: str(rule[authorityRuleSlots.ruleId]),
subjectPrefix: str(rule[authorityRuleSlots.subjectPrefix]),
claimKinds: stringList(rule[authorityRuleSlots.claimKinds]),
mode: str(rule[authorityRuleSlots.mode]),
runtimeIds: stringList(rule[authorityRuleSlots.runtimeIds]),
leaseScope: str(rule[authorityRuleSlots.leaseScope]),
priority: num(rule[authorityRuleSlots.priority]),
})),
};
}
export function readStarbridgeSessionSummaryDocument(summaryDocument: unknown): StarbridgeSessionDocument {
const summary = arr(summaryDocument);
return {
schema: str(summary[starbridgeSummarySlots.schema]) || AetheriaRtsSchemas.starbridgeSessionSummary,
frameId: num(summary[starbridgeSummarySlots.frameId]),
publishedAtUtc: str(summary[starbridgeSummarySlots.publishedAtUtc]),
sessionId: str(summary[starbridgeSummarySlots.sessionId]) || "starbridge-session",
scenarioId: str(summary[starbridgeSummarySlots.scenarioId]) || "starbridge.local",
scenarioName: str(summary[starbridgeSummarySlots.scenarioName]) || "Starbridge",
runId: str(summary[starbridgeSummarySlots.runId]) || "local-starbridge",
zoneIndex: num(summary[starbridgeSummarySlots.zoneIndex]),
zoneName: str(summary[starbridgeSummarySlots.zoneName]),
phase: str(summary[starbridgeSummarySlots.phase]) || "setup",
currentWaveIndex: num(summary[starbridgeSummarySlots.currentWaveIndex]),
baseStatus: toStarbridgeBaseStatus(arr(summary[starbridgeSummarySlots.baseStatus])),
stationStock: list<unknown[]>(summary[starbridgeSummarySlots.stationStock]).map(toStarbridgeStationStockItem),
waveForecast: list<unknown[]>(summary[starbridgeSummarySlots.waveForecast]).map(toStarbridgeWaveForecast),
runtimeRoles: list<unknown[]>(summary[starbridgeSummarySlots.runtimeRoles]).map(toStarbridgeRuntimeRole),
};
}
export function readAssetManifestDocument(assetManifestDocument: unknown): AssetManifestDocument {
const manifest = arr(assetManifestDocument);
return {
schema: str(manifest[assetManifestSlots.schema]) || AetheriaRtsSchemas.assetManifest,
publishedAtUtc: str(manifest[assetManifestSlots.publishedAtUtc]),
runId: str(manifest[assetManifestSlots.runId]),
baseUri: str(manifest[assetManifestSlots.baseUri]),
assets: list<unknown[]>(manifest[assetManifestSlots.assets]).map(toAssetManifestEntry),
};
}
function readViewportBounds(value: unknown): ViewportRequest {
const bounds = arr(value);
return cultMeshViewportRequest(
cultMeshRectFromBounds(
num(bounds[viewportBoundsSlots.minX]),
num(bounds[viewportBoundsSlots.minY]),
num(bounds[viewportBoundsSlots.maxX]),
num(bounds[viewportBoundsSlots.maxY]),
),
[],
);
}
function readViewportObjects(objects: unknown[][]): ViewObject[] {
return objects.map(readViewportObject);
}
function readNullableViewportObject(value: unknown): ViewObject | null {
return Array.isArray(value) ? readViewportObject(value) : null;
}
function readViewportObject(object: unknown[]): ViewObject {
return {
entityIndex: num(object[viewportObjectSlots.entityIndex], -1),
entityKey: str(object[viewportObjectSlots.entityKey]),
displayName: str(object[viewportObjectSlots.displayName]),
kind: str(object[viewportObjectSlots.kind]),
factionKey: str(object[viewportObjectSlots.factionKey]),
x: num(object[viewportObjectSlots.x]),
y: num(object[viewportObjectSlots.y]),
z: num(object[viewportObjectSlots.z]),
directionX: num(object[viewportObjectSlots.directionX]),
directionY: num(object[viewportObjectSlots.directionY]),
velocityX: num(object[viewportObjectSlots.velocityX]),
velocityY: num(object[viewportObjectSlots.velocityY]),
controlled: bool(object[viewportObjectSlots.controlled]),
targetEntityIndex: num(object[viewportObjectSlots.targetEntityIndex], -1),
isActive: object[viewportObjectSlots.isActive] !== false,
visibility: num(object[viewportObjectSlots.visibility]),
iconAsset: assetRef(arr(object[viewportObjectSlots.iconAsset])),
status: readViewportStatus(object[viewportObjectSlots.status]),
inventory: list<unknown[]>(object[viewportObjectSlots.inventory]).map(readViewportInventoryItem),
};
}
function readViewportStatus(value: unknown): EntityStatus {
const status = arr(value);
return {
hull: num(status[viewportStatusSlots.hull]),
shield: num(status[viewportStatusSlots.shield]),
heat: num(status[viewportStatusSlots.heat]),
};
}
function readViewportInventoryItem(value: unknown[]): InventoryItem {
const itemKey = str(value[viewportInventoryItemSlots.itemKey]);
return {
source: str(value[viewportInventoryItemSlots.source]),
itemKey,
quantity: num(value[viewportInventoryItemSlots.quantity]),
quality: num(value[viewportInventoryItemSlots.quality]),
durability: num(value[viewportInventoryItemSlots.durability]),
enabled: value[viewportInventoryItemSlots.enabled] !== false,
iconAsset: assetRef(arr(value[viewportInventoryItemSlots.iconAsset])),
};
}
function readGravityInfluences(influences: unknown[][]): GravityInfluence[] {
return influences.map(influence => ({
bodyKey: str(influence[gravityInfluenceSlots.bodyKey]),
orbitKey: str(influence[gravityInfluenceSlots.orbitKey]),
kind: str(influence[gravityInfluenceSlots.kind]),
x: num(influence[gravityInfluenceSlots.x]),
y: num(influence[gravityInfluenceSlots.y]),
radius: num(influence[gravityInfluenceSlots.radius]),
gravityDepth: num(influence[gravityInfluenceSlots.gravityDepth]),
gravityDepthExponent: num(influence[gravityInfluenceSlots.gravityDepthExponent]),
waveRadius: num(influence[gravityInfluenceSlots.waveRadius]),
waveDepth: num(influence[gravityInfluenceSlots.waveDepth]),
waveSpeed: num(influence[gravityInfluenceSlots.waveSpeed]),
}));
}
function readBodyViews(bodies: unknown[][]): BodyView[] {
return bodies.map(body => {
const kind = str(body[bodyViewSlots.kind]);
return {
bodyKey: str(body[bodyViewSlots.bodyKey]),
orbitKey: str(body[bodyViewSlots.orbitKey]),
name: str(body[bodyViewSlots.name]),
kind,
x: num(body[bodyViewSlots.x]),
y: num(body[bodyViewSlots.y]),
radius: num(body[bodyViewSlots.radius]),
isAsteroidBelt: bool(body[bodyViewSlots.isAsteroidBelt]),
body: num(body[bodyViewSlots.body]),
iconAsset: assetRef(arr(body[bodyViewSlots.iconAsset])),
iconSize: num(body[bodyViewSlots.iconSize]),
};
});
}
function readRenderSplatLayer(layer: unknown[]): RenderSplatsViewportResponse["layers"][number] {
return {
layerKey: str(layer[renderSplatLayerSlots.layerKey]),
displayName: str(layer[renderSplatLayerSlots.displayName]),
channel: num(layer[renderSplatLayerSlots.channel]),
blendMode: str(layer[renderSplatLayerSlots.blendMode]),
graphicsFormat: str(layer[renderSplatLayerSlots.graphicsFormat]),
clearBeforeDraw: bool(layer[renderSplatLayerSlots.clearBeforeDraw]),
clearR: num(layer[renderSplatLayerSlots.clearR]),
clearG: num(layer[renderSplatLayerSlots.clearG]),
clearB: num(layer[renderSplatLayerSlots.clearB]),
clearA: num(layer[renderSplatLayerSlots.clearA]),
};
}
function readRenderSplatSoa(value: unknown): RenderSplatsViewportResponse["splats"] {
const splats = arr(value);
return {
count: num(splats[renderSplatSoaSlots.count]),
centerX: numberList(splats[renderSplatSoaSlots.centerX]),
centerY: numberList(splats[renderSplatSoaSlots.centerY]),
halfExtentX: numberList(splats[renderSplatSoaSlots.halfExtentX]),
halfExtentY: numberList(splats[renderSplatSoaSlots.halfExtentY]),
rotationCos: numberList(splats[renderSplatSoaSlots.rotationCos]),
rotationSin: numberList(splats[renderSplatSoaSlots.rotationSin]),
channel: numberList(splats[renderSplatSoaSlots.channel]),
falloff: numberList(splats[renderSplatSoaSlots.falloff]),
valueR: numberList(splats[renderSplatSoaSlots.valueR]),
valueG: numberList(splats[renderSplatSoaSlots.valueG]),
valueB: numberList(splats[renderSplatSoaSlots.valueB]),
valueA: numberList(splats[renderSplatSoaSlots.valueA]),
sourceKey: stringList(splats[renderSplatSoaSlots.sourceKey]),
layerIndex: numberList(splats[renderSplatSoaSlots.layerIndex]),
sourceKind: numberList(splats[renderSplatSoaSlots.sourceKind]),
frequencyX: numberList(splats[renderSplatSoaSlots.frequencyX]),
frequencyY: numberList(splats[renderSplatSoaSlots.frequencyY]),
phaseX: numberList(splats[renderSplatSoaSlots.phaseX]),
phaseY: numberList(splats[renderSplatSoaSlots.phaseY]),
animationSpeed: numberList(splats[renderSplatSoaSlots.animationSpeed]),
sourceFlags: numberList(splats[renderSplatSoaSlots.sourceFlags]),
};
}
function toStarbridgeBaseStatus(status: unknown[]): StarbridgeSessionDocument["baseStatus"] {
return {
entityKey: str(status[starbridgeBaseSlots.entityKey]),
displayName: str(status[starbridgeBaseSlots.displayName]),
hull: num(status[starbridgeBaseSlots.hull]),
shield: num(status[starbridgeBaseSlots.shield]),
heat: num(status[starbridgeBaseSlots.heat]),
isActive: bool(status[starbridgeBaseSlots.isActive]),
};
}
function toStarbridgeStationStockItem(item: unknown[]): StarbridgeSessionDocument["stationStock"][number] {
return {
itemKey: str(item[starbridgeStockSlots.itemKey]),
quantity: num(item[starbridgeStockSlots.quantity]),
quality: num(item[starbridgeStockSlots.quality]),
durability: num(item[starbridgeStockSlots.durability]),
source: str(item[starbridgeStockSlots.source]) || "station",
iconAsset: assetRef(arr(item[starbridgeStockSlots.iconAsset])),
};
}
function toStarbridgeWaveForecast(wave: unknown[]): StarbridgeSessionDocument["waveForecast"][number] {
return {
waveIndex: num(wave[starbridgeWaveForecastSlots.waveIndex]),
displayName: str(wave[starbridgeWaveForecastSlots.displayName]),
attackerKeys: stringList(wave[starbridgeWaveForecastSlots.attackerKeys]),
bossKey: str(wave[starbridgeWaveForecastSlots.bossKey]),
recoveredTechnologyKeys: stringList(wave[starbridgeWaveForecastSlots.recoveredTechnologyKeys]),
};
}
function toStarbridgeRuntimeRole(role: unknown[]): StarbridgeSessionDocument["runtimeRoles"][number] {
return {
runtimeId: str(role[starbridgeRoleSlots.runtimeId]),
role: str(role[starbridgeRoleSlots.role]),
entityKey: str(role[starbridgeRoleSlots.entityKey]),
};
}
function toAssetManifestEntry(entry: unknown[]): AssetManifestDocument["assets"][number] {
return {
ref: assetRef(arr(entry[assetManifestEntrySlots.ref])),
sizeBytes: num(entry[assetManifestEntrySlots.sizeBytes]),
width: num(entry[assetManifestEntrySlots.width]),
height: num(entry[assetManifestEntrySlots.height]),
tags: stringList(entry[assetManifestEntrySlots.tags]),
};
}
function assetRef(value: unknown[]): AssetRef {
const assetKey = str(value[assetRefSlots.assetKey]);
if (assetKey.length === 0)
return emptyAsset();
return asset(
assetKey,
str(value[assetRefSlots.kind]),
str(value[assetRefSlots.uri]),
str(value[assetRefSlots.transport]),
str(value[assetRefSlots.mimeType]),
str(value[assetRefSlots.contentHash]),
stringRecord(value[assetRefSlots.metadata]),
);
}
function asset(
assetKey: string,
kind: string,
uri: string,
transport: string,
mimeType: string,
contentHash = "",
metadata: Record<string, string> = {},
): AssetRef {
return {
assetKey,
kind,
uri,
transport,
contentHash,
mimeType,
metadata,
};
}
function emptyAsset(kind = ""): AssetRef {
return asset("", kind, "", "", "");
}
function arr(value: unknown): unknown[] {
return Array.isArray(value) ? value : [];
}
function list<T extends unknown[]>(value: unknown): T[] {
return Array.isArray(value) ? value.filter(Array.isArray) as T[] : [];
}
function numberList(value: unknown): number[] {
return Array.isArray(value) ? value.map(candidate => num(candidate)) : [];
}
function stringList(value: unknown): string[] {
return Array.isArray(value) ? value.map(candidate => str(candidate)).filter(candidate => candidate.length > 0) : [];
}
function stringRecord(value: unknown): Record<string, string> {
if (!value || typeof value !== "object" || Array.isArray(value))
return {};
return Object.fromEntries(
Object.entries(value)
.map(([key, candidate]) => [key, str(candidate)])
.filter(([, candidate]) => candidate.length > 0),
);
}
function str(value: unknown): string {
return typeof value === "string" ? value : "";
}
function num(value: unknown, fallback = 0): number {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}
function bool(value: unknown): boolean {
return value === true;
}

View file

@ -0,0 +1,432 @@
import { app, BrowserWindow, ipcMain, protocol, shell } from "electron";
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
import { createWriteStream, existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { startEveElectronProviderHost } from "@gamecult/eve-electron/live-provider-host";
import { CultMesh } from "cultmesh-ts";
import { encode } from "@msgpack/msgpack";
const __dirname = dirname(fileURLToPath(import.meta.url));
const projectRoot = resolve(__dirname, "..");
const repoRoot = resolve(projectRoot, "..");
const logsRoot = resolve(projectRoot, "logs");
const runtimeRoot = resolve(process.env.AETHERIA_RUNTIME_ROOT ?? resolve(projectRoot, "runtime"));
const runtimeStatePath = resolve(runtimeRoot, "aetheria.cc");
const daemonDll = resolve(repoRoot, "Aetheria.State.Daemon", "bin", "Debug", "net10.0", "Aetheria.State.Daemon.dll");
const rendererIndex = resolve(projectRoot, "wwwroot", "index.html");
const clientCultMeshPort = Number.parseInt(process.env.AETHERIA_CLIENT_CULTMESH_PORT ?? "3076", 10);
const verseId = process.env.AETHERIA_VERSE_ID?.trim() || "aetheria.local";
const daemonId = process.env.AETHERIA_DAEMON_ID?.trim() || "aetheria-daemon";
const configuredDaemonUri = process.env.AETHERIA_CULTMESH_URI?.trim() ?? "";
const launchLocalDaemon = configuredDaemonUri.length === 0;
const daemonCultMeshUri = configuredDaemonUri || `cultmesh://aetheria/daemon/${encodeURIComponent(daemonId)}`;
const clientCultMeshAdvertiseHost = process.env.AETHERIA_CLIENT_CULTMESH_ADVERTISE_HOST?.trim() || "127.0.0.1";
const localDaemonRudpEndpoint = launchLocalDaemon ? `rudp://127.0.0.1:${clientCultMeshPort}` : "";
const electronSmoke = process.env.AETHERIA_ELECTRON_SMOKE === "1";
const electronSmokeResultPath = process.env.AETHERIA_ELECTRON_SMOKE_RESULT;
let daemonProcess: ChildProcessWithoutNullStreams | null = null;
let mainWindow: BrowserWindow | null = null;
let eveHost: { close(): Promise<void>; window: BrowserWindow } | null = null;
let isQuitting = false;
app.whenReady().then(async () => {
writeElectronSmokeResult({ ok: false, stage: "electron-ready" });
try {
if (launchLocalDaemon) {
await ensureDotnetBuild();
mkdirSync(runtimeRoot, { recursive: true });
daemonProcess = startDotnet("aetheria-daemon", daemonDll, [
"--state",
runtimeStatePath,
"--verse-id",
verseId,
"--daemon-id",
daemonId,
"--client-cultmesh-port",
clientCultMeshPort.toString(),
"--client-cultmesh-advertise-host",
clientCultMeshAdvertiseHost,
"--tick-interval-ms",
"20",
"--fixed-delta-ms",
"20",
"--api-publication-interval-ms",
"100000",
"--no-odin-announcements",
]);
} else {
mkdirSync(runtimeRoot, { recursive: true });
}
eveHost = await startEveElectronProviderHost({
electron: { app, BrowserWindow, ipcMain, protocol, shell },
dependencies: { CultMesh, encode },
providerTarget: {
providerId: "aetheria.daemon",
advertisementRecordRef: "eve:provider:aetheria.daemon",
peerId: daemonId,
verseId,
role: "aetheria-daemon",
endpoints: localDaemonRudpEndpoint ? [localDaemonRudpEndpoint] : [],
},
renderer: rendererIndex,
runtimeId: "eve-electron-aetheria",
surfaceId: process.env.EVE_SURFACE_ID || "aetheria.starbridge.commander",
window: {
show: !electronSmoke,
backgroundColor: "#0b1016",
title: "Aetheria Starbridge",
},
});
mainWindow = eveHost.window;
writeElectronSmokeResult({ ok: false, stage: "renderer-loaded" });
if (electronSmoke) {
await withTimeout(mainWindow.webContents.executeJavaScript("window.eveProvider.providerAdvertisement()"), 10000, "provider advertisement smoke");
writeElectronSmokeResult({ ok: false, stage: "provider-advertisement-readable" });
await withTimeout(mainWindow.webContents.executeJavaScript("window.eveProvider.surface({ surfaceId: 'aetheria.starbridge.commander' })"), 10000, "surface smoke");
writeElectronSmokeResult({ ok: false, stage: "provider-surface-readable" });
await withTimeout(mainWindow.webContents.executeJavaScript(`window.eveProvider.submitCommand({
providerId: 'aetheria.daemon', surfaceId: 'aetheria.starbridge.commander', command: 'aetheria.daemon.commands.SensorPing',
clientId: 'aetheria-electron-smoke-preflight', payload: {}
})`), 10000, "command receipt smoke");
writeElectronSmokeResult({ ok: false, stage: "provider-command-receipt-readable" });
const result = await runElectronSmoke(mainWindow);
writeElectronSmokeResult({ ok: true, result });
console.log(JSON.stringify(result, null, 2));
exitElectronSmoke(0);
}
} catch (error) {
if (electronSmoke) {
writeElectronSmokeResult({
ok: false,
error: error instanceof Error ? error.stack ?? error.message : String(error),
});
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
exitElectronSmoke(1);
} else {
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
}
}
});
app.on("before-quit", () => {
isQuitting = true;
stopChild(daemonProcess);
});
async function runElectronSmoke(window: BrowserWindow): Promise<Record<string, unknown>> {
const started = Date.now();
let lastResult: Record<string, unknown> | null = null;
while (Date.now() - started < 15000) {
lastResult = await window.webContents.executeJavaScript(`
(async () => {
await new Promise(resolve => setTimeout(resolve, 100));
const eveProvider = window.eveProvider;
const status = document.querySelector("#status")?.textContent ?? "";
const bodyMode = document.body.className;
const eveHostText = document.querySelector("#eve-surface-host")?.textContent ?? "";
const worldScene = document.querySelector(".cultui-world-scene");
const worldEntityCount = worldScene?.querySelectorAll(".cultui-world-entity").length ?? 0;
const controlledWorldEntityCount = worldScene?.querySelectorAll('.cultui-world-entity[data-controlled="true"]').length ?? 0;
const workerRosterCount = document.querySelectorAll('.cultui-list-item[data-component-kind="agent.item"]').length;
const providerAdvertisement = eveProvider ? await eveProvider.providerAdvertisement() : null;
const eveSurface = eveProvider ? await eveProvider.surface({ surfaceId: "aetheria.starbridge.commander" }) : null;
const pilotSurface = eveProvider ? await eveProvider.surface({ surfaceId: "aetheria.pilot" }) : null;
const findComponent = (component, predicate) => {
if (!component || typeof component !== "object") return null;
if (predicate(component)) return component;
for (const child of component.children ?? []) {
const match = findComponent(child, predicate);
if (match) return match;
}
return null;
};
const eveFieldSurface = findComponent(pilotSurface?.surface?.root, component =>
component.kind === "field.surface2d" || component.kind === "gravity.surface");
const embedded = slotId => eveFieldSurface?.embeddedDocuments?.find(slot => slot.slotId === slotId) ?? null;
const resolveEmbedded = async slot => slot ? eveProvider.document({ documentId: slot.documentId, schemaId: slot.schemaId }) : null;
const [renderSplatsResolved, gravityResolved, objectsResolved] = await Promise.all([
resolveEmbedded(embedded("renderSplats")),
resolveEmbedded(embedded("gravity")),
resolveEmbedded(embedded("objects")),
]);
const assetProbe = await new Promise(resolve => {
const image = new Image();
image.onload = () => resolve({ loaded: true, width: image.naturalWidth, height: image.naturalHeight });
image.onerror = () => resolve({ loaded: false, width: 0, height: 0 });
image.src = "eve-asset://asset?uri=" + encodeURIComponent("cultmesh://aetheria/assets/map/entity/player");
});
const eveReceipt = eveProvider ? await eveProvider.submitCommand({
providerId: "aetheria.daemon",
surfaceId: "aetheria.starbridge.commander",
command: "aetheria.daemon.commands.SensorPing",
clientId: "aetheria-electron-smoke",
payload: {},
}) : null;
return {
hasApi: !!eveProvider &&
typeof eveProvider.providerAdvertisement === "function" &&
typeof eveProvider.document === "function" &&
typeof eveProvider.surface === "function" &&
typeof eveProvider.submitCommand === "function",
status,
bodyMode,
eveHostText,
worldSceneReady: !!worldScene,
worldEntityCount,
controlledWorldEntityCount,
workerRosterCount,
providerAdvertisement,
eveSurface,
pilotSurface,
renderSplatsResolved,
gravityResolved,
objectsResolved,
assetProbe,
eveFieldSurface,
eveReceipt
};
})()
`, true) as Record<string, unknown>;
if (isElectronSmokeReady(lastResult))
return lastResult;
await delay(250);
}
throw new Error(`Aetheria Starbridge Electron smoke did not reach ready state. Last result: ${JSON.stringify(lastResult)}`);
}
function isElectronSmokeReady(result: Record<string, unknown>): boolean {
const status = stringValue(result.status);
const bodyMode = stringValue(result.bodyMode);
const eveHostText = stringValue(result.eveHostText);
const eveSurface = objectValue(result.eveSurface);
const providerAdvertisement = objectValue(result.providerAdvertisement);
const renderSplatsResolved = objectValue(result.renderSplatsResolved);
const gravityResolved = objectValue(result.gravityResolved);
const objectsResolved = objectValue(result.objectsResolved);
const eveFieldSurface = objectValue(result.eveFieldSurface);
const eveReceipt = objectValue(result.eveReceipt);
return result.hasApi === true &&
status.includes("Starbridge Commander") &&
bodyMode.includes("eve-game-mode") &&
result.worldSceneReady === true &&
Number(result.worldEntityCount) > 0 &&
Number(result.controlledWorldEntityCount) === 1 &&
Number(result.workerRosterCount) > 0 &&
eveHostText.includes("Station Stock") &&
eveHostText.includes("Wave Forecast") &&
arrayValue(providerAdvertisement?.surfaces).some(surface => objectValue(surface)?.surfaceId === "aetheria.starbridge.commander") &&
objectValue(eveSurface?.surface)?.id === "aetheria.starbridge.commander" &&
stringValue(renderSplatsResolved?.documentId).length > 0 &&
stringValue(gravityResolved?.documentId).length > 0 &&
stringValue(objectsResolved?.documentId).length > 0 &&
renderSplatsResolved?.schemaId === "gamecult.fields.splats.v1" &&
gravityResolved?.schemaId === "gamecult.fields.gravity.v1" &&
objectsResolved?.schemaId === "gamecult.fields.objects.v1" &&
objectValue(result.assetProbe)?.loaded === true &&
Number(objectValue(result.assetProbe)?.width) > 0 &&
Number(objectValue(result.assetProbe)?.height) > 0 &&
stringValue(eveFieldSurface?.id).length > 0 &&
arrayValue(eveFieldSurface?.embeddedDocuments).some(slot =>
objectValue(slot)?.slotId === "renderSplats") &&
arrayValue(eveFieldSurface?.embeddedDocuments).some(slot =>
objectValue(slot)?.slotId === "gravity") &&
arrayValue(eveFieldSurface?.embeddedDocuments).some(slot =>
objectValue(slot)?.slotId === "objects") &&
stringValue(eveReceipt?.commandId).length > 0 &&
objectValue(eveReceipt?.route)?.kind === "network" &&
eveReceipt?.accepted === true;
}
function stringValue(value: unknown): string {
return typeof value === "string" ? value : "";
}
function objectValue(value: unknown): Record<string, unknown> | null {
return value != null && typeof value === "object" ? value as Record<string, unknown> : null;
}
function arrayValue(value: unknown): unknown[] {
return Array.isArray(value) ? value : [];
}
function writeElectronSmokeResult(result: Record<string, unknown>): void {
if (!electronSmokeResultPath)
return;
writeFileSync(electronSmokeResultPath, JSON.stringify(result, null, 2), "utf8");
}
function exitElectronSmoke(exitCode: number): void {
isQuitting = true;
void eveHost?.close();
eveHost = null;
stopChild(daemonProcess);
daemonProcess = null;
mainWindow?.destroy();
mainWindow = null;
app.exit(exitCode);
}
function withTimeout<T>(work: Promise<T>, timeoutMs: number, label: string): Promise<T> {
return Promise.race([
work,
new Promise<T>((_resolve, reject) => setTimeout(() => reject(new Error(`Timed out during ${label}.`)), timeoutMs)),
]);
}
async function ensureDotnetBuild(): Promise<void> {
if (existsSync(daemonDll) && !daemonBuildInputsAreNewer(daemonDll))
return;
await runProcess("dotnet", ["build", resolve(repoRoot, "Aetheria.State.Daemon", "Aetheria.State.Daemon.csproj")], "Aetheria.State.Daemon.build");
}
function daemonBuildInputsAreNewer(outputPath: string): boolean {
const outputMtime = statSync(outputPath).mtimeMs;
const sourceRoots = [
resolve(repoRoot, "Aetheria.State.Daemon"),
resolve(repoRoot, "Aetheria.State"),
resolve(repoRoot, "Packages", "org.gamecult.aetheria.state", "Runtime"),
];
return sourceRoots.some(sourceRoot => newestSourceMtime(sourceRoot) > outputMtime);
}
function newestSourceMtime(root: string): number {
if (!existsSync(root))
return 0;
const stat = statSync(root);
if (stat.isFile())
return isBuildInput(root) ? stat.mtimeMs : 0;
let newest = 0;
for (const entry of readdirSync(root, { withFileTypes: true })) {
const path = resolve(root, entry.name);
if (entry.isDirectory()) {
if (entry.name === "bin" || entry.name === "obj")
continue;
newest = Math.max(newest, newestSourceMtime(path));
} else if (entry.isFile() && isBuildInput(path)) {
newest = Math.max(newest, statSync(path).mtimeMs);
}
}
return newest;
}
function isBuildInput(path: string): boolean {
return path.endsWith(".cs") || path.endsWith(".csproj") || path.endsWith(".props") || path.endsWith(".targets");
}
function startDotnet(name: string, dllPath: string, args: string[]): ChildProcessWithoutNullStreams {
mkdirSync(logsRoot, { recursive: true });
const child = spawn("dotnet", [dllPath, ...args], {
cwd: repoRoot,
windowsHide: true,
});
pipeChildLogs(name, child);
child.once("exit", (code, signal) => {
if (!isQuitting) {
void showFailure(new Error(`${name} exited unexpectedly (${signal ?? code ?? "unknown"}). Check ${logsRoot}.`));
}
});
return child;
}
function runProcess(command: string, args: string[], logName: string): Promise<void> {
mkdirSync(logsRoot, { recursive: true });
const child = spawn(command, args, {
cwd: repoRoot,
windowsHide: true,
});
pipeChildLogs(logName, child);
return new Promise((resolvePromise, reject) => {
child.once("error", reject);
child.once("exit", code => {
if (code === 0) {
resolvePromise();
return;
}
reject(new Error(`${command} ${args.join(" ")} failed with exit code ${code}. Check ${logsRoot}.`));
});
});
}
function pipeChildLogs(name: string, child: ChildProcessWithoutNullStreams): void {
const stdout = createWriteStream(resolve(logsRoot, `${name}.log`), { flags: "a" });
const stderr = createWriteStream(resolve(logsRoot, `${name}.err.log`), { flags: "a" });
child.stdout.pipe(stdout);
child.stderr.pipe(stderr);
}
function stopChild(child: ChildProcessWithoutNullStreams | null): void {
if (!child || child.killed)
return;
child.kill();
}
function delay(milliseconds: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}
async function showFailure(error: unknown): Promise<void> {
const message = error instanceof Error ? error.message : String(error);
await mainWindow?.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(startupHtml("Aetheria Starbridge failed to start", message))}`);
}
function startupHtml(title: string, detail: string): string {
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>${escapeHtml(title)}</title>
<style>
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
background: #0b1016;
color: #dbe7f3;
font-family: "Segoe UI", sans-serif;
}
main {
width: min(520px, calc(100vw - 48px));
}
h1 {
margin: 0 0 12px;
font-size: 24px;
font-weight: 650;
}
p {
margin: 0;
color: #8fa3b7;
line-height: 1.5;
}
</style>
</head>
<body>
<main>
<h1>${escapeHtml(title)}</h1>
<p>${escapeHtml(detail)}</p>
</main>
</body>
</html>`;
}
function escapeHtml(value: string): string {
return value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}

305
Aetheria.Rts.Web/package-lock.json generated Normal file
View file

@ -0,0 +1,305 @@
{
"name": "aetheria-rts-web",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "aetheria-rts-web",
"version": "0.1.0",
"dependencies": {
"@gamecult/eve-browser-lowering": "file:../../Eve/packages/eve-browser-lowering",
"@gamecult/eve-electron": "file:../../EveElectron",
"@msgpack/msgpack": "^3.1.2",
"cultcache-ts": "file:../../CultLib/packages/cultcache-ts",
"cultmesh-ts": "file:../../CultLib/packages/cultmesh-ts",
"cultnet-ts": "file:../../CultLib/packages/cultnet-ts"
},
"devDependencies": {
"@types/node": "^26.0.0",
"electron": "^42.4.1",
"typescript": "^5.6.3"
}
},
"../../CultLib/packages/cultcache-ts": {
"version": "0.13.5",
"license": "MIT",
"dependencies": {
"@msgpack/msgpack": "^3.1.3",
"zod": "^3.25.76"
},
"devDependencies": {
"@types/node": "^24.0.0",
"typescript": "^5.9.0"
}
},
"../../CultLib/packages/cultmesh-ts": {
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@msgpack/msgpack": "^3.1.2",
"cultcache-ts": "file:../cultcache-ts",
"cultnet-ts": "file:../cultnet-ts"
},
"devDependencies": {
"@types/node": "^24.0.0",
"typescript": "^5.9.0",
"zod": "^3.25.76"
}
},
"../../CultLib/packages/cultnet-ts": {
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@msgpack/msgpack": "^3.1.3",
"ajv": "^8.17.1",
"cultcache-ts": "file:../cultcache-ts"
},
"devDependencies": {
"@types/node": "^24.0.0",
"json-schema-to-typescript": "^15.0.4",
"typescript": "^5.9.0",
"zod": "^3.25.76"
}
},
"../../Eve/packages/eve-browser-lowering": {
"name": "@gamecult/eve-browser-lowering",
"version": "0.1.0",
"devDependencies": {
"typescript": "^5.6.3"
}
},
"../../EveElectron": {
"name": "@gamecult/eve-electron",
"version": "0.1.0",
"license": "MPL-2.0",
"peerDependencies": {
"electron": ">=30"
},
"peerDependenciesMeta": {
"electron": {
"optional": true
}
}
},
"node_modules/@electron-internal/extract-zip": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.3.tgz",
"integrity": "sha512-OjKpjB7gohtEjZiq6nDx1egqjZJhGPN1iFOIED+NFhB/MMkXw/XRcHjh1DGXKT5z2W9eW7Jy2UKU3gpjvusFTQ==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=22.12.0"
}
},
"node_modules/@electron/get": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@electron/get/-/get-5.0.0.tgz",
"integrity": "sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.1.1",
"env-paths": "^3.0.0",
"graceful-fs": "^4.2.11",
"progress": "^2.0.3",
"semver": "^7.6.3",
"sumchecker": "^3.0.1"
},
"engines": {
"node": ">=22.12.0"
},
"optionalDependencies": {
"undici": "^7.24.4"
}
},
"node_modules/@gamecult/eve-browser-lowering": {
"resolved": "../../Eve/packages/eve-browser-lowering",
"link": true
},
"node_modules/@gamecult/eve-electron": {
"resolved": "../../EveElectron",
"link": true
},
"node_modules/@msgpack/msgpack": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz",
"integrity": "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==",
"license": "ISC",
"engines": {
"node": ">= 18"
}
},
"node_modules/@types/node": {
"version": "26.0.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz",
"integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~8.3.0"
}
},
"node_modules/cultcache-ts": {
"resolved": "../../CultLib/packages/cultcache-ts",
"link": true
},
"node_modules/cultmesh-ts": {
"resolved": "../../CultLib/packages/cultmesh-ts",
"link": true
},
"node_modules/cultnet-ts": {
"resolved": "../../CultLib/packages/cultnet-ts",
"link": true
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/electron": {
"version": "42.4.1",
"resolved": "https://registry.npmjs.org/electron/-/electron-42.4.1.tgz",
"integrity": "sha512-8CYHJP5O4wFO+ycoJR98yy907MmPeo+vWXrzjxmGGgRNKqv8pOjjm+wphO0CCgQJnBU7+QUPSJS4QXhbKrO50w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron-internal/extract-zip": "^1.0.1",
"@electron/get": "^5.0.0",
"@types/node": "^24.9.0"
},
"bin": {
"electron": "cli.js",
"install-electron": "install.js"
},
"engines": {
"node": ">= 22.12.0"
}
},
"node_modules/electron/node_modules/@types/node": {
"version": "24.13.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz",
"integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.18.0"
}
},
"node_modules/electron/node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"dev": true,
"license": "MIT"
},
"node_modules/env-paths": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz",
"integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/sumchecker": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
"integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.1.0"
},
"engines": {
"node": ">= 8.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
"integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.18.1"
}
},
"node_modules/undici-types": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
"dev": true,
"license": "MIT"
}
}
}

View file

@ -0,0 +1,33 @@
{
"name": "aetheria-rts-web",
"private": true,
"version": "0.1.0",
"type": "module",
"main": "electron-dist/main.js",
"scripts": {
"build": "C:\\Progra~1\\nodejs\\node.exe ./node_modules/typescript/bin/tsc -p tsconfig.json && C:\\Progra~1\\nodejs\\node.exe ./node_modules/typescript/bin/tsc -p tsconfig.electron.json",
"build:renderer": "C:\\Progra~1\\nodejs\\node.exe ./node_modules/typescript/bin/tsc -p tsconfig.json",
"build:electron": "C:\\Progra~1\\nodejs\\node.exe ./node_modules/typescript/bin/tsc -p tsconfig.electron.json",
"generate:rts-bindings": "C:\\Progra~1\\nodejs\\node.exe scripts/generate-rts-bindings.mjs",
"check:rts-bindings": "C:\\Progra~1\\nodejs\\node.exe scripts/generate-rts-bindings.mjs --check",
"verify:stage7b": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\\verify-stage7b-rts-client.ps1",
"verify:stage7c": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\\verify-stage7c-local-runtime.ps1",
"verify:stage7c:electron": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\\verify-stage7c-electron-rts.ps1",
"verify:stage7d:remote": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\\verify-stage7d-remote-cultmesh.ps1",
"electron": "npm run build && .\\node_modules\\electron\\dist\\electron.exe .",
"watch": "C:\\Progra~1\\nodejs\\node.exe ./node_modules/typescript/bin/tsc -p tsconfig.json --watch"
},
"devDependencies": {
"@types/node": "^26.0.0",
"electron": "^42.4.1",
"typescript": "^5.6.3"
},
"dependencies": {
"@msgpack/msgpack": "^3.1.2",
"@gamecult/eve-browser-lowering": "file:../../Eve/packages/eve-browser-lowering",
"@gamecult/eve-electron": "file:../../EveElectron",
"cultcache-ts": "file:../../CultLib/packages/cultcache-ts",
"cultmesh-ts": "file:../../CultLib/packages/cultmesh-ts",
"cultnet-ts": "file:../../CultLib/packages/cultnet-ts"
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,576 @@
$ErrorActionPreference = "Stop"
$root = Split-Path -Parent $PSScriptRoot
Set-Location $root
& node scripts/generate-rts-bindings.mjs --check
if ($LASTEXITCODE -ne 0) {
Write-Error "Stage 7B verifier failed: generated RTS bindings are stale."
}
$forbidden = @(
'window\.aetheriaRts\.command',
'window\.aetheriaRts\.viewport',
'ipcMain\.handle\("aetheria-rts:command',
'ipcMain\.handle\("aetheria-rts:viewport',
'function createCommandDocument',
'export type RtsCommandRequest',
'public async command\(',
'public async viewport\('
)
$forbiddenPattern = ($forbidden -join '|')
$forbiddenHits = & rg -n $forbiddenPattern Client Electron 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Error "Stage 7B verifier failed: public generic RTS command/viewport surface still exists.`n$forbiddenHits"
}
if ($LASTEXITCODE -gt 1) {
Write-Error "Stage 7B verifier could not run rg for forbidden public surfaces."
}
$forbiddenFileBackedDebugSurfaceHits = & rg -n 'debugSurface|debug-surface|watchDebugSurface|AETHERIA_CULTUI_DEBUG_SURFACE_PATH|cultui-debug-surface|file_surface|debug-log' Client Electron scripts/generate-rts-bindings.mjs 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Error "Stage 7B verifier failed: Electron must consume daemon Eve surfaces, not file-backed CultUI debug surfaces.`n$forbiddenFileBackedDebugSurfaceHits"
}
if ($LASTEXITCODE -gt 1) {
Write-Error "Stage 7B verifier could not run rg for file-backed debug surface checks."
}
$forbiddenRtsSurfaceContractHits = & rg -n 'gamecult\.aetheria\.rts\.(surfaces|viewport_feed)' Client Electron scripts/generate-rts-bindings.mjs 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Error "Stage 7B verifier failed: Aetheria CultMesh surface catalog/feed ids must not be RTS-branded.`n$forbiddenRtsSurfaceContractHits"
}
if ($LASTEXITCODE -gt 1) {
Write-Error "Stage 7B verifier could not run rg for RTS-branded surface contract checks."
}
$required = @(
'mapViewport',
'objectsViewport',
'gravityViewport',
'selectedObject',
'inventory',
'daemonHealth',
'authorityStatus',
'starbridgeSession',
'setMoveVector',
'setTarget',
'surfaceCatalogIndex',
'AetheriaRtsIpcChannels'
)
foreach ($symbol in $required) {
& rg -q $symbol Client Electron
if ($LASTEXITCODE -ne 0) {
Write-Error "Stage 7B verifier failed: expected typed RTS client symbol '$symbol' was not found."
}
}
$rendererText = Get-Content Client/app.ts -Raw
if (-not $rendererText.Contains('window.aetheriaRts.eveSurface') -or
-not $rendererText.Contains('renderEveSurface') -or
-not $rendererText.Contains('resolveEveDocument') -or
-not $rendererText.Contains('submitEveCommand')) {
Write-Error "Stage 7B verifier failed: RTS renderer is not lowering daemon Eve through the generated preload API."
}
$indexText = Get-Content wwwroot/index.html -Raw
if (-not $indexText.Contains('eve-surface-host')) {
Write-Error "Stage 7B verifier failed: RTS renderer is not mounting the daemon Eve surface host."
}
$transportLayoutPatterns = @(
'new Array<unknown>',
'bounds\[',
'viewportSchema',
'commandSchema',
'createBaseCommandDocument',
'decodeViewObject',
'decodeInventoryItem',
'decodeGravityInfluence',
'decodeBodyView',
'fetchDocument\(',
'sendSnapshotRequest',
'schemaIds',
'recordKeys',
'CultNetSnapshotResponseRawMessage',
'AetheriaRtsSchemas\.rtsViewport'
)
$transportLayoutPattern = ($transportLayoutPatterns -join '|')
$transportLayoutHits = & rg -n $transportLayoutPattern Electron/aetheria-cultmesh.ts 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Error "Stage 7B verifier failed: RTS CultMesh transport wrapper still owns document layout/codec details.`n$transportLayoutHits"
}
if ($LASTEXITCODE -gt 1) {
Write-Error "Stage 7B verifier could not run rg for transport wrapper layout checks."
}
$remotePublicationReaderPatterns = @(
'sendSnapshotRequest',
'snapshot_response_raw',
'CultNetSnapshotResponseRawMessage',
'decode\('
)
$remotePublicationReaderPattern = ($remotePublicationReaderPatterns -join '|')
$remotePublicationReaderHits = & rg -n $remotePublicationReaderPattern Electron/aetheria-cultmesh.ts 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Error "Stage 7B verifier failed: RTS CultMesh client still owns raw snapshot/codec details.`n$remotePublicationReaderHits"
}
if ($LASTEXITCODE -gt 1) {
Write-Error "Stage 7B verifier could not run rg for remote publication layout checks."
}
$requiredBindingSymbols = @(
'createAetheriaRuntimeGameOperationHandles',
'createAetheriaRuntimeGameQueryHandles',
'AetheriaRtsSchemas'
)
foreach ($symbol in $requiredBindingSymbols) {
& rg -q $symbol Electron/aetheria-rts-bindings.ts
if ($LASTEXITCODE -ne 0) {
Write-Error "Stage 7B verifier failed: expected RTS binding symbol '$symbol' was not found."
}
}
$requiredGeneratedBindingSymbols = @(
'AetheriaRtsIpcChannels',
'registerAetheriaRtsIpcHandlers',
'export type AetheriaRtsMainClient',
'aetheria-rts:map-viewport',
'aetheria-rts:objects-viewport',
'aetheria-rts:viewport-feed',
'aetheria-rts:viewport-feed-update',
'aetheria-rts:set-move-vector',
'export type AetheriaRuntimeSelectedObjectRequest',
'export type AetheriaRuntimeViewportFeedRequest',
'export type AetheriaRuntimeViewportFeedSnapshot',
'aetheriaRuntimeDaemonCommandDocumentSlots',
'encodeSetMoveVectorCommand',
'encodeSetTargetCommand',
'AetheriaRuntimeGameDocumentSources',
'AetheriaRuntimeGameQueryDiagnostic',
'AetheriaRuntimeGameLiveFeedDiagnostic',
'AetheriaRuntimeGameSurfaceCatalogDiagnostic',
'AetheriaRuntimeGameDocuments',
'AetheriaRuntimeGameDocumentResolvers',
'CultMeshSurfaceCatalogDiagnostic',
'CultMeshSurfaceCatalogIndexDiagnostic',
'CultMeshOperationReceipt',
'aetheria-rts:surface-catalog',
'aetheria-rts:surface-catalog-index',
'surfaceCatalogDiagnostics(): CultMeshSurfaceCatalogDiagnostic',
'surfaceCatalogIndexDiagnostics(): CultMeshSurfaceCatalogIndexDiagnostic',
'ipcMain.handle(AetheriaRtsIpcChannels.surfaceCatalog',
'ipcMain.handle(AetheriaRtsIpcChannels.surfaceCatalogIndex',
'CultMesh.querySource',
'CultMesh.query',
'CultMeshQuerySource',
'routeHint: CultMeshRouteHint',
'CultMeshQueryWatcher',
'AetheriaRuntimeGameQueryWatchers',
'createAetheriaRuntimeGameDocuments',
'CultMesh.document',
'CultMesh.bindDocument',
'CultMesh.describeSurface(documents.daemonFrame)',
'watchQuery: watchers.objectsViewport',
'sources: [AetheriaRuntimeGameDocumentSources.daemonFrame]',
'describeAetheriaRuntimeGameQueryHandles',
'describeAetheriaRuntimeGameLiveFeedSurface',
'describeAetheriaRuntimeGameSurfaceCatalog',
'CultMesh.describeQuerySurface',
'CultMesh.describeLiveFeed',
'CultMesh.describeSurfaceCatalog',
'CultMesh.describeSurface',
'CultMesh.describeSurface(operations.setMoveVector)',
'CultMesh.describeSurface(operations.setTarget)',
'CultMesh.operation',
'CultMesh.operationReceipt',
'AetheriaRuntimeGameOperationHandles',
'AetheriaRuntimeGameVerseHandles',
'createAetheriaRuntimeGameVerseHandles',
'CultMesh.bindQuery',
'CultMesh.bindOperation',
'zone: (_zoneId =',
'visibleWithin',
'entity: (entityKey: string)',
'pilot',
'selectedObject: CultMesh.query',
'inventory: CultMesh.query',
'daemonHealth: CultMesh.query',
'authorityStatus: CultMesh.query',
'starbridgeSession: CultMesh.query'
)
foreach ($symbol in $requiredGeneratedBindingSymbols) {
& rg -F -q $symbol Electron/aetheria-rts-generated-bindings.ts
if ($LASTEXITCODE -ne 0) {
Write-Error "Stage 7B verifier failed: expected generated RTS binding symbol '$symbol' was not found."
}
}
$forbiddenGeneratedQuerySurfaceHits = & rg -n 'CultMesh\.(projectionRecipe|projectionSource)\s*[<(]' Electron/aetheria-rts-generated-bindings.ts 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Error "Stage 7B verifier failed: generated RTS read surfaces should use CultMesh query/document handles, not projection recipes or projection sources.`n$forbiddenGeneratedQuerySurfaceHits"
}
if ($LASTEXITCODE -gt 1) {
Write-Error "Stage 7B verifier could not run rg for generated projection vocabulary checks."
}
$forbiddenGeneratedContextPlumbingHits = & rg -n 'CultMesh\.(queryContextFromVerse|operationContextFromVerse)|\.execute\([^,\n]+,\s*queryContext\(|\.invoke\([^,\n]+,\s*operationContext\(' Electron/aetheria-rts-generated-bindings.ts 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Error "Stage 7B verifier failed: generated RTS handles should bind surfaces to Verse contexts instead of threading context plumbing.`n$forbiddenGeneratedContextPlumbingHits"
}
if ($LASTEXITCODE -gt 1) {
Write-Error "Stage 7B verifier could not run rg for generated Verse binding checks."
}
& rg -q 'createAetheriaRuntimeGameQueryHandles\(' Electron/aetheria-cultmesh.ts
if ($LASTEXITCODE -ne 0) {
Write-Error "Stage 7B verifier failed: RTS client is not creating generated query handles."
}
$clientText = Get-Content Electron/aetheria-cultmesh.ts -Raw
if (-not $clientText.Contains('this.documents = createAetheriaRuntimeGameDocuments(') -or
-not $clientText.Contains('this.queryVerse.context.routeHint,')) {
Write-Error "Stage 7B verifier failed: RTS client is not creating generated document handles with its shared Verse route."
}
if (-not $clientText.Contains('daemonFrame: async () => this.fetchLatestFrameDocument()') -or
-not $clientText.Contains('authorityPolicy: async () => this.fetchAuthorityPolicyDocument()')) {
Write-Error "Stage 7B verifier failed: RTS client document handles are not resolving through the daemon publication catalog."
}
if (-not $clientText.Contains('this.verse = CultMesh.verse("aetheria.local", this.runtimeId)')) {
Write-Error "Stage 7B verifier failed: RTS client should bind to the shared CultMesh Verse primitive instead of constructing local contexts directly."
}
if (-not $clientText.Contains('? this.verse.withRoute("network", this.publicationDescription)') -or
-not $clientText.Contains(': this.verse.withRoute("shared-memory", this.publicationDescription)')) {
Write-Error "Stage 7B verifier failed: RTS client is not deriving publication-mode Verse views for query reads."
}
if (-not $clientText.Contains('this.commandVerse = this.verse') -or
-not $clientText.Contains('.withRoute("network", this.daemonTarget.uri)') -or
-not $clientText.Contains('.withClaim("commander-control"')) {
Write-Error "Stage 7B verifier failed: RTS client is not deriving a network Verse view with an explicit command authority claim."
}
if (-not $clientText.Contains('this.publicationDescription = publicationMode === "remote" ? this.daemonTarget.uri : statePath')) {
Write-Error "Stage 7B verifier failed: RTS client query route metadata does not include the local publication source description."
}
if (-not $clientText.Contains('return this.queryVerse.queryContext();')) {
Write-Error "Stage 7B verifier failed: RTS query call sites should derive query contexts from the shared Verse primitive."
}
if (-not $clientText.Contains('this.aetheria = createAetheriaRuntimeGameVerseHandles(')) {
Write-Error "Stage 7B verifier failed: RTS client should bind generated Aetheria domain sugar to its Verse contexts."
}
if (-not $clientText.Contains('return this.aetheria.zone().objects.visibleWithin(request);') -or
-not $clientText.Contains('return this.aetheria.entity(request.actorEntityKey).pilot.move(') -or
-not $clientText.Contains('return this.aetheria.entity(request.actorEntityKey).pilot.target(')) {
Write-Error "Stage 7B verifier failed: RTS client should route viewport and pilot operations through generated Verse domain sugar."
}
$forbiddenRawContextHits = & rg -n 'CultMesh\.(operationContext|queryContext)\(this\.runtimeId' Electron/aetheria-cultmesh.ts 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Error "Stage 7B verifier failed: RTS client still constructs raw runtime-id contexts instead of using the shared Verse primitive.`n$forbiddenRawContextHits"
}
if ($LASTEXITCODE -gt 1) {
Write-Error "Stage 7B verifier could not run rg for raw context checks."
}
if (-not $clientText.Contains('queryDiagnostics()')) {
Write-Error "Stage 7B verifier failed: RTS client does not expose generated query diagnostics."
}
if (-not $clientText.Contains('describeAetheriaRuntimeGameQueryHandles(this.queries)')) {
Write-Error "Stage 7B verifier failed: RTS query diagnostics should be read from generated query handle metadata."
}
if (-not $clientText.Contains('liveFeedDiagnostics()')) {
Write-Error "Stage 7B verifier failed: RTS client does not expose generated live feed diagnostics."
}
if (-not $clientText.Contains('describeAetheriaRuntimeGameLiveFeedSurface(this.createViewportFeed())')) {
Write-Error "Stage 7B verifier failed: RTS live feed diagnostics should be read from generated live feed metadata."
}
if (-not $clientText.Contains('surfaceCatalogDiagnostics()')) {
Write-Error "Stage 7B verifier failed: RTS client does not expose a unified CultMesh surface catalog diagnostic."
}
if (-not $clientText.Contains('surfaceCatalogIndexDiagnostics()')) {
Write-Error "Stage 7B verifier failed: RTS client does not expose a grouped CultMesh surface catalog diagnostic."
}
if (-not $clientText.Contains('CultMesh.surfaceCatalogIndex(this.surfaceCatalogDiagnostics())')) {
Write-Error "Stage 7B verifier failed: RTS grouped surface catalog should use the shared CultMesh catalog index primitive."
}
if (-not $clientText.Contains('describeAetheriaRuntimeGameSurfaceCatalog(this.queries, this.operations, this.documents)')) {
Write-Error "Stage 7B verifier failed: RTS surface catalog should be read from generated query, operation, and document metadata."
}
$forbiddenDirectClientOperationHits = & rg -n 'operations\.set(MoveVector|Target)\.invoke' Electron/aetheria-cultmesh.ts 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Error "Stage 7B verifier failed: RTS client still invokes operation handles directly instead of generated domain sugar.`n$forbiddenDirectClientOperationHits"
}
if ($LASTEXITCODE -gt 1) {
Write-Error "Stage 7B verifier could not run rg for direct operation handle checks."
}
if (-not $clientText.Contains('CultMesh.liveFeed<AetheriaRuntimeViewportFeedRequest, AetheriaRuntimeViewportFeedSnapshot>')) {
Write-Error "Stage 7B verifier failed: RTS viewport feed should use the shared CultMesh live feed primitive."
}
$requiredGeneratedPreloadSymbols = @(
'<auto-generated',
'channels.mapViewport',
'channels.objectsViewport',
'channels.gravityViewport',
'watchViewportFeed',
'channels.viewportFeedUpdate',
'channels.viewportFeedStop',
'channels.setMoveVector',
'channels.setTarget',
'channels.surfaceCatalog',
'channels.surfaceCatalogIndex'
)
foreach ($symbol in $requiredGeneratedPreloadSymbols) {
& rg -F -q $symbol Electron/preload.cjs
if ($LASTEXITCODE -ne 0) {
Write-Error "Stage 7B verifier failed: expected generated preload symbol '$symbol' was not found."
}
}
$forbiddenMainIpcStringHits = & rg -n 'ipcMain\.handle\("aetheria-rts:' Electron/main.ts 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Error "Stage 7B verifier failed: Electron main still registers raw RTS IPC channel strings.`n$forbiddenMainIpcStringHits"
}
if ($LASTEXITCODE -gt 1) {
Write-Error "Stage 7B verifier could not run rg for raw Electron main IPC checks."
}
$forbiddenMainGeneratedHandlerMirrors = @(
'AetheriaRtsIpcChannels\.mapViewport',
'AetheriaRtsIpcChannels\.objectsViewport',
'AetheriaRtsIpcChannels\.setMoveVector'
)
$forbiddenMainGeneratedHandlerPattern = ($forbiddenMainGeneratedHandlerMirrors -join '|')
$forbiddenMainGeneratedHandlerHits = & rg -n $forbiddenMainGeneratedHandlerPattern Electron/main.ts 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Error "Stage 7B verifier failed: Electron main still mirrors generated IPC handler registration.`n$forbiddenMainGeneratedHandlerHits"
}
if ($LASTEXITCODE -gt 1) {
Write-Error "Stage 7B verifier could not run rg for generated IPC handler mirror checks."
}
& rg -q 'registerAetheriaRtsIpcHandlers' Electron/main.ts
if ($LASTEXITCODE -ne 0) {
Write-Error "Stage 7B verifier failed: Electron main is not using generated RTS IPC handler registration."
}
$forbiddenClientDirectDocumentReturns = @(
'return projectSelectedObjectFromFrame',
'return projectInventoryFromFrame',
'return projectDaemonHealth',
'return projectAuthorityStatus',
'return projectStarbridgeSessionSummary'
)
$forbiddenClientDirectDocumentPattern = ($forbiddenClientDirectDocumentReturns -join '|')
$forbiddenClientDirectDocumentHits = & rg -n $forbiddenClientDirectDocumentPattern Electron/aetheria-cultmesh.ts 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Error "Stage 7B verifier failed: RTS client read methods bypass generated query handles.`n$forbiddenClientDirectDocumentHits"
}
if ($LASTEXITCODE -gt 1) {
Write-Error "Stage 7B verifier could not run rg for direct document return checks."
}
$requiredRendererContractSymbols = @(
'export type AetheriaRtsApi',
'export type ViewportResponse',
'export type ObjectsViewportResponse',
'export type GravityViewportResponse',
'export type AetheriaRuntimeViewportFeedRequest',
'export type AetheriaRuntimeViewportFeedSnapshot',
'watchViewportFeed',
'export type AetheriaRuntimeSetMoveVectorRequest',
'export type AetheriaRuntimeSetTargetRequest'
)
foreach ($symbol in $requiredRendererContractSymbols) {
& rg -q $symbol Client/aetheria-rts-contract.ts
if ($LASTEXITCODE -ne 0) {
Write-Error "Stage 7B verifier failed: expected generated renderer contract symbol '$symbol' was not found."
}
}
$forbiddenRendererAppPatterns = @(
'^type ViewportResponse =',
'^type ObjectsViewportResponse =',
'^type GravityViewportResponse =',
'^type AetheriaRtsApi =',
'^type ViewObject =',
'^type InventoryItem ='
)
$forbiddenRendererAppPattern = ($forbiddenRendererAppPatterns -join '|')
$forbiddenRendererAppHits = & rg -n $forbiddenRendererAppPattern Client/app.ts 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Error "Stage 7B verifier failed: renderer still owns duplicated RTS contract types.`n$forbiddenRendererAppHits"
}
if ($LASTEXITCODE -gt 1) {
Write-Error "Stage 7B verifier could not run rg for duplicated renderer contract checks."
}
$requiredEveRendererSymbols = @(
'renderEveSurface',
'eve-surface-host',
'window.aetheriaRts.eveSurface',
'resolveEveDocument',
'gamecult.eve.surface.v1',
'aetheria.daemon.game'
)
foreach ($symbol in $requiredEveRendererSymbols) {
& rg -F -q $symbol Client/app.ts wwwroot/index.html
if ($LASTEXITCODE -ne 0) {
Write-Error "Stage 7B verifier failed: expected daemon Eve renderer symbol '$symbol' was not found."
}
}
$forbiddenBindingPatterns = @(
'decodeViewportDocument',
'viewportRecordKey',
'aetheriaRuntimeRtsViewportDocumentSlots',
'aetheriaRuntimeRtsViewportObjectSlots',
'^export type ViewportResponse =',
'^export type ObjectsViewportResponse =',
'^export type GravityViewportResponse =',
'^export type ViewObject =',
'^export type InventoryItem =',
'^export type SelectedObjectRequest ='
)
$forbiddenBindingPattern = ($forbiddenBindingPatterns -join '|')
$forbiddenBindingHits = & rg -n $forbiddenBindingPattern Electron/aetheria-rts-bindings.ts 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Error "Stage 7B verifier failed: stale remote viewport decoder surface still exists in RTS bindings.`n$forbiddenBindingHits"
}
if ($LASTEXITCODE -gt 1) {
Write-Error "Stage 7B verifier could not run rg for stale binding checks."
}
$requiredLocalDocumentSymbols = @(
'readViewportDocument',
'readObjectsViewportDocument',
'readGravityViewportDocument',
'readRenderSplatsViewportDocument',
'AetheriaRtsSchemas.objectsViewport',
'AetheriaRtsSchemas.gravityViewport',
'readSelectedObjectDocument',
'readInventoryDocument',
'AetheriaRtsSchemas.selectedObject',
'AetheriaRtsSchemas.inventory',
'readDaemonHealthDocument',
'readAuthorityStatusDocument',
'readStarbridgeSessionSummaryDocument',
'AetheriaRtsSchemas.starbridgeSessionSummary'
)
foreach ($symbol in $requiredLocalDocumentSymbols) {
& rg -q $symbol Electron/aetheria-rts-local-documents.ts
if ($LASTEXITCODE -ne 0) {
Write-Error "Stage 7B verifier failed: expected daemon-authored document decoder symbol '$symbol' was not found."
}
}
$forbiddenLocalDocumentBuilders = & rg -n 'build(Viewport|ObjectsViewport|GravityViewport|RenderSplatsViewport|SelectedObject|Inventory)DocumentFromFrame|frameContext\(frameDocument' Electron/aetheria-rts-local-documents.ts 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Error "Stage 7B verifier failed: Electron must decode daemon-authored managed documents, not rebuild them from daemonFrame.`n$forbiddenLocalDocumentBuilders"
}
if ($LASTEXITCODE -gt 1) {
Write-Error "Stage 7B verifier could not run rg for stale local document builder checks."
}
& rg -q 'CultMesh.documentsFromPublication' Electron/aetheria-cultmesh.ts
if ($LASTEXITCODE -ne 0) {
Write-Error "Stage 7B verifier failed: RTS publications should be read through a configured CultMesh document catalog."
}
$forbiddenPublicationReaderFiles = & rg -n 'AetheriaLocalPublicationReader|AetheriaRemotePublicationReader|aetheria-local-publication-reader|aetheria-remote-publication-reader' Electron 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Error "Stage 7B verifier failed: RTS publication reader wrappers should be collapsed into the CultMesh document catalog.`n$forbiddenPublicationReaderFiles"
}
if ($LASTEXITCODE -gt 1) {
Write-Error "Stage 7B verifier could not run rg for stale publication reader checks."
}
& rg -q 'AetheriaRtsSchemas.starbridgeSessionSummary' Electron/aetheria-cultmesh.ts
if ($LASTEXITCODE -ne 0) {
Write-Error "Stage 7B verifier failed: Starbridge summary publication binding was not found."
}
$forbiddenLocalPublicationSidecars = & rg -n '\.(daemon\.frame|daemon\.health|authority\.policy|daemon\.starbridge\.session|daemon\.assets)\.cc' Electron/aetheria-cultmesh.ts 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Error "Stage 7B verifier failed: RTS client should read local publications from the canonical daemon store, not obsolete sidecar files.`n$forbiddenLocalPublicationSidecars"
}
if ($LASTEXITCODE -gt 1) {
Write-Error "Stage 7B verifier could not run rg for obsolete local publication sidecar checks."
}
& rg -q 'localPath: statePath' Electron/aetheria-cultmesh.ts
if ($LASTEXITCODE -ne 0) {
Write-Error "Stage 7B verifier failed: RTS client local publication bindings do not point at the canonical daemon store."
}
$requiredGeneratedBindingSymbols = @(
'aetheriaRuntimeDaemonCommandDocumentSlots',
'aetheriaRuntimeDaemonFrameDocumentSlots',
'aetheriaRuntimeDaemonHealthDocumentSlots',
'aetheriaRuntimeGameViewportDocumentSlots',
'aetheriaRuntimeObjectsViewportDocumentSlots',
'aetheriaRuntimeGravityViewportDocumentSlots',
'aetheriaRuntimeRenderSplatsViewportDocumentSlots',
'aetheriaRuntimeSelectedObjectDocumentSlots',
'aetheriaRuntimeInventoryDocumentSlots',
'aetheriaRuntimeStarbridgeScenarioDocumentSlots',
'aetheriaRuntimeStarbridgeSessionDocumentSlots',
'aetheriaRuntimeStarbridgeSessionSummaryDocumentSlots',
'aetheriaRuntimeStarbridgeStationStockItemSlots',
'aetheriaRuntimeStarbridgeWaveDefinitionSlots',
'aetheriaRuntimeStarbridgeWaveForecastSlots',
'aetheriaRuntimeStarbridgeRuntimeRoleSlots',
'aetheriaRuntimeStarbridgeBaseStatusSlots',
'aetheriaRuntimeGameViewportBoundsSlots',
'aetheriaRuntimeGameViewportObjectSlots',
'aetheriaRuntimeVerseAuthorityPolicyDocumentSlots',
'aetheriaRuntimeAuthorityRuleSlots',
'aetheriaRuntimeAuthorityLeaseDocumentSlots',
'aetheriaRuntimeRunCheckpointCommitSlots',
'aetheriaRuntimeZoneSnapshotCommitSlots',
'aetheriaRuntimeEntitySnapshotCommitSlots',
'aetheriaRuntimeBodySnapshotCommitSlots',
'aetheriaRuntimeEntityStatGridCommitSlots',
'aetheriaRuntimeCargoBayLoadoutCommitSlots',
'aetheriaRuntimeLoadoutItemSlotCommitSlots',
'aetheriaRuntimeLoadoutItemCommitSlots',
'AetheriaRuntimeDaemonCommandKinds',
'AetheriaRtsSchemas'
)
foreach ($symbol in $requiredGeneratedBindingSymbols) {
& rg -q $symbol Electron/aetheria-rts-generated-bindings.ts
if ($LASTEXITCODE -ne 0) {
Write-Error "Stage 7B verifier failed: expected generated RTS binding symbol '$symbol' was not found."
}
}
Write-Host "Stage 7B RTS client verifier passed."

View file

@ -0,0 +1,136 @@
$ErrorActionPreference = "Stop"
$root = Split-Path -Parent $PSScriptRoot
Set-Location $root
npm run build
if ($LASTEXITCODE -ne 0) {
Write-Error "Stage 7C Electron RTS verifier failed: RTS build failed."
}
if (-not (Get-Content wwwroot\index.html -Raw).Contains('eve-surface-host')) {
Write-Error "Stage 7C Electron RTS verifier failed: built renderer markup does not host the daemon Eve surface."
}
$appText = Get-Content wwwroot\app.js -Raw
if (-not $appText.Contains('mountEveElectronProvider') -or
$appText.Contains('window.aetheriaRts.eveProviderAdvertisement') -or
$appText.Contains('window.aetheriaRts.eveDocument') -or
$appText.Contains('eve:surface:aetheria.daemon.game') -or
$appText.Contains('gamecult.fields.gravity.v1') -or
$appText.Contains('gamecult.fields.splats.v1') -or
$appText.Contains('gamecult.fields.objects.v1') -or
$appText.Contains('legacyViewportMode') -or
$appText.Contains('mainMenuSurface') -or
$appText.Contains('main-menu-mode')) {
Write-Error "Stage 7C Electron RTS verifier failed: renderer is not daemon-Eve-only."
}
$rendererRuntimeText = Get-Content node_modules\@gamecult\eve-electron\src\eve-electron-renderer.mjs -Raw
if (-not $rendererRuntimeText.Contains('EveBrowserProviderHost') -or
-not $rendererRuntimeText.Contains('eveProvider.providerAdvertisement') -or
-not $rendererRuntimeText.Contains('eveProvider.document')) {
Write-Error "Stage 7C Electron RTS verifier failed: EveElectron does not own the generic provider renderer bootstrap."
}
if ($appText.Contains('aetheria.rts.electron') -or
$appText.Contains('Aetheria RTS')) {
Write-Error "Stage 7C Electron RTS verifier failed: renderer still advertises an RTS-branded client identity."
}
$mainText = Get-Content Electron\main.ts -Raw
if (-not $mainText.Contains('startEveElectronProviderHost') -or (Test-Path Electron\preload.cjs)) {
Write-Error "Stage 7C Electron RTS verifier failed: Electron does not consume the EveElectron-owned preload entry."
}
$hostRuntimeText = Get-Content node_modules\@gamecult\eve-electron\src\live-provider-host.mjs -Raw
if (-not $hostRuntimeText.Contains('eve-provider-preload-entry.cjs') -or
-not $hostRuntimeText.Contains('registerEveProviderIpc') -or
-not $hostRuntimeText.Contains('registerEveAssetProtocol') -or
-not $hostRuntimeText.Contains('createEveElectronWindow')) {
Write-Error "Stage 7C Electron RTS verifier failed: EveElectron does not own the live provider host lifecycle."
}
if (-not $mainText.Contains('eve-asset://asset') -or
-not $mainText.Contains('assetProbe') -or
$mainText.Contains('aetheria-cdn')) {
Write-Error "Stage 7C Electron RTS verifier failed: Electron smoke does not verify EveElectron-owned provider asset delivery."
}
if (-not $mainText.Contains('await eveProvider.providerAdvertisement()')) {
Write-Error "Stage 7C Electron RTS verifier failed: Electron smoke does not verify provider advertisement discovery through preload."
}
if (-not $mainText.Contains('resolveEmbedded') -or
-not $mainText.Contains('renderSplatsResolved') -or
-not $mainText.Contains('gravityResolved') -or
-not $mainText.Contains('objectsResolved') -or
-not $mainText.Contains('eveFieldSurface') -or
-not $mainText.Contains('embeddedDocuments')) {
Write-Error "Stage 7C Electron RTS verifier failed: Electron smoke does not verify daemon Eve field document lowering."
}
if (-not $mainText.Contains('await eveProvider.submitCommand') -or
-not $mainText.Contains('eveReceipt?.commandId') -or
-not $mainText.Contains('eveReceipt?.accepted') -or
-not $mainText.Contains('eveReceipt?.route')) {
Write-Error "Stage 7C Electron RTS verifier failed: Electron smoke does not verify typed operation receipts through preload."
}
if (-not $mainText.Contains('AETHERIA_VERSE_ID') -or
-not $mainText.Contains('--verse-id') -or
-not $mainText.Contains('AETHERIA_DAEMON_ID') -or
-not $mainText.Contains('--daemon-id')) {
Write-Error "Stage 7C Electron RTS verifier failed: Electron launcher does not configure daemon Verse identity."
}
if ($mainText.Contains('AETHERIA_RTS_') -or
$mainText.Contains('Aetheria RTS') -or
$mainText.Contains('starfire-rts') -or
$mainText.Contains('aetheria-rts-daemon') -or
$mainText.Contains('aetheria.rts.electron') -or
$mainText.Contains('aetheria-rts-electron') -or
$mainText.Contains('rtsCultMesh') -or
$mainText.Contains('rtsDaemonId') -or
$mainText.Contains('rtsVerseId')) {
Write-Error "Stage 7C Electron RTS verifier failed: Electron launcher still uses RTS-branded daemon connection authority."
}
$electron = Join-Path $root "node_modules\electron\dist\electron.exe"
if (!(Test-Path -LiteralPath $electron)) {
Write-Error "Stage 7C Electron RTS verifier failed: electron.exe was not found at $electron."
}
$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("aetheria-stage7c-electron-" + [Guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Path $tempRoot | Out-Null
$resultPath = Join-Path $tempRoot "electron-smoke-result.json"
$udp = [System.Net.Sockets.UdpClient]::new(0)
$port = ([System.Net.IPEndPoint]$udp.Client.LocalEndPoint).Port
$udp.Close()
try {
$env:AETHERIA_ELECTRON_SMOKE = "1"
$env:AETHERIA_RUNTIME_ROOT = $tempRoot
$env:AETHERIA_CLIENT_CULTMESH_PORT = $port.ToString()
$env:AETHERIA_VERSE_ID = "aetheria.stage7c.electron"
$env:AETHERIA_DAEMON_ID = "stage7c-aetheria-daemon"
$env:AETHERIA_ELECTRON_SMOKE_RESULT = $resultPath
$process = Start-Process -FilePath $electron -ArgumentList "." -WorkingDirectory $root -Wait -PassThru
if ($process.ExitCode -ne 0) {
Write-Error "Stage 7C Electron RTS verifier failed: Electron exited with code $($process.ExitCode)."
}
if (!(Test-Path -LiteralPath $resultPath)) {
Write-Error "Stage 7C Electron RTS verifier failed: Electron did not write a smoke result."
}
$result = Get-Content -Raw -LiteralPath $resultPath | ConvertFrom-Json
if ($result.ok -ne $true) {
Write-Error "Stage 7C Electron RTS verifier failed: $($result.error)"
}
}
finally {
Remove-Item Env:\AETHERIA_ELECTRON_SMOKE -ErrorAction SilentlyContinue
Remove-Item Env:\AETHERIA_RUNTIME_ROOT -ErrorAction SilentlyContinue
Remove-Item Env:\AETHERIA_CLIENT_CULTMESH_PORT -ErrorAction SilentlyContinue
Remove-Item Env:\AETHERIA_VERSE_ID -ErrorAction SilentlyContinue
Remove-Item Env:\AETHERIA_DAEMON_ID -ErrorAction SilentlyContinue
Remove-Item Env:\AETHERIA_ELECTRON_SMOKE_RESULT -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
}
Write-Host "Stage 7C Electron RTS verifier passed."

View file

@ -0,0 +1,195 @@
$ErrorActionPreference = "Stop"
$root = Split-Path -Parent $PSScriptRoot
$repoRoot = Split-Path -Parent $root
Set-Location $root
npm run build
if ($LASTEXITCODE -ne 0) {
Write-Error "Stage 7C verifier failed: RTS build failed."
}
$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("aetheria-stage7c-" + [Guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Path $tempRoot | Out-Null
$statePath = Join-Path $tempRoot "aetheria.cc"
$daemonOut = Join-Path $tempRoot "daemon.out.log"
$daemonErr = Join-Path $tempRoot "daemon.err.log"
$daemonProject = Join-Path $repoRoot "Aetheria.State.Daemon\Aetheria.State.Daemon.csproj"
$port = Get-Random -Minimum 41000 -Maximum 64000
$endpoint = $null
$daemon = $null
try {
$daemon = Start-Process -FilePath "dotnet" -ArgumentList @(
"run",
"--project",
$daemonProject,
"--",
"--state",
$statePath,
"--client-cultmesh-host",
"127.0.0.1",
"--client-cultmesh-port",
$port,
"--tick-interval-ms",
"20",
"--fixed-delta-ms",
"20",
"--api-publication-interval-ms",
"100000",
"--no-odin-announcements"
) -RedirectStandardOutput $daemonOut -RedirectStandardError $daemonErr -WindowStyle Hidden -PassThru
$deadline = (Get-Date).AddSeconds(40)
while ((Get-Date) -lt $deadline) {
if ($daemon.HasExited) {
Write-Error "Stage 7C verifier failed: daemon exited early ($($daemon.ExitCode)).`n$(if (Test-Path $daemonErr) { Get-Content $daemonErr -Raw })`n$(if (Test-Path $daemonOut) { Get-Content $daemonOut -Raw })"
}
if ((Test-Path $daemonOut) -and ((Get-Content $daemonOut -Raw) -match "Aetheria client CultMesh endpoint: (rudp://127\.0\.0\.1:\d+)")) {
$endpoint = $Matches[1]
break
}
Start-Sleep -Milliseconds 250
}
if (Test-Path $daemonOut) {
Get-Content $daemonOut | Out-Host
}
if ([string]::IsNullOrWhiteSpace($endpoint)) {
Write-Error "Stage 7C verifier failed: daemon did not report a client CultMesh endpoint.`n$(if (Test-Path $daemonErr) { Get-Content $daemonErr -Raw })`n$(if (Test-Path $daemonOut) { Get-Content $daemonOut -Raw })"
}
$smokeScript = Join-Path $tempRoot "stage7c-smoke.mjs"
@'
import { pathToFileURL } from "node:url";
const statePath = process.env.AETHERIA_STAGE7C_STATE_PATH;
const endpoint = process.env.AETHERIA_STAGE7C_ENDPOINT;
const clientModulePath = process.env.AETHERIA_STAGE7C_CLIENT_MODULE;
if (!statePath || !endpoint)
throw new Error("Stage 7C verifier missing state path or endpoint.");
if (!clientModulePath)
throw new Error("Stage 7C verifier missing compiled client module path.");
const { AetheriaCultMeshClient } = await import(pathToFileURL(clientModulePath).href);
const client = new AetheriaCultMeshClient({
uri: "cultmesh://aetheria/daemon/stage7c-aetheria-daemon",
peerId: "stage7c-daemon",
verseId: "aetheria.local",
role: "aetheria-daemon",
endpoints: [endpoint],
}, statePath, "stage7c-verifier");
const viewport = await client.mapViewport({ minX: -5000, minY: -5000, maxX: 5000, maxY: 5000 });
const objectsViewport = await client.objectsViewport({ minX: -5000, minY: -5000, maxX: 5000, maxY: 5000 });
const gravityViewport = await client.gravityViewport({ minX: -5000, minY: -5000, maxX: 5000, maxY: 5000 });
if (!Number.isFinite(viewport.frameId))
throw new Error("Viewport query did not produce a frame id.");
if (!Array.isArray(viewport.objects) || viewport.objects.length === 0)
throw new Error("Viewport query did not expose visible objects.");
if (objectsViewport.schema !== "gamecult.fields.objects.v1")
throw new Error(`Objects viewport used unexpected schema: ${objectsViewport.schema}`);
if (gravityViewport.schema !== "gamecult.fields.gravity.v1")
throw new Error(`Gravity viewport used unexpected schema: ${gravityViewport.schema}`);
if (!Array.isArray(objectsViewport.objects) || objectsViewport.objects.length !== viewport.objects.length)
throw new Error("Objects viewport did not match composed map object set.");
if (!Array.isArray(gravityViewport.gravityInfluences) || gravityViewport.gravityInfluences.length !== viewport.gravityInfluences.length)
throw new Error("Gravity viewport did not match composed map gravity set.");
const selectedIndex = viewport.controlledEntityIndices[0] ?? viewport.objects[0]?.entityIndex;
if (!Number.isFinite(selectedIndex))
throw new Error("No entity was available for the selected-object query.");
const selected = await client.selectedObject({ entityIndex: selectedIndex });
if (!selected.selected)
throw new Error(`Selected-object query did not resolve entity ${selectedIndex}.`);
const inventory = await client.inventory({ entityIndex: selectedIndex });
if (!Array.isArray(inventory.equipment) || !Array.isArray(inventory.cargo))
throw new Error("Inventory query did not return equipment and cargo arrays.");
const health = await client.daemonHealth();
if (health.status !== "healthy")
throw new Error(`Daemon health document was not healthy: ${health.status}`);
const authority = await client.authorityStatus();
if (authority.policyId !== "aetheria.trusted-coop.v1")
throw new Error(`Unexpected authority policy id: ${authority.policyId}`);
const starbridge = await client.starbridgeSession();
if (starbridge.schema !== "gamecult.aetheria.starbridge_session_summary.v1")
throw new Error(`Unexpected Starbridge schema: ${starbridge.schema}`);
if (starbridge.scenarioName !== "Frontier Fabricator Defense")
throw new Error(`Unexpected Starbridge scenario: ${starbridge.scenarioName}`);
if (!Array.isArray(starbridge.stationStock) || !starbridge.stationStock.some(item => item.itemKey === "repair-parts"))
throw new Error("Starbridge session did not expose daemon-seeded station stock.");
if (!Array.isArray(starbridge.waveForecast) || !starbridge.waveForecast.some(wave => wave.displayName === "Scout Probe"))
throw new Error("Starbridge session did not expose daemon-seeded wave forecast.");
const surfaceCatalog = client.surfaceCatalogDiagnostics();
if (surfaceCatalog.catalogId !== "gamecult.aetheria.surfaces.v1")
throw new Error(`Unexpected Aetheria surface catalog id: ${surfaceCatalog.catalogId}`);
if (!surfaceCatalog.surfaces.some(surface => surface.surfaceId === "gamecult.aetheria.viewport_feed.v1" && surface.kind === "live-feed"))
throw new Error("Aetheria surface catalog did not expose the viewport live feed.");
if (!surfaceCatalog.surfaces.some(surface => surface.surfaceId === "gamecult.aetheria.pilot.set_move_vector.v1" && surface.kind === "operation"))
throw new Error("RTS surface catalog did not expose the move operation.");
if (!surfaceCatalog.surfaces.some(surface =>
surface.surfaceId === "daemon:aetheria.frame.latest.v1" &&
surface.kind === "document" &&
surface.routeHint?.kind === "shared-memory" &&
surface.sources?.some(source => source.schemaId === "gamecult.aetheria.daemon_frame.v1")))
throw new Error("RTS surface catalog did not expose the daemon frame document with shared route/source metadata.");
const surfaceIndex = client.surfaceCatalogIndexDiagnostics();
if (surfaceIndex.catalogId !== surfaceCatalog.catalogId)
throw new Error("RTS surface catalog index did not preserve the catalog id.");
if (!Array.isArray(surfaceIndex.operations) || surfaceIndex.operations.length < 2)
throw new Error("RTS surface catalog index did not group operation surfaces.");
if (!Array.isArray(surfaceIndex.liveFeeds) || !surfaceIndex.liveFeeds.some(surface => surface.surfaceId === "gamecult.aetheria.viewport_feed.v1"))
throw new Error("Aetheria surface catalog index did not group the viewport live feed.");
if (!Array.isArray(surfaceIndex.queries) || !surfaceIndex.queries.some(surface => surface.surfaceId === "gamecult.fields.objects.v1"))
throw new Error("RTS surface catalog index did not group generated query surfaces.");
if (!Array.isArray(surfaceIndex.documents) || !surfaceIndex.documents.some(surface => surface.surfaceId === "daemon:aetheria.frame.latest.v1"))
throw new Error("RTS surface catalog index did not group generated document surfaces.");
console.log(JSON.stringify({
frameId: viewport.frameId,
visibleObjects: viewport.objects.length,
gravityInfluences: gravityViewport.gravityInfluences.length,
selectedEntityIndex: selected.selected.entityIndex,
equipmentItems: inventory.equipment.length,
cargoItems: inventory.cargo.length,
health: health.status,
policyId: authority.policyId,
starbridgeScenario: starbridge.scenarioName,
starbridgeStock: starbridge.stationStock.length,
surfaceCatalog: {
surfaces: surfaceCatalog.surfaces.length,
operations: surfaceIndex.operations.length,
queries: surfaceIndex.queries.length,
liveFeeds: surfaceIndex.liveFeeds.length,
documents: surfaceIndex.documents.length
}
}, null, 2));
await client.close();
'@ | Set-Content -LiteralPath $smokeScript -Encoding UTF8
$env:AETHERIA_STAGE7C_STATE_PATH = $statePath
$env:AETHERIA_STAGE7C_ENDPOINT = $endpoint
$env:AETHERIA_STAGE7C_CLIENT_MODULE = Join-Path $root "electron-dist\aetheria-cultmesh.js"
node $smokeScript
if ($LASTEXITCODE -ne 0) {
Write-Error "Stage 7C verifier failed: local Electron runtime handles could not read daemon publications."
}
}
finally {
if ($daemon -and -not $daemon.HasExited) {
Stop-Process -Id $daemon.Id -Force
$daemon.WaitForExit()
}
Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
}
Write-Host "Stage 7C local runtime verifier passed."

View file

@ -0,0 +1,145 @@
$ErrorActionPreference = "Stop"
$root = Split-Path -Parent $PSScriptRoot
$repoRoot = Split-Path -Parent $root
Set-Location $root
npm run build
if ($LASTEXITCODE -ne 0) {
Write-Error "Stage 7D remote CultMesh verifier failed: RTS build failed."
}
$daemonProject = Join-Path $repoRoot "Aetheria.State.Daemon\Aetheria.State.Daemon.csproj"
dotnet build $daemonProject -v:minimal
if ($LASTEXITCODE -ne 0) {
Write-Error "Stage 7D remote CultMesh verifier failed: daemon build failed."
}
$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("aetheria-stage7d-remote-" + [Guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Path $tempRoot | Out-Null
$statePath = Join-Path $tempRoot "aetheria-raven.cc"
$daemonOut = Join-Path $tempRoot "daemon.out.log"
$daemonErr = Join-Path $tempRoot "daemon.err.log"
$port = Get-Random -Minimum 41000 -Maximum 64000
$endpoint = "rudp://127.0.0.1:$port"
$daemon = $null
try {
$daemon = Start-Process -FilePath "dotnet" -ArgumentList @(
"run",
"--no-build",
"--project",
$daemonProject,
"--",
"--state",
$statePath,
"--client-cultmesh-host",
"127.0.0.1",
"--client-cultmesh-port",
$port,
"--tick-interval-ms",
"20",
"--fixed-delta-ms",
"20",
"--api-publication-interval-ms",
"100000"
) -RedirectStandardOutput $daemonOut -RedirectStandardError $daemonErr -WindowStyle Hidden -PassThru
$deadline = (Get-Date).AddSeconds(40)
while ((Get-Date) -lt $deadline) {
if ($daemon.HasExited) {
Write-Error "Stage 7D remote CultMesh verifier failed: daemon exited early ($($daemon.ExitCode)).`n$(Get-Content $daemonErr -Raw)`n$(Get-Content $daemonOut -Raw)"
}
if ((Test-Path $daemonOut) -and ((Get-Content $daemonOut -Raw) -match "Aetheria Verse daemon published frame")) {
break
}
Start-Sleep -Milliseconds 250
}
if (-not (Test-Path $daemonOut) -or -not ((Get-Content $daemonOut -Raw) -match "Aetheria Verse daemon published frame")) {
Write-Error "Stage 7D remote CultMesh verifier failed: daemon did not publish a frame.`n$(if (Test-Path $daemonOut) { Get-Content $daemonOut -Raw })"
}
$smokeScript = Join-Path $tempRoot "remote-smoke.mjs"
@'
import { pathToFileURL } from "node:url";
const endpoint = process.env.AETHERIA_STAGE7D_RESOLVED_RUDP_ENDPOINT;
const clientModulePath = process.env.AETHERIA_STAGE7D_REMOTE_CLIENT_MODULE;
if (!endpoint)
throw new Error("Stage 7D verifier missing resolved RUDP endpoint.");
if (!clientModulePath)
throw new Error("Stage 7D verifier missing compiled client module path.");
const { AetheriaCultMeshClient } = await import(pathToFileURL(clientModulePath).href);
const client = new AetheriaCultMeshClient({
uri: "cultmesh://aetheria/daemon/stage7d-aetheria-daemon",
peerId: "stage7d-daemon",
verseId: "aetheria.local",
role: "aetheria-daemon",
endpoints: [endpoint],
}, "unused-local-state.cc", "stage7d-verifier", {
publicationMode: "remote",
snapshotTimeoutMs: 5000,
});
try {
await client.waitForFrame(15000);
const viewport = await client.mapViewport({ minX: -5000, minY: -5000, maxX: 5000, maxY: 5000 });
const actor = viewport.objects.find(object => object.controlled);
if (!actor)
throw new Error("Remote CultMesh viewport did not expose a controlled unit.");
const health = await client.daemonHealth();
const authority = await client.authorityStatus();
const starbridge = await client.starbridgeSession();
const receipt = await client.setMoveVector({
actorEntityKey: actor.entityKey,
directionX: 1,
directionY: 0,
scalar: 0.1,
observedFrameId: viewport.frameId,
});
if (health.status !== "healthy")
throw new Error(`Unexpected daemon health ${health.status}.`);
if (authority.policyId !== "aetheria.trusted-coop.v1")
throw new Error(`Unexpected authority policy ${authority.policyId}.`);
if (starbridge.scenarioName !== "Frontier Fabricator Defense")
throw new Error(`Unexpected Starbridge scenario ${starbridge.scenarioName}.`);
if (!receipt.accepted)
throw new Error(`Remote setMoveVector was rejected: ${receipt.diagnostic ?? "no diagnostic"}`);
console.log(JSON.stringify({
endpoint,
frameId: viewport.frameId,
visibleObjects: viewport.objects.length,
health: health.status,
policyId: authority.policyId,
starbridgeScenario: starbridge.scenarioName,
operationId: receipt.operationId,
}, null, 2));
} finally {
await client.close();
}
'@ | Set-Content -LiteralPath $smokeScript -Encoding UTF8
$env:AETHERIA_STAGE7D_RESOLVED_RUDP_ENDPOINT = $endpoint
$env:AETHERIA_STAGE7D_REMOTE_CLIENT_MODULE = Join-Path $root "electron-dist\aetheria-cultmesh.js"
node $smokeScript
if ($LASTEXITCODE -ne 0) {
Write-Error "Stage 7D remote CultMesh verifier failed: remote RTS client could not read or command the daemon over CultMesh."
}
}
finally {
Remove-Item Env:\AETHERIA_STAGE7D_RESOLVED_RUDP_ENDPOINT -ErrorAction SilentlyContinue
Remove-Item Env:\AETHERIA_STAGE7D_REMOTE_CLIENT_MODULE -ErrorAction SilentlyContinue
if ($daemon -and -not $daemon.HasExited) {
Stop-Process -Id $daemon.Id -Force
}
Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
}
Write-Host "Stage 7D remote CultMesh verifier passed."

View file

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"preserveSymlinks": true,
"skipLibCheck": true,
"strict": true,
"noEmitOnError": true,
"outDir": "electron-dist",
"rootDir": "Electron",
"types": [
"node"
]
},
"include": [
"Electron/**/*.ts"
]
}

View file

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
"strict": true,
"noEmitOnError": true,
"outDir": "wwwroot",
"rootDir": "Client"
},
"include": [
"Client/**/*.ts"
]
}

View file

@ -0,0 +1,21 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Aetheria Starbridge</title>
<link rel="stylesheet" href="../node_modules/@gamecult/eve-browser-lowering/styles.css">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="status" class="status">Connecting</div>
<div class="window-drag-region" aria-hidden="true"></div>
<div class="window-controls" aria-label="Window controls">
<button class="window-control" type="button" data-window-control="minimize" aria-label="Minimize">-</button>
<button class="window-control" type="button" data-window-control="maximize" aria-label="Maximize">[]</button>
<button class="window-control window-control-close" type="button" data-window-control="close" aria-label="Close">x</button>
</div>
<main id="eve-surface-host" class="eve-surface-host" aria-label="Aetheria daemon Eve surface"></main>
<script type="module" src="app.js"></script>
</body>
</html>

View file

@ -0,0 +1,90 @@
:root {
color-scheme: dark;
background: #020909;
color: #e8edf0;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
* {
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
}
body {
-webkit-app-region: drag;
overflow: hidden;
background: #020909;
}
.eve-surface-host {
-webkit-app-region: no-drag;
display: block;
width: 100%;
min-height: 100%;
}
.status {
position: fixed;
right: 112px;
bottom: 10px;
z-index: 30;
max-width: min(560px, calc(100vw - 140px));
overflow: hidden;
color: rgba(219, 250, 255, 0.45);
font: 400 11px/1.2 "Ubuntu", system-ui, sans-serif;
text-overflow: ellipsis;
white-space: nowrap;
pointer-events: none;
}
.window-drag-region {
-webkit-app-region: drag;
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: 15;
height: 44px;
}
.window-controls {
-webkit-app-region: no-drag;
position: fixed;
top: 8px;
right: 10px;
z-index: 20;
display: flex;
gap: 2px;
}
.window-control {
min-width: 0;
width: 30px;
height: 24px;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
color: rgba(219, 250, 255, 0.5);
font-family: "Ubuntu", system-ui, sans-serif;
font-size: 12px;
line-height: 24px;
}
.window-control:hover,
.window-control:focus-visible {
background: rgba(219, 250, 255, 0.09);
color: rgba(240, 252, 255, 0.95);
outline: 0;
}
.window-control-close:hover,
.window-control-close:focus-visible {
background: rgba(255, 88, 88, 0.28);
}

View file

@ -1,36 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- The game simulation built without Unity: the headless counterpart of the Aetheria.Shared.Unity assembly.
Compiles ServerShared and its dependencies from source with no Unity defines, so a successful build is the
check that the simulation stays independent of the engine. Tools and test harnesses reference this project. -->
<PropertyGroup>
<!-- Unity compiles ServerShared against .NET Standard 2.1 -->
<TargetFramework>netstandard2.1</TargetFramework>
<LangVersion>9.0</LangVersion>
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<!-- UniRx's own switch for building outside Unity -->
<DefineConstants>$(DefineConstants);UniRxLibrary</DefineConstants>
<!-- Existing code warnings; the build is a Unity-independence check, not a lint pass -->
<NoWarn>$(NoWarn);CS0108;CS0114;CS0162;CS0168;CS0169;CS0219;CS0414;CS0618;CS0649;CS8321;CS8981;SYSLIB0021</NoWarn>
<Assets>$(MSBuildThisFileDirectory)..\Assets\</Assets>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(Assets)Scripts\ServerShared\**\*.cs" />
<Compile Include="$(Assets)Plugins\UniRx\Scripts\**\*.cs" Exclude="$(Assets)Plugins\UniRx\Scripts\UnityEngineBridge\**;$(Assets)Plugins\UniRx\Scripts\UnityWinRTBridge\**;$(Assets)Plugins\UniRx\Scripts\InternalUtil\MicroCoroutine.cs" />
<!-- The reactive collections live in UniRx's Unity bridge but fence their Unity parts behind UniRxLibrary -->
<Compile Include="$(Assets)Plugins\UniRx\Scripts\UnityEngineBridge\ReactiveProperty.cs;$(Assets)Plugins\UniRx\Scripts\UnityEngineBridge\ReactiveCollection.cs;$(Assets)Plugins\UniRx\Scripts\UnityEngineBridge\ReactiveDictionary.cs" />
<Compile Include="$(Assets)Plugins\Ink\**\*.cs" />
</ItemGroup>
<ItemGroup>
<!-- Tests record reveals through Entity.RecordReveal, the book's one writer -->
<InternalsVisibleTo Include="Aetheria.Shared.Tests" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(CultMathRoot)\packages\cultmath\src\CultMath\CultMath.csproj" />
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Caching\GameCult.Caching.csproj" />
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Caching.MessagePack\GameCult.Caching.MessagePack.csproj" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Aetheria.State.Daemon\Aetheria.State.Daemon.csproj" />
</ItemGroup>
</Project>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\Aetheria.State\Aetheria.State.Dependencies.props" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Aetheria.State.Daemon\Aetheria.State.Daemon.csproj" />
</ItemGroup>
<Import Project="..\Aetheria.State\Aetheria.State.Dependencies.targets" />
</Project>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\Aetheria.State\Aetheria.State.Dependencies.props" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Aetheria.State\Aetheria.State.csproj" />
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Mesh.Quic\GameCult.Mesh.Quic.csproj" />
<ProjectReference Include="$(YmirRoot)\src\Ymir.Core\Ymir.Core.csproj" />
</ItemGroup>
<Import Project="..\Aetheria.State\Aetheria.State.Dependencies.targets" />
</Project>

View file

@ -0,0 +1,72 @@
using Aetheria.State.Documents;
internal static class AetheriaDaemonGeneratedEntityFactory
{
public static AetheriaEntitySnapshot Create(
string name,
string kind,
string factionKey,
AetheriaDaemonLoadout loadout,
double x = 0,
double z = 0,
string orbitKey = "",
int securityLevel = 0,
double securityRadius = 0)
{
var orbital = IsOrbital(kind);
return new AetheriaEntitySnapshot
{
Name = name,
Kind = kind,
Position = new AetheriaVector3 { X = x, Z = z },
Direction = new AetheriaVector2 { Y = 1 },
LookDirection = new AetheriaVector2 { Y = 1 },
Velocity = new AetheriaVector2(),
FactionKey = factionKey,
HullItemKey = loadout.HullItemKey,
LoadoutGeneration = loadout.Receipt,
OrbitKey = orbitKey,
SecurityLevel = securityLevel,
SecurityRadius = securityRadius,
IsActive = true,
HeatsinksEnabled = true,
Visibility = orbital ? 760 : 420,
VisibilitySourceCount = 1,
StatGrids =
[
StatGrid("hull", orbital ? 420 : 130),
StatGrid("shield", orbital ? 130 : 50),
StatGrid("heat", 0)
],
Equipment = loadout.Equipment,
CargoBays = loadout.CargoBays,
DockingBays = loadout.DockingBays,
WeaponGroups = loadout.WeaponGroups
.Select(indices => new AetheriaWeaponGroupSnapshot { EquipmentIndices = indices })
.ToArray(),
CargoContents = loadout.CargoBays
.Select((_, index) => new AetheriaCargoBayLoadout
{
Items = index == 0 ? loadout.Cargo : []
})
.ToArray(),
DockingBayContents = loadout.DockingBays
.Select(_ => new AetheriaCargoBayLoadout())
.ToArray(),
DockingBayAssignments = loadout.DockingBays.Select(_ => -1).ToArray()
};
}
private static bool IsOrbital(string kind) =>
string.Equals(kind, "station", StringComparison.OrdinalIgnoreCase) ||
string.Equals(kind, "turret", StringComparison.OrdinalIgnoreCase) ||
string.Equals(kind, "orbital", StringComparison.OrdinalIgnoreCase);
private static AetheriaEntityStatGrid StatGrid(string name, double value) => new()
{
Name = name,
Width = 1,
Height = 1,
Values = [value]
};
}

View file

@ -0,0 +1,448 @@
using Aetheria.State.Documents;
using GameCult.Aetheria.State.Verse;
using Random = CultMath.Random;
public sealed class AetheriaDaemonLoadoutGenerator
{
private const double RareGenerationWeight = 0.05;
private readonly AetheriaRuntimeCatalogSnapshot _catalog;
private Random _random;
private readonly uint _seed;
private readonly int _zoneIndex;
private readonly IReadOnlyDictionary<string, int> _homeZones;
private readonly IReadOnlyDictionary<int, IReadOnlyList<int>> _adjacency;
private readonly double _priceExponent;
private readonly bool _isPrelude;
public AetheriaDaemonLoadoutGenerator(
AetheriaRuntimeCatalogSnapshot catalog,
uint seed,
int zoneIndex,
IReadOnlyDictionary<string, int> homeZones,
IReadOnlyDictionary<int, IReadOnlyList<int>> adjacency,
double priceExponent = 0.5,
bool isPrelude = false)
{
_catalog = catalog ?? throw new ArgumentNullException(nameof(catalog));
_seed = seed == 0 ? 1u : seed;
_random = new Random(_seed);
_zoneIndex = zoneIndex;
_homeZones = homeZones ?? throw new ArgumentNullException(nameof(homeZones));
_adjacency = adjacency ?? throw new ArgumentNullException(nameof(adjacency));
_priceExponent = priceExponent;
_isPrelude = isPrelude;
}
public AetheriaDaemonLoadout Build(
string entityKind,
string availabilityFactionKey,
string hullItemKey = "")
{
var hullType = string.Equals(entityKind, "station", StringComparison.OrdinalIgnoreCase) ? "Station" : "Ship";
var hull = Pick(availabilityFactionKey, 0, item =>
string.Equals(item.Category, AetheriaRuntimeItemCategories.Hull, StringComparison.Ordinal) &&
string.Equals(item.HullType, hullType, StringComparison.Ordinal) &&
(string.IsNullOrWhiteSpace(hullItemKey) || string.Equals(item.ItemKey, hullItemKey, StringComparison.Ordinal)))
?? throw new InvalidOperationException($"No available {hullType} hull for faction {availabilityFactionKey}.");
var occupied = new HashSet<(int X, int Y)>();
foreach (var hardpoint in hull.Hardpoints ?? Array.Empty<AetheriaRuntimeHardpoint>())
foreach (var cell in hardpoint.ShapeCells ?? Array.Empty<AetheriaRuntimeShapeCell>())
occupied.Add((hardpoint.PositionX + cell.X, hardpoint.PositionY + cell.Y));
var slots = new List<AetheriaEntityItemSlot>();
var hasDedicatedDockingMounts = (hull.Hardpoints ?? Array.Empty<AetheriaRuntimeHardpoint>())
.Any(hardpoint => string.Equals(hardpoint.Type, "DockingBay", StringComparison.Ordinal));
if (string.Equals(entityKind, "station", StringComparison.OrdinalIgnoreCase) && !hasDedicatedDockingMounts)
AddFreeSpaceItem(hull, availabilityFactionKey, occupied, slots, 2,
item => string.Equals(item.Category, AetheriaRuntimeItemCategories.DockingBay, StringComparison.Ordinal) &&
!string.Equals(item.HardpointType, "DockingBay", StringComparison.Ordinal));
var previous = new List<AetheriaRuntimeCatalogItem>();
foreach (var hardpoint in (hull.Hardpoints ?? Array.Empty<AetheriaRuntimeHardpoint>())
.OrderByDescending(value => value.OccupiedCells))
{
var controllerKind = string.Equals(hardpoint.Type, "ControlModule", StringComparison.Ordinal)
? string.Equals(entityKind, "ship", StringComparison.OrdinalIgnoreCase) ? "Cockpit" : "TurretController"
: "";
var item = previous.FirstOrDefault(candidate => FitsHardpoint(candidate, hardpoint));
item ??= PickHardpoint(availabilityFactionKey, hardpoint, candidate =>
(IsGear(candidate) || IsDockingBay(candidate)) && FitsHardpoint(candidate, hardpoint) &&
(controllerKind.Length == 0 || HasBehavior(candidate, controllerKind)));
if (item == null)
{
if (controllerKind.Length > 0)
throw new InvalidOperationException($"No available {controllerKind} fits {hardpoint.Type}.");
continue;
}
var rotation = ParseRotation(hardpoint.Rotation);
slots.Add(Slot(hardpoint.PositionX, hardpoint.PositionY, item.ItemKey, rotation));
Reserve(occupied, item, hardpoint.PositionX, hardpoint.PositionY, rotation);
previous.Add(item);
}
var cargoBay = AddFreeSpaceItem(hull, availabilityFactionKey, occupied, slots, 3, item =>
string.Equals(item.Category, AetheriaRuntimeItemCategories.CargoBay, StringComparison.Ordinal) &&
item.InteriorOccupiedCells >= 8);
AddFreeSpaceItem(hull, availabilityFactionKey, occupied, slots, 2, item =>
IsGear(item) && HasBehavior(item, "Reactor"));
AddFreeSpaceItem(hull, availabilityFactionKey, occupied, slots, 2, item =>
IsGear(item) && HasBehavior(item, "Capacitor"));
var cargo = PackCargo(cargoBay, availabilityFactionKey,
string.Equals(entityKind, "station", StringComparison.OrdinalIgnoreCase));
var equipment = slots
.Where(value => !IsBay(_catalog.FindItem(value.ItemKey)))
.ToArray();
var cargoBays = slots
.Where(value => string.Equals(
_catalog.FindItem(value.ItemKey)?.Category,
AetheriaRuntimeItemCategories.CargoBay,
StringComparison.Ordinal))
.ToArray();
var dockingBays = slots
.Where(value => string.Equals(
_catalog.FindItem(value.ItemKey)?.Category,
AetheriaRuntimeItemCategories.DockingBay,
StringComparison.Ordinal))
.ToArray();
var selectedKeys = new[] { (Role: "hull", Key: hull.ItemKey) }
.Concat(equipment.Select(value => (Role: "equipment", Key: value.ItemKey)))
.Concat(cargoBays.Select(value => (Role: "cargo-bay", Key: value.ItemKey)))
.Concat(dockingBays.Select(value => (Role: "docking-bay", Key: value.ItemKey)))
.Concat(cargo.Select(value => (Role: "cargo", Key: value.Item.ItemKey)));
var receipt = new AetheriaLoadoutGenerationReceipt
{
Seed = _seed,
SourceZoneIndex = _zoneIndex,
AvailabilityFactionKey = availabilityFactionKey,
PriceExponent = _priceExponent,
Selections = selectedKeys.Select(value => Selection(value.Role, value.Key, availabilityFactionKey)).ToArray()
};
var defaultWeaponGroup = equipment
.Select((slot, index) => (slot, index))
.Where(value => IsWeapon(_catalog.FindItem(value.slot.ItemKey)))
.Select(value => value.index)
.ToArray();
var weaponGroups = defaultWeaponGroup.Length == 0
? Array.Empty<int[]>()
: new[] { defaultWeaponGroup };
return new AetheriaDaemonLoadout(
hull.ItemKey,
equipment,
cargoBays,
dockingBays,
cargo,
weaponGroups,
receipt);
}
private AetheriaLoadoutGenerationSelection Selection(string role, string itemKey, string factionKey)
{
var item = _catalog.FindItem(itemKey) ?? throw new InvalidOperationException($"Generated item {itemKey} left the catalog.");
var faction = _catalog.FindCorporation(factionKey);
var allegiance = string.Equals(item.ManufacturerKey, factionKey, StringComparison.Ordinal)
? 1
: (faction?.Allegiances ?? Array.Empty<AetheriaRuntimeCorporationAllegiance>())
.FirstOrDefault(value => value.CorporationKey == item.ManufacturerKey)?.Weight ?? 0;
return new AetheriaLoadoutGenerationSelection
{
Role = role,
ItemKey = item.ItemKey,
ManufacturerKey = item.ManufacturerKey,
Price = item.Price,
ManufacturerDistance = _homeZones.TryGetValue(item.ManufacturerKey, out var home) ? Distance(_zoneIndex, home) : 1,
Allegiance = allegiance
};
}
private AetheriaRuntimeCatalogItem AddFreeSpaceItem(
AetheriaRuntimeCatalogItem hull,
string factionKey,
HashSet<(int X, int Y)> occupied,
List<AetheriaEntityItemSlot> slots,
double sizeExponent,
Func<AetheriaRuntimeCatalogItem, bool> filter)
{
var candidates = Available(factionKey)
.Where(filter)
.Select(item => (Item: item, Fit: FindFit(hull, item, occupied)))
.Where(value => value.Fit.HasValue)
.ToArray();
var selected = PickWeighted(candidates, sizeExponent, factionKey,
value => value.Item);
if (selected.Item == null || !selected.Fit.HasValue)
throw new InvalidOperationException("No available mandatory equipment fits the remaining hull space.");
var fit = selected.Fit.Value;
slots.Add(Slot(fit.X, fit.Y, selected.Item.ItemKey, fit.Rotation));
Reserve(occupied, selected.Item, fit.X, fit.Y, fit.Rotation);
return selected.Item;
}
private AetheriaRuntimeCatalogItem? TryAddFreeSpaceItem(
AetheriaRuntimeCatalogItem hull,
string factionKey,
HashSet<(int X, int Y)> occupied,
List<AetheriaEntityItemSlot> slots,
double sizeExponent,
Func<AetheriaRuntimeCatalogItem, bool> filter)
{
var candidates = Available(factionKey)
.Where(filter)
.Select(item => (Item: item, Fit: FindFit(hull, item, occupied)))
.Where(value => value.Fit.HasValue)
.ToArray();
var selected = PickWeighted(candidates, sizeExponent, factionKey, value => value.Item);
if (selected.Item == null || !selected.Fit.HasValue)
return null;
var fit = selected.Fit.Value;
slots.Add(Slot(fit.X, fit.Y, selected.Item.ItemKey, fit.Rotation));
Reserve(occupied, selected.Item, fit.X, fit.Y, fit.Rotation);
return selected.Item;
}
private AetheriaLoadoutItemSlot[] PackCargo(
AetheriaRuntimeCatalogItem cargoBay,
string factionKey,
bool includeStationInventory)
{
var candidates = new List<AetheriaRuntimeCatalogItem>();
if (includeStationInventory)
{
var pool = Available(factionKey)
.Where(item => !string.Equals(item.Category, AetheriaRuntimeItemCategories.CargoBay, StringComparison.Ordinal) &&
!string.Equals(item.Category, AetheriaRuntimeItemCategories.DockingBay, StringComparison.Ordinal) &&
(!string.Equals(item.Category, AetheriaRuntimeItemCategories.Hull, StringComparison.Ordinal) ||
string.Equals(item.HullType, "Ship", StringComparison.Ordinal)))
.ToList();
for (var draw = 0; draw < 16 && pool.Count > 0; draw++)
{
var selected = PickWeighted(pool, 1, factionKey, item => item);
if (selected == null) break;
candidates.Add(selected);
pool.Remove(selected);
}
}
var interiorCells = Cells(cargoBay.InteriorShapeCells);
var width = Math.Max(1, cargoBay.InteriorShapeWidth);
var height = Math.Max(1, cargoBay.InteriorShapeHeight);
var occupied = new HashSet<(int X, int Y)>();
var packed = new List<AetheriaLoadoutItemSlot>();
foreach (var item in candidates.OrderByDescending(value => value.OccupiedCells))
{
var fit = FindFit(interiorCells, width, height, item, occupied);
if (!fit.HasValue) continue;
Reserve(occupied, item, fit.Value.X, fit.Value.Y, fit.Value.Rotation);
packed.Add(CargoSlot(fit.Value.X, fit.Value.Y, item.ItemKey, fit.Value.Rotation));
}
return packed.ToArray();
}
private AetheriaRuntimeCatalogItem? Pick(
string factionKey,
double sizeExponent,
Func<AetheriaRuntimeCatalogItem, bool> filter)
{
return PickWeighted(Available(factionKey).Where(filter).ToArray(), sizeExponent, factionKey, item => item);
}
private AetheriaRuntimeCatalogItem? PickHardpoint(
string factionKey,
AetheriaRuntimeHardpoint hardpoint,
Func<AetheriaRuntimeCatalogItem, bool> filter)
{
var candidates = Available(factionKey).Where(filter).ToArray();
if (candidates.Length == 0)
return null;
var largestFootprint = candidates.Max(item => item.OccupiedCells);
return PickWeighted(
candidates.Where(item => item.OccupiedCells == largestFootprint).ToArray(),
2,
factionKey,
item => item);
}
private T PickWeighted<T>(
IReadOnlyList<T> values,
double sizeExponent,
string factionKey,
Func<T, AetheriaRuntimeCatalogItem> itemOf)
{
if (values.Count == 0) return default!;
var weights = values.Select(value => Weight(itemOf(value), factionKey, sizeExponent)).ToArray();
var total = weights.Sum();
if (total <= 0) return default!;
var cursor = _random.NextFloat() * total;
for (var i = 0; i < values.Count; i++)
{
cursor -= weights[i];
if (cursor <= 0) return values[i];
}
return values[^1];
}
private IEnumerable<AetheriaRuntimeCatalogItem> Available(string factionKey)
{
var faction = _catalog.Corporations.FirstOrDefault(value =>
string.Equals(value.CorporationKey, factionKey, StringComparison.Ordinal));
if (faction == null) yield break;
var allegiances = (faction.Allegiances ?? Array.Empty<AetheriaRuntimeCorporationAllegiance>())
.ToDictionary(value => value.CorporationKey, value => value.Weight, StringComparer.Ordinal);
foreach (var item in _catalog.EquipmentItems)
{
if (item.Price <= 0 || string.IsNullOrWhiteSpace(item.ManufacturerKey))
continue;
if (!_isPrelude &&
(!_homeZones.ContainsKey(item.ManufacturerKey) || !allegiances.ContainsKey(item.ManufacturerKey)))
continue;
yield return item;
}
}
private double Weight(AetheriaRuntimeCatalogItem item, string factionKey, double sizeExponent)
{
var faction = _catalog.Corporations.First(value => value.CorporationKey == factionKey);
double allegiance;
if (string.Equals(item.ManufacturerKey, factionKey, StringComparison.Ordinal))
{
allegiance = 1;
}
else
{
var authoredWeight = (faction.Allegiances ?? Array.Empty<AetheriaRuntimeCorporationAllegiance>())
.FirstOrDefault(value => value.CorporationKey == item.ManufacturerKey)?.Weight ?? 0;
var distance = _homeZones.TryGetValue(item.ManufacturerKey, out var home)
? Distance(_zoneIndex, home)
: 1;
allegiance = authoredWeight / Math.Max(1, distance);
}
return allegiance * Math.Pow(Math.Max(1, item.OccupiedCells), sizeExponent) /
Math.Pow(item.Price, _priceExponent) * RarityWeight(item);
}
private static double RarityWeight(AetheriaRuntimeCatalogItem item) =>
(item.Tags ?? Array.Empty<string>()).Contains("rarity:rare", StringComparer.Ordinal)
? RareGenerationWeight
: 1;
private int Distance(int start, int target)
{
if (start == target) return 1;
var visited = new HashSet<int> { start };
var queue = new Queue<(int Zone, int Distance)>();
queue.Enqueue((start, 0));
while (queue.Count > 0)
{
var current = queue.Dequeue();
foreach (var adjacent in _adjacency.TryGetValue(current.Zone, out var values) ? values : Array.Empty<int>())
{
if (!visited.Add(adjacent)) continue;
if (adjacent == target) return current.Distance + 1;
queue.Enqueue((adjacent, current.Distance + 1));
}
}
return 1;
}
private static bool FitsHardpoint(AetheriaRuntimeCatalogItem item, AetheriaRuntimeHardpoint hardpoint)
{
if (!string.Equals(item.HardpointType, hardpoint.Type, StringComparison.Ordinal))
return false;
var target = Cells(hardpoint.ShapeCells);
return RotatedCells(item, ParseRotation(hardpoint.Rotation)).All(target.Contains);
}
private static (int X, int Y, int Rotation)? FindFit(
AetheriaRuntimeCatalogItem hull,
AetheriaRuntimeCatalogItem item,
HashSet<(int X, int Y)> occupied)
{
return FindFit(Cells(hull.ShapeCells), Math.Max(1, hull.ShapeWidth), Math.Max(1, hull.ShapeHeight), item, occupied);
}
private static (int X, int Y, int Rotation)? FindFit(
HashSet<(int X, int Y)> targetCells,
int width,
int height,
AetheriaRuntimeCatalogItem item,
HashSet<(int X, int Y)> occupied)
{
for (var rotation = 0; rotation < 4; rotation++)
{
var cells = RotatedCells(item, rotation).ToArray();
for (var y = 0; y < height; y++)
for (var x = 0; x < width; x++)
{
if (cells.All(cell => targetCells.Contains((cell.X + x, cell.Y + y)) &&
!occupied.Contains((cell.X + x, cell.Y + y))))
return (x, y, rotation);
}
}
return null;
}
private static void Reserve(HashSet<(int X, int Y)> occupied, AetheriaRuntimeCatalogItem item, int x, int y, int rotation)
{
foreach (var cell in RotatedCells(item, rotation)) occupied.Add((cell.X + x, cell.Y + y));
}
private static HashSet<(int X, int Y)> Cells(IReadOnlyList<AetheriaRuntimeShapeCell>? cells) =>
(cells ?? Array.Empty<AetheriaRuntimeShapeCell>()).Select(value => (value.X, value.Y)).ToHashSet();
private static IEnumerable<(int X, int Y)> RotatedCells(AetheriaRuntimeCatalogItem item, int rotation)
=> AetheriaRuntimeEquipmentGridGeometry.RotatedCells(item, rotation);
private static int ParseRotation(string value) =>
AetheriaRuntimeEquipmentGridGeometry.ParseRotation(value);
private static string RotationName(int rotation) =>
AetheriaRuntimeEquipmentGridGeometry.RotationName(rotation);
private static bool HasBehavior(AetheriaRuntimeCatalogItem item, string kind) =>
(item.BehaviorKinds ?? Array.Empty<string>()).Contains(kind, StringComparer.Ordinal);
private static bool IsGear(AetheriaRuntimeCatalogItem item) =>
string.Equals(item.Category, AetheriaRuntimeItemCategories.Gear, StringComparison.Ordinal) ||
string.Equals(item.Category, AetheriaRuntimeItemCategories.Weapon, StringComparison.Ordinal);
private static bool IsWeapon(AetheriaRuntimeCatalogItem? item) =>
item != null &&
(string.Equals(item.Category, AetheriaRuntimeItemCategories.Weapon, StringComparison.Ordinal) ||
(item.BehaviorKinds ?? Array.Empty<string>()).Any(kind =>
kind.Contains("Weapon", StringComparison.Ordinal)));
private static bool IsBay(AetheriaRuntimeCatalogItem? item) =>
item != null &&
(string.Equals(item.Category, AetheriaRuntimeItemCategories.CargoBay, StringComparison.Ordinal) ||
string.Equals(item.Category, AetheriaRuntimeItemCategories.DockingBay, StringComparison.Ordinal));
private static bool IsDockingBay(AetheriaRuntimeCatalogItem item) =>
string.Equals(item.Category, AetheriaRuntimeItemCategories.DockingBay, StringComparison.Ordinal);
private static AetheriaEntityItemSlot Slot(int x, int y, string itemKey, int rotation = 0) => new()
{
Position = new AetheriaGridCoord { X = x, Y = y }, ItemKey = itemKey,
Quality = 1, Durability = 1, Quantity = 1, Enabled = true,
Rotation = RotationName(rotation)
};
private static AetheriaLoadoutItemSlot CargoSlot(int index, string itemKey) =>
CargoSlot(index % 4, index / 4, itemKey);
private static AetheriaLoadoutItemSlot CargoSlot(int x, int y, string itemKey, int rotation = 0) => new()
{
Position = new AetheriaGridCoord { X = x, Y = y },
Item = new AetheriaLoadoutItem { ItemKey = itemKey, Quality = 1, Durability = 1, Quantity = 1, Enabled = true },
Rotation = RotationName(rotation)
};
}
public sealed record AetheriaDaemonLoadout(
string HullItemKey,
AetheriaEntityItemSlot[] Equipment,
AetheriaEntityItemSlot[] CargoBays,
AetheriaEntityItemSlot[] DockingBays,
AetheriaLoadoutItemSlot[] Cargo,
int[][] WeaponGroups,
AetheriaLoadoutGenerationReceipt Receipt);

View file

@ -0,0 +1,232 @@
using Aetheria.State;
using Aetheria.State.Documents;
using MessagePack;
using System.Security.Cryptography;
internal static class AetheriaDaemonNativeCatalog
{
private const string NativeCatalogRevisionTag = "native-catalog-revision:1";
private const string ZenithHullLegacyId = "82efc0a5-1ba5-4ff3-a281-b2e6e247521d";
private const string MediumDockingBayLegacyId = "3e930a2c-ac72-4385-98aa-1c5b0b90db46";
private const string DockyardHullLegacyId = "ca098005-8cc8-47f4-be99-7bc842805359";
private const string DockyardBayLegacyId = "8ec30f8d-8536-48b4-bd64-65f29f229895";
public static string DockyardHullItemKey => ItemKey(DockyardHullLegacyId);
public static string DockyardBayItemKey => ItemKey(DockyardBayLegacyId);
public static async Task<bool> EnsureAsync(AetheriaStateNode node)
{
ArgumentNullException.ThrowIfNull(node);
var zenith = await node.MutableDocument<AetheriaItemDefinition>(
AetheriaCatalogKeys.ItemDefinitionFromLegacyId(ZenithHullLegacyId))
.ReadAsync()
.ConfigureAwait(false)
?? throw new InvalidDataException("The daemon-native dockyard requires the canonical Zenith station hull.");
var mediumBay = await node.MutableDocument<AetheriaItemDefinition>(
AetheriaCatalogKeys.ItemDefinitionFromLegacyId(MediumDockingBayLegacyId))
.ReadAsync()
.ConfigureAwait(false)
?? throw new InvalidDataException("The daemon-native dockyard requires the canonical medium docking bay.");
var expectedHull = CreateDockyardHull(zenith);
var expectedBay = CreateDockyardBay(mediumBay);
var hullDocument = node.MutableDocument<AetheriaItemDefinition>(
AetheriaCatalogKeys.ItemDefinitionFromLegacyId(DockyardHullLegacyId));
var bayDocument = node.MutableDocument<AetheriaItemDefinition>(
AetheriaCatalogKeys.ItemDefinitionFromLegacyId(DockyardBayLegacyId));
var existingHull = await hullDocument.ReadAsync().ConfigureAwait(false);
var existingBay = await bayDocument.ReadAsync().ConfigureAwait(false);
var hullCurrent = HasGenerationTags(existingHull, expectedHull.Tags);
var bayCurrent = HasGenerationTags(existingBay, expectedBay.Tags);
if (!hullCurrent)
await hullDocument.ReplaceAsync(expectedHull).ConfigureAwait(false);
if (!bayCurrent)
await bayDocument.ReplaceAsync(expectedBay).ConfigureAwait(false);
return !hullCurrent || !bayCurrent;
}
private static AetheriaItemDefinition CreateDockyardHull(AetheriaItemDefinition source)
{
const int width = 24;
const int height = 24;
var hardpoints = Enumerable.Range(0, 6)
.SelectMany(index => new[]
{
Hardpoint("DockingBay", index * 4, 0, 4, 4, source.HullArmor),
Hardpoint("DockingBay", index * 4, 20, 4, 4, source.HullArmor)
})
.Concat(new[]
{
Hardpoint("Reactor", 10, 10, 4, 4, source.HullArmor),
Hardpoint("Radiator", 8, 10, 2, 2, source.HullArmor),
Hardpoint("Radiator", 14, 10, 2, 2, source.HullArmor),
Hardpoint("Radiator", 10, 14, 2, 2, source.HullArmor),
Hardpoint("Radiator", 12, 14, 2, 2, source.HullArmor),
Hardpoint("Sensors", 11, 16, 2, 2, source.HullArmor)
})
.ToArray();
var hullCells = RectangleCells(0, 0, width, height);
var interiorCells = RectangleCells(1, 1, width - 2, height - 2);
var hull = Copy(source, DockyardHullLegacyId, "Zenith Dockyard");
hull.Description = "A high-capacity Zenith station variant built around twelve dedicated docking berths.";
hull.Mass = source.Mass * 4.5;
hull.Volume = hullCells.Length;
hull.Price = checked(source.Price * 5);
hull.ShapeWidth = width;
hull.ShapeHeight = height;
hull.OccupiedCells = hullCells.Length;
hull.ShapeCells = hullCells;
hull.InteriorShapeWidth = width;
hull.InteriorShapeHeight = height;
hull.InteriorOccupiedCells = interiorCells.Length;
hull.InteriorShapeCells = interiorCells;
hull.Hardpoints = hardpoints;
hull.HullDrag = source.HullDrag * 1.75;
hull.Tags = Tags(
source.Tags,
"origin:aetheria-daemon",
NativeCatalogRevisionTag,
SourceFingerprintTag(source),
"station-role:dockyard",
"docking-capacity:12",
$"presentation-hull-item:{ItemKey(ZenithHullLegacyId)}");
return hull;
}
private static AetheriaItemDefinition CreateDockyardBay(AetheriaItemDefinition source)
{
var bay = Copy(source, DockyardBayLegacyId, "Dockyard Berth");
bay.Description = "A medium docking bay packaged for a dedicated station docking hardpoint.";
bay.HardpointType = "DockingBay";
bay.Price = checked(source.Price * 2);
bay.Tags = Tags(
source.Tags,
"origin:aetheria-daemon",
NativeCatalogRevisionTag,
SourceFingerprintTag(source),
"station-role:dockyard");
return bay;
}
private static bool HasGenerationTags(AetheriaItemDefinition? existing, IEnumerable<string> expectedTags)
{
if (existing == null)
return false;
var expectedGenerationTags = expectedTags.Where(tag =>
tag.StartsWith("native-catalog-revision:", StringComparison.Ordinal) ||
tag.StartsWith("native-source-sha256:", StringComparison.Ordinal));
return expectedGenerationTags.All(tag => existing.Tags.Contains(tag, StringComparer.Ordinal));
}
private static string SourceFingerprintTag(AetheriaItemDefinition source)
{
var payload = MessagePackSerializer.Serialize(source);
return "native-source-sha256:" + Convert.ToHexString(SHA256.HashData(payload)).ToLowerInvariant();
}
private static AetheriaItemDefinition Copy(AetheriaItemDefinition source, string legacyId, string name) => new()
{
Name = name,
Category = source.Category,
LegacyId = legacyId,
Description = source.Description,
Mass = source.Mass,
Volume = source.Volume,
Tags = (source.Tags ?? []).ToArray(),
ManufacturerLegacyId = source.ManufacturerLegacyId,
Price = source.Price,
ShapeWidth = source.ShapeWidth,
ShapeHeight = source.ShapeHeight,
OccupiedCells = source.OccupiedCells,
HardpointType = source.HardpointType,
HullType = source.HullType,
BehaviorKinds = (source.BehaviorKinds ?? []).ToArray(),
BehaviorCount = source.BehaviorCount,
MaxStack = source.MaxStack,
Stackable = source.Stackable,
Duration = source.Duration,
Durability = source.Durability,
WeaponRange = source.WeaponRange,
WeaponCaliber = source.WeaponCaliber,
WeaponType = source.WeaponType,
WeaponFireTypes = source.WeaponFireTypes,
WeaponModifiers = source.WeaponModifiers,
ShapeCells = Clone(source.ShapeCells),
InteriorShapeWidth = source.InteriorShapeWidth,
InteriorShapeHeight = source.InteriorShapeHeight,
InteriorOccupiedCells = source.InteriorOccupiedCells,
InteriorShapeCells = Clone(source.InteriorShapeCells),
Hardpoints = (source.Hardpoints ?? []).Select(Clone).ToArray(),
BehaviorPayloads = source.BehaviorPayloads ?? [],
MinimumTemperature = source.MinimumTemperature,
MaximumTemperature = source.MaximumTemperature,
ThermalPerformanceCurveKeys = source.ThermalPerformanceCurveKeys ?? [],
HullPrefab = source.HullPrefab,
SimpleCommodityCategory = source.SimpleCommodityCategory,
CompoundCommodityCategory = source.CompoundCommodityCategory,
SpecificHeat = source.SpecificHeat,
Conductivity = source.Conductivity,
HullGridOffset = source.HullGridOffset,
HullArmor = source.HullArmor,
HullDrag = source.HullDrag,
HullCanTow = source.HullCanTow,
DockingMaxSizeX = source.DockingMaxSizeX,
DockingMaxSizeY = source.DockingMaxSizeY,
ActionBarIcon = source.ActionBarIcon,
ThermalResilience = source.ThermalResilience,
AudioStats = source.AudioStats ?? [],
EffectivenessCurveKeys = source.EffectivenessCurveKeys ?? []
};
private static AetheriaItemHardpoint Clone(AetheriaItemHardpoint source) => new()
{
Type = source.Type,
PositionX = source.PositionX,
PositionY = source.PositionY,
ShapeWidth = source.ShapeWidth,
ShapeHeight = source.ShapeHeight,
OccupiedCells = source.OccupiedCells,
ShapeCells = Clone(source.ShapeCells),
Transform = source.Transform,
Rotation = source.Rotation,
Armor = source.Armor
};
private static AetheriaItemHardpoint Hardpoint(
string type,
int x,
int y,
int width,
int height,
double armor) => new()
{
Type = type,
PositionX = x,
PositionY = y,
ShapeWidth = width,
ShapeHeight = height,
OccupiedCells = width * height,
ShapeCells = RectangleCells(0, 0, width, height),
Rotation = "None",
Armor = armor
};
private static AetheriaShapeCell[] RectangleCells(int x, int y, int width, int height) =>
Enumerable.Range(y, height)
.SelectMany(row => Enumerable.Range(x, width)
.Select(column => new AetheriaShapeCell { X = column, Y = row }))
.ToArray();
private static AetheriaShapeCell[] Clone(IEnumerable<AetheriaShapeCell>? cells) =>
(cells ?? []).Select(cell => new AetheriaShapeCell { X = cell.X, Y = cell.Y }).ToArray();
private static string[] Tags(IEnumerable<string>? tags, params string[] added) =>
(tags ?? []).Concat(added).Distinct(StringComparer.Ordinal).ToArray();
private static string ItemKey(string legacyId) => $"aetheria.item_definition:legacy:{legacyId}";
}

View file

@ -0,0 +1,134 @@
using Aetheria.State;
using Aetheria.State.Documents;
using GameCult.Aetheria.State.Verse;
using GameCult.Caching;
public static class AetheriaDaemonRegularRunWriter
{
public static async Task<AetheriaDaemonWrittenRun> WriteAsync(
AetheriaStateNode node,
AetheriaRuntimeCatalogSnapshot catalog,
string now,
uint generationSeed,
AetheriaDaemonRegularTopologySettings? topologySettings = null)
{
if (node == null) throw new ArgumentNullException(nameof(node));
if (catalog == null) throw new ArgumentNullException(nameof(catalog));
if (generationSeed == 0) throw new ArgumentOutOfRangeException(nameof(generationSeed));
var factions = AetheriaDaemonRegularTopologyGenerator.ResolveFossilFactions(catalog);
var world = AetheriaDaemonTutorialWorldGenerator.GenerateRegular(
factions, generationSeed, topologySettings);
var materialized = AetheriaDaemonTutorialWorldMaterializer.Materialize(
world, factions, catalog, isPrelude: false, identityPrefix: "regular");
var runId = $"sector-{generationSeed:x8}";
var runKey = RunKey(runId);
var corporationIndices = (catalog.Corporations ?? Array.Empty<AetheriaRuntimeCorporation>())
.Select((corporation, index) => (corporation.CorporationKey, index))
.Where(value => !string.IsNullOrWhiteSpace(value.CorporationKey))
.ToDictionary(value => value.CorporationKey, value => value.index, StringComparer.Ordinal);
var zoneKeys = materialized.Topology.Zones
.OrderBy(zone => zone.ZoneIndex)
.Select(zone => ZoneKey(runId, zone.ZoneIndex))
.ToArray();
var playerEntityKey = EntityKey(runId, materialized.PlayerZoneIndex, materialized.PlayerEntityIndex);
foreach (var zone in materialized.Zones.Values.OrderBy(zone => zone.Topology.ZoneIndex))
{
var entityKeys = Enumerable.Range(0, zone.Entities.Length)
.Select(index => EntityKey(runId, zone.Topology.ZoneIndex, index))
.ToArray();
await node.MutableDocument<AetheriaZoneState>(new CultRecordKey(ZoneKey(runId, zone.Topology.ZoneIndex)))
.ReplaceAsync(new AetheriaZoneState
{
Name = zone.Topology.Name,
Position = new AetheriaVector2 { X = zone.Topology.X, Y = zone.Topology.Y },
AdjacentZoneIndices = zone.Topology.AdjacentZoneIndices.ToArray(),
FactionIndices = zone.Topology.FactionKeys.Select(key => corporationIndices[key]).ToArray(),
OwnerFactionIndex = string.IsNullOrWhiteSpace(zone.Topology.OwnerFactionKey)
? -1
: corporationIndices[zone.Topology.OwnerFactionKey],
EntityKeys = entityKeys,
Orbits = zone.Orbits,
Bodies = zone.CelestialPlan.Bodies,
GravityTerrainRadius = zone.CelestialPlan.Radius,
GravityTerrainDepth = zone.CelestialPlan.GravityTerrainDepth,
GravityTerrainDepthExponent = zone.CelestialPlan.GravityTerrainDepthExponent,
GravityTerrainBoundaryFog = zone.CelestialPlan.GravityTerrainBoundaryFog,
GravityTerrainWaveFrequency = 1,
DroppedPickups = [],
NextPickupIndex = 0
}).ConfigureAwait(false);
for (var entityIndex = 0; entityIndex < zone.Entities.Length; entityIndex++)
{
await node.MutableDocument<AetheriaEntitySnapshot>(new CultRecordKey(entityKeys[entityIndex]))
.ReplaceAsync(zone.Entities[entityIndex])
.ConfigureAwait(false);
}
}
await node.MutableDocument<AetheriaRunState>(new CultRecordKey(runKey)).ReplaceAsync(new AetheriaRunState
{
RunId = runId,
IsTutorial = false,
GameMode = AetheriaGameSessionState.AetheriaMode,
EntranceZoneIndex = materialized.Topology.EntranceZoneIndex,
ExitZoneIndex = materialized.Topology.ExitZoneIndex,
CurrentZoneIndex = materialized.PlayerZoneIndex,
DiscoveredZoneIndices = materialized.Topology.DiscoveredZoneIndices.ToArray(),
ZoneKeys = zoneKeys,
FactionRelationships = materialized.Topology.HomeZoneByFactionKey.Keys
.Select(key => new AetheriaFactionRelationshipState
{
FactionKey = key,
Relationship = "Neutral",
Standing = 0
})
.ToArray(),
HomeZones = materialized.Topology.HomeZoneByFactionKey
.OrderBy(pair => pair.Key, StringComparer.Ordinal)
.Select(pair => new AetheriaFactionZoneState
{
FactionIndex = corporationIndices[pair.Key],
ZoneIndex = pair.Value
})
.ToArray(),
BossZones = materialized.Topology.BossZoneByFactionKey
.OrderBy(pair => pair.Key, StringComparer.Ordinal)
.Select(pair => new AetheriaFactionZoneState
{
FactionIndex = corporationIndices[pair.Key],
ZoneIndex = pair.Value
})
.ToArray(),
GenerationSeed = generationSeed,
CurrentEntityKey = playerEntityKey,
LifecyclePhase = AetheriaRuntimeRunLifecycle.Active,
TerminalFrameId = -1,
AgentTasks = materialized.Zones.Values
.OrderBy(zone => zone.Topology.ZoneIndex)
.SelectMany(zone => zone.AgentTasks)
.ToArray(),
UpdatedAtUtc = now
}).ConfigureAwait(false);
var settings = await node.MutableDocument<AetheriaPlayerSettings>(AetheriaStateNode.PlayerSettingsKey)
.ReadAsync().ConfigureAwait(false) ?? new AetheriaPlayerSettings();
settings.ActiveRunKey = runKey;
settings.PlayerName = string.IsNullOrWhiteSpace(settings.PlayerName) ? "Pilot" : settings.PlayerName;
settings.LastUpdatedAtUtc = now;
await node.MutableDocument<AetheriaPlayerSettings>(AetheriaStateNode.PlayerSettingsKey)
.ReplaceAsync(settings).ConfigureAwait(false);
await node.FlushAsync().ConfigureAwait(false);
return new AetheriaDaemonWrittenRun(runId, runKey, playerEntityKey, false)
{
SessionMode = AetheriaGameSessionState.AetheriaMode
};
}
public static string RunKey(string runId) => $"global:aetheria.run_state.{runId}.v1";
public static string ZoneKey(string runId, int zoneIndex) =>
$"global:aetheria.zone_state.{runId}.{zoneIndex}.v1";
public static string EntityKey(string runId, int zoneIndex, int entityIndex) =>
$"global:aetheria.run_state.{runId}.zone.{zoneIndex}.entity.{entityIndex}.v1";
}

View file

@ -0,0 +1,258 @@
public sealed record AetheriaDaemonRegularTopologySettings(
int ZoneCount,
float LinkDensity,
int MegaCount,
int BossCount)
{
public static AetheriaDaemonRegularTopologySettings Fossil { get; } =
new(196, 0.5f, 12, 3);
}
/// <summary>
/// Daemon-owned port of the fossil non-tutorial Galaxy constructor. It owns
/// regular-sector corporation selection, entrance/exit, boss chokepoints,
/// full-radius influence, discovery, and names. Zone contents consume this
/// graph and cannot revise it.
/// </summary>
public static class AetheriaDaemonRegularTopologyGenerator
{
public static AetheriaDaemonTutorialTopology GenerateFossil(
IReadOnlyList<AetheriaDaemonTutorialFactionInput> availableFactions,
uint seed,
AetheriaDaemonRegularTopologySettings? settings = null)
{
settings ??= AetheriaDaemonRegularTopologySettings.Fossil;
if (settings.ZoneCount < 3)
throw new ArgumentOutOfRangeException(nameof(settings), "Regular topology requires at least three zones.");
if (settings.MegaCount < 1)
throw new ArgumentOutOfRangeException(nameof(settings), "Regular topology requires at least one corporation.");
if (availableFactions == null || availableFactions.Count == 0)
throw new InvalidDataException("Regular topology requires typed corporations.");
var random = new CultMath.Random(seed);
var factions = availableFactions
.OrderBy(_ => random.NextFloat())
.Take(settings.MegaCount)
.ToArray();
var points = AetheriaDaemonTutorialTopologyGenerator.GeneratePoints(
settings.ZoneCount, ref random, MainCloudDensity);
var links = AetheriaDaemonTutorialTopologyGenerator.DelaunayLinks(points);
AetheriaDaemonTutorialTopologyGenerator.PruneLinks(
points, links, settings.LinkDensity, MainCloudDensity);
var distances = AetheriaDaemonTutorialTopologyGenerator.BuildDistances(points.Length, links);
var exit = AetheriaDaemonTutorialTopologyGenerator.MaxZone(points.Length, zone =>
Enumerable.Range(0, points.Length).Sum(other => distances[zone, other]));
var entrance = AetheriaDaemonTutorialTopologyGenerator.MaxZone(points.Length, zone =>
distances[exit, zone]);
var bossZones = PlaceBosses(
factions, settings.BossCount, entrance, exit, links, distances);
var homes = PlaceHomes(factions, entrance, exit, bossZones, distances);
var zoneFactions = new string[points.Length][];
var owners = new string[points.Length];
for (var zoneIndex = 0; zoneIndex < points.Length; zoneIndex++)
{
zoneFactions[zoneIndex] = factions
.Where(faction => distances[zoneIndex, homes[faction.CorporationKey]] <= faction.InfluenceDistance)
.Select(faction => faction.CorporationKey)
.ToArray();
owners[zoneIndex] = factions
.Where(faction => distances[zoneIndex, homes[faction.CorporationKey]] <= faction.InfluenceDistance)
.OrderBy(faction => distances[zoneIndex, homes[faction.CorporationKey]])
.ThenBy(faction => Array.IndexOf(factions, faction))
.Select(faction => faction.CorporationKey)
.FirstOrDefault() ?? "";
}
var discovered = new[] { entrance }
.Concat(AetheriaDaemonTutorialTopologyGenerator.Neighbors(entrance, links))
.Distinct()
.OrderBy(index => index)
.ToArray();
var nameSeed = (uint)random.NextInt(1, int.MaxValue);
var zoneNames = AetheriaDaemonTutorialTopologyGenerator.GenerateNames(
factions, owners, nameSeed, order: 4);
var zones = Enumerable.Range(0, points.Length)
.Select(index => new AetheriaDaemonTutorialZoneTopology(
index,
zoneNames[index],
points[index].X,
points[index].Y,
AetheriaDaemonTutorialTopologyGenerator.Neighbors(index, links).OrderBy(value => value).ToArray(),
zoneFactions[index],
owners[index]))
.ToArray();
return new AetheriaDaemonTutorialTopology(
seed, 91.29439f, entrance, discovered, homes, zones)
{
ExitZoneIndex = exit,
BossZoneByFactionKey = bossZones
};
}
public static IReadOnlyList<AetheriaDaemonTutorialFactionInput> ResolveFossilFactions(
GameCult.Aetheria.State.Verse.AetheriaRuntimeCatalogSnapshot catalog)
{
if (catalog == null) throw new ArgumentNullException(nameof(catalog));
return (catalog.Corporations ?? Array.Empty<GameCult.Aetheria.State.Verse.AetheriaRuntimeCorporation>())
.Where(corporation => corporation != null && !string.IsNullOrWhiteSpace(corporation.CorporationKey))
.Where(corporation => !string.IsNullOrWhiteSpace(corporation.GeonameFileKey))
.Select(corporation =>
{
var names = catalog.FindNameFile(corporation.GeonameFileKey);
if (names == null || names.Names.Count == 0)
throw new InvalidDataException(
$"Regular-sector corporation '{corporation.Name}' has no typed geoname corpus.");
return new AetheriaDaemonTutorialFactionInput(
corporation.CorporationKey,
corporation.Name ?? "",
corporation.ShortName ?? "",
corporation.InfluenceDistance)
{
TrainingNames = names.Names,
BossHullItemKey = corporation.BossHullItemKey ?? ""
};
})
.ToArray();
}
public static float MainCloudDensity(float x, float y)
{
var point = new CultMath.float2(x + 91.29439f, y + 91.29439f);
var frequency = 1f;
var amplitude = 0.5f;
var sum = 0f;
for (var octave = 0; octave < 10; octave++)
{
var noise = MathF.Abs(CultMath.math.simplex_noise(point * frequency));
sum += (octave < 4 ? 1f - noise : noise) * amplitude;
frequency *= 2f;
amplitude *= 0.7f;
}
return MathF.Pow(sum + 0.3f, 10f) * 0.01f;
}
private static Dictionary<string, int> PlaceBosses(
IReadOnlyList<AetheriaDaemonTutorialFactionInput> factions,
int bossCount,
int entrance,
int exit,
IReadOnlySet<AetheriaDaemonTutorialTopologyGenerator.Edge> links,
int[,] distances)
{
var path = ShortestPath(entrance, exit, links);
var chokepoints = path
.Where(zone => AetheriaDaemonTutorialTopologyGenerator.Neighbors(zone, links).Count() > 2)
.Where(zone => !ConnectedWithoutZone(entrance, exit, zone, links, distances.GetLength(0)))
.ToArray();
var bossZones = new Dictionary<string, int>(StringComparer.Ordinal);
foreach (var faction in factions
.Where(value => !string.IsNullOrWhiteSpace(value.BossHullItemKey))
.Take(Math.Max(0, bossCount)))
{
var candidates = chokepoints.Length == 0 ? path : chokepoints;
bossZones[faction.CorporationKey] = AetheriaDaemonTutorialTopologyGenerator.MaxZone(candidates, zone =>
{
var score = (double)distances[exit, zone] * distances[entrance, zone];
foreach (var occupied in bossZones.Values)
score *= distances[occupied, zone];
return score;
});
}
return bossZones;
}
private static Dictionary<string, int> PlaceHomes(
IReadOnlyList<AetheriaDaemonTutorialFactionInput> factions,
int entrance,
int exit,
IReadOnlyDictionary<string, int> bossZones,
int[,] distances)
{
var homes = new Dictionary<string, int>(StringComparer.Ordinal);
foreach (var faction in factions.Where(value => bossZones.ContainsKey(value.CorporationKey)))
{
var candidates = Enumerable.Range(0, distances.GetLength(0))
.Where(zone => distances[zone, bossZones[faction.CorporationKey]] <= faction.InfluenceDistance);
homes[faction.CorporationKey] = AetheriaDaemonTutorialTopologyGenerator.MaxZone(candidates, zone =>
{
var score = (double)AetheriaDaemonTutorialTopologyGenerator.RegionSize(
zone, faction.InfluenceDistance, distances);
foreach (var occupied in homes.Values)
score *= Math.Sqrt(distances[occupied, zone]);
return score;
});
}
foreach (var faction in factions.Where(value => !homes.ContainsKey(value.CorporationKey)))
{
homes[faction.CorporationKey] = AetheriaDaemonTutorialTopologyGenerator.MaxZone(
distances.GetLength(0), zone =>
{
var score = Math.Pow(
AetheriaDaemonTutorialTopologyGenerator.RegionSize(
zone, faction.InfluenceDistance, distances), homes.Count);
score *= distances[exit, zone] * distances[entrance, zone];
foreach (var occupied in homes.Values)
score *= Math.Sqrt(distances[occupied, zone]);
foreach (var boss in bossZones.Values)
score *= Math.Sqrt(distances[boss, zone]);
return score;
});
}
return homes;
}
private static int[] ShortestPath(
int source,
int target,
IReadOnlySet<AetheriaDaemonTutorialTopologyGenerator.Edge> links)
{
var queue = new Queue<int>();
var parents = new Dictionary<int, int>();
queue.Enqueue(source);
parents[source] = -1;
while (queue.Count > 0)
{
var current = queue.Dequeue();
if (current == target) break;
foreach (var neighbor in AetheriaDaemonTutorialTopologyGenerator.Neighbors(current, links).OrderBy(value => value))
{
if (parents.ContainsKey(neighbor)) continue;
parents[neighbor] = current;
queue.Enqueue(neighbor);
}
}
var path = new List<int>();
for (var cursor = target; cursor >= 0; cursor = parents[cursor])
path.Add(cursor);
path.Reverse();
return path.ToArray();
}
private static bool ConnectedWithoutZone(
int source,
int target,
int ignored,
IReadOnlySet<AetheriaDaemonTutorialTopologyGenerator.Edge> links,
int zoneCount)
{
if (source == ignored || target == ignored) return false;
var queue = new Queue<int>();
var visited = new bool[zoneCount];
queue.Enqueue(source);
visited[source] = true;
while (queue.Count > 0)
{
var current = queue.Dequeue();
foreach (var neighbor in AetheriaDaemonTutorialTopologyGenerator.Neighbors(current, links))
{
if (neighbor == ignored || visited[neighbor]) continue;
if (neighbor == target) return true;
visited[neighbor] = true;
queue.Enqueue(neighbor);
}
}
return false;
}
}

View file

@ -0,0 +1,39 @@
using Aetheria.State;
using Aetheria.State.Documents;
using GameCult.Aetheria.State.Verse;
public static class AetheriaDaemonRunFactory
{
public static async Task<AetheriaDaemonWrittenRun> WriteAsync(
AetheriaStateNode node,
AetheriaRuntimeCatalogSnapshot catalog,
string now,
string generationIdentity,
AetheriaDaemonRegularTopologySettings? regularTopologySettings = null)
{
if (node == null) throw new ArgumentNullException(nameof(node));
if (catalog == null) throw new ArgumentNullException(nameof(catalog));
var settings = await node.MutableDocument<AetheriaPlayerSettings>(AetheriaStateNode.PlayerSettingsKey)
.ReadAsync().ConfigureAwait(false) ?? new AetheriaPlayerSettings();
if (!settings.TutorialPassed)
return await AetheriaDaemonTutorialRunWriter.WriteAsync(node, catalog, now).ConfigureAwait(false);
return await AetheriaDaemonRegularRunWriter.WriteAsync(
node,
catalog,
now,
StableSeed(generationIdentity),
regularTopologySettings).ConfigureAwait(false);
}
public static uint StableSeed(string value)
{
var hash = 2166136261u;
foreach (var character in value ?? "")
{
hash ^= character;
hash *= 16777619u;
}
return hash == 0 ? 1u : hash;
}
}

View file

@ -0,0 +1,134 @@
using Aetheria.State;
using Aetheria.State.Documents;
using GameCult.Aetheria.State.Verse;
using GameCult.Caching;
public sealed record AetheriaDaemonWrittenRun(
string RunId,
string RunKey,
string CurrentEntityKey,
bool IsTutorial)
{
public string SessionMode { get; init; } = AetheriaGameSessionState.AetheriaMode;
}
public static class AetheriaDaemonTutorialRunWriter
{
public const string RunId = "tutorial";
public const uint GenerationSeed = 0xA37E_2026u;
public static async Task<AetheriaDaemonWrittenRun> WriteAsync(
AetheriaStateNode node,
AetheriaRuntimeCatalogSnapshot catalog,
string now)
{
var factions = AetheriaDaemonTutorialTopologyGenerator.ResolveFossilFactions(catalog);
var world = AetheriaDaemonTutorialWorldGenerator.Generate(factions, GenerationSeed);
var materialized = AetheriaDaemonTutorialWorldMaterializer.Materialize(world, factions, catalog);
var runKey = RunKey();
var zoneKeys = materialized.Topology.Zones
.OrderBy(zone => zone.ZoneIndex)
.Select(zone => ZoneKey(zone.ZoneIndex))
.ToArray();
var corporationIndices = (catalog.Corporations ?? Array.Empty<AetheriaRuntimeCorporation>())
.Select((corporation, index) => (corporation.CorporationKey, index))
.Where(value => !string.IsNullOrWhiteSpace(value.CorporationKey))
.ToDictionary(value => value.CorporationKey, value => value.index, StringComparer.Ordinal);
var playerEntityKey = EntityKey(materialized.PlayerZoneIndex, materialized.PlayerEntityIndex);
foreach (var zone in materialized.Zones.Values.OrderBy(zone => zone.Topology.ZoneIndex))
{
var entityKeys = Enumerable.Range(0, zone.Entities.Length)
.Select(index => EntityKey(zone.Topology.ZoneIndex, index))
.ToArray();
await node.MutableDocument<AetheriaZoneState>(new CultRecordKey(ZoneKey(zone.Topology.ZoneIndex)))
.ReplaceAsync(new AetheriaZoneState
{
Name = zone.Topology.Name,
Position = new AetheriaVector2 { X = zone.Topology.X, Y = zone.Topology.Y },
AdjacentZoneIndices = zone.Topology.AdjacentZoneIndices.ToArray(),
FactionIndices = zone.Topology.FactionKeys
.Select(key => corporationIndices[key])
.ToArray(),
OwnerFactionIndex = string.IsNullOrWhiteSpace(zone.Topology.OwnerFactionKey)
? -1
: corporationIndices[zone.Topology.OwnerFactionKey],
EntityKeys = entityKeys,
Orbits = zone.Orbits,
Bodies = zone.CelestialPlan.Bodies,
GravityTerrainRadius = zone.CelestialPlan.Radius,
GravityTerrainDepth = zone.CelestialPlan.GravityTerrainDepth,
GravityTerrainDepthExponent = zone.CelestialPlan.GravityTerrainDepthExponent,
GravityTerrainBoundaryFog = zone.CelestialPlan.GravityTerrainBoundaryFog,
GravityTerrainWaveFrequency = 1,
DroppedPickups = [],
NextPickupIndex = 0
}).ConfigureAwait(false);
for (var entityIndex = 0; entityIndex < zone.Entities.Length; entityIndex++)
{
await node.MutableDocument<AetheriaEntitySnapshot>(new CultRecordKey(entityKeys[entityIndex]))
.ReplaceAsync(zone.Entities[entityIndex])
.ConfigureAwait(false);
}
}
await node.MutableDocument<AetheriaRunState>(new CultRecordKey(runKey)).ReplaceAsync(new AetheriaRunState
{
RunId = RunId,
IsTutorial = true,
EntranceZoneIndex = materialized.Topology.EntranceZoneIndex,
ExitZoneIndex = -1,
CurrentZoneIndex = materialized.PlayerZoneIndex,
DiscoveredZoneIndices = materialized.Topology.DiscoveredZoneIndices.ToArray(),
ZoneKeys = zoneKeys,
FactionRelationships = factions.Select(faction => new AetheriaFactionRelationshipState
{
FactionKey = faction.CorporationKey,
Relationship = "Neutral",
Standing = 0
}).ToArray(),
HomeZones = materialized.Topology.HomeZoneByFactionKey
.OrderBy(pair => pair.Key, StringComparer.Ordinal)
.Select(pair => new AetheriaFactionZoneState
{
FactionIndex = corporationIndices[pair.Key],
ZoneIndex = pair.Value
})
.ToArray(),
BossZones = materialized.Topology.BossZoneByFactionKey
.OrderBy(pair => pair.Key, StringComparer.Ordinal)
.Select(pair => new AetheriaFactionZoneState
{
FactionIndex = corporationIndices[pair.Key],
ZoneIndex = pair.Value
})
.ToArray(),
GenerationSeed = GenerationSeed,
CurrentEntityKey = playerEntityKey,
LifecyclePhase = AetheriaRuntimeRunLifecycle.Active,
TerminalFrameId = -1,
GameMode = AetheriaGameSessionState.AetheriaMode,
AgentTasks = materialized.Zones.Values
.OrderBy(zone => zone.Topology.ZoneIndex)
.SelectMany(zone => zone.AgentTasks)
.ToArray(),
UpdatedAtUtc = now
}).ConfigureAwait(false);
var settings = await node.MutableDocument<AetheriaPlayerSettings>(AetheriaStateNode.PlayerSettingsKey)
.ReadAsync().ConfigureAwait(false) ?? new AetheriaPlayerSettings();
settings.ActiveRunKey = runKey;
settings.PlayerName = string.IsNullOrWhiteSpace(settings.PlayerName) ? "Pilot" : settings.PlayerName;
settings.LastUpdatedAtUtc = now;
await node.MutableDocument<AetheriaPlayerSettings>(AetheriaStateNode.PlayerSettingsKey)
.ReplaceAsync(settings)
.ConfigureAwait(false);
await node.FlushAsync().ConfigureAwait(false);
return new AetheriaDaemonWrittenRun(RunId, runKey, playerEntityKey, true);
}
public static string RunKey() => $"global:aetheria.run_state.{RunId}.v1";
public static string ZoneKey(int zoneIndex) => $"global:aetheria.zone_state.{RunId}.{zoneIndex}.v1";
public static string EntityKey(int zoneIndex, int entityIndex) =>
$"global:aetheria.run_state.{RunId}.zone.{zoneIndex}.entity.{entityIndex}.v1";
}

View file

@ -0,0 +1,725 @@
using GameCult.Aetheria.State.Verse;
public sealed record AetheriaDaemonTutorialFactionInput(
string CorporationKey,
string Name,
string ShortName,
int InfluenceDistance)
{
public IReadOnlyList<string> TrainingNames { get; init; } = Array.Empty<string>();
public string BossHullItemKey { get; init; } = "";
}
public sealed record AetheriaDaemonTutorialTopologySettings(
int ZoneCount,
float LinkDensity,
string ProtagonistFaction,
string AntagonistFaction,
string BufferFaction,
IReadOnlyList<string> NeutralFactions,
string QuestFaction)
{
public static AetheriaDaemonTutorialTopologySettings Fossil { get; } = new(
64,
0.5f,
"Miss",
"Zhe",
"Luc",
new[] { "Aero", "Finch" },
"Adras");
}
public sealed record AetheriaDaemonTutorialZoneTopology(
int ZoneIndex,
string Name,
float X,
float Y,
IReadOnlyList<int> AdjacentZoneIndices,
IReadOnlyList<string> FactionKeys,
string OwnerFactionKey);
public sealed record AetheriaDaemonTutorialTopology(
uint GenerationSeed,
float NoisePosition,
int EntranceZoneIndex,
IReadOnlyList<int> DiscoveredZoneIndices,
IReadOnlyDictionary<string, int> HomeZoneByFactionKey,
IReadOnlyList<AetheriaDaemonTutorialZoneTopology> Zones)
{
public int ExitZoneIndex { get; init; } = -1;
public IReadOnlyDictionary<string, int> BossZoneByFactionKey { get; init; } =
new Dictionary<string, int>(StringComparer.Ordinal);
}
/// <summary>
/// Daemon-owned migration of the fossil tutorial galaxy topology. This organ
/// deliberately stops before zone contents: materialization consumes this
/// result, but cannot change its graph, faction placement, or discovery truth.
/// </summary>
public static class AetheriaDaemonTutorialTopologyGenerator
{
internal const int CandidateMultiplier = 8;
public static AetheriaDaemonTutorialTopology GenerateFossil(
IReadOnlyList<AetheriaDaemonTutorialFactionInput> factions,
uint seed,
AetheriaDaemonTutorialTopologySettings? settings = null)
{
var noiseRandom = new CultMath.Random(seed ^ 0x74A7_91D3u);
float noisePosition;
var attempts = 0;
do
{
if (++attempts > 1_000_000)
throw new InvalidOperationException("Tutorial cloud field did not produce an admissible entrance region.");
noisePosition = noiseRandom.NextFloat(0, 1000);
} while (TutorialCloudDensity(0.5f, 0.5f, noisePosition) < 0.5f);
return Generate(
factions,
seed,
(x, y) => TutorialCloudDensity(x, y, noisePosition),
settings) with
{
NoisePosition = noisePosition
};
}
public static float TutorialCloudDensity(float x, float y, float noisePosition)
{
var point = new CultMath.float2(x + noisePosition, y + noisePosition);
var frequency = 0.1f;
var amplitude = 0.5f;
var sum = 0f;
for (var octave = 0; octave < 10; octave++)
{
var noise = MathF.Abs(CultMath.math.simplex_noise(point * frequency));
sum += (octave < 4 ? 1f - noise : noise) * amplitude;
frequency *= 2f;
amplitude *= 0.7f;
}
return MathF.Pow(sum + 0.3f, 10f) * 0.01f;
}
public static IReadOnlyList<AetheriaDaemonTutorialFactionInput> ResolveFossilFactions(
AetheriaRuntimeCatalogSnapshot catalog,
AetheriaDaemonTutorialTopologySettings? settings = null)
{
settings ??= AetheriaDaemonTutorialTopologySettings.Fossil;
var requested = new[]
{
settings.ProtagonistFaction,
settings.AntagonistFaction,
settings.BufferFaction
}
.Concat(settings.NeutralFactions)
.Append(settings.QuestFaction)
.ToArray();
return requested.Select(name =>
{
var matches = (catalog.Corporations ?? Array.Empty<AetheriaRuntimeCorporation>())
.Where(value => MatchesAuthoredSelector(value, name))
.ToArray();
if (matches.Length == 0)
throw new InvalidDataException($"Tutorial generation requires the authored faction '{name}'.");
if (matches.Length > 1)
throw new InvalidDataException(
$"Tutorial faction selector '{name}' is ambiguous: {string.Join(", ", matches.Select(value => value.Name))}.");
var match = matches[0];
if (string.IsNullOrWhiteSpace(match.CorporationKey))
throw new InvalidDataException($"Tutorial faction '{name}' has no stable corporation key.");
var nameFile = catalog.FindNameFile(match.GeonameFileKey);
if (nameFile == null || nameFile.Names.Count == 0)
throw new InvalidDataException($"Tutorial faction '{name}' has no typed geoname corpus.");
return new AetheriaDaemonTutorialFactionInput(
match.CorporationKey,
match.Name ?? "",
match.ShortName ?? "",
match.InfluenceDistance)
{
TrainingNames = nameFile.Names
};
}).ToArray();
}
private static bool MatchesAuthoredSelector(AetheriaRuntimeCorporation corporation, string selector)
{
return string.Equals(corporation.CorporationKey, selector, StringComparison.OrdinalIgnoreCase) ||
StartsWithSelector(corporation.ShortName, selector) ||
StartsWithSelector(corporation.Name, selector);
}
private static bool StartsWithSelector(string? value, string selector) =>
!string.IsNullOrWhiteSpace(value) &&
value.StartsWith(selector, StringComparison.OrdinalIgnoreCase);
public static AetheriaDaemonTutorialTopology Generate(
IReadOnlyList<AetheriaDaemonTutorialFactionInput> factions,
uint seed,
Func<float, float, float> cloudDensity,
AetheriaDaemonTutorialTopologySettings? settings = null)
{
settings ??= AetheriaDaemonTutorialTopologySettings.Fossil;
if (settings.ZoneCount < 3)
throw new ArgumentOutOfRangeException(nameof(settings), "Tutorial topology requires at least three zones.");
if (cloudDensity == null)
throw new ArgumentNullException(nameof(cloudDensity));
var roles = ResolveRoles(factions, settings);
var random = new CultMath.Random(seed);
var points = GeneratePoints(settings.ZoneCount, ref random, cloudDensity);
var links = DelaunayLinks(points);
PruneLinks(points, links, settings.LinkDensity, cloudDensity);
var distances = BuildDistances(points.Length, links);
var home = PlaceFactions(points.Length, links, distances, roles);
var influence = roles.All.ToDictionary(
faction => faction.CorporationKey,
faction => Math.Max(0, (faction.InfluenceDistance + 1) / 2),
StringComparer.Ordinal);
var zoneFactions = new string[points.Length][];
var owners = new string[points.Length];
for (var zoneIndex = 0; zoneIndex < points.Length; zoneIndex++)
{
zoneFactions[zoneIndex] = roles.All
.Where(faction => distances[zoneIndex, home[faction.CorporationKey]] <= influence[faction.CorporationKey])
.Select(faction => faction.CorporationKey)
.ToArray();
owners[zoneIndex] = roles.All
.Where(faction => distances[zoneIndex, home[faction.CorporationKey]] <= influence[faction.CorporationKey])
.OrderBy(faction => distances[zoneIndex, home[faction.CorporationKey]])
.ThenBy(faction => Array.IndexOf(roles.All, faction))
.Select(faction => faction.CorporationKey)
.FirstOrDefault() ?? "";
}
var entranceCandidates = Enumerable.Range(0, points.Length)
.Where(index => owners[index].Length == 0)
.ToArray();
if (entranceCandidates.Length == 0)
throw new InvalidOperationException("Tutorial topology produced no unowned entrance zone.");
var entrance = entranceCandidates
.OrderBy(index => distances[index, home[roles.Protagonist.CorporationKey]])
.ThenBy(index => index)
.First();
var discovered = new[] { entrance }
.Concat(Neighbors(entrance, links))
.Distinct()
.OrderBy(index => index)
.ToArray();
var nameSeed = (uint)random.NextInt(1, int.MaxValue);
var zoneNames = GenerateNames(roles.All, owners, nameSeed);
var zones = Enumerable.Range(0, points.Length)
.Select(index => new AetheriaDaemonTutorialZoneTopology(
index,
zoneNames[index],
points[index].X,
points[index].Y,
Neighbors(index, links).OrderBy(value => value).ToArray(),
zoneFactions[index],
owners[index]))
.ToArray();
return new AetheriaDaemonTutorialTopology(seed, float.NaN, entrance, discovered, home, zones);
}
private static TutorialRoles ResolveRoles(
IReadOnlyList<AetheriaDaemonTutorialFactionInput> factions,
AetheriaDaemonTutorialTopologySettings settings)
{
AetheriaDaemonTutorialFactionInput Resolve(string name)
{
var matches = factions.Where(value =>
string.Equals(value.CorporationKey, name, StringComparison.OrdinalIgnoreCase) ||
StartsWithSelector(value.ShortName, name) ||
StartsWithSelector(value.Name, name))
.ToArray();
if (matches.Length == 0)
throw new InvalidDataException($"Tutorial topology is missing faction '{name}'.");
if (matches.Length > 1)
throw new InvalidDataException(
$"Tutorial topology faction selector '{name}' is ambiguous: {string.Join(", ", matches.Select(value => value.Name))}.");
return matches[0];
}
var protagonist = Resolve(settings.ProtagonistFaction);
var antagonist = Resolve(settings.AntagonistFaction);
var buffer = Resolve(settings.BufferFaction);
var neutrals = settings.NeutralFactions.Select(Resolve).ToArray();
var quest = Resolve(settings.QuestFaction);
var all = new[] { protagonist, antagonist, buffer }
.Concat(neutrals)
.Append(quest)
.DistinctBy(value => value.CorporationKey, StringComparer.Ordinal)
.ToArray();
if (all.Length != 4 + neutrals.Length)
throw new InvalidDataException("Tutorial faction roles must resolve to distinct corporations.");
return new TutorialRoles(protagonist, antagonist, buffer, neutrals, quest, all);
}
internal static string[] GenerateNames(
IReadOnlyList<AetheriaDaemonTutorialFactionInput> factions,
IReadOnlyList<string> ownerKeys,
uint seed,
int order = 3)
{
var generators = new Dictionary<string, TutorialMarkovNameGenerator>(StringComparer.Ordinal);
foreach (var faction in factions)
{
if (faction.TrainingNames.Count == 0)
throw new InvalidDataException($"Tutorial faction '{faction.ShortName}' has no geoname training rows.");
generators[faction.CorporationKey] = new TutorialMarkovNameGenerator(
seed,
faction.TrainingNames,
order: order,
minimumLength: 5,
maximumLength: 10);
}
var catalogRandom = new CultMath.Random(seed);
var used = new HashSet<string>(StringComparer.Ordinal);
var output = new string[ownerKeys.Count];
for (var zoneIndex = 0; zoneIndex < ownerKeys.Count; zoneIndex++)
{
var owner = ownerKeys[zoneIndex];
if (owner.Length == 0)
{
string candidate;
do candidate = $"EAC-{catalogRandom.NextInt(9999)}";
while (!used.Add(candidate));
output[zoneIndex] = candidate;
continue;
}
output[zoneIndex] = generators[owner].Next(used);
}
return output;
}
internal static Point[] GeneratePoints(
int count,
ref CultMath.Random random,
Func<float, float, float> density)
{
var candidates = new Point[count * CandidateMultiplier];
var accepted = 0;
var accumulator = 0f;
var attempts = 0;
while (accepted < candidates.Length)
{
if (++attempts > candidates.Length * 1_000_000)
throw new InvalidOperationException("Tutorial density did not admit enough topology samples.");
var point = new Point(random.NextFloat(), random.NextFloat());
var dx = point.X - 0.5f;
var dy = point.Y - 0.5f;
var envelope = Saturate((0.2f - (dx * dx + dy * dy)) * 4f);
var sampleDensity = Saturate(density(point.X, point.Y));
accumulator += sampleDensity * sampleDensity * envelope;
if (accumulator <= 0.5f)
continue;
accumulator = 0;
candidates[accepted++] = point;
}
var dMax = 2f * MathF.Sqrt((1f / count) / (2f * MathF.Sqrt(3f)));
var dMaxSquared = dMax * dMax;
var weights = new float[candidates.Length];
var densities = candidates.Select(point => density(point.X, point.Y)).ToArray();
var active = Enumerable.Repeat(true, candidates.Length).ToArray();
for (var i = 0; i < candidates.Length; i++)
for (var j = 0; j < candidates.Length; j++)
{
var distanceSquared = DistanceSquared(candidates[i], candidates[j]);
if (distanceSquared > dMaxSquared)
continue;
weights[i] += Weight(distanceSquared, dMax, densities[i]);
}
for (var remaining = candidates.Length; remaining > count; remaining--)
{
var eliminated = Enumerable.Range(0, candidates.Length)
.Where(index => active[index])
.OrderByDescending(index => weights[index])
.ThenBy(index => index)
.First();
active[eliminated] = false;
for (var neighbor = 0; neighbor < candidates.Length; neighbor++)
{
if (!active[neighbor])
continue;
var distanceSquared = DistanceSquared(candidates[neighbor], candidates[eliminated]);
if (distanceSquared <= dMaxSquared)
weights[neighbor] -= Weight(distanceSquared, dMax, densities[neighbor]);
}
}
return Enumerable.Range(0, candidates.Length)
.Where(index => active[index])
.Select(index => candidates[index])
.ToArray();
}
private static float Weight(float distanceSquared, float dMax, float density)
{
var distance = MathF.Sqrt(distanceSquared);
return MathF.Pow(1f - distance / dMax, 5f + 6f * density);
}
internal static HashSet<Edge> DelaunayLinks(IReadOnlyList<Point> points)
{
var vertices = points.Concat(new[]
{
new Point(-16, -8),
new Point(16, -8),
new Point(0, 16)
}).ToArray();
var superA = points.Count;
var superB = points.Count + 1;
var superC = points.Count + 2;
var triangles = new List<Triangle> { Orient(vertices, new Triangle(superA, superB, superC)) };
for (var pointIndex = 0; pointIndex < points.Count; pointIndex++)
{
var bad = triangles.Where(triangle => ContainsInCircumcircle(vertices, triangle, vertices[pointIndex])).ToArray();
var boundary = bad
.SelectMany(TriangleEdges)
.GroupBy(edge => edge)
.Where(group => group.Count() == 1)
.Select(group => group.Key)
.ToArray();
foreach (var triangle in bad)
triangles.Remove(triangle);
triangles.AddRange(boundary.Select(edge => Orient(vertices, new Triangle(edge.A, edge.B, pointIndex))));
}
return triangles
.Where(triangle => triangle.A < points.Count && triangle.B < points.Count && triangle.C < points.Count)
.SelectMany(TriangleEdges)
.ToHashSet();
}
private static Triangle Orient(IReadOnlyList<Point> points, Triangle triangle)
{
return Cross(points[triangle.A], points[triangle.B], points[triangle.C]) >= 0
? triangle
: new Triangle(triangle.B, triangle.A, triangle.C);
}
private static bool ContainsInCircumcircle(IReadOnlyList<Point> points, Triangle triangle, Point point)
{
var a = points[triangle.A];
var b = points[triangle.B];
var c = points[triangle.C];
var ax = a.X - point.X;
var ay = a.Y - point.Y;
var bx = b.X - point.X;
var by = b.Y - point.Y;
var cx = c.X - point.X;
var cy = c.Y - point.Y;
var determinant = (ax * ax + ay * ay) * (bx * cy - cx * by)
- (bx * bx + by * by) * (ax * cy - cx * ay)
+ (cx * cx + cy * cy) * (ax * by - bx * ay);
return determinant > 1e-7f;
}
private static IEnumerable<Edge> TriangleEdges(Triangle triangle)
{
yield return new Edge(triangle.A, triangle.B);
yield return new Edge(triangle.B, triangle.C);
yield return new Edge(triangle.C, triangle.A);
}
internal static void PruneLinks(
IReadOnlyList<Point> points,
HashSet<Edge> links,
float linkDensity,
Func<float, float, float> density)
{
var target = (int)MathF.Ceiling(Saturate(linkDensity) * links.Count);
while (links.Count > target)
{
var degrees = Enumerable.Range(0, points.Count)
.Select(index => links.Count(edge => edge.Contains(index)))
.ToArray();
var removable = links
.Where(edge => IsConnectedWithout(points.Count, links, edge.A, edge.B, edge))
.Select(edge => new { Edge = edge, Weight = LinkWeight(points, edge, degrees, density) })
.OrderByDescending(value => value.Weight)
.ThenBy(value => value.Edge.A)
.ThenBy(value => value.Edge.B)
.FirstOrDefault();
if (removable == null)
break;
links.Remove(removable.Edge);
}
}
private static float LinkWeight(
IReadOnlyList<Point> points,
Edge edge,
IReadOnlyList<int> degrees,
Func<float, float, float> density)
{
var a = points[edge.A];
var b = points[edge.B];
var midpointDensity = Saturate(density((a.X + b.X) * 0.5f, (a.Y + b.Y) * 0.5f));
var densityFactor = midpointDensity <= 0 ? float.PositiveInfinity : 1f / midpointDensity;
return densityFactor * DistanceSquared(a, b) * (degrees[edge.A] - 1) * (degrees[edge.B] - 1);
}
private static Dictionary<string, int> PlaceFactions(
int zoneCount,
HashSet<Edge> links,
int[,] distances,
TutorialRoles roles)
{
var home = new Dictionary<string, int>(StringComparer.Ordinal);
var influence = roles.All.ToDictionary(
faction => faction.CorporationKey,
faction => Math.Max(0, (faction.InfluenceDistance + 1) / 2),
StringComparer.Ordinal);
home[roles.Protagonist.CorporationKey] = MaxZone(zoneCount, zone =>
RegionSize(zone, influence[roles.Protagonist.CorporationKey], distances));
home[roles.Antagonist.CorporationKey] = MaxZone(zoneCount, zone =>
RegionSize(zone, influence[roles.Antagonist.CorporationKey], distances) *
distances[zone, home[roles.Protagonist.CorporationKey]]);
home[roles.Protagonist.CorporationKey] = MaxZone(zoneCount, zone =>
RegionSize(zone, influence[roles.Protagonist.CorporationKey], distances) *
Math.Sqrt(distances[zone, home[roles.Antagonist.CorporationKey]]));
var minimumDifference = Enumerable.Range(0, zoneCount)
.Min(zone => Math.Abs(
distances[zone, home[roles.Antagonist.CorporationKey]] -
distances[zone, home[roles.Protagonist.CorporationKey]]));
home[roles.Buffer.CorporationKey] = MaxZone(
Enumerable.Range(0, zoneCount).Where(zone => Math.Abs(
distances[zone, home[roles.Antagonist.CorporationKey]] -
distances[zone, home[roles.Protagonist.CorporationKey]]) == minimumDifference),
zone => RegionSize(zone, influence[roles.Buffer.CorporationKey], distances));
foreach (var neutral in roles.Neutrals)
{
home[neutral.CorporationKey] = MaxZone(zoneCount, zone =>
{
var score = (double)RegionSize(zone, influence[neutral.CorporationKey], distances);
foreach (var occupied in home.Values)
score *= Math.Sqrt(distances[zone, occupied]);
return score;
});
}
var present = BuildPresence(zoneCount, roles.All.Where(value => value != roles.Quest), home, influence, distances);
var preferredQuestZones = Enumerable.Range(0, zoneCount)
.Where(zone => present[zone].Contains(roles.Antagonist.CorporationKey) && present[zone].Contains(roles.Buffer.CorporationKey))
.ToArray();
if (preferredQuestZones.Length > 0)
{
home[roles.Quest.CorporationKey] = MaxZone(preferredQuestZones, zone =>
distances[zone, home[roles.Antagonist.CorporationKey]] *
RegionSize(zone, influence[roles.Quest.CorporationKey], distances));
}
else
{
var antagonistZones = Enumerable.Range(0, zoneCount)
.Where(zone => present[zone].Contains(roles.Antagonist.CorporationKey));
home[roles.Quest.CorporationKey] = antagonistZones
.OrderBy(zone => distances[zone, home[roles.Buffer.CorporationKey]])
.ThenBy(zone => zone)
.First();
}
return home;
}
private static HashSet<string>[] BuildPresence(
int zoneCount,
IEnumerable<AetheriaDaemonTutorialFactionInput> factions,
IReadOnlyDictionary<string, int> home,
IReadOnlyDictionary<string, int> influence,
int[,] distances)
{
return Enumerable.Range(0, zoneCount)
.Select(zone => factions
.Where(faction => distances[zone, home[faction.CorporationKey]] <= influence[faction.CorporationKey])
.Select(faction => faction.CorporationKey)
.ToHashSet(StringComparer.Ordinal))
.ToArray();
}
internal static int[,] BuildDistances(int zoneCount, IReadOnlySet<Edge> links)
{
var output = new int[zoneCount, zoneCount];
for (var source = 0; source < zoneCount; source++)
{
var queue = new Queue<int>();
var visited = new bool[zoneCount];
queue.Enqueue(source);
visited[source] = true;
while (queue.Count > 0)
{
var current = queue.Dequeue();
foreach (var neighbor in Neighbors(current, links))
{
if (visited[neighbor])
continue;
visited[neighbor] = true;
output[source, neighbor] = output[source, current] + 1;
queue.Enqueue(neighbor);
}
}
if (visited.Any(value => !value))
throw new InvalidOperationException("Tutorial topology graph is disconnected.");
}
return output;
}
internal static bool IsConnectedWithout(
int zoneCount,
IReadOnlySet<Edge> links,
int source,
int target,
Edge ignored)
{
var queue = new Queue<int>();
var visited = new bool[zoneCount];
queue.Enqueue(source);
visited[source] = true;
while (queue.Count > 0)
{
var current = queue.Dequeue();
foreach (var edge in links)
{
if (edge == ignored || !edge.Contains(current))
continue;
var neighbor = edge.Other(current);
if (neighbor == target)
return true;
if (visited[neighbor])
continue;
visited[neighbor] = true;
queue.Enqueue(neighbor);
}
}
return false;
}
internal static IEnumerable<int> Neighbors(int zone, IEnumerable<Edge> links)
{
return links.Where(edge => edge.Contains(zone)).Select(edge => edge.Other(zone));
}
internal static int RegionSize(int zone, int maxDistance, int[,] distances)
{
return Enumerable.Range(0, distances.GetLength(0)).Count(other => distances[zone, other] <= maxDistance);
}
internal static int MaxZone(int zoneCount, Func<int, double> score) =>
MaxZone(Enumerable.Range(0, zoneCount), score);
internal static int MaxZone(IEnumerable<int> zones, Func<int, double> score) =>
zones.Select(zone => new { Zone = zone, Score = score(zone) })
.OrderByDescending(value => value.Score)
.ThenBy(value => value.Zone)
.First().Zone;
private static float DistanceSquared(Point a, Point b)
{
var x = a.X - b.X;
var y = a.Y - b.Y;
return x * x + y * y;
}
private static float Cross(Point a, Point b, Point c) =>
(b.X - a.X) * (c.Y - a.Y) - (b.Y - a.Y) * (c.X - a.X);
private static float Saturate(float value) => value < 0 ? 0 : value > 1 ? 1 : value;
internal readonly record struct Point(float X, float Y);
private readonly record struct Triangle(int A, int B, int C);
internal readonly record struct Edge
{
public Edge(int a, int b)
{
A = Math.Min(a, b);
B = Math.Max(a, b);
}
public int A { get; }
public int B { get; }
public bool Contains(int value) => A == value || B == value;
public int Other(int value) => A == value ? B : B == value ? A : throw new ArgumentOutOfRangeException(nameof(value));
}
private sealed record TutorialRoles(
AetheriaDaemonTutorialFactionInput Protagonist,
AetheriaDaemonTutorialFactionInput Antagonist,
AetheriaDaemonTutorialFactionInput Buffer,
IReadOnlyList<AetheriaDaemonTutorialFactionInput> Neutrals,
AetheriaDaemonTutorialFactionInput Quest,
AetheriaDaemonTutorialFactionInput[] All);
private sealed class TutorialMarkovNameGenerator
{
private readonly Dictionary<string, List<char>> _chains = new(StringComparer.Ordinal);
private readonly List<string> _samples = [];
private readonly int _order;
private readonly int _minimumLength;
private readonly int _maximumLength;
private CultMath.Random _random;
public TutorialMarkovNameGenerator(
uint seed,
IEnumerable<string> sampleNames,
int order,
int minimumLength,
int maximumLength)
{
_random = new CultMath.Random(seed);
_order = Math.Max(1, order);
_minimumLength = Math.Max(1, minimumLength);
_maximumLength = maximumLength;
var names = new HashSet<string>(StringComparer.Ordinal);
foreach (var line in sampleNames)
foreach (var word in (line ?? "").ToUpperInvariant().Split(' ', ',', '.', '"'))
if (word.Length >= _minimumLength) names.Add(word);
foreach (var line in names)
foreach (var word in line.Split(' ', '\'', '(', ')'))
{
var lower = word.Trim().ToLowerInvariant();
if (lower.Length >= _order + 1) _samples.Add(lower + "|");
}
foreach (var word in _samples)
for (var letter = 0; letter < word.Length - _order; letter++)
{
var token = word.Substring(letter, _order);
if (!_chains.TryGetValue(token, out var row))
_chains[token] = row = [];
row.Add(word[letter + _order]);
}
if (_samples.Count == 0)
throw new InvalidDataException("Tutorial geoname corpus contains no usable Markov samples.");
}
public string Next(ISet<string> used)
{
for (var attempt = 0; attempt < 1_000_000; attempt++)
{
var sample = _samples[_random.NextInt(0, _samples.Count)];
var value = sample.Substring(0, _order);
while (_chains.TryGetValue(value.Substring(value.Length - _order, _order), out var row))
{
var next = row[_random.NextInt(0, row.Count)];
if (next == '|') break;
value += next;
}
value = char.ToUpperInvariant(value[0]) + value.Substring(1);
if (value.Length >= _minimumLength && value.Length <= _maximumLength && used.Add(value))
return value;
}
throw new InvalidOperationException("Tutorial Markov corpus could not produce another unique zone name.");
}
}
}

View file

@ -0,0 +1,276 @@
using Aetheria.State.Documents;
using GameCult.Aetheria.State.Verse;
public sealed record AetheriaDaemonMaterializedTutorialZone(
AetheriaDaemonTutorialZoneTopology Topology,
AetheriaDaemonGeneratedZonePlan CelestialPlan,
string NearestFactionKey,
int FactionPresence,
AetheriaOrbitSnapshot[] Orbits,
AetheriaEntitySnapshot[] Entities,
AetheriaAgentTaskState[] AgentTasks,
int PlayerEntityIndex);
public sealed record AetheriaDaemonMaterializedTutorialWorld(
AetheriaDaemonTutorialTopology Topology,
IReadOnlyDictionary<int, AetheriaDaemonMaterializedTutorialZone> Zones,
int PlayerZoneIndex,
int PlayerEntityIndex);
/// <summary>
/// Daemon-owned port of the fossil ZoneGenerator population pass. Celestial
/// generation hands its exact remaining random state to this organ, so adding
/// presentation work cannot perturb station, turret, or enemy placement.
/// </summary>
public static class AetheriaDaemonTutorialWorldMaterializer
{
public static AetheriaDaemonMaterializedTutorialWorld Materialize(
AetheriaDaemonTutorialWorldPlan world,
IReadOnlyList<AetheriaDaemonTutorialFactionInput> factions,
AetheriaRuntimeCatalogSnapshot catalog,
bool isPrelude = true,
string identityPrefix = "tutorial")
{
if (world == null) throw new ArgumentNullException(nameof(world));
if (catalog == null) throw new ArgumentNullException(nameof(catalog));
var homes = world.Topology.HomeZoneByFactionKey;
factions = homes.Keys
.Select(key => factions.First(faction =>
string.Equals(faction.CorporationKey, key, StringComparison.Ordinal)))
.ToArray();
var adjacency = world.Topology.Zones.ToDictionary(
zone => zone.ZoneIndex,
zone => zone.AdjacentZoneIndices,
EqualityComparer<int>.Default);
var distances = factions.ToDictionary(
faction => faction.CorporationKey,
faction => DistancesFrom(homes[faction.CorporationKey], adjacency),
StringComparer.Ordinal);
var zones = new Dictionary<int, AetheriaDaemonMaterializedTutorialZone>();
var playerEntityIndex = -1;
foreach (var topology in world.Topology.Zones.OrderBy(zone => zone.ZoneIndex))
{
var celestial = world.Zones[topology.ZoneIndex];
var random = new CultMath.Random(celestial.PostBodyRandomState);
var spawnRandom = new CultMath.Random(celestial.PostBodyRandomState ^ 0x51A7_10C5u);
var nearest = factions
.OrderBy(faction => distances[faction.CorporationKey][topology.ZoneIndex])
.First();
var homeDistance = distances[nearest.CorporationKey][topology.ZoneIndex];
var influence = isPrelude
? Math.Max(0, (nearest.InfluenceDistance + 1) / 2)
: Math.Max(0, nearest.InfluenceDistance);
var factionPresence = influence - homeDistance + 1;
var stationCount = (int)(random.NextFloat() * (factionPresence + 1));
var orbits = celestial.Orbits.ToList();
var entities = new List<AetheriaEntitySnapshot>();
var agentTasks = new List<AetheriaAgentTaskState>();
var generators = new Dictionary<string, AetheriaDaemonLoadoutGenerator>(StringComparer.Ordinal);
AetheriaDaemonLoadoutGenerator Generator(string factionKey)
{
if (!generators.TryGetValue(factionKey, out var generator))
{
generator = new AetheriaDaemonLoadoutGenerator(
catalog,
(uint)random.NextInt(1, int.MaxValue),
topology.ZoneIndex,
homes,
adjacency,
isPrelude: isPrelude);
generators.Add(factionKey, generator);
}
return generator;
}
var potential = celestial.Orbits
.Where(orbit => !string.IsNullOrWhiteSpace(orbit.ParentOrbitKey))
.Where(orbit => !celestial.Orbits.Any(sibling =>
!ReferenceEquals(sibling, orbit) &&
string.Equals(sibling.ParentOrbitKey, orbit.ParentOrbitKey, StringComparison.Ordinal) &&
Math.Abs(sibling.Distance - orbit.Distance) < 0.1))
.OrderBy(orbit => orbit.Distance)
.ToArray();
var selected = potential.Skip(potential.Length / 2).Take(Math.Max(0, stationCount)).ToArray();
for (var stationIndex = 0; stationIndex < selected.Length; stationIndex++)
{
var baseOrbit = selected[stationIndex];
var stationOrbit = new AetheriaOrbitSnapshot
{
OrbitKey = $"{identityPrefix}.zone.{topology.ZoneIndex}.station.{stationIndex}.orbit",
ParentOrbitKey = baseOrbit.ParentOrbitKey,
Distance = baseOrbit.Distance,
Phase = baseOrbit.Phase + MathF.PI / 3 * Sign(random.NextFloat() - 0.5f),
Period = baseOrbit.Distance,
FixedPosition = new AetheriaVector2()
};
orbits.Add(stationOrbit);
var security = (int)((1f - MathF.Pow(random.NextFloat(), factionPresence / 2f)) * 3f);
entities.Add(AetheriaDaemonGeneratedEntityFactory.Create(
$"{topology.Name} Station {stationIndex + 1}",
"station",
nearest.CorporationKey,
Generator(nearest.CorporationKey).Build("station", nearest.CorporationKey),
orbitKey: stationOrbit.OrbitKey,
securityLevel: security,
securityRadius: celestial.Radius));
for (var turretIndex = 0; turretIndex < 2; turretIndex++)
{
var distanceMultiplier = turretIndex / 2;
if (turretIndex % 2 == 0) distanceMultiplier = -distanceMultiplier;
var turretOrbit = new AetheriaOrbitSnapshot
{
OrbitKey = $"{identityPrefix}.zone.{topology.ZoneIndex}.station.{stationIndex}.turret.{turretIndex}.orbit",
ParentOrbitKey = stationOrbit.ParentOrbitKey,
Distance = stationOrbit.Distance,
Phase = stationOrbit.Phase + 20f * distanceMultiplier / stationOrbit.Distance,
Period = stationOrbit.Distance,
FixedPosition = new AetheriaVector2()
};
orbits.Add(turretOrbit);
entities.Add(AetheriaDaemonGeneratedEntityFactory.Create(
$"{topology.Name} Turret {stationIndex + 1}-{turretIndex + 1}",
"turret",
nearest.CorporationKey,
Generator(nearest.CorporationKey).Build("turret", nearest.CorporationKey),
orbitKey: turretOrbit.OrbitKey,
securityLevel: security,
securityRadius: celestial.Radius));
}
}
var enemyCount = (int)(random.NextFloat() * factionPresence * 2) + stationCount;
for (var enemyIndex = 0; enemyIndex < Math.Max(0, enemyCount); enemyIndex++)
{
var halfRadius = celestial.Radius * 0.5;
var enemy = AetheriaDaemonGeneratedEntityFactory.Create(
$"{nearest.ShortName} Ship {enemyIndex + 1}",
"ship",
nearest.CorporationKey,
Generator(nearest.CorporationKey).Build("ship", nearest.CorporationKey),
spawnRandom.NextFloat((float)-halfRadius, (float)halfRadius),
spawnRandom.NextFloat((float)-halfRadius, (float)halfRadius));
var entityIndex = entities.Count;
var taskId = $"{identityPrefix}.zone.{topology.ZoneIndex}.entity.{entityIndex}.patrol";
enemy.AgentTaskCapabilities =
[
AetheriaRuntimeAgentTaskTypes.Attack,
AetheriaRuntimeAgentTaskTypes.Defend,
AetheriaRuntimeAgentTaskTypes.Explore,
AetheriaRuntimeAgentTaskTypes.Patrol
];
enemy.AssignedAgentTaskId = taskId;
agentTasks.Add(new AetheriaAgentTaskState
{
TaskId = taskId,
CorporationKey = nearest.CorporationKey,
TaskType = AetheriaRuntimeAgentTaskTypes.Patrol,
Priority = 0,
ZoneIndex = topology.ZoneIndex,
Status = AetheriaRuntimeAgentTaskStatuses.Assigned,
AssignedEntityIndex = entityIndex,
CompletionRadius = 10,
TargetOrbitKeys = orbits
.OrderBy(_ => spawnRandom.NextFloat())
.Take(4)
.Select(orbit => orbit.OrbitKey)
.ToArray()
});
entities.Add(enemy);
}
var localPlayerIndex = -1;
if (topology.ZoneIndex == world.Topology.EntranceZoneIndex)
{
var protagonist = factions[0];
var starter = new AetheriaDaemonLoadoutGenerator(
catalog,
world.Topology.GenerationSeed ^ 0x50A7_EA11u,
topology.ZoneIndex,
homes,
adjacency,
isPrelude: isPrelude).Build("ship", protagonist.CorporationKey);
localPlayerIndex = entities.Count;
entities.Add(AetheriaDaemonGeneratedEntityFactory.Create(
"Pilot",
"ship",
protagonist.CorporationKey,
starter));
playerEntityIndex = localPlayerIndex;
}
ApplyOrbitalPositions(orbits, entities);
zones.Add(topology.ZoneIndex, new AetheriaDaemonMaterializedTutorialZone(
topology,
celestial,
nearest.CorporationKey,
factionPresence,
orbits.ToArray(),
entities.ToArray(),
agentTasks.ToArray(),
localPlayerIndex));
}
if (playerEntityIndex < 0)
throw new InvalidOperationException("Tutorial materialization produced no player entry ship.");
return new AetheriaDaemonMaterializedTutorialWorld(
world.Topology,
zones,
world.Topology.EntranceZoneIndex,
playerEntityIndex);
}
private static void ApplyOrbitalPositions(
IReadOnlyList<AetheriaOrbitSnapshot> orbits,
IReadOnlyList<AetheriaEntitySnapshot> entities)
{
var runtimeZone = new AetheriaRuntimeZoneSnapshotCommit
{
Orbits = orbits.Select(orbit => new AetheriaRuntimeOrbitSnapshotCommit
{
OrbitKey = orbit.OrbitKey,
ParentOrbitKey = orbit.ParentOrbitKey,
Distance = orbit.Distance,
Phase = orbit.Phase,
Period = orbit.Period,
FixedPositionX = orbit.FixedPosition?.X ?? 0,
FixedPositionY = orbit.FixedPosition?.Y ?? 0
}).ToArray()
};
var positions = AetheriaRuntimeOrbitQueries.BuildPositions(runtimeZone);
foreach (var entity in entities.Where(entity => !string.IsNullOrWhiteSpace(entity.OrbitKey)))
{
if (!positions.TryGetValue(entity.OrbitKey, out var position))
throw new InvalidDataException(
$"Generated orbital entity '{entity.Name}' references missing orbit '{entity.OrbitKey}'.");
entity.Position.X = position.x;
entity.Position.Z = position.z;
}
}
private static IReadOnlyDictionary<int, int> DistancesFrom(
int start,
IReadOnlyDictionary<int, IReadOnlyList<int>> adjacency)
{
var result = adjacency.Keys.ToDictionary(index => index, _ => int.MaxValue);
var queue = new Queue<int>();
result[start] = 0;
queue.Enqueue(start);
while (queue.Count > 0)
{
var current = queue.Dequeue();
foreach (var neighbor in adjacency[current])
{
if (result[neighbor] <= result[current] + 1) continue;
result[neighbor] = result[current] + 1;
queue.Enqueue(neighbor);
}
}
return result;
}
private static int Sign(float value) => value < 0 ? -1 : value > 0 ? 1 : 0;
}

View file

@ -0,0 +1,508 @@
using Aetheria.State.Documents;
public sealed record AetheriaDaemonGeneratedZonePlan(
uint GenerationSeed,
uint PostBodyRandomState,
double Radius,
double Mass,
double HierarchyMass,
int EmptyOrbitCount,
AetheriaOrbitSnapshot[] Orbits,
AetheriaBodySnapshot[] Bodies)
{
public double GravityTerrainDepth { get; init; } = 64;
public double GravityTerrainDepthExponent { get; init; } = 2;
public double GravityTerrainBoundaryFog { get; init; }
}
public sealed record AetheriaDaemonTutorialWorldPlan(
AetheriaDaemonTutorialTopology Topology,
IReadOnlyDictionary<int, AetheriaDaemonGeneratedZonePlan> Zones);
public static class AetheriaDaemonTutorialWorldGenerator
{
public static AetheriaDaemonTutorialWorldPlan Generate(
IReadOnlyList<AetheriaDaemonTutorialFactionInput> factions,
uint seed,
AetheriaDaemonTutorialTopologySettings? settings = null)
{
var topology = AetheriaDaemonTutorialTopologyGenerator.GenerateFossil(factions, seed, settings);
var zones = topology.Zones.ToDictionary(
zone => zone.ZoneIndex,
zone => AetheriaDaemonZoneBodyGenerator.Generate(
seed,
zone,
AetheriaDaemonTutorialTopologyGenerator.TutorialCloudDensity(
zone.X,
zone.Y,
topology.NoisePosition)));
return new AetheriaDaemonTutorialWorldPlan(topology, zones);
}
public static AetheriaDaemonTutorialWorldPlan GenerateRegular(
IReadOnlyList<AetheriaDaemonTutorialFactionInput> factions,
uint seed,
AetheriaDaemonRegularTopologySettings? settings = null)
{
var topology = AetheriaDaemonRegularTopologyGenerator.GenerateFossil(factions, seed, settings);
var zones = topology.Zones.ToDictionary(
zone => zone.ZoneIndex,
zone => AetheriaDaemonZoneBodyGenerator.Generate(
seed,
zone,
AetheriaDaemonRegularTopologyGenerator.MainCloudDensity(zone.X, zone.Y),
"regular"));
return new AetheriaDaemonTutorialWorldPlan(topology, zones);
}
}
/// <summary>
/// Daemon-owned port of the fossil ZoneGenerator celestial hierarchy. Entity
/// population consumes this plan later; it cannot regenerate bodies or orbits.
/// </summary>
public static class AetheriaDaemonZoneBodyGenerator
{
private const int MaximumPlacementSamples = 32;
public static AetheriaDaemonGeneratedZonePlan Generate(
uint galaxySeed,
AetheriaDaemonTutorialZoneTopology zone,
float cloudDensity,
string identityPrefix = "tutorial")
{
var density = Saturate(cloudDensity / 2f);
var radius = ExponentialLerp(density, 1.5f, 1000, 10000);
var mass = ExponentialLerp(density, 1.5f, 10000, 500000);
var targetSubzoneCount = ExponentialLerp(density, 1.5f, 0, 8);
var zoneSeed = StableZoneSeed(galaxySeed, zone);
var random = new CultMath.Random(zoneSeed);
var planets = new List<PlanetNode>();
if (targetSubzoneCount > 1)
{
var boundary = new Circle(new CultMath.float2(0, 0), radius * 0.9f);
var occupied = new List<Circle>();
var start = random.NextFloat(radius * 0.25f, radius * 0.5f) * random.NextFloat2Direction();
occupied.Add(new Circle(start, -boundary.DistanceTo(start)));
var samples = 0;
while (occupied.Count < targetSubzoneCount && samples < MaximumPlacementSamples)
{
samples = 0;
for (var i = 0; i < MaximumPlacementSamples; i++)
{
var point = random.NextFloat2(-radius, radius);
var tangentRadius = MathF.Min(
-boundary.DistanceTo(point),
occupied.Min(circle => circle.DistanceTo(point)));
if (tangentRadius > 0)
{
occupied.Add(new Circle(point, tangentRadius));
break;
}
samples++;
}
}
var totalArea = occupied.Sum(circle => circle.Area);
foreach (var circle in occupied)
planets.AddRange(GenerateSystem(
ref random,
circle.Area / totalArea * mass,
circle.Radius,
circle.Center));
}
else
{
planets.AddRange(GenerateSystem(ref random, mass, radius, new CultMath.float2(0, 0)));
}
var orbitKeys = planets.Select((_, index) => $"{identityPrefix}.zone.{zone.ZoneIndex}.orbit.{index}").ToArray();
var nodeIndices = planets.Select((node, index) => (node, index)).ToDictionary(value => value.node, value => value.index);
var orbits = planets.Select((planet, index) => new AetheriaOrbitSnapshot
{
OrbitKey = orbitKeys[index],
ParentOrbitKey = planet.Parent == null ? "" : orbitKeys[nodeIndices[planet.Parent]],
FixedPosition = new AetheriaVector2 { X = planet.FixedPosition.x, Y = planet.FixedPosition.y },
Distance = planet.Distance,
Phase = planet.Phase,
Period = planet.Distance
}).ToArray();
var bodies = new List<AetheriaBodySnapshot>();
for (var nodeIndex = 0; nodeIndex < planets.Count; nodeIndex++)
{
var planet = planets[nodeIndex];
if (planet.Empty)
continue;
bodies.Add(ProjectBody(
identityPrefix, zone.ZoneIndex, bodies.Count, planet, orbits[nodeIndex], ref random));
}
return new AetheriaDaemonGeneratedZonePlan(
zoneSeed,
random.state,
radius,
mass,
planets.Sum(planet => (double)planet.Mass),
planets.Count(planet => planet.Empty),
orbits,
bodies.ToArray());
}
private static PlanetNode[] GenerateSystem(
ref CultMath.Random random,
float mass,
float radius,
CultMath.float2 fixedPosition)
{
var root = new PlanetNode
{
Mass = mass,
ChildDistanceMaximum = radius * 0.75f,
ChildDistanceMinimum = PlanetSafetyRadius(mass),
FixedPosition = fixedPosition
};
var rosette = random.NextFloat() < 0.1f;
if (rosette)
{
root.ExpandRosette(ref random, (int)(random.NextFloat(1, 5) + random.NextFloat(1, 5)));
root.ExpandSolar(
ref random,
(int)(random.NextFloat(1, 3) * random.NextFloat(1, 2)),
0.6f, 0.8f, 1.25f, 1.75f, 1, 0.1f);
var averageChildMass = root.Children.Sum(value => value.Mass) / root.Children.Count;
foreach (var planet in root.Children.Where(value => value.Mass > 1000).ToArray())
{
var scale = planet.Mass / averageChildMass;
planet.ExpandSolar(
ref random,
(int)(random.NextFloat(1, 3 * scale) + random.NextFloat(1, 3 * scale)),
0.75f, 2.5f, 1 + scale * 0.25f, 1.05f + scale * 0.5f,
random.NextFloat() * random.NextFloat() * 10 + 1,
0.5f);
}
}
else
{
root.ExpandSolar(
ref random,
random.NextInt(5, 15),
0.75f, 2.5f, 1.1f, 1.25f,
random.NextFloat() * random.NextFloat() * 10 + 1,
0.25f);
}
var expanded = new HashSet<PlanetNode>();
var binaries = new HashSet<PlanetNode>();
for (var pass = 0; pass < 5; pass++)
{
var candidates = root.All()
.Where(planet => planet != root &&
(!rosette || planet.Parent != root) &&
planet.Mass > 100 &&
!expanded.Contains(planet))
.ToArray();
foreach (var planet in candidates)
{
if (random.NextFloat() >= 0.25f)
continue;
if (random.NextFloat() < 0.1f)
{
planet.ExpandRosette(ref random, 2);
binaries.UnionWith(planet.Children);
}
else
{
planet.ExpandSolar(
ref random,
planet.Mass < 1000 ? random.NextInt(1, 3) : random.NextInt(2, 6),
0.75f, 1.5f, 1.05f, 1.25f, 1, 0.15f);
}
expanded.Add(planet);
}
}
var beltCandidates = root.All()
.Where(planet => planet != root &&
(!rosette || planet.Parent != root) &&
planet.Mass < 500 &&
!binaries.Contains(planet) &&
planet.Children.Count == 0)
.Reverse()
.ToArray();
foreach (var planet in beltCandidates)
{
if (random.NextFloat() < 0.25f && planet.Parent!.Children.All(sibling => !sibling.Belt))
planet.Belt = true;
}
var all = root.All().ToArray();
var totalMass = all.Sum(planet => planet.Mass);
foreach (var planet in all)
planet.Mass = planet.Mass / totalMass * mass;
return all;
}
private static AetheriaBodySnapshot ProjectBody(
string identityPrefix,
int zoneIndex,
int bodyIndex,
PlanetNode planet,
AetheriaOrbitSnapshot orbit,
ref CultMath.Random random)
{
var kind = planet.Belt ? "asteroid_belt" :
planet.Mass > 5000 ? "sun" :
planet.Mass > 1000 ? "gas_giant" :
planet.Mass > 100 ? "planet" : "planetoid";
var body = new AetheriaBodySnapshot
{
BodyKey = $"{identityPrefix}.zone.{zoneIndex}.body.{bodyIndex}",
Kind = kind,
Name = $"Z{zoneIndex:D2}-{bodyIndex:D3}",
OrbitKey = orbit.OrbitKey,
Mass = planet.Mass,
Resources = [],
BodyRadiusMultiplier = 1,
GravityRadiusMultiplier = 1,
GravityDepthMultiplier = 1,
GravityDepthExponent = 2,
GravityInfluenceCenterX = double.NaN,
GravityInfluenceCenterZ = double.NaN,
GravityInfluenceRadius = 500 * Math.Pow(planet.Mass, 0.25),
GravityWellDepth = 30 * Math.Pow(planet.Mass, 0.175),
GravityWaveRadius = 500 * Math.Pow(planet.Mass, 0.25),
GravityWaveDepth = 0.5 * Math.Pow(planet.Mass, 0.3),
GravityWaveSpeed = 0.025,
Asteroids = [],
GasGiantVisual = new AetheriaGasGiantVisualState(),
SunVisual = new AetheriaSunVisualState()
};
if (kind == "asteroid_belt")
{
var count = Math.Max(0, (int)(MathF.Pow(planet.Mass * planet.Distance, 0.5f) + 11));
var asteroids = new AetheriaAsteroidSnapshot[count];
for (var asteroidIndex = 0; asteroidIndex < count; asteroidIndex++)
{
asteroids[asteroidIndex] = new AetheriaAsteroidSnapshot
{
Distance = planet.Distance + random.NextFloat() * (random.NextFloat() - 0.5f) *
(5 * MathF.Pow(planet.Distance, 0.666f) + 50),
Phase = random.NextFloat(),
Size = random.NextFloat(),
RotationSpeed = ExponentialLerp(random.NextFloat(), 2, 0.1f, 0.5f),
MiningAccumulators = []
};
}
body.Asteroids = asteroids;
}
else if (kind is "gas_giant" or "sun")
{
var colors = new List<AetheriaColor>();
if (kind == "sun")
{
var primary = random.NextFloat();
var secondary = Fraction(primary + 1 + 0.33f * (random.NextFloat() > 0.5f ? 1 : -1));
colors.Add(Color(Hsv(primary, 0.85f, 0.5f), 0));
colors.Add(Color(Hsv(secondary, 0.85f, 1), 1));
var fog = Hsv(primary, 0.55f, 1);
var light = Hsv(primary, 0.5f, 1);
body.SunVisual = new AetheriaSunVisualState
{
FogTintColor = Vector(fog),
LightColor = Vector(light),
LightRadiusMultiplier = 1
};
}
else
{
var primary = random.NextFloat();
var right = Fraction(primary + 0.25f);
var left = Fraction(primary + 0.75f);
var bandCount = (int)(ExponentialLerp(random.NextFloat(), 2, 5, 8) + 0.5f);
for (var band = 0; band < bandCount; band++)
{
var time = (float)band / (bandCount - 1);
var hue = random.NextFloat() > 0.25f ? primary : random.NextFloat() > 0.5f ? right : left;
colors.Add(Color(Hsv(
hue,
ExponentialLerp(random.NextFloat(), 0.5f, 0, 0.8f),
ExponentialLerp(random.NextFloat(), 0.5f, 0, 0.8f)), time));
}
}
body.GasGiantVisual = new AetheriaGasGiantVisualState
{
FirstOffsetDomainRotationSpeed = kind == "sun" ? 5 : 0,
FirstOffsetRotationSpeed = 5,
SecondOffsetDomainRotationSpeed = -25,
SecondOffsetRotationSpeed = 10,
AlbedoRotationSpeed = -3,
Colors = colors.ToArray(),
MaterialOverrides = []
};
}
return body;
}
private static uint StableZoneSeed(uint galaxySeed, AetheriaDaemonTutorialZoneTopology zone)
{
var hash = 2166136261u ^ galaxySeed;
foreach (var value in System.Text.Encoding.UTF8.GetBytes(zone.Name ?? ""))
hash = (hash ^ value) * 16777619u;
hash = (hash ^ unchecked((uint)BitConverter.SingleToInt32Bits(zone.X))) * 16777619u;
hash = (hash ^ unchecked((uint)BitConverter.SingleToInt32Bits(zone.Y))) * 16777619u;
hash = (hash ^ unchecked((uint)zone.ZoneIndex)) * 16777619u;
return hash == 0 ? 0x6E62_4EB7u : hash;
}
private static float PlanetSafetyRadius(float mass) => 2.5f * MathF.Pow(mass, 0.25f);
private static float ExponentialLerp(float value, float exponent, float minimum, float maximum) =>
minimum + MathF.Pow(Saturate(value), exponent) * (maximum - minimum);
private static float Saturate(float value) => value < 0 ? 0 : value > 1 ? 1 : value;
private static float Fraction(float value) => value - MathF.Floor(value);
private static CultMath.float3 Hsv(float hue, float saturation, float value)
{
hue = Fraction(hue);
var sector = hue * 6;
var index = (int)MathF.Floor(sector);
var fraction = sector - index;
var p = value * (1 - saturation);
var q = value * (1 - saturation * fraction);
var t = value * (1 - saturation * (1 - fraction));
return index switch
{
0 => new CultMath.float3(value, t, p),
1 => new CultMath.float3(q, value, p),
2 => new CultMath.float3(p, value, t),
3 => new CultMath.float3(p, q, value),
4 => new CultMath.float3(t, p, value),
_ => new CultMath.float3(value, p, q)
};
}
private static AetheriaColor Color(CultMath.float3 value, float time) => new()
{
X = value.x,
Y = value.y,
Z = value.z,
W = time
};
private static AetheriaVector3 Vector(CultMath.float3 value) => new()
{
X = value.x,
Y = value.y,
Z = value.z
};
private sealed record Circle(CultMath.float2 Center, float Radius)
{
public float Area => MathF.PI * Radius * Radius;
public float DistanceTo(CultMath.float2 point) => CultMath.math.length(point - Center) - Radius;
}
private sealed class PlanetNode
{
public float Distance;
public float Phase;
public float Mass;
public float ChildDistanceMinimum;
public float ChildDistanceMaximum;
public bool Empty;
public bool Belt;
public List<PlanetNode> Children { get; } = [];
public PlanetNode? Parent;
public CultMath.float2 FixedPosition;
public IEnumerable<PlanetNode> All() => new[] { this }.Concat(Children.SelectMany(child => child.All()));
public void ExpandRosette(ref CultMath.Random random, int vertices)
{
Empty = true;
var sharedMass = Mass / vertices * 2;
var proportion = vertices % 2 == 0 ? random.NextFloat(0.5f, 0.95f) : 0.5f;
var distance = (ChildDistanceMinimum + ChildDistanceMaximum) / 2;
var p0 = new CultMath.float2(0, distance);
var angle = 1f / vertices * MathF.PI * 2;
var p1 = new CultMath.float2(MathF.Cos(angle), MathF.Sin(angle)) * distance;
var neighborDistance = CultMath.math.length(p0 - p1);
var p0ChildDistance = (neighborDistance * proportion - PlanetSafetyRadius(sharedMass * (1 - proportion))) * 0.75f;
var p1ChildDistance = (neighborDistance * (1 - proportion) - PlanetSafetyRadius(sharedMass * proportion)) * 0.75f;
for (var index = 0; index < vertices; index++)
{
var child = new PlanetNode
{
Parent = this,
Mass = sharedMass * (index % 2 == 0 ? proportion : 1 - proportion),
Distance = distance,
Phase = (float)index / vertices,
ChildDistanceMaximum = index % 2 == 0 ? p0ChildDistance : p1ChildDistance
};
child.ChildDistanceMinimum = PlanetSafetyRadius(child.Mass) * 2;
Children.Add(child);
}
ChildDistanceMinimum = distance + p0ChildDistance;
}
public void ExpandSolar(
ref CultMath.Random random,
int count,
float massMultiplierMinimum,
float massMultiplierMaximum,
float distanceMultiplierMinimum,
float distanceMultiplierMaximum,
float jupiterJump,
float massFraction)
{
if (count == 0 || ChildDistanceMaximum < ChildDistanceMinimum)
return;
var masses = new float[count];
var distances = new float[count];
float massTotal = distances[0] = masses[0] = 1;
for (var index = 1; index < count; index++)
{
massTotal += masses[index] = masses[index - 1] *
random.NextFloat(massMultiplierMinimum, massMultiplierMaximum) *
(count / 2 == index ? jupiterJump : 1);
distances[index] = distances[index - 1] *
random.NextFloat(distanceMultiplierMinimum, distanceMultiplierMaximum);
}
for (var index = 0; index < count; index++)
masses[index] *= random.NextFloat(0.1f, 1);
for (var index = 0; index < count; index++)
masses[index] = masses[index] / massTotal * Mass * massFraction;
if (count > 1)
{
var original = distances.ToArray();
for (var index = 0; index < count; index++)
distances[index] = ChildDistanceMinimum +
(ChildDistanceMaximum - ChildDistanceMinimum) *
((original[index] - original[0]) / (original[^1] - original[0])) +
PlanetSafetyRadius(masses[index]);
}
for (var index = 0; index < count; index++)
{
if (masses[index] <= 1)
continue;
var child = new PlanetNode
{
Parent = this,
Mass = masses[index],
Distance = distances[index],
Phase = random.NextFloat()
};
child.ChildDistanceMinimum = PlanetSafetyRadius(child.Mass) * 2;
child.ChildDistanceMaximum = MathF.Min(
index == 0 ? child.Distance - ChildDistanceMinimum : child.Distance - distances[index - 1],
index < count - 1 ? distances[index + 1] - child.Distance : float.PositiveInfinity);
if (float.IsNaN(child.Distance))
throw new NotFiniteNumberException("Planet created with NaN distance.");
if (child.ChildDistanceMaximum > child.ChildDistanceMinimum)
Children.Add(child);
}
}
}
}

View file

@ -0,0 +1,524 @@
using Aetheria.State;
using Aetheria.State.Documents;
using GameCult.Aetheria.State.Verse;
using GameCult.Caching;
internal static class AetheriaDaemonTerminusScenarios
{
public const string Standard = "standard";
public const string ReleasedClientProof = "released-client-proof";
public const string CargoCapacityRejectionProof = "cargo-capacity-rejection-proof";
public static string Parse(string? value)
{
var scenario = string.IsNullOrWhiteSpace(value) ? Standard : value.Trim().ToLowerInvariant();
return scenario switch
{
Standard => Standard,
ReleasedClientProof => ReleasedClientProof,
CargoCapacityRejectionProof => CargoCapacityRejectionProof,
_ => throw new ArgumentException(
$"Unknown Terminus scenario '{value}'. Expected {Standard}, {ReleasedClientProof}, or {CargoCapacityRejectionProof}.")
};
}
}
internal static class AetheriaDaemonZoneGenerator
{
public const string RunId = "local-terminus";
public const uint GenerationSeed = 0xA37E_2026u;
public static async Task WritePlayableRunAsync(
AetheriaStateNode node,
AetheriaRuntimeCatalogSnapshot catalog,
string now,
string scenario)
{
scenario = AetheriaDaemonTerminusScenarios.Parse(scenario);
var runId = RunIdFor(scenario);
var runKey = new CultRecordKey($"global:aetheria.run_state.{runId}.v1");
var zoneKey = new CultRecordKey($"global:aetheria.zone_state.{runId}.0.v1");
var corporationKeys = (catalog.Corporations ?? Array.Empty<AetheriaRuntimeCorporation>())
.Select(value => value.CorporationKey)
.Where(value => !string.IsNullOrWhiteSpace(value))
.ToArray();
if (corporationKeys.Length == 0)
{
throw new InvalidDataException(
"Terminus generation requires at least one typed corporation with a stable corporation key.");
}
var homeZones = corporationKeys.ToDictionary(value => value, _ => 0, StringComparer.Ordinal);
var availabilityFactions = new Dictionary<string, string>(StringComparer.Ordinal)
{
["player"] = corporationKeys.ElementAtOrDefault(0) ?? "",
["raider"] = corporationKeys.ElementAtOrDefault(1) ?? corporationKeys.ElementAtOrDefault(0) ?? "",
["neutral"] = corporationKeys.ElementAtOrDefault(2) ?? corporationKeys.ElementAtOrDefault(0) ?? ""
};
var rootRandom = new CultMath.Random(GenerationSeed);
var adjacency = new Dictionary<int, IReadOnlyList<int>> { [0] = Array.Empty<int>() };
var loadouts = availabilityFactions.Values
.Where(value => !string.IsNullOrWhiteSpace(value))
.Distinct(StringComparer.Ordinal)
.OrderBy(value => value, StringComparer.Ordinal)
.ToDictionary(
value => value,
_ => new AetheriaDaemonLoadoutGenerator(
catalog,
(uint)rootRandom.NextInt(1, int.MaxValue),
0,
homeZones,
adjacency),
StringComparer.Ordinal);
var entities = GenerateEntities(loadouts, availabilityFactions, catalog, scenario);
var entityKeys = Enumerable.Range(0, entities.Length)
.Select(index => EntityKey(scenario, 0, index))
.ToArray();
var settings = await node.MutableDocument<AetheriaPlayerSettings>(AetheriaStateNode.PlayerSettingsKey)
.ReadAsync()
.ConfigureAwait(false) ?? new AetheriaPlayerSettings();
settings.ActiveRunKey = runKey.ToString();
settings.PlayerName = string.IsNullOrWhiteSpace(settings.PlayerName) ? "Terminus Pilot" : settings.PlayerName;
settings.LastUpdatedAtUtc = now;
await node.MutableDocument<AetheriaPlayerSettings>(AetheriaStateNode.PlayerSettingsKey)
.ReplaceAsync(settings)
.ConfigureAwait(false);
await node.MutableDocument<AetheriaRunState>(runKey).ReplaceAsync(new AetheriaRunState
{
RunId = runId,
EntranceZoneIndex = 0,
ExitZoneIndex = 0,
CurrentZoneIndex = 0,
DiscoveredZoneIndices = [0],
ZoneKeys = [zoneKey.ToString()],
GenerationSeed = GenerationSeed,
CurrentEntityKey = entityKeys[1],
LifecyclePhase = AetheriaRuntimeRunLifecycle.Active,
TerminalFrameId = -1,
GameMode = AetheriaGameSessionState.TerminusMode,
UpdatedAtUtc = now
}).ConfigureAwait(false);
await node.MutableDocument<AetheriaZoneState>(zoneKey).ReplaceAsync(new AetheriaZoneState
{
Name = "Daemon Generated Terminus",
Position = Vec2(0, 0),
EntityKeys = entityKeys,
FactionIndices = [0, 1, 2],
OwnerFactionIndex = 0,
Orbits = GenerateOrbits(),
Bodies = GenerateBodies(),
GravityTerrainRadius = 1900,
GravityTerrainDepth = 7,
GravityTerrainDepthExponent = 1.18,
GravityTerrainBoundaryFog = 0.25,
GravityTerrainWaveFrequency = 0.55,
NextPickupIndex = 0,
DroppedPickups = Array.Empty<AetheriaDroppedPickupSnapshot>()
}).ConfigureAwait(false);
for (var i = 0; i < entities.Length; i++)
{
await node.MutableDocument<AetheriaEntitySnapshot>(new CultRecordKey(entityKeys[i]))
.ReplaceAsync(entities[i])
.ConfigureAwait(false);
}
await node.FlushAsync().ConfigureAwait(false);
}
public static string RunIdFor(string scenario)
{
scenario = AetheriaDaemonTerminusScenarios.Parse(scenario);
return string.Equals(scenario, AetheriaDaemonTerminusScenarios.Standard, StringComparison.Ordinal)
? RunId
: $"{RunId}-{scenario}";
}
public static string EntityKey(string scenario, int zoneIndex, int entityIndex)
{
return $"global:aetheria.run_state.{RunIdFor(scenario)}.zone.{zoneIndex}.entity.{entityIndex}.v1";
}
private static AetheriaOrbitSnapshot[] GenerateOrbits()
{
var bodies = BodyPlans();
return bodies.Select(body => new AetheriaOrbitSnapshot
{
OrbitKey = $"{body.Key}.orbit",
ParentOrbitKey = body.ParentKey.Length == 0 ? "" : $"{body.ParentKey}.orbit",
Distance = body.Distance,
Phase = body.Phase,
FixedPosition = Vec2(body.X, body.Z)
}).ToArray();
}
private static AetheriaBodySnapshot[] GenerateBodies()
{
return BodyPlans().Select(body => new AetheriaBodySnapshot
{
BodyKey = body.Key,
Kind = body.Kind,
Name = body.Name,
OrbitKey = $"{body.Key}.orbit",
Mass = body.Mass,
BodyRadiusMultiplier = body.BodyRadius,
GravityRadiusMultiplier = 1,
GravityDepthMultiplier = 1,
GravityDepthExponent = body.DepthExponent,
GravityInfluenceCenterX = body.X,
GravityInfluenceCenterZ = body.Z,
GravityInfluenceRadius = body.GravityRadius,
GravityWellDepth = body.GravityDepth,
GravityWaveRadius = body.WaveRadius,
GravityWaveDepth = body.WaveDepth,
GravityWaveSpeed = body.WaveSpeed,
Asteroids = body.Kind == "asteroid_belt" ? Asteroids(body.Distance) : [],
GasGiantVisual = GasGiantVisual(body),
SunVisual = SunVisual(body)
}).ToArray();
}
private static BodyPlan[] BodyPlans()
{
return
[
Body("local.sun", "", "sun", "Terminus", 0, 0, 1600, 2.8, 980, 92, 3.0, 480, 11, 1.8),
Body("local.inner", "local.sun", "planet", "Cairn", 250, 0.45, 110, 0.78, 230, 20, 2.2, 92, 3, 1.0),
Body("local.blue", "local.sun", "planet", "Blueglass", 440, 2.45, 260, 1.18, 360, 36, 2.55, 150, 5, 1.1),
Body("local.blue.moon", "local.blue", "moon", "Latch", 520, 2.78, 70, 0.58, 170, 15, 2.8, 70, 2.2, 1.7),
Body("local.ember", "local.sun", "planet", "Emberhook", 640, 3.78, 220, 1.05, 330, 31, 2.25, 128, 4.2, 1.3),
Body("local.ember.moon", "local.ember", "moon", "Ashwake", 735, 3.45, 86, 0.62, 185, 16, 2.7, 72, 2.4, 1.45),
Body("local.belt", "local.sun", "asteroid_belt", "Cinder Belt", 760, -0.28, 140, 1.0, 310, 24, 2.0, 122, 4, 0.8),
Body("local.green", "local.sun", "planet", "Marrow", 930, 1.28, 180, 0.96, 300, 26, 2.35, 112, 3.6, 1.0),
Body("local.outer", "local.sun", "gas_giant", "Vesper", 1120, 0.58, 420, 1.55, 500, 48, 2.4, 230, 7, 0.72),
Body("local.outer.moon.a", "local.outer", "moon", "Iris", 1240, 0.88, 92, 0.66, 195, 17, 2.75, 78, 2.5, 1.2),
Body("local.outer.moon.b", "local.outer", "moon", "Hush", 1025, 0.28, 76, 0.55, 180, 15, 2.9, 70, 2.1, 1.5),
Body("local.deep", "local.sun", "gas_giant", "Nacre", 1420, 5.72, 340, 1.36, 440, 41, 2.55, 200, 5.8, 0.95)
];
}
private static BodyPlan Body(
string key,
string parentKey,
string kind,
string name,
double distance,
double phase,
double mass,
double bodyRadius,
double gravityRadius,
double gravityDepth,
double depthExponent,
double waveRadius,
double waveDepth,
double waveSpeed)
{
if (gravityDepth <= 0)
throw new ArgumentOutOfRangeException(nameof(gravityDepth), "Gravity depth is an unsigned well magnitude; Ymir positive radial strength attracts.");
return new BodyPlan(
key,
parentKey,
kind,
name,
distance,
phase,
Math.Cos(phase) * distance,
Math.Sin(phase) * distance,
mass,
bodyRadius,
gravityRadius,
gravityDepth,
depthExponent,
waveRadius,
waveDepth,
waveSpeed);
}
private static AetheriaEntitySnapshot[] GenerateEntities(
IReadOnlyDictionary<string, AetheriaDaemonLoadoutGenerator> loadouts,
IReadOnlyDictionary<string, string> availabilityFactions,
AetheriaRuntimeCatalogSnapshot catalog,
string scenario)
{
var keys = Enumerable.Range(0, 12)
.Select(index => EntityKey(scenario, 0, index))
.ToArray();
var entities = new[]
{
Entity(loadouts, availabilityFactions, "Anchor Station", "station", -50, -30, 0, 0, "player", 760, keys[6], [keys[1], keys[2], keys[3], keys[4], keys[6], keys[7], keys[11]], 2, 900, AetheriaDaemonNativeCatalog.DockyardHullItemKey),
Entity(loadouts, availabilityFactions, "Vanguard One", "ship", -40, -30, 0, 0, "player", 540, keys[6], [keys[0], keys[2], keys[4], keys[6], keys[7]]),
Entity(loadouts, availabilityFactions, "Wing Two", "ship", 145, 125, -5, 7, "player", 450, keys[6], [keys[0], keys[1], keys[6], keys[8]]),
Entity(loadouts, availabilityFactions, "Torch Three", "ship", -235, 210, 8, -4, "player", 500, keys[7], [keys[0], keys[1], keys[7]]),
Entity(loadouts, availabilityFactions, "Foundry Tug", "ship", 330, -155, -2, 3, "player", 390, keys[6], [keys[0], keys[1], keys[6], keys[10]]),
Entity(loadouts, availabilityFactions, "Derelict Relay", "station", -390, 270, 0, 0, "neutral", 160, "", [keys[0], keys[3]]),
Entity(loadouts, availabilityFactions, "Ash Raider", "ship", 20, -30, -5, -2, "raider", 320, "", [keys[0], keys[1], keys[2], keys[4]]),
Entity(loadouts, availabilityFactions, "Cinder Knife", "ship", 585, -115, -7, 1, "raider", 280, "", [keys[0], keys[3]]),
Entity(loadouts, availabilityFactions, "Blackwake", "ship", 690, 300, -4, -3, "raider", 230, "", [keys[2]]),
Entity(loadouts, availabilityFactions, "Vesper Sloop", "ship", 960, 620, -3, -5, "raider", 250, "", [keys[4]]),
Entity(loadouts, availabilityFactions, "Survey Skiff", "ship", -610, -365, 3, 5, "neutral", 210, "", [keys[4], keys[5]]),
Entity(loadouts, availabilityFactions, "Lagrange Beacon", "station", 105, 510, 0, 0, "neutral", 190, "", [keys[0], keys[1], keys[6]])
};
foreach (var index in new[] { 1, 2, 3, 4 })
entities[index].HomeEntityKey = keys[0];
if (!string.Equals(scenario, AetheriaDaemonTerminusScenarios.Standard, StringComparison.Ordinal))
ApplyReleasedClientProofScenario(entities, catalog, scenario);
entities[2].AgentTaskCapabilities = ["attack", "defend", "explore"];
entities[3].AgentTaskCapabilities = ["attack", "defend"];
entities[4].AgentTaskCapabilities = ["mine", "haul", "tow", "explore"];
return entities;
}
private static void ApplyReleasedClientProofScenario(
AetheriaEntitySnapshot[] entities,
AetheriaRuntimeCatalogSnapshot catalog,
string scenario)
{
// Start the proof pilot at rest on the station's authored center. The proof session owns
// its paused initial clock, so client connection latency cannot alter this starting state.
entities[1].Position.X = entities[0].Position.X;
entities[1].Position.Z = entities[0].Position.Z;
entities[1].Velocity = Vec2(0, 0);
var playerFactionKey = entities[1].FactionKey;
for (var index = 0; index < entities.Length; index++)
if (index != 6)
entities[index].FactionKey = playerFactionKey;
entities[1].CargoContents = [new AetheriaCargoBayLoadout()];
entities[6].StatGrids =
[
StatGrid("hull", 1),
StatGrid("shield", 0),
StatGrid("heat", 0)
];
var salvage = entities[6].Equipment.FirstOrDefault()
?? throw new InvalidOperationException("The starter raider loadout produced no canonical salvage item.");
var generation = entities[6].LoadoutGeneration
?? throw new InvalidOperationException("The starter raider loadout produced no generation receipt.");
var selections = generation.Selections ?? Array.Empty<AetheriaLoadoutGenerationSelection>();
var salvageSelection = selections.First(selection =>
string.Equals(selection.Role, "equipment", StringComparison.Ordinal) &&
string.Equals(selection.ItemKey, salvage.ItemKey, StringComparison.Ordinal));
entities[6].CargoContents =
[
new AetheriaCargoBayLoadout
{
Items =
[
new AetheriaLoadoutItemSlot
{
Item = new AetheriaLoadoutItem
{
ItemKey = salvage.ItemKey,
Quality = salvage.Quality,
Durability = salvage.Durability,
Quantity = 1,
Enabled = true
}
}
]
}
];
entities[6].Equipment = Array.Empty<AetheriaEntityItemSlot>();
entities[6].WeaponGroups = Array.Empty<AetheriaWeaponGroupSnapshot>();
generation.Selections = selections
.Where(selection => string.Equals(selection.Role, "hull", StringComparison.Ordinal))
.Append(new AetheriaLoadoutGenerationSelection
{
Role = "cargo",
ItemKey = salvageSelection.ItemKey,
ManufacturerKey = salvageSelection.ManufacturerKey,
Price = salvageSelection.Price,
ManufacturerDistance = salvageSelection.ManufacturerDistance,
Allegiance = salvageSelection.Allegiance
})
.ToArray();
if (string.Equals(scenario, AetheriaDaemonTerminusScenarios.CargoCapacityRejectionProof, StringComparison.Ordinal))
FillCargoBays(entities[1], salvage.ItemKey, catalog);
}
private static void FillCargoBays(
AetheriaEntitySnapshot entity,
string itemKey,
AetheriaRuntimeCatalogSnapshot catalog)
{
var item = catalog.FindItem(itemKey)
?? throw new InvalidOperationException($"The cargo-capacity proof item '{itemKey}' is absent from the typed catalog.");
if (item.Volume <= 0)
throw new InvalidOperationException($"The cargo-capacity proof item '{itemKey}' must have positive volume.");
var cargoBays = (entity.CargoBays ?? Array.Empty<AetheriaEntityItemSlot>())
.Select(slot => catalog.FindItem(slot.ItemKey))
.Where(candidate => candidate != null && candidate.InteriorOccupiedCells > 0)
.ToArray();
if (cargoBays.Length == 0)
throw new InvalidOperationException("The cargo-capacity proof ship has no generated cargo bay.");
entity.CargoContents = cargoBays.Select((bay, index) =>
{
var quantity = (int)Math.Floor(bay!.InteriorOccupiedCells / item.Volume);
if (quantity <= 0)
throw new InvalidOperationException($"Cargo bay {index} cannot contain one '{itemKey}' proof item.");
return new AetheriaCargoBayLoadout
{
Items =
[
new AetheriaLoadoutItemSlot
{
Item = new AetheriaLoadoutItem
{
ItemKey = itemKey,
Quantity = quantity,
Quality = 1,
Durability = 1,
Enabled = true
}
}
]
};
}).ToArray();
}
private static AetheriaEntitySnapshot Entity(
IReadOnlyDictionary<string, AetheriaDaemonLoadoutGenerator> loadouts,
IReadOnlyDictionary<string, string> availabilityFactions,
string name,
string kind,
double x,
double z,
double vx,
double vy,
string faction,
double visibility,
string target,
string[] contactKeys,
int securityLevel = 0,
double securityRadius = 0,
string hullItemKey = "")
{
var availabilityFaction = availabilityFactions[faction];
var loadout = loadouts[availabilityFaction].Build(kind, availabilityFaction, hullItemKey);
var entity = AetheriaDaemonGeneratedEntityFactory.Create(
name,
kind,
availabilityFaction,
loadout,
x,
z,
securityLevel: securityLevel,
securityRadius: securityRadius);
entity.Direction = Vec2(vx == 0 && vy == 0 ? 0 : vx, vx == 0 && vy == 0 ? 1 : vy);
entity.LookDirection = Vec2(vx == 0 && vy == 0 ? 0 : vx, vx == 0 && vy == 0 ? 1 : vy);
entity.Velocity = Vec2(vx, vy);
entity.Visibility = visibility;
entity.TargetEntityKey = target;
entity.Contacts = contactKeys.Select(key => new AetheriaEntityContactSnapshot
{
TargetEntityKey = key,
InfoGathered = 1
}).ToArray();
if (string.Equals(faction, "raider", StringComparison.OrdinalIgnoreCase))
entity.StatGrids[0].Values[0] = 85;
return entity;
}
private static AetheriaGasGiantVisualState GasGiantVisual(BodyPlan body)
{
if (body.Kind != "gas_giant" && body.Kind != "sun")
return new AetheriaGasGiantVisualState();
return new AetheriaGasGiantVisualState
{
FirstOffsetDomainRotationSpeed = body.Kind == "sun" ? 5 : 0,
FirstOffsetRotationSpeed = 5,
SecondOffsetRotationSpeed = 10,
SecondOffsetDomainRotationSpeed = -25,
AlbedoRotationSpeed = -3,
WaveRadiusMultiplier = 1,
WaveDepthMultiplier = 1,
Colors =
[
Color(0.85, 0.32, 0.18, 0),
Color(0.18, 0.54, 0.92, 0.5),
Color(0.92, 0.78, 0.38, 1)
]
};
}
private static AetheriaSunVisualState SunVisual(BodyPlan body)
{
return body.Kind == "sun"
? new AetheriaSunVisualState
{
LightColor = Vec3(1, 0.86, 0.58),
FogTintColor = Vec3(0.32, 0.52, 1),
LightRadiusMultiplier = 1.8
}
: new AetheriaSunVisualState();
}
private static AetheriaAsteroidSnapshot[] Asteroids(double orbitDistance)
{
return Enumerable.Range(0, 18)
.Select(index =>
{
var phase = index * Math.Tau / 18.0;
return new AetheriaAsteroidSnapshot
{
Distance = orbitDistance + Math.Sin(phase * 3.0) * 42.0,
Phase = phase,
Size = 0.65 + (index % 5) * 0.24,
RotationSpeed = (index % 2 == 0 ? 1 : -1) * (0.04 + index * 0.006)
};
})
.ToArray();
}
private static AetheriaEntityStatGrid StatGrid(string name, double value)
{
return new AetheriaEntityStatGrid
{
Name = name,
Width = 1,
Height = 1,
Values = [value]
};
}
private static AetheriaColor Color(double x, double y, double z, double w)
{
return new AetheriaColor { X = x, Y = y, Z = z, W = w };
}
private static AetheriaVector2 Vec2(double x, double y)
{
return new AetheriaVector2 { X = x, Y = y };
}
private static AetheriaVector3 Vec3(double x, double y, double z)
{
return new AetheriaVector3 { X = x, Y = y, Z = z };
}
private sealed record BodyPlan(
string Key,
string ParentKey,
string Kind,
string Name,
double Distance,
double Phase,
double X,
double Z,
double Mass,
double BodyRadius,
double GravityRadius,
double GravityDepth,
double DepthExponent,
double WaveRadius,
double WaveDepth,
double WaveSpeed);
}

View file

@ -0,0 +1,11 @@
using Ymir.Core;
namespace Aetheria.State.Daemon;
public sealed record AetheriaYmirZonePersistenceCapture(
string RunId,
int ZoneIndex,
long FrameId,
int SimulationStepIndex,
YmirSessionPersistenceCapture World,
YmirSessionPersistenceCapture? Payload);

View file

@ -0,0 +1,398 @@
using System.Globalization;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
using Aetheria.State.Documents;
using GameCult.Aetheria.State.Verse;
using GameCult.Caching;
using GameCult.Caching.MessagePack;
using GameCult.Mesh;
using Ymir.Core;
namespace Aetheria.State.Daemon;
/// <summary>
/// Owns daemon-private Ymir restart material. The public Aetheria frame remains the
/// sole durable commit marker; private chunks and resumes are never advertised.
/// </summary>
public sealed class AetheriaYmirPersistenceCoordinator : IDisposable
{
private const string WorldChannel = "world";
private const string PayloadChannel = "payloads";
private readonly CultCache _cache;
private readonly AetheriaYmirWorldPhysics _physics;
private readonly Dictionary<CursorKey, Cursor> _cursors = new();
private bool _activated;
private AetheriaYmirPersistenceCoordinator(
CultCache cache,
AetheriaYmirWorldPhysics physics,
bool activated)
{
_cache = cache;
_physics = physics;
_activated = activated;
}
public static string PrivateStatePath(string publicStatePath) => publicStatePath + ".ymir.cc";
public static async Task<AetheriaYmirPersistenceCoordinator> OpenAsync(
AetheriaStateNode node,
AetheriaYmirWorldPhysics physics,
AetheriaRuntimeDaemonFrameDocument? frame)
{
var trace = string.Equals(
Environment.GetEnvironmentVariable("AETHERIA_TRACE_STARTUP_PHASES"),
"1",
StringComparison.Ordinal);
var phase = Stopwatch.StartNew();
void Trace(string name)
{
if (trace)
Console.WriteLine($"Aetheria Ymir restore phase {name} took {phase.Elapsed.TotalMilliseconds:0.###}ms.");
phase.Restart();
}
ArgumentNullException.ThrowIfNull(node);
ArgumentNullException.ThrowIfNull(physics);
var registry = CultMesh.CreateCultCacheDocumentRegistry(
typeof(AetheriaYmirPersistenceMarkerDocument),
typeof(AetheriaYmirJournalChunkDocument),
typeof(AetheriaYmirResumeDocument));
var ownedSchemaIds = registry.AllDescriptors
.Select(descriptor => descriptor.SchemaId)
.ToHashSet(StringComparer.Ordinal);
var currentResumeKeys = frame?.Run == null || string.IsNullOrWhiteSpace(frame.Run.RunId)
? new HashSet<string>(StringComparer.Ordinal)
: (frame.Run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
.Where(zone => zone != null)
.Select(zone => ResumeRecordKey(frame.Run.RunId, zone.ZoneIndex, frame.FrameId))
.ToHashSet(StringComparer.Ordinal);
var cache = await CultCacheMessagePack.OpenAsync(
PrivateStatePath(node.StatePath),
new CultCacheOpenOptions
{
Registry = registry,
PullOnOpen = true,
UseDirectoryStore = true,
DirectoryStoreHydrationFilter = record =>
ownedSchemaIds.Contains(record.SchemaId) &&
(string.Equals(record.Key, AetheriaYmirPersistenceMarkerDocument.RecordKey, StringComparison.Ordinal) ||
currentResumeKeys.Contains(record.Key)),
FlushOnDispose = true,
StoreFlushOnDispose = true
}).ConfigureAwait(false);
Trace("open-private-cache");
try
{
var activated = cache.Get<AetheriaYmirPersistenceMarkerDocument>(
new CultRecordKey(AetheriaYmirPersistenceMarkerDocument.RecordKey)) != null;
var coordinator = new AetheriaYmirPersistenceCoordinator(cache, physics, activated);
if (frame?.Run == null || string.IsNullOrWhiteSpace(frame.Run.RunId))
return coordinator;
var zones = (frame.Run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
.Where(zone => zone != null)
.OrderBy(zone => zone.ZoneIndex)
.ToArray();
var resumes = zones
.Select(zone => cache.Get<AetheriaYmirResumeDocument>(
new CultRecordKey(ResumeRecordKey(frame.Run.RunId, zone.ZoneIndex, frame.FrameId))))
.ToArray();
Trace("resolve-resumes");
if (resumes.All(value => value == null) && !activated)
return coordinator;
if (resumes.Any(value => value == null))
throw new InvalidOperationException(
$"Aetheria frame {frame.FrameId} has incomplete daemon-private Ymir resume state.");
var journalRanges = zones.Zip(resumes, (zone, resume) =>
JournalRanges(frame.Run.RunId, zone.ZoneIndex, resume!))
.SelectMany(value => value)
.ToArray();
await cache.PullBackingStoreRecordsAsync(metadata =>
journalRanges.Any(range => range.Contains(metadata.Key))).ConfigureAwait(false);
Trace("hydrate-journals");
foreach (var pair in zones.Zip(resumes, (zone, resume) => (zone, resume: resume!)))
coordinator.RestoreZone(frame.Run.RunId, frame.FrameId, pair.zone.ZoneIndex, pair.resume);
Trace("restore-zones");
return coordinator;
}
catch
{
cache.Dispose();
throw;
}
}
public IReadOnlyList<AetheriaYmirZonePersistenceCapture> Capture(
AetheriaRuntimeDaemonFrameDocument frame)
{
ArgumentNullException.ThrowIfNull(frame);
var run = frame.Run ?? throw new InvalidOperationException("Ymir persistence requires an authoritative run.");
if (string.IsNullOrWhiteSpace(run.RunId))
throw new InvalidOperationException("Ymir persistence requires a stable run id.");
var zones = (run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
.Where(zone => zone != null)
.OrderBy(zone => zone.ZoneIndex)
.ToArray();
foreach (var zone in zones)
_physics.SynchronizePersistenceFrame(
run.RunId,
frame.FrameId,
zone,
zone.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>());
return zones
.Select(zone =>
{
var world = CursorFor(run.RunId, zone.ZoneIndex, WorldChannel);
var payload = CursorFor(run.RunId, zone.ZoneIndex, PayloadChannel);
return _physics.CapturePersistence(
run.RunId,
zone.ZoneIndex,
frame.FrameId,
world.Generation,
world.EntryCount,
payload.Generation,
payload.EntryCount);
})
.ToArray();
}
public async Task PersistPrivateAsync(IReadOnlyList<AetheriaYmirZonePersistenceCapture> captures)
{
ArgumentNullException.ThrowIfNull(captures);
foreach (var capture in captures)
{
await PutChunkAsync(capture, WorldChannel, capture.World.JournalChunk).ConfigureAwait(false);
if (capture.Payload != null)
await PutChunkAsync(capture, PayloadChannel, capture.Payload.JournalChunk).ConfigureAwait(false);
}
await _cache.FlushAsync(soft: false).ConfigureAwait(false);
foreach (var capture in captures)
{
var world = capture.World.ResumeDescriptor;
var payload = capture.Payload?.ResumeDescriptor;
var resume = new AetheriaYmirResumeDocument
{
RunId = capture.RunId,
ZoneIndex = capture.ZoneIndex,
FrameId = capture.FrameId,
SimulationStepIndex = capture.SimulationStepIndex,
WorldSessionGeneration = world.SessionGeneration,
WorldJournalEntryCount = world.JournalEntryCount,
WorldDescriptorPayload = YmirSessionCheckpointCodec.Encode(world),
PayloadSessionGeneration = payload?.SessionGeneration ?? "",
PayloadJournalEntryCount = payload?.JournalEntryCount ?? 0,
PayloadDescriptorPayload = payload == null ? [] : YmirSessionCheckpointCodec.Encode(payload)
};
await PutImmutableAsync(
new CultRecordKey(ResumeRecordKey(capture.RunId, capture.ZoneIndex, capture.FrameId)),
resume,
Equivalent).ConfigureAwait(false);
}
await _cache.FlushAsync(soft: false).ConfigureAwait(false);
foreach (var capture in captures)
{
Remember(capture.RunId, capture.ZoneIndex, WorldChannel, capture.World.ResumeDescriptor);
if (capture.Payload != null)
Remember(capture.RunId, capture.ZoneIndex, PayloadChannel, capture.Payload.ResumeDescriptor);
}
}
/// <summary>
/// Activates fail-closed restart only after the matching public frame has been hard-flushed.
/// </summary>
public async Task ActivateAsync()
{
if (_activated)
return;
await _cache.UpsertAsync(
new AetheriaYmirPersistenceMarkerDocument
{
ActivatedAtUtc = DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture)
},
new CultRecordHandle<AetheriaYmirPersistenceMarkerDocument>(
new CultRecordKey(AetheriaYmirPersistenceMarkerDocument.RecordKey))).ConfigureAwait(false);
await _cache.FlushAsync(soft: false).ConfigureAwait(false);
_activated = true;
}
public static string ResumeRecordKey(string runId, int zoneIndex, long frameId) =>
$"private:aetheria.ymir.resume.v1:{RunKey(runId)}:{zoneIndex}:{frameId:D20}";
public void Dispose() => _cache.Dispose();
private async Task PutChunkAsync(
AetheriaYmirZonePersistenceCapture capture,
string channel,
YmirSessionJournalChunk? chunk)
{
if (chunk == null)
return;
var document = new AetheriaYmirJournalChunkDocument
{
RunId = capture.RunId,
ZoneIndex = capture.ZoneIndex,
Channel = channel,
SessionGeneration = chunk.SessionGeneration,
FirstEntryIndex = chunk.FirstEntryIndex,
EntryCount = chunk.Entries.Count,
Payload = YmirSessionCheckpointCodec.Encode(chunk)
};
await PutImmutableAsync(
new CultRecordKey(JournalRecordKey(
capture.RunId,
capture.ZoneIndex,
channel,
chunk.SessionGeneration,
chunk.FirstEntryIndex)),
document,
Equivalent).ConfigureAwait(false);
}
private async Task PutImmutableAsync<T>(
CultRecordKey key,
T document,
Func<T, T, bool> equivalent)
where T : class
{
var existing = _cache.Get<T>(key);
if (existing != null)
{
if (!equivalent(existing, document))
throw new InvalidOperationException($"Immutable Ymir persistence record {key.Value} conflicts with durable state.");
return;
}
await _cache.UpsertAsync(document, new CultRecordHandle<T>(key)).ConfigureAwait(false);
}
private void RestoreZone(string runId, long frameId, int zoneIndex, AetheriaYmirResumeDocument resume)
{
if (!string.Equals(resume.RunId, runId, StringComparison.Ordinal) ||
resume.ZoneIndex != zoneIndex || resume.FrameId != frameId ||
resume.WorldDescriptorPayload.Length == 0)
throw new InvalidOperationException("Aetheria private Ymir resume identity does not match the durable frame.");
var world = YmirSessionCheckpointCodec.DecodeResumeDescriptor(resume.WorldDescriptorPayload);
var payload = resume.PayloadDescriptorPayload.Length == 0
? null
: YmirSessionCheckpointCodec.DecodeResumeDescriptor(resume.PayloadDescriptorPayload);
if (!string.Equals(world.SessionGeneration, resume.WorldSessionGeneration, StringComparison.Ordinal) ||
world.JournalEntryCount != resume.WorldJournalEntryCount ||
!string.Equals(payload?.SessionGeneration ?? "", resume.PayloadSessionGeneration, StringComparison.Ordinal) ||
(payload?.JournalEntryCount ?? 0) != resume.PayloadJournalEntryCount)
throw new InvalidOperationException("Aetheria private Ymir resume metadata disagrees with its checksummed payload.");
var worldChunks = Chunks(runId, zoneIndex, WorldChannel, world.SessionGeneration, world.JournalEntryCount);
var payloadChunks = payload == null
? Array.Empty<YmirSessionJournalChunk>()
: Chunks(runId, zoneIndex, PayloadChannel, payload.SessionGeneration, payload.JournalEntryCount);
_physics.RestorePersistence(
runId, zoneIndex, frameId, resume.SimulationStepIndex,
world, worldChunks, payload, payloadChunks);
Remember(runId, zoneIndex, WorldChannel, world);
if (payload != null)
Remember(runId, zoneIndex, PayloadChannel, payload);
}
private YmirSessionJournalChunk[] Chunks(
string runId,
int zoneIndex,
string channel,
string generation,
long entryCount) =>
_cache.GetAll<AetheriaYmirJournalChunkDocument>()
.Where(value => value != null &&
string.Equals(value.RunId, runId, StringComparison.Ordinal) &&
value.ZoneIndex == zoneIndex &&
string.Equals(value.Channel, channel, StringComparison.Ordinal) &&
string.Equals(value.SessionGeneration, generation, StringComparison.Ordinal) &&
value.FirstEntryIndex < entryCount)
.OrderBy(value => value.FirstEntryIndex)
.Select(value =>
{
var chunk = YmirSessionCheckpointCodec.DecodeJournalChunk(value.Payload);
if (chunk.FirstEntryIndex != value.FirstEntryIndex || chunk.Entries.Count != value.EntryCount)
throw new InvalidOperationException("Aetheria Ymir journal metadata disagrees with its checksummed payload.");
return chunk;
})
.ToArray();
private Cursor CursorFor(string runId, int zoneIndex, string channel) =>
_cursors.TryGetValue(new CursorKey(runId, zoneIndex, channel), out var cursor)
? cursor
: new Cursor("", 0);
private void Remember(
string runId,
int zoneIndex,
string channel,
YmirSessionResumeDescriptor descriptor) =>
_cursors[new CursorKey(runId, zoneIndex, channel)] =
new Cursor(descriptor.SessionGeneration, descriptor.JournalEntryCount);
private static bool Equivalent(AetheriaYmirJournalChunkDocument left, AetheriaYmirJournalChunkDocument right) =>
left.RunId == right.RunId && left.ZoneIndex == right.ZoneIndex && left.Channel == right.Channel &&
left.SessionGeneration == right.SessionGeneration && left.FirstEntryIndex == right.FirstEntryIndex &&
left.EntryCount == right.EntryCount && left.Payload.AsSpan().SequenceEqual(right.Payload);
private static bool Equivalent(AetheriaYmirResumeDocument left, AetheriaYmirResumeDocument right) =>
left.RunId == right.RunId && left.ZoneIndex == right.ZoneIndex && left.FrameId == right.FrameId &&
left.SimulationStepIndex == right.SimulationStepIndex &&
left.WorldSessionGeneration == right.WorldSessionGeneration &&
left.WorldJournalEntryCount == right.WorldJournalEntryCount &&
left.WorldDescriptorPayload.AsSpan().SequenceEqual(right.WorldDescriptorPayload) &&
left.PayloadSessionGeneration == right.PayloadSessionGeneration &&
left.PayloadJournalEntryCount == right.PayloadJournalEntryCount &&
left.PayloadDescriptorPayload.AsSpan().SequenceEqual(right.PayloadDescriptorPayload);
private static string JournalRecordKey(
string runId,
int zoneIndex,
string channel,
string generation,
long firstEntryIndex) =>
$"private:aetheria.ymir.journal.v1:{RunKey(runId)}:{zoneIndex}:{channel}:{generation}:{firstEntryIndex:D20}";
private static JournalRange[] JournalRanges(
string runId,
int zoneIndex,
AetheriaYmirResumeDocument resume)
{
var world = YmirSessionCheckpointCodec.DecodeResumeDescriptor(resume.WorldDescriptorPayload);
var ranges = new List<JournalRange>
{
new(JournalRecordPrefix(runId, zoneIndex, WorldChannel, world.SessionGeneration), world.JournalEntryCount)
};
if (resume.PayloadDescriptorPayload.Length > 0)
{
var payload = YmirSessionCheckpointCodec.DecodeResumeDescriptor(resume.PayloadDescriptorPayload);
ranges.Add(new JournalRange(
JournalRecordPrefix(runId, zoneIndex, PayloadChannel, payload.SessionGeneration),
payload.JournalEntryCount));
}
return ranges.ToArray();
}
private static string JournalRecordPrefix(
string runId,
int zoneIndex,
string channel,
string generation) =>
$"private:aetheria.ymir.journal.v1:{RunKey(runId)}:{zoneIndex}:{channel}:{generation}:";
private static string RunKey(string runId) =>
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(runId))).ToLowerInvariant();
private readonly record struct CursorKey(string RunId, int ZoneIndex, string Channel);
private readonly record struct Cursor(string Generation, long EntryCount);
private readonly record struct JournalRange(string Prefix, long EntryCount)
{
public bool Contains(string key) =>
key.StartsWith(Prefix, StringComparison.Ordinal) &&
long.TryParse(key.AsSpan(Prefix.Length), NumberStyles.None, CultureInfo.InvariantCulture, out var firstEntryIndex) &&
firstEntryIndex < EntryCount;
}
}

View file

@ -0,0 +1,43 @@
using GameCult.Caching;
using MessagePack;
namespace Aetheria.State.Daemon;
[CultDocument("gamecult.aetheria.ymir_persistence_marker", "gamecult.aetheria.ymir_persistence_marker.v1")]
[MessagePackObject]
public sealed class AetheriaYmirPersistenceMarkerDocument
{
public const string RecordKey = "private:aetheria.ymir.persistence.v1";
[Key(0)] public string FormatId { get; set; } = "gamecult.aetheria.ymir.persistence.v1";
[Key(1)] public string ActivatedAtUtc { get; set; } = "";
}
[CultDocument("gamecult.aetheria.ymir_journal_chunk", "gamecult.aetheria.ymir_journal_chunk.v1")]
[MessagePackObject]
public sealed class AetheriaYmirJournalChunkDocument
{
[Key(0)] public string RunId { get; set; } = "";
[Key(1)] public int ZoneIndex { get; set; }
[Key(2)] public string Channel { get; set; } = "";
[Key(3)] public string SessionGeneration { get; set; } = "";
[Key(4)] public long FirstEntryIndex { get; set; }
[Key(5)] public int EntryCount { get; set; }
[Key(6)] public byte[] Payload { get; set; } = [];
}
[CultDocument("gamecult.aetheria.ymir_resume", "gamecult.aetheria.ymir_resume.v1")]
[MessagePackObject]
public sealed class AetheriaYmirResumeDocument
{
[Key(0)] public string RunId { get; set; } = "";
[Key(1)] public int ZoneIndex { get; set; }
[Key(2)] public long FrameId { get; set; }
[Key(3)] public int SimulationStepIndex { get; set; }
[Key(4)] public string WorldSessionGeneration { get; set; } = "";
[Key(5)] public long WorldJournalEntryCount { get; set; }
[Key(6)] public byte[] WorldDescriptorPayload { get; set; } = [];
[Key(7)] public string PayloadSessionGeneration { get; set; } = "";
[Key(8)] public long PayloadJournalEntryCount { get; set; }
[Key(9)] public byte[] PayloadDescriptorPayload { get; set; } = [];
}

View file

@ -0,0 +1,630 @@
using GameCult.Aetheria.State.Verse;
using Ymir.Core;
namespace Aetheria.State.Daemon;
public sealed class AetheriaYmirWorldPhysics : IAetheriaRuntimeWorldPhysics, IDisposable
{
private const long PersistenceRolloverJournalEntries = 64;
private const string EntityPrefix = "aetheria.daemon.entity.";
private const string PickupPrefix = "aetheria.daemon.pickup.";
private const string PhysicalPayloadPrefix = "aetheria.physical-payload.";
private const ulong WorldEntityCollisionCategory = 1UL;
private const ulong ShipCollisionCategory = 2UL;
private const ulong PickupCollisionCategory = 4UL;
private readonly Dictionary<WorldKey, SessionState> _sessions = new();
public string ImplementationId => "ymir.box3d.retained-session.v1";
public void SynchronizePersistenceFrame(
string runId,
long frameId,
AetheriaRuntimeZoneSnapshotCommit zone,
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities)
{
ArgumentNullException.ThrowIfNull(zone);
if (string.IsNullOrWhiteSpace(runId))
throw new ArgumentException("A retained Ymir world requires a run id.", nameof(runId));
var key = new WorldKey(runId, zone.ZoneIndex);
if (_sessions.TryGetValue(key, out var existing) && existing.LastFrameId == frameId)
return;
if (existing != null && frameId < existing.LastFrameId)
throw new InvalidOperationException(
$"Ymir session '{existing.WorldSession.Info.SessionId}' cannot synchronize backward from " +
$"frame {existing.LastFrameId} to {frameId}.");
var desiredBodies = BuildBodies(ActiveEntities(entities), ActivePickups(zone));
var state = existing ?? CreateSession(key, zone, desiredBodies);
if (existing == null)
_sessions.Add(key, state);
else
{
var commandOrdinal = 0;
Reconcile(
state.WorldSession,
desiredBodies,
frameId,
state.LastSimulationStepIndex,
ref commandOrdinal);
}
state.LastFrameId = frameId;
state.LastResult = null;
if (state.PayloadSession != null)
{
state.LastPayloadFrameId = frameId;
state.LastPayloadSimulationStepIndex = state.LastSimulationStepIndex;
state.LastPayloadResult = null;
}
}
public AetheriaYmirZonePersistenceCapture CapturePersistence(
string runId,
int zoneIndex,
long frameId,
string persistedWorldSessionGeneration,
long persistedWorldJournalEntryCount,
string persistedPayloadSessionGeneration,
long persistedPayloadJournalEntryCount)
{
if (!_sessions.TryGetValue(new WorldKey(runId, zoneIndex), out var state) ||
state.LastFrameId != frameId)
throw new InvalidOperationException(
$"Cannot persist Ymir world for run '{runId}' zone {zoneIndex} at frame {frameId}.");
if (state.PayloadSession != null &&
(state.LastPayloadFrameId != frameId || state.LastPayloadSimulationStepIndex != state.LastSimulationStepIndex))
throw new InvalidOperationException(
$"Cannot persist Ymir payload world for run '{runId}' zone {zoneIndex} at frame {frameId}.");
TryRollPersistenceGeneration(state, payload: false);
if (state.PayloadSession != null)
TryRollPersistenceGeneration(state, payload: true);
return new AetheriaYmirZonePersistenceCapture(
runId,
zoneIndex,
frameId,
state.LastSimulationStepIndex,
state.WorldSession.CapturePersistence(
string.Equals(persistedWorldSessionGeneration, state.WorldSession.Info.SessionGeneration, StringComparison.Ordinal)
? persistedWorldJournalEntryCount
: 0),
state.PayloadSession?.CapturePersistence(
string.Equals(persistedPayloadSessionGeneration, state.PayloadSession.Info.SessionGeneration, StringComparison.Ordinal)
? persistedPayloadJournalEntryCount
: 0));
}
public void RestorePersistence(
string runId,
int zoneIndex,
long frameId,
int simulationStepIndex,
YmirSessionResumeDescriptor worldDescriptor,
IReadOnlyList<YmirSessionJournalChunk> worldChunks,
YmirSessionResumeDescriptor? payloadDescriptor,
IReadOnlyList<YmirSessionJournalChunk> payloadChunks)
{
var key = new WorldKey(runId, zoneIndex);
if (_sessions.ContainsKey(key))
throw new InvalidOperationException($"Ymir world for run '{runId}' zone {zoneIndex} is already live.");
var sessionId = $"aetheria.run.{runId}.zone.{zoneIndex}";
if (!string.Equals(worldDescriptor.SessionId, sessionId + ".world", StringComparison.Ordinal) ||
(payloadDescriptor != null &&
!string.Equals(payloadDescriptor.SessionId, sessionId + ".payloads", StringComparison.Ordinal)))
throw new InvalidOperationException("Aetheria Ymir resume belongs to another run or zone.");
if (payloadDescriptor == null && payloadChunks.Count > 0)
throw new InvalidOperationException("Aetheria Ymir resume has payload journal chunks without a descriptor.");
var world = YmirSession.Restore(worldDescriptor, worldChunks);
YmirSession? payload = null;
try
{
payload = payloadDescriptor == null ? null : YmirSession.Restore(payloadDescriptor, payloadChunks);
_sessions.Add(key, new SessionState(world)
{
PayloadSession = payload,
LastFrameId = frameId,
LastSimulationStepIndex = simulationStepIndex,
LastPayloadFrameId = payload == null ? -1 : frameId,
LastPayloadSimulationStepIndex = payload == null ? -1 : simulationStepIndex
});
}
catch
{
payload?.Dispose();
world.Dispose();
throw;
}
}
public void RetainWorlds(string runId, IReadOnlyList<int> zoneIndices)
{
var retainedZones = (zoneIndices ?? Array.Empty<int>()).ToHashSet();
foreach (var key in _sessions.Keys
.Where(key => !string.Equals(key.RunId, runId, StringComparison.Ordinal) ||
!retainedZones.Contains(key.ZoneIndex))
.ToArray())
{
_sessions[key].WorldSession.Dispose();
_sessions[key].PayloadSession?.Dispose();
_sessions.Remove(key);
}
}
public AetheriaRuntimeWorldStep Step(
string runId,
long frameId,
int simulationStepIndex,
AetheriaRuntimeZoneSnapshotCommit zone,
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities,
double deltaSeconds)
{
ArgumentNullException.ThrowIfNull(zone);
if (string.IsNullOrWhiteSpace(runId))
throw new ArgumentException("A retained Ymir world requires a run id.", nameof(runId));
if (deltaSeconds <= 0)
throw new ArgumentOutOfRangeException(nameof(deltaSeconds));
var key = new WorldKey(runId, zone.ZoneIndex);
if (_sessions.TryGetValue(key, out var existing) &&
existing.LastFrameId == frameId && existing.LastSimulationStepIndex == simulationStepIndex &&
existing.LastResult != null)
return existing.LastResult;
if (existing != null &&
(frameId < existing.LastFrameId ||
(frameId == existing.LastFrameId && simulationStepIndex < existing.LastSimulationStepIndex)))
throw new InvalidOperationException(
$"Ymir session '{existing.WorldSession.Info.SessionId}' cannot step backward from " +
$"frame/step {existing.LastFrameId}/{existing.LastSimulationStepIndex} to {frameId}/{simulationStepIndex}.");
var active = ActiveEntities(entities);
var pickups = ActivePickups(zone);
var desiredBodies = BuildBodies(active, pickups);
var state = existing ?? CreateSession(key, zone, desiredBodies);
if (existing == null)
_sessions.Add(key, state);
var commandOrdinal = 0;
Reconcile(state.WorldSession, desiredBodies, frameId, simulationStepIndex, ref commandOrdinal);
ApplyTractorForces(state.WorldSession, active, pickups, frameId, simulationStepIndex, ref commandOrdinal);
var step = state.WorldSession.Step(new YmirStepSessionCommand(
Header(state.WorldSession, frameId, simulationStepIndex, "step", "world", ref commandOrdinal),
(float)deltaSeconds,
GravityFields(zone)));
var result = Lower(step, active);
state.LastFrameId = frameId;
state.LastSimulationStepIndex = simulationStepIndex;
state.LastResult = result;
return result;
}
public AetheriaRuntimePhysicalPayloadStep StepPhysicalPayloads(
string runId,
long frameId,
int simulationStepIndex,
AetheriaRuntimeZoneSnapshotCommit zone,
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities,
double deltaSeconds)
{
ArgumentNullException.ThrowIfNull(zone);
if (!_sessions.TryGetValue(new WorldKey(runId, zone.ZoneIndex), out var state) ||
state.LastFrameId != frameId || state.LastSimulationStepIndex != simulationStepIndex)
throw new InvalidOperationException(
$"Payload phase requires the retained world phase for run '{runId}' zone {zone.ZoneIndex} " +
$"at frame/step {frameId}/{simulationStepIndex}.");
if (state.LastPayloadFrameId == frameId && state.LastPayloadSimulationStepIndex == simulationStepIndex &&
state.LastPayloadResult != null)
return state.LastPayloadResult;
if (deltaSeconds <= 0)
throw new ArgumentOutOfRangeException(nameof(deltaSeconds));
var payloadSession = state.PayloadSession ??= YmirSession.Create(new YmirSessionCreateRequest(
$"aetheria.run.{runId}.zone.{zone.ZoneIndex}.payloads",
[],
(float)zone.SimulationTimeSeconds));
var payloads = (zone.PhysicalPayloads ?? Array.Empty<AetheriaRuntimePhysicalPayloadCommit>())
.Where(payload => payload != null && payload.Active)
.OrderBy(payload => payload.PayloadId, StringComparer.Ordinal)
.ToArray();
var desiredBodies = payloads.Select(PayloadBody).ToArray();
var commandOrdinal = 0;
Reconcile(payloadSession, desiredBodies, frameId, simulationStepIndex, ref commandOrdinal);
var starts = payloadSession.Snapshot().Bodies.ToDictionary(body => body.Id, StringComparer.Ordinal);
var stepped = payloadSession.Step(new YmirStepSessionCommand(
Header(payloadSession, frameId, simulationStepIndex, "step", "payloads", ref commandOrdinal),
(float)deltaSeconds,
[]));
var moved = stepped.World.Bodies.ToDictionary(body => body.Id, StringComparer.Ordinal);
var activeEntities = ActiveEntities(entities);
var entitiesByBodyId = activeEntities.ToDictionary(BodyId, StringComparer.Ordinal);
var survivors = new List<AetheriaRuntimePhysicalPayloadCommit>();
var hits = new List<AetheriaRuntimePhysicalPayloadHit>();
foreach (var payload in payloads)
{
var payloadBodyId = PhysicalPayloadPrefix + payload.PayloadId;
var start = starts[payloadBodyId];
var body = moved[payloadBodyId];
payload.PositionX = body.Position.X;
payload.PositionZ = body.Position.Y;
payload.VelocityX = body.Velocity.X;
payload.VelocityY = body.Velocity.Y;
var candidates = activeEntities
.Where(entity => entity.EntityIndex != payload.SourceEntityIndex)
.Select(BodyId)
.OrderBy(id => id, StringComparer.Ordinal)
.ToArray();
var dx = body.Position.X - start.Position.X;
var dz = body.Position.Y - start.Position.Y;
var distance = MathF.Sqrt(dx * dx + dz * dz);
var queryId = $"aetheria:frame:{frameId}:step:{simulationStepIndex}:payload:{payload.PayloadId}";
CircleCastHit? castHit = null;
CircleOverlapHit? overlapHit = null;
if (distance > 1.0e-6f)
{
var cast = state.WorldSession.CastCircle(new YmirSessionCircleCastQuery(
queryId + ":cast",
state.WorldSession.Info.Revision,
start.Position,
new Vec2(dx, dz),
distance,
(float)payload.Radius,
candidates));
RequireQuery(cast.Error, cast.QueryId);
castHit = cast.Hits.FirstOrDefault();
queryId = cast.QueryId;
}
else
{
var overlap = state.WorldSession.OverlapCircle(new YmirSessionCircleOverlapQuery(
queryId + ":overlap",
state.WorldSession.Info.Revision,
body.Position,
(float)payload.Radius,
candidates));
RequireQuery(overlap.Error, overlap.QueryId);
overlapHit = overlap.Hits.FirstOrDefault();
queryId = overlap.QueryId;
}
var targetBodyId = castHit?.BodyId ?? overlapHit?.BodyId;
if (targetBodyId == null || !entitiesByBodyId.TryGetValue(targetBodyId, out var target))
{
survivors.Add(payload);
continue;
}
var point = castHit?.Point ?? overlapHit!.Point;
var normal = castHit?.Normal ?? overlapHit!.Normal;
hits.Add(new AetheriaRuntimePhysicalPayloadHit
{
Payload = payload,
QueryId = queryId,
PhysicalPayloadBodyId = payloadBodyId,
TargetEntityIndex = target.EntityIndex,
TargetBodyId = targetBodyId,
PointX = point.X,
PointZ = point.Y,
NormalX = normal.X,
NormalZ = normal.Y
});
if (string.Equals(payload.PayloadKind, "mine", StringComparison.Ordinal))
{
payload.Stationary = true;
payload.VelocityX = 0;
payload.VelocityY = 0;
if (!body.IsStatic)
RequireAccepted(payloadSession.SetVelocity(new YmirSetBodyVelocityCommand(
Header(payloadSession, frameId, simulationStepIndex, "stop", payload.PayloadId, ref commandOrdinal),
payloadBodyId,
Vec2.Zero,
0)));
survivors.Add(payload);
}
else
{
RequireAccepted(payloadSession.Remove(new YmirRemoveBodyCommand(
Header(payloadSession, frameId, simulationStepIndex, "remove-hit", payload.PayloadId, ref commandOrdinal),
payloadBodyId)));
}
}
var result = new AetheriaRuntimePhysicalPayloadStep(survivors, hits);
state.LastPayloadFrameId = frameId;
state.LastPayloadSimulationStepIndex = simulationStepIndex;
state.LastPayloadResult = result;
return result;
}
public void Dispose()
{
foreach (var state in _sessions.Values)
{
state.WorldSession.Dispose();
state.PayloadSession?.Dispose();
}
_sessions.Clear();
}
private static SessionState CreateSession(
WorldKey key,
AetheriaRuntimeZoneSnapshotCommit zone,
IReadOnlyList<PhysicsBody> bodies)
{
var sessionId = $"aetheria.run.{key.RunId}.zone.{key.ZoneIndex}";
return new SessionState(YmirSession.Create(new YmirSessionCreateRequest(
sessionId + ".world",
bodies,
(float)zone.SimulationTimeSeconds)));
}
private static void Reconcile(
YmirSession session,
IReadOnlyList<PhysicsBody> desiredBodies,
long frameId,
int simulationStepIndex,
ref int commandOrdinal)
{
var desired = desiredBodies.ToDictionary(body => body.Id, StringComparer.Ordinal);
var current = session.Snapshot().Bodies.ToDictionary(body => body.Id, StringComparer.Ordinal);
foreach (var body in current.Values.Where(body => !desired.ContainsKey(body.Id)).OrderBy(body => body.Id, StringComparer.Ordinal))
RequireAccepted(session.Remove(new YmirRemoveBodyCommand(
Header(session, frameId, simulationStepIndex, "remove", body.Id, ref commandOrdinal), body.Id)));
foreach (var body in desired.Values.OrderBy(body => body.Id, StringComparer.Ordinal))
{
if (!current.TryGetValue(body.Id, out var present))
{
RequireAccepted(session.Spawn(new YmirSpawnBodyCommand(
Header(session, frameId, simulationStepIndex, "spawn", body.Id, ref commandOrdinal), body)));
continue;
}
if (present.IsBullet != body.IsBullet ||
present.ParticipatesInFields != body.ParticipatesInFields ||
present.CollisionCategoryBits != body.CollisionCategoryBits ||
present.CollisionMaskBits != body.CollisionMaskBits ||
present.CollisionGroupIndex != body.CollisionGroupIndex)
{
RequireAccepted(session.Remove(new YmirRemoveBodyCommand(
Header(session, frameId, simulationStepIndex, "replace-profile", body.Id, ref commandOrdinal), body.Id)));
RequireAccepted(session.Spawn(new YmirSpawnBodyCommand(
Header(session, frameId, simulationStepIndex, "respawn-profile", body.Id, ref commandOrdinal), body)));
continue;
}
if (present.Radius != body.Radius || present.Mass != body.Mass ||
present.IsStatic != body.IsStatic || present.IsKinematic != body.IsKinematic ||
present.Restitution != body.Restitution)
RequireAccepted(session.Configure(new YmirConfigureBodyCommand(
Header(session, frameId, simulationStepIndex, "configure", body.Id, ref commandOrdinal),
body.Id, body.Radius, body.Mass, body.IsStatic, body.Restitution, body.IsKinematic)));
if (present.Position != body.Position || present.Direction != body.Direction)
RequireAccepted(session.Teleport(new YmirTeleportBodyCommand(
Header(session, frameId, simulationStepIndex, "pose", body.Id, ref commandOrdinal),
body.Id, body.Position, body.Direction ?? new Vec2(0, 1))));
if (!body.IsStatic && (present.Velocity != body.Velocity || present.AngularVelocity != body.AngularVelocity))
RequireAccepted(session.SetVelocity(new YmirSetBodyVelocityCommand(
Header(session, frameId, simulationStepIndex, "velocity", body.Id, ref commandOrdinal),
body.Id, body.Velocity, body.AngularVelocity)));
}
}
private static void ApplyTractorForces(
YmirSession session,
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> active,
IReadOnlyList<AetheriaRuntimeDroppedPickupCommit> pickups,
long frameId,
int simulationStepIndex,
ref int commandOrdinal)
{
foreach (var actor in active.Where(entity =>
entity.TractorPower > AetheriaRuntimeTractorMechanics.ActivationThreshold))
{
var forward = Normalize(actor.LookDirectionX, actor.LookDirectionY, 0, 1);
foreach (var pickup in pickups)
{
var dx = pickup.PositionX - actor.PositionX;
var dz = pickup.PositionZ - actor.PositionZ;
var along = dx * forward.X + dz * forward.Y;
var lateral = Math.Abs(dx * forward.Y - dz * forward.X);
var distance = Math.Sqrt(dx * dx + dz * dz);
if (along < 0 || along > AetheriaRuntimeTractorMechanics.Distance ||
lateral > AetheriaRuntimeTractorMechanics.Radius || distance <= 0.001)
continue;
var force = AetheriaRuntimeTractorMechanics.Traction;
var pickupId = PickupPrefix + pickup.PickupIndex;
RequireAccepted(session.ApplyForce(new YmirApplyForceCommand(
Header(session, frameId, simulationStepIndex, "tractor", $"{actor.EntityIndex}:{pickup.PickupIndex}", ref commandOrdinal),
pickupId,
new Vec2((float)(-dx / distance * force), (float)(-dz / distance * force)))));
}
}
}
private static AetheriaRuntimeWorldStep Lower(
YmirSessionStepResult step,
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> active)
{
var bodies = step.World.Bodies;
var entitiesByBodyId = active.ToDictionary(BodyId, StringComparer.Ordinal);
return new AetheriaRuntimeWorldStep(
bodies.Where(body => body.Id.StartsWith(EntityPrefix, StringComparison.Ordinal))
.Select(body => new AetheriaRuntimeWorldBodyStep
{
EntityIndex = entitiesByBodyId[body.Id].EntityIndex,
PositionX = body.Position.X,
PositionZ = body.Position.Y,
VelocityX = body.Velocity.X,
VelocityY = body.Velocity.Y,
DirectionX = body.Direction?.X ?? 0,
DirectionY = body.Direction?.Y ?? 1
}).ToArray(),
bodies.Where(body => body.Id.StartsWith(PickupPrefix, StringComparison.Ordinal))
.Select(body => new AetheriaRuntimeWorldPickupStep
{
PickupIndex = ParseIndex(body.Id, PickupPrefix),
PositionX = body.Position.X,
PositionZ = body.Position.Y,
VelocityX = body.Velocity.X,
VelocityZ = body.Velocity.Y
})
.ToArray());
}
private static IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> ActiveEntities(
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities)
{
var source = entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>();
var attached = source.SelectMany(entity => entity.ChildEntityIndices ?? Array.Empty<int>()).ToHashSet();
return source.Where(entity => entity.IsActive && entity.WormholeTransition == null &&
!attached.Contains(entity.EntityIndex)).ToArray();
}
private static IReadOnlyList<AetheriaRuntimeDroppedPickupCommit> ActivePickups(
AetheriaRuntimeZoneSnapshotCommit zone) =>
(zone.DroppedPickups ?? Array.Empty<AetheriaRuntimeDroppedPickupCommit>())
.Where(pickup => pickup != null && pickup.AgeSeconds < pickup.LifetimeSeconds)
.ToArray();
private static IReadOnlyList<PhysicsBody> BuildBodies(
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> active,
IReadOnlyList<AetheriaRuntimeDroppedPickupCommit> pickups) =>
active.Select(entity =>
{
if (string.IsNullOrWhiteSpace(entity.EntityId))
throw new InvalidOperationException(
$"Entity index {entity.EntityIndex} has no stable runtime identity for retained Ymir ownership.");
var isStatic = string.Equals(entity.Kind, "station", StringComparison.OrdinalIgnoreCase);
var isShip = string.Equals(entity.Kind, "ship", StringComparison.OrdinalIgnoreCase);
var direction = Normalize(entity.DirectionX, entity.DirectionY, 0, 1);
return new PhysicsBody(
BodyId(entity),
new Vec2((float)entity.PositionX, (float)entity.PositionZ),
isStatic ? new Vec2(0, 0) : new Vec2((float)entity.VelocityX, (float)entity.VelocityY),
isStatic ? 48 : 20,
1,
IsStatic: isStatic,
Restitution: 0.2f,
Direction: new Vec2((float)direction.X, (float)direction.Y),
CollisionCategoryBits: isShip ? ShipCollisionCategory : WorldEntityCollisionCategory,
CollisionMaskBits: isShip ? ulong.MaxValue : ulong.MaxValue & ~PickupCollisionCategory,
// The fossil moves entities directly and has no rigidbody collision response
// between ships/stations. Keep their Ymir bodies for fields and queries, but
// do not let invented circle radii hold a ship outside the 25-unit dock rule.
// Pickups retain group zero so Ymir can resolve their physical motion against ships.
// The daemon independently owns collection through post-step XZ proximity.
CollisionGroupIndex: -1);
}).Concat(pickups.Select(pickup => new PhysicsBody(
PickupPrefix + pickup.PickupIndex,
new Vec2((float)pickup.PositionX, (float)pickup.PositionZ),
new Vec2((float)pickup.VelocityX, (float)pickup.VelocityZ),
5,
1,
Restitution: 0.2f,
CollisionCategoryBits: PickupCollisionCategory,
CollisionMaskBits: ShipCollisionCategory)))
.ToArray();
private static string BodyId(AetheriaRuntimeEntitySnapshotCommit entity) => EntityPrefix + entity.EntityId;
private static PhysicsBody PayloadBody(AetheriaRuntimePhysicalPayloadCommit payload) => new(
PhysicalPayloadPrefix + payload.PayloadId,
new Vec2((float)payload.PositionX, (float)payload.PositionZ),
payload.Stationary ? Vec2.Zero : new Vec2((float)payload.VelocityX, (float)payload.VelocityY),
(float)payload.Radius,
1,
IsStatic: payload.Stationary,
Restitution: 0,
Direction: new Vec2(0, 1),
IsBullet: !payload.Stationary,
ParticipatesInFields: false,
CollisionCategoryBits: 2,
CollisionMaskBits: 0);
private static IReadOnlyList<RadialField> GravityFields(AetheriaRuntimeZoneSnapshotCommit zone) =>
AetheriaRuntimeDaemonRenderQueries.QueryBodyPoses(zone)
.Select(pose => (Pose: pose, Body: (zone.Bodies ?? Array.Empty<AetheriaRuntimeBodySnapshotCommit>())
.FirstOrDefault(body => body != null && body.BodyKey == pose.BodyKey)))
.Where(pair => pair.Body != null && pair.Body.GravityInfluenceRadius > 0 && pair.Body.GravityWellDepth != 0)
.Select(pair => new RadialField(
"aetheria.daemon.gravity." + pair.Pose.BodyKey,
new Vec2((float)pair.Pose.CenterX, (float)pair.Pose.CenterZ),
(float)pair.Body!.GravityWellDepth,
(float)pair.Body.GravityInfluenceRadius))
.ToArray();
private static YmirCommandHeader Header(
YmirSession session,
long frameId,
int simulationStepIndex,
string kind,
string subject,
ref int ordinal) =>
new($"aetheria:frame:{frameId}:step:{simulationStepIndex}:{ordinal++}:{kind}:{subject}", session.Info.Revision);
private static void RequireAccepted(YmirCommandReceipt receipt)
{
if (receipt.Outcome != YmirCommandOutcome.Accepted)
throw new InvalidOperationException(
$"Ymir rejected Aetheria command '{receipt.CommandId}' with {receipt.Error}.");
}
private static void RequireQuery(YmirQueryError error, string queryId)
{
if (error != YmirQueryError.None)
throw new InvalidOperationException($"Ymir rejected Aetheria query '{queryId}' with {error}.");
}
private static int ParseIndex(string id, string prefix) =>
id.StartsWith(prefix, StringComparison.Ordinal) &&
int.TryParse(id.AsSpan(prefix.Length), out var value)
? value
: -1;
private static void TryRollPersistenceGeneration(SessionState state, bool payload)
{
var current = payload ? state.PayloadSession : state.WorldSession;
if (current == null || current.PersistenceJournalEntryCount < PersistenceRolloverJournalEntries ||
!current.TryReplaceWithCompactedPersistenceBaseline(out var replacement))
return;
if (payload)
state.PayloadSession = replacement!;
else
state.WorldSession = replacement!;
}
private static (double X, double Y) Normalize(
double x,
double y,
double fallbackX,
double fallbackY)
{
var length = Math.Sqrt(x * x + y * y);
return length <= 0.001 ? (fallbackX, fallbackY) : (x / length, y / length);
}
private sealed class SessionState(YmirSession worldSession)
{
public YmirSession WorldSession { get; set; } = worldSession;
public YmirSession? PayloadSession { get; set; }
public long LastFrameId { get; set; } = -1;
public int LastSimulationStepIndex { get; set; } = -1;
public AetheriaRuntimeWorldStep? LastResult { get; set; }
public long LastPayloadFrameId { get; set; } = -1;
public int LastPayloadSimulationStepIndex { get; set; } = -1;
public AetheriaRuntimePhysicalPayloadStep? LastPayloadResult { get; set; }
}
private readonly record struct WorldKey(string RunId, int ZoneIndex);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Aetheria.State.Daemon.Smoke")]

View file

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Aetheria.State\Aetheria.State.csproj" />
<ProjectReference Include="..\..\CultMath\src\CultMath\CultMath.csproj" />
<Compile Include="..\Assets\Scripts\ServerShared\YmirPhysicsContracts.cs" Link="YmirPhysicsContracts.cs" />
</ItemGroup>
</Project>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\Aetheria.State\Aetheria.State.Dependencies.props" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Aetheria.State\Aetheria.State.csproj" />
<ProjectReference Include="..\..\EvePlugins\plugins\eve-plugin-fields\GameCult.Eve.PluginFields.csproj" />
</ItemGroup>
<Import Project="..\Aetheria.State\Aetheria.State.Dependencies.targets" />
</Project>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<Description>Replica sync worker for pulling daemon-owned Aetheria Verse state into a local cache-only .cc witness.</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Aetheria.State\Aetheria.State.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,151 @@
using Aetheria.State;
return await ProgramMainAsync(args).ConfigureAwait(false);
static async Task<int> ProgramMainAsync(string[] args)
{
if (args.Length == 0)
{
PrintUsage();
return 1;
}
var mode = args[0].Trim();
var options = ParseOptions(args.Skip(1));
var endpoint = RequireOption(options, "endpoint");
var runtimeId = ReadOption(options, "runtime-id", "aetheria-verse-replica");
var replicaPath = ResolveReplicaPath(options);
SeedBaselineReplica(options, replicaPath);
switch (mode)
{
case "sync":
{
var appliedSequence = await AetheriaVerseReplica
.SyncSnapshotAsync(replicaPath, endpoint, runtimeId)
.ConfigureAwait(false);
Console.WriteLine($"Synced Aetheria replica to {replicaPath} from {endpoint} at shard sequence {appliedSequence}.");
return 0;
}
case "follow":
{
var pollSeconds = ReadDoubleOption(options, "poll-seconds", 1d);
using var cancellation = new CancellationTokenSource();
Console.CancelKeyPress += (_, eventArgs) =>
{
eventArgs.Cancel = true;
cancellation.Cancel();
};
Console.WriteLine(
$"Following Aetheria replica at {replicaPath} from {endpoint} every {pollSeconds:0.###} seconds. Press Ctrl+C to stop.");
await AetheriaVerseReplica
.RunReplicaAsync(replicaPath, endpoint, TimeSpan.FromSeconds(pollSeconds), cancellation.Token, runtimeId)
.ConfigureAwait(false);
return 0;
}
default:
Console.Error.WriteLine($"Unknown mode '{mode}'.");
PrintUsage();
return 1;
}
}
static string ResolveReplicaPath(IReadOnlyDictionary<string, string> options)
{
var explicitPath = ReadOption(options, "replica", "");
if (!string.IsNullOrWhiteSpace(explicitPath))
return Path.GetFullPath(explicitPath);
var gameDataRoot = RequireOption(options, "game-data-root");
var verseId = RequireOption(options, "verse-id");
return Path.Combine(
Path.GetFullPath(gameDataRoot),
"Verses",
$"{SanitizeVerseId(verseId)}.cc");
}
static void SeedBaselineReplica(IReadOnlyDictionary<string, string> options, string replicaPath)
{
var baselinePath = ReadOption(options, "baseline-state", "");
if (string.IsNullOrWhiteSpace(baselinePath) ||
File.Exists(replicaPath) ||
!File.Exists(baselinePath))
{
return;
}
Directory.CreateDirectory(Path.GetDirectoryName(replicaPath) ?? ".");
File.Copy(baselinePath, replicaPath, overwrite: false);
}
static string SanitizeVerseId(string verseId)
{
var safeVerseId = string.IsNullOrWhiteSpace(verseId)
? "unknown-verse"
: new string((verseId ?? "")
.Select(ch => char.IsLetterOrDigit(ch) || ch == '-' || ch == '_' || ch == '.'
? ch
: '-')
.ToArray())
.Trim('-');
return string.IsNullOrWhiteSpace(safeVerseId)
? "unknown-verse"
: safeVerseId;
}
static Dictionary<string, string> ParseOptions(IEnumerable<string> args)
{
var options = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
string? pendingKey = null;
foreach (var arg in args)
{
if (arg.StartsWith("--", StringComparison.Ordinal))
{
pendingKey = arg[2..];
options[pendingKey] = "true";
continue;
}
if (!string.IsNullOrWhiteSpace(pendingKey))
{
options[pendingKey] = arg;
pendingKey = null;
}
}
return options;
}
static string RequireOption(IReadOnlyDictionary<string, string> options, string key)
{
var value = ReadOption(options, key, "");
if (!string.IsNullOrWhiteSpace(value))
return value;
throw new InvalidOperationException($"Missing required option --{key}.");
}
static string ReadOption(IReadOnlyDictionary<string, string> options, string key, string fallback)
{
return options.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value)
? value
: fallback;
}
static double ReadDoubleOption(IReadOnlyDictionary<string, string> options, string key, double fallback)
{
var value = ReadOption(options, key, "");
return double.TryParse(value, out var parsed) && parsed > 0
? parsed
: fallback;
}
static void PrintUsage()
{
Console.WriteLine("Usage:");
Console.WriteLine(" sync --endpoint cultnet://host:3075 --replica path-to-replica.cc [--runtime-id aetheria-verse-replica]");
Console.WriteLine(" sync --endpoint cultnet://host:3075 --game-data-root GameData --verse-id verse.id [--runtime-id aetheria-verse-replica]");
Console.WriteLine(" follow --endpoint cultnet://host:3075 --replica path-to-replica.cc [--poll-seconds 1] [--runtime-id aetheria-verse-replica]");
}

View file

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\Aetheria.State\Aetheria.State.Dependencies.props" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Aetheria.State\Aetheria.State.csproj" />
</ItemGroup>
<Import Project="..\Aetheria.State\Aetheria.State.Dependencies.targets" />
</Project>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Aetheria.State.Unity\Aetheria.State.Unity.csproj" />
<ProjectReference Include="..\GameCult.Aetheria.State.Unity.csproj" Aliases="PackageUnity" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,281 @@
extern alias PackageUnity;
using Aetheria.State;
using Aetheria.State.Unity;
using PackageUnity::GameCult.Aetheria.State.Verse;
var root = args.Length > 0 ? Path.GetFullPath(args[0]) : Directory.GetCurrentDirectory();
var statePath = args.Length > 1
? Path.GetFullPath(args[1])
: AetheriaStatePaths.ResolveDefaultStatePath(root);
await using var client = await AetheriaRuntimeCatalogClient.OpenAsync(statePath);
var catalog = client.ReadCatalog();
var packageCatalog = AetheriaRuntimeCatalogStore.OpenReadOnly(statePath);
var surface = client.ReadCatalogSurface();
var commitSmokeDirectory = Path.Combine(Path.GetTempPath(), "aetheria-state-unity-smoke", Guid.NewGuid().ToString("N"));
var commitSmokeStatePath = Path.Combine(commitSmokeDirectory, "aetheria-world.cc");
Directory.CreateDirectory(commitSmokeDirectory);
if (catalog.Items.Count != 115)
{
throw new InvalidOperationException($"Expected 115 runtime catalog items, found {catalog.Items.Count}.");
}
if (packageCatalog.Items.Count != catalog.Items.Count ||
packageCatalog.Corporations.Count != catalog.Corporations.Count ||
packageCatalog.NameFiles.Count != catalog.NameFiles.Count)
{
throw new InvalidOperationException(
$"Package catalog store mismatch: {packageCatalog.Items.Count}/{packageCatalog.Corporations.Count}/{packageCatalog.NameFiles.Count} " +
$"!= {catalog.Items.Count}/{catalog.Corporations.Count}/{catalog.NameFiles.Count}.");
}
if (catalog.TradeItems.Count == 0)
{
throw new InvalidOperationException("Runtime catalog has no trade items.");
}
if (catalog.EquipmentItems.Count == 0)
{
throw new InvalidOperationException("Runtime catalog has no equipment items.");
}
var shaped = catalog.Items.FirstOrDefault(item => item.ShapeCells.Count > 0)
?? throw new InvalidOperationException("Runtime catalog has no typed shape masks.");
if (string.IsNullOrWhiteSpace(shaped.ItemKey))
{
throw new InvalidOperationException($"Runtime shape sample has no typed item key: {shaped.Name}.");
}
var packageShaped = packageCatalog.FindItem(shaped.ItemKey);
if (packageShaped == null ||
packageShaped.Name != shaped.Name ||
packageShaped.ShapeCells.Count != shaped.ShapeCells.Count)
{
throw new InvalidOperationException($"Package catalog store did not read the expected typed item payload for {shaped.ItemKey}.");
}
if (shaped.ShapeCells.Count != shaped.OccupiedCells)
{
throw new InvalidOperationException(
$"Runtime shape mask count mismatch for {shaped.Name}: cells={shaped.ShapeCells.Count}, occupied={shaped.OccupiedCells}.");
}
if (shaped.ShapeCells.Any(cell => cell.X < 0 || cell.Y < 0 || cell.X >= shaped.ShapeWidth || cell.Y >= shaped.ShapeHeight))
{
throw new InvalidOperationException($"Runtime shape mask has out-of-bounds cells for {shaped.Name}.");
}
var interior = catalog.Items.FirstOrDefault(item => item.InteriorShapeCells.Count > 0)
?? throw new InvalidOperationException("Runtime catalog has no typed interior shape masks.");
if (interior.InteriorShapeCells.Count != interior.InteriorOccupiedCells)
{
throw new InvalidOperationException(
$"Runtime interior shape mask count mismatch for {interior.Name}: cells={interior.InteriorShapeCells.Count}, occupied={interior.InteriorOccupiedCells}.");
}
var hardpointHost = catalog.Items.FirstOrDefault(item => item.Hardpoints.Count > 0)
?? throw new InvalidOperationException("Runtime catalog has no typed hardpoints.");
var hardpoint = hardpointHost.Hardpoints.First();
if (hardpoint.ShapeCells.Count != hardpoint.OccupiedCells)
{
throw new InvalidOperationException(
$"Runtime hardpoint shape count mismatch for {hardpointHost.Name}: cells={hardpoint.ShapeCells.Count}, occupied={hardpoint.OccupiedCells}.");
}
var behaviorKind = catalog.Items.SelectMany(item => item.BehaviorKinds).FirstOrDefault()
?? throw new InvalidOperationException("Runtime catalog has no behavior kind fingerprints.");
if (!catalog.FindItemsByBehavior(behaviorKind).Any())
{
throw new InvalidOperationException($"Runtime catalog behavior lookup failed for {behaviorKind}.");
}
var behaviorHost = catalog.Items.FirstOrDefault(item => item.BehaviorPayloads.Count > 0)
?? throw new InvalidOperationException("Runtime catalog has no typed behavior payloads.");
if (!behaviorHost.BehaviorPayloads.All(payload => behaviorHost.BehaviorKinds.Contains(payload.Kind)))
{
throw new InvalidOperationException(
$"Runtime behavior payload kind index mismatch for {behaviorHost.Name}.");
}
var behaviorPayload = behaviorHost.BehaviorPayloads.First();
if (behaviorPayload.Fields.Count == 0)
{
throw new InvalidOperationException($"Runtime behavior payload has no fields for {behaviorHost.Name}.");
}
var behaviorItemRefsMissingItemKeys = packageCatalog.Items
.SelectMany(item => item.BehaviorPayloads)
.Sum(CountRequiredBehaviorItemRefsMissingItemKeys);
if (behaviorItemRefsMissingItemKeys > 0)
{
throw new InvalidOperationException(
$"Runtime behavior payload item refs missing item-key projections: {behaviorItemRefsMissingItemKeys}.");
}
var equipment = catalog.EquipmentItems.First();
if (!catalog.FindItemsByHardpoint(equipment.HardpointType).Any())
{
throw new InvalidOperationException($"Runtime catalog hardpoint lookup failed for {equipment.HardpointType}.");
}
var manufactured = catalog.Items.FirstOrDefault(item => !string.IsNullOrWhiteSpace(item.ManufacturerKey))
?? throw new InvalidOperationException("Runtime catalog has no manufactured item.");
if (string.IsNullOrWhiteSpace(manufactured.ManufacturerKey) ||
catalog.FindCorporation(manufactured.ManufacturerKey) == null ||
catalog.GetManufacturer(manufactured) == null)
{
throw new InvalidOperationException($"Runtime catalog typed manufacturer lookup failed for {manufactured.Name}.");
}
var packageCorporation = packageCatalog.Corporations.FirstOrDefault(corporation => !string.IsNullOrWhiteSpace(corporation.GeonameFileKey));
if (packageCorporation == null)
{
throw new InvalidOperationException("Package catalog store did not read any corporation name-file links.");
}
var packageNameFile = packageCatalog.GetNameFile(packageCorporation);
if (string.IsNullOrWhiteSpace(packageCorporation.GeonameFileKey) ||
packageCatalog.FindNameFile(packageCorporation.GeonameFileKey) == null ||
packageNameFile == null ||
string.IsNullOrWhiteSpace(packageNameFile.NameFileKey) ||
packageNameFile.Names.Count == 0 ||
packageNameFile.Names.Count != packageNameFile.NameCount)
{
throw new InvalidOperationException("Package catalog store did not read typed corporation/name-file links with full names.");
}
if (packageCorporation.Allegiances.Count == 0 || packageCorporation.Allegiances.Count != packageCorporation.AllegianceCount)
{
throw new InvalidOperationException("Package catalog store did not read corporation allegiance edges.");
}
if (surface?.Schema != "gamecult.eve.surface.v1" ||
surface.Surface.Id != AetheriaEveSurfaceDocuments.CatalogSurfaceId)
{
throw new InvalidOperationException("Runtime catalog client did not read the typed Eve surface.");
}
if (surface == null ||
surface.Schema != "gamecult.eve.surface.v1" ||
surface.ProviderId != "aetheria" ||
surface.Surface.Root.Kind != "surface" ||
surface.Surface.Root.Children.Length == 0 ||
surface.Commands.All(command => command.Command != AetheriaRuntimeCatalogCommands.Refresh))
{
throw new InvalidOperationException("Unity runtime catalog client did not read the typed Eve surface contract from the managed document.");
}
try
{
var eveCommand = AetheriaRuntimeEveCommands.SubmitCatalogCommand(
commitSmokeStatePath,
AetheriaRuntimeEveCommandKind.CatalogRefresh,
"aetheria-state-unity-smoke");
await using var eveCommandNode = await AetheriaStateNode.OpenAsync(
commitSmokeStatePath,
"aetheria-unity-runtime-eve-command-smoke");
await eveCommandNode.SubmitEveCommandAsync(new GameCult.Aetheria.State.Verse.AetheriaRuntimeEveCommandDocument
{
Schema = eveCommand.Schema,
CommandId = eveCommand.CommandId,
ProviderId = eveCommand.ProviderId,
SurfaceId = eveCommand.SurfaceId,
Command = eveCommand.Command,
Kind = (GameCult.Aetheria.State.Verse.AetheriaRuntimeEveCommandKind)(int)eveCommand.Kind,
IssuedAtUtc = eveCommand.IssuedAtUtc,
ClientId = eveCommand.ClientId,
PlayerSettings = new GameCult.Aetheria.State.Verse.AetheriaRuntimePlayerSettingsCommandBody
{
PlayerName = eveCommand.PlayerSettings.PlayerName
},
InputSettings = new GameCult.Aetheria.State.Verse.AetheriaRuntimeInputSettingsCommandBody
{
ActionName = eveCommand.InputSettings.ActionName,
BindingIndex = eveCommand.InputSettings.BindingIndex,
InputSystemPath = eveCommand.InputSettings.InputSystemPath,
Enabled = eveCommand.InputSettings.Enabled
}
});
await eveCommandNode.FlushAsync();
await using var eveCommandCheckNode = await AetheriaStateNode.OpenAsync(
commitSmokeStatePath,
"aetheria-unity-runtime-eve-command-check");
var eveSubmitted = eveCommandCheckNode.Documents<AetheriaRuntimeEveCommandDocument>()
.Select(AetheriaRuntimeEveCommandClient.NormalizeDocument)
.OrderBy(command => command.IssuedAtUtc ?? "", StringComparer.Ordinal)
.ThenBy(command => command.CommandId ?? "", StringComparer.Ordinal)
.ToArray();
if (eveSubmitted.Length != 1 ||
eveSubmitted[0].Schema != AetheriaRuntimeEveCommandClient.CommandSchema ||
eveSubmitted[0].CommandId != eveCommand.CommandId ||
eveSubmitted[0].ProviderId != "aetheria" ||
eveSubmitted[0].SurfaceId != AetheriaRuntimeCatalogCommands.SurfaceId ||
eveSubmitted[0].Command != AetheriaRuntimeCatalogCommands.Refresh ||
(int)eveSubmitted[0].Kind != (int)AetheriaRuntimeEveCommandKind.CatalogRefresh ||
eveSubmitted[0].PlayerSettings == null ||
eveSubmitted[0].InputSettings == null)
{
var submitted = eveSubmitted.Length > 0 ? eveSubmitted[0] : null;
throw new InvalidOperationException(
"Runtime Eve command state record did not preserve typed command envelopes separately from state commits. " +
$"Count={eveSubmitted.Length}, " +
$"Schema={submitted?.Schema ?? "<null>"}, " +
$"CommandId={submitted?.CommandId ?? "<null>"} expected {eveCommand.CommandId}, " +
$"ProviderId={submitted?.ProviderId ?? "<null>"}, " +
$"SurfaceId={submitted?.SurfaceId ?? "<null>"}, " +
$"Command={submitted?.Command ?? "<null>"}, " +
$"Kind={submitted?.Kind.ToString() ?? "<null>"}, " +
$"PlayerSettings={(submitted?.PlayerSettings == null ? "null" : "present")}, " +
$"InputSettings={(submitted?.InputSettings == null ? "null" : "present")}.");
}
}
finally
{
if (Environment.GetEnvironmentVariable("AETHERIA_KEEP_SMOKE_STATE") == "1")
Console.Error.WriteLine($"Kept smoke state: {commitSmokeDirectory}");
else
Directory.Delete(commitSmokeDirectory, true);
}
Console.WriteLine($"Aetheria Unity runtime catalog smoke passed: {statePath}");
Console.WriteLine($"Items/trade/equipment: {catalog.Items.Count}/{catalog.TradeItems.Count}/{catalog.EquipmentItems.Count}");
Console.WriteLine($"Shape mask sample: {shaped.Name} {shaped.ShapeWidth}x{shaped.ShapeHeight}/{shaped.ShapeCells.Count}");
Console.WriteLine($"Interior/hardpoint sample: {interior.Name} {interior.InteriorShapeCells.Count}; {hardpointHost.Name} {hardpoint.Type} {hardpoint.ShapeCells.Count}");
Console.WriteLine($"Behavior payload sample: {behaviorHost.Name} {behaviorPayload.Kind}/{behaviorPayload.Fields.Count}");
Console.WriteLine($"Behavior sample: {behaviorKind}");
Console.WriteLine($"Eve surface: {surface.Surface.Id}");
Console.WriteLine($"Catalog Eve surface document: {surface.Surface.Id}");
Console.WriteLine("Runtime Eve command smoke: surface command document is a typed state record");
static int CountRequiredBehaviorItemRefsMissingItemKeys(AetheriaRuntimeBehaviorPayload payload)
{
return payload.Fields.Count(field =>
IsBehaviorItemRefField(payload.Kind, field.Key) &&
IsNonEmptyLegacyRefMissingItemKey(field.Value));
}
static bool IsBehaviorItemRefField(string behaviorKind, int fieldKey)
{
return (string.Equals(behaviorKind, "ItemUsage", StringComparison.OrdinalIgnoreCase) && fieldKey == 1) ||
(IsWeaponBehavior(behaviorKind) && fieldKey == 12);
}
static bool IsWeaponBehavior(string behaviorKind)
{
return behaviorKind is "GuidedWeapon" or "InstantWeapon" or "ConstantWeapon" or "ChargedWeapon" or "AutoWeapon" or "LockWeapon";
}
static bool IsNonEmptyLegacyRefMissingItemKey(AetheriaRuntimeBehaviorValue value)
{
return string.Equals(value.Kind, "legacy-id", StringComparison.OrdinalIgnoreCase) &&
!IsEmptyLegacyId(value.LegacyIdValue) &&
string.IsNullOrWhiteSpace(value.ItemKeyValue);
}
static bool IsEmptyLegacyId(string legacyId)
{
return string.IsNullOrWhiteSpace(legacyId) ||
(Guid.TryParse(legacyId, out var parsed) && parsed == Guid.Empty);
}

View file

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Description>Unity-facing runtime read facade over typed Aetheria CultCache state.</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Aetheria.State\Aetheria.State.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,43 @@
using GameCult.Aetheria.State.Verse;
using GameCult.Mesh;
using EveSurfaceDocument = GameCult.Eve.Surface.EveSurfaceDocument;
namespace Aetheria.State.Unity;
public sealed class AetheriaRuntimeCatalogClient : IAsyncDisposable, IDisposable
{
private readonly AetheriaStateNode _node;
private AetheriaRuntimeCatalogClient(AetheriaStateNode node)
{
_node = node;
}
public static async Task<AetheriaRuntimeCatalogClient> OpenAsync(string statePath)
{
var node = await AetheriaStateNode
.OpenAsync(statePath, "aetheria-unity-runtime-catalog", enableDurableShardLogs: false)
.ConfigureAwait(false);
return new AetheriaRuntimeCatalogClient(node);
}
public AetheriaRuntimeCatalogSnapshot ReadCatalog()
{
return _node.RuntimeCatalog().Latest();
}
public EveSurfaceDocument? ReadCatalogSurface()
{
return _node.CatalogSurface().Latest();
}
public ValueTask DisposeAsync()
{
return _node.DisposeAsync();
}
public void Dispose()
{
_node.Dispose();
}
}

View file

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\Aetheria.State\Aetheria.State.Dependencies.props" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Aetheria.State\Aetheria.State.csproj" />
<ProjectReference Include="..\Aetheria.State.Unity\Aetheria.State.Unity.csproj" />
</ItemGroup>
<Import Project="..\Aetheria.State\Aetheria.State.Dependencies.targets" />
</Project>

View file

@ -0,0 +1,38 @@
internal enum AetheriaVerifyHardpointType
{
Hull,
Tool,
Thermal,
Thruster,
WarpDrive,
Reactor,
Radiator,
Shield,
Sensors,
Energy,
Ballistic,
Launcher,
ControlModule,
AetherDrive
}
internal enum AetheriaVerifySimpleCommodityCategory
{
Minerals,
Metals,
Alloys,
Compounds,
Organics,
Ammo,
Consumer
}
internal enum AetheriaVerifyCompoundCommodityCategory
{
Wearables,
Consumables,
Luxuries,
Tools,
Manufacturing,
Assemblies
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,7 @@
<Project>
<PropertyGroup>
<CultLibRoot Condition="'$(CultLibRoot)' == ''">$(MSBuildThisFileDirectory)..\..\CultLib</CultLibRoot>
<EveUnityRoot Condition="'$(EveUnityRoot)' == ''">$(MSBuildThisFileDirectory)..\..\EveUnity</EveUnityRoot>
<YmirRoot Condition="'$(YmirRoot)' == ''">$(MSBuildThisFileDirectory)..\..\Ymir</YmirRoot>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,20 @@
<Project>
<Target Name="VerifyGameCultMeshReference" AfterTargets="ResolveAssemblyReferences"
Condition="'$(DesignTimeBuild)' != 'true'">
<ItemGroup>
<_GameCultMeshReference Include="@(ReferencePath)"
Condition="'%(ReferencePath.Filename)%(ReferencePath.Extension)' == 'GameCult.Mesh.dll'" />
</ItemGroup>
<Error Condition="@(_GameCultMeshReference->Count()) != 1"
Text="Expected exactly one resolved GameCult.Mesh reference, found @(_GameCultMeshReference->Count()): @(_GameCultMeshReference, ', ')" />
<Error Condition="$([System.String]::Copy('%(_GameCultMeshReference.FullPath)').StartsWith($([System.IO.Path]::GetFullPath('$(CultLibRoot)\')), System.StringComparison.OrdinalIgnoreCase)) != 'true'"
Text="GameCult.Mesh resolved outside CultLibRoot '$(CultLibRoot)': %(_GameCultMeshReference.FullPath)" />
<GetFileHash Files="@(_GameCultMeshReference)" Algorithm="SHA256">
<Output TaskParameter="Items" ItemName="_GameCultMeshReferenceHash" />
</GetFileHash>
<Message Importance="High"
Text="GameCult.Mesh reference: %(_GameCultMeshReferenceHash.FullPath) SHA256=%(_GameCultMeshReferenceHash.FileHash)" />
</Target>
</Project>

View file

@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="Aetheria.State.Dependencies.props" />
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Description>Typed CultCache and CultMesh state spine for Aetheria.</Description>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\*.cs" Link="Runtime\%(Filename)%(Extension)" />
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Caching\GameCult.Caching.csproj" />
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Caching.MessagePack\GameCult.Caching.MessagePack.csproj" />
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Caching.MessagePack.Generator\GameCult.Caching.MessagePack.Generator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Mesh\GameCult.Mesh.csproj" />
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Networking\GameCult.Networking.csproj" />
<ProjectReference Include="..\..\CultMath\src\CultMath\CultMath.csproj" />
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Geometry\GameCult.Geometry.csproj" />
<ProjectReference Include="$(EveUnityRoot)\packages\org.gamecult.eve.surface\GameCult.Eve.Surface.csproj"
AdditionalProperties="CultLibRoot=$(CultLibRoot)" />
<ProjectReference Include="..\..\EvePlugins\plugins\eve-plugin-fields\GameCult.Eve.PluginFields.csproj"
AdditionalProperties="CultLibRoot=$(CultLibRoot)" />
</ItemGroup>
<Import Project="Aetheria.State.Dependencies.targets" />
</Project>

View file

@ -0,0 +1,31 @@
using GameCult.Caching;
namespace Aetheria.State;
public static class AetheriaCatalogKeys
{
public static CultRecordKey ItemDefinitionFromLegacyId(string legacyId)
{
return CreateLegacyKey("item", legacyId);
}
public static CultRecordKey CorporationFromLegacyId(string legacyId)
{
return CreateLegacyKey("faction", legacyId);
}
public static CultRecordKey NameFileFromLegacyId(string legacyId)
{
return CreateLegacyKey("name-file", legacyId);
}
private static CultRecordKey CreateLegacyKey(string catalogKind, string legacyId)
{
if (string.IsNullOrWhiteSpace(legacyId))
{
throw new ArgumentException("Legacy catalog id must be non-empty.", nameof(legacyId));
}
return new CultRecordKey($"legacy:{catalogKind}:{legacyId.Trim()}");
}
}

Some files were not shown because too many files have changed in this diff Show more