Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c983a5ee2 | ||
|
|
3ec6222f82 | ||
|
|
f361125a3a | ||
|
|
251efa176c | ||
|
|
6e561d336f | ||
|
|
7e4feed75b | ||
|
|
20cd4e8c80 | ||
|
|
e04b613cc5 | ||
|
|
557bc71f7a | ||
|
|
26948f81b6 |
4850 changed files with 422252 additions and 244504 deletions
4
.gitattributes
vendored
4
.gitattributes
vendored
|
|
@ -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
116
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
81
AGENTS.md
Normal 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
BIN
Aetheria RTS.lnk
Normal file
Binary file not shown.
|
|
@ -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>
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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'.
|
||||
4
Aetheria.Rts.Web/Client/app.ts
Normal file
4
Aetheria.Rts.Web/Client/app.ts
Normal 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 });
|
||||
797
Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts
Normal file
797
Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts
Normal 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));
|
||||
}
|
||||
58
Aetheria.Rts.Web/Electron/aetheria-rts-bindings.ts
Normal file
58
Aetheria.Rts.Web/Electron/aetheria-rts-bindings.ts
Normal 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;
|
||||
2596
Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts
Normal file
2596
Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts
Normal file
File diff suppressed because it is too large
Load diff
495
Aetheria.Rts.Web/Electron/aetheria-rts-local-documents.ts
Normal file
495
Aetheria.Rts.Web/Electron/aetheria-rts-local-documents.ts
Normal 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;
|
||||
}
|
||||
432
Aetheria.Rts.Web/Electron/main.ts
Normal file
432
Aetheria.Rts.Web/Electron/main.ts
Normal 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("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """);
|
||||
}
|
||||
305
Aetheria.Rts.Web/package-lock.json
generated
Normal file
305
Aetheria.Rts.Web/package-lock.json
generated
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Aetheria.Rts.Web/package.json
Normal file
33
Aetheria.Rts.Web/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
1641
Aetheria.Rts.Web/scripts/generate-rts-bindings.mjs
Normal file
1641
Aetheria.Rts.Web/scripts/generate-rts-bindings.mjs
Normal file
File diff suppressed because it is too large
Load diff
576
Aetheria.Rts.Web/scripts/verify-stage7b-rts-client.ps1
Normal file
576
Aetheria.Rts.Web/scripts/verify-stage7b-rts-client.ps1
Normal 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."
|
||||
136
Aetheria.Rts.Web/scripts/verify-stage7c-electron-rts.ps1
Normal file
136
Aetheria.Rts.Web/scripts/verify-stage7c-electron-rts.ps1
Normal 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."
|
||||
195
Aetheria.Rts.Web/scripts/verify-stage7c-local-runtime.ps1
Normal file
195
Aetheria.Rts.Web/scripts/verify-stage7c-local-runtime.ps1
Normal 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."
|
||||
145
Aetheria.Rts.Web/scripts/verify-stage7d-remote-cultmesh.ps1
Normal file
145
Aetheria.Rts.Web/scripts/verify-stage7d-remote-cultmesh.ps1
Normal 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."
|
||||
19
Aetheria.Rts.Web/tsconfig.electron.json
Normal file
19
Aetheria.Rts.Web/tsconfig.electron.json
Normal 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"
|
||||
]
|
||||
}
|
||||
14
Aetheria.Rts.Web/tsconfig.json
Normal file
14
Aetheria.Rts.Web/tsconfig.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"noEmitOnError": true,
|
||||
"outDir": "wwwroot",
|
||||
"rootDir": "Client"
|
||||
},
|
||||
"include": [
|
||||
"Client/**/*.ts"
|
||||
]
|
||||
}
|
||||
21
Aetheria.Rts.Web/wwwroot/index.html
Normal file
21
Aetheria.Rts.Web/wwwroot/index.html
Normal 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>
|
||||
90
Aetheria.Rts.Web/wwwroot/styles.css
Normal file
90
Aetheria.Rts.Web/wwwroot/styles.css
Normal 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);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
2026
Aetheria.State.AuthoritySmoke/Program.cs
Normal file
2026
Aetheria.State.AuthoritySmoke/Program.cs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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>
|
||||
9746
Aetheria.State.Daemon.Smoke/Program.cs
Normal file
9746
Aetheria.State.Daemon.Smoke/Program.cs
Normal file
File diff suppressed because it is too large
Load diff
19
Aetheria.State.Daemon/Aetheria.State.Daemon.csproj
Normal file
19
Aetheria.State.Daemon/Aetheria.State.Daemon.csproj
Normal 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>
|
||||
|
|
@ -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]
|
||||
};
|
||||
}
|
||||
448
Aetheria.State.Daemon/AetheriaDaemonLoadoutGenerator.cs
Normal file
448
Aetheria.State.Daemon/AetheriaDaemonLoadoutGenerator.cs
Normal 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);
|
||||
232
Aetheria.State.Daemon/AetheriaDaemonNativeCatalog.cs
Normal file
232
Aetheria.State.Daemon/AetheriaDaemonNativeCatalog.cs
Normal 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}";
|
||||
}
|
||||
134
Aetheria.State.Daemon/AetheriaDaemonRegularRunWriter.cs
Normal file
134
Aetheria.State.Daemon/AetheriaDaemonRegularRunWriter.cs
Normal 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";
|
||||
}
|
||||
258
Aetheria.State.Daemon/AetheriaDaemonRegularTopologyGenerator.cs
Normal file
258
Aetheria.State.Daemon/AetheriaDaemonRegularTopologyGenerator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
39
Aetheria.State.Daemon/AetheriaDaemonRunFactory.cs
Normal file
39
Aetheria.State.Daemon/AetheriaDaemonRunFactory.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
134
Aetheria.State.Daemon/AetheriaDaemonTutorialRunWriter.cs
Normal file
134
Aetheria.State.Daemon/AetheriaDaemonTutorialRunWriter.cs
Normal 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";
|
||||
}
|
||||
725
Aetheria.State.Daemon/AetheriaDaemonTutorialTopologyGenerator.cs
Normal file
725
Aetheria.State.Daemon/AetheriaDaemonTutorialTopologyGenerator.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
276
Aetheria.State.Daemon/AetheriaDaemonTutorialWorldMaterializer.cs
Normal file
276
Aetheria.State.Daemon/AetheriaDaemonTutorialWorldMaterializer.cs
Normal 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;
|
||||
}
|
||||
508
Aetheria.State.Daemon/AetheriaDaemonZoneBodyGenerator.cs
Normal file
508
Aetheria.State.Daemon/AetheriaDaemonZoneBodyGenerator.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
524
Aetheria.State.Daemon/AetheriaDaemonZoneGenerator.cs
Normal file
524
Aetheria.State.Daemon/AetheriaDaemonZoneGenerator.cs
Normal 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);
|
||||
|
||||
}
|
||||
11
Aetheria.State.Daemon/AetheriaYmirPersistence.cs
Normal file
11
Aetheria.State.Daemon/AetheriaYmirPersistence.cs
Normal 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);
|
||||
398
Aetheria.State.Daemon/AetheriaYmirPersistenceCoordinator.cs
Normal file
398
Aetheria.State.Daemon/AetheriaYmirPersistenceCoordinator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
43
Aetheria.State.Daemon/AetheriaYmirPersistenceDocuments.cs
Normal file
43
Aetheria.State.Daemon/AetheriaYmirPersistenceDocuments.cs
Normal 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; } = [];
|
||||
}
|
||||
630
Aetheria.State.Daemon/AetheriaYmirWorldPhysics.cs
Normal file
630
Aetheria.State.Daemon/AetheriaYmirWorldPhysics.cs
Normal 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);
|
||||
}
|
||||
4199
Aetheria.State.Daemon/Program.cs
Normal file
4199
Aetheria.State.Daemon/Program.cs
Normal file
File diff suppressed because it is too large
Load diff
3
Aetheria.State.Daemon/Properties/AssemblyInfo.cs
Normal file
3
Aetheria.State.Daemon/Properties/AssemblyInfo.cs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Aetheria.State.Daemon.Smoke")]
|
||||
15
Aetheria.State.Freeze/Aetheria.State.Freeze.csproj
Normal file
15
Aetheria.State.Freeze/Aetheria.State.Freeze.csproj
Normal 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>
|
||||
1440
Aetheria.State.Freeze/Program.cs
Normal file
1440
Aetheria.State.Freeze/Program.cs
Normal file
File diff suppressed because it is too large
Load diff
18
Aetheria.State.Import/Aetheria.State.Import.csproj
Normal file
18
Aetheria.State.Import/Aetheria.State.Import.csproj
Normal 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>
|
||||
1560
Aetheria.State.Import/Program.cs
Normal file
1560
Aetheria.State.Import/Program.cs
Normal file
File diff suppressed because it is too large
Load diff
14
Aetheria.State.Replica/Aetheria.State.Replica.csproj
Normal file
14
Aetheria.State.Replica/Aetheria.State.Replica.csproj
Normal 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>
|
||||
151
Aetheria.State.Replica/Program.cs
Normal file
151
Aetheria.State.Replica/Program.cs
Normal 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]");
|
||||
}
|
||||
17
Aetheria.State.Smoke/Aetheria.State.Smoke.csproj
Normal file
17
Aetheria.State.Smoke/Aetheria.State.Smoke.csproj
Normal 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>
|
||||
1352
Aetheria.State.Smoke/Program.cs
Normal file
1352
Aetheria.State.Smoke/Program.cs
Normal file
File diff suppressed because it is too large
Load diff
14
Aetheria.State.Unity.Smoke/Aetheria.State.Unity.Smoke.csproj
Normal file
14
Aetheria.State.Unity.Smoke/Aetheria.State.Unity.Smoke.csproj
Normal 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>
|
||||
281
Aetheria.State.Unity.Smoke/Program.cs
Normal file
281
Aetheria.State.Unity.Smoke/Program.cs
Normal 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);
|
||||
}
|
||||
13
Aetheria.State.Unity/Aetheria.State.Unity.csproj
Normal file
13
Aetheria.State.Unity/Aetheria.State.Unity.csproj
Normal 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>
|
||||
43
Aetheria.State.Unity/AetheriaRuntimeCatalogClient.cs
Normal file
43
Aetheria.State.Unity/AetheriaRuntimeCatalogClient.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
18
Aetheria.State.Verify/Aetheria.State.Verify.csproj
Normal file
18
Aetheria.State.Verify/Aetheria.State.Verify.csproj
Normal 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>
|
||||
38
Aetheria.State.Verify/AetheriaCatalogEnumProbeTypes.cs
Normal file
38
Aetheria.State.Verify/AetheriaCatalogEnumProbeTypes.cs
Normal 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
|
||||
}
|
||||
21377
Aetheria.State.Verify/Program.cs
Normal file
21377
Aetheria.State.Verify/Program.cs
Normal file
File diff suppressed because it is too large
Load diff
7
Aetheria.State/Aetheria.State.Dependencies.props
Normal file
7
Aetheria.State/Aetheria.State.Dependencies.props
Normal 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>
|
||||
20
Aetheria.State/Aetheria.State.Dependencies.targets
Normal file
20
Aetheria.State/Aetheria.State.Dependencies.targets
Normal 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>
|
||||
30
Aetheria.State/Aetheria.State.csproj
Normal file
30
Aetheria.State/Aetheria.State.csproj
Normal 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>
|
||||
31
Aetheria.State/AetheriaCatalogKeys.cs
Normal file
31
Aetheria.State/AetheriaCatalogKeys.cs
Normal 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
Loading…
Reference in a new issue