diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a7ac47a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,87 @@ +name: Create Release + +on: + workflow_dispatch: + inputs: + prerelease: + description: Prerelease + type: boolean + bypassCheck: + description: Bypass Version Check + type: boolean + +env: + PROJ_USERNAME: OlegSkutte + PROJ_NAME: TrajectoryPrediction + +jobs: + pre_job: + name: Check For Other Releases + outputs: + version: ${{ steps.out.outputs.version }} + exists: ${{ steps.out.outputs.exists }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: "actions/checkout@v3" + + - name: Fetch + run: git fetch + + - name: Read Manifest + id: read-manifest + run: echo "manifest=$(< ./${{ env.PROJ_NAME }}/manifest.json sed ':a;N;$!ba;s/\n/ /g')" >> $GITHUB_OUTPUT + + - name: Check For Release + id: check-tag + run: echo "exists=$(git ls-remote --exit-code --tags origin ${{ env.TAG }} >/dev/null 2>&1 && echo true || echo false)" >> $GITHUB_OUTPUT + env: + TAG: "v${{fromJson(steps.read-manifest.outputs.manifest).version}}" + + - name: Output Version Info + id: out + run: | + echo "version=${{fromJson(steps.read-manifest.outputs.manifest).version}}" >> $GITHUB_OUTPUT + echo "exists=${{steps.check-tag.outputs.exists}}" >> $GITHUB_OUTPUT + + - name: Error + if: ${{ steps.out.outputs.exists != 'false' && (!inputs.bypassCheck) }} + run: echo "::error file=manifest.json,title=Refusing to Release::Your mod was not released because there is already a release with the version in manifest.json" + release: + needs: pre_job + if: ${{ (needs.pre_job.outputs.version != '0.0.0') && (needs.pre_job.outputs.exists == 'false') || (inputs.bypassCheck) }} + name: Create Release + runs-on: windows-latest + steps: + - name: Checkout + uses: "actions/checkout@v3" + + - name: Setup .NET + uses: "actions/setup-dotnet@v3" + + - name: Remove .csproj.user + run: rm ${{ env.PROJ_NAME }}/${{ env.PROJ_NAME }}.csproj.user + + - name: Build Mod + run: dotnet build -c Release + + - name: Upload Artifact + uses: "actions/upload-artifact@v3" + with: + name: "${{ env.PROJ_USERNAME }}.${{ env.PROJ_NAME }}" + path: "${{ env.PROJ_NAME }}/bin/Release" + + - name: Zip For Release + run: 7z a ${{ env.PROJ_USERNAME }}.${{ env.PROJ_NAME }}.zip ${{ env.PROJ_NAME }}/bin/Release/** + + - name: Create Release + uses: "ncipollo/release-action@v1" + with: + allowUpdates: true + commit: ${{ github.ref_name }} + tag: v${{ needs.pre_job.outputs.version }} + name: Version ${{ needs.pre_job.outputs.version }} + omitBodyDuringUpdate: true + artifacts: "${{ env.PROJ_USERNAME}}.${{ env.PROJ_NAME }}.zip" + draft: true + prerelease: ${{ inputs.prerelease }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..426d76d --- /dev/null +++ b/.gitignore @@ -0,0 +1,398 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml diff --git a/.idea/.idea.TrajectoryPrediction/.idea/.gitignore b/.idea/.idea.TrajectoryPrediction/.idea/.gitignore new file mode 100644 index 0000000..8a663a3 --- /dev/null +++ b/.idea/.idea.TrajectoryPrediction/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/contentModel.xml +/.idea.TrajectoryPrediction.iml +/projectSettingsUpdater.xml +/modules.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/.idea.TrajectoryPrediction/.idea/encodings.xml b/.idea/.idea.TrajectoryPrediction/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.TrajectoryPrediction/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.TrajectoryPrediction/.idea/indexLayout.xml b/.idea/.idea.TrajectoryPrediction/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.TrajectoryPrediction/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.TrajectoryPrediction/.idea/vcs.xml b/.idea/.idea.TrajectoryPrediction/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/.idea.TrajectoryPrediction/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7b4b897 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Oleg Skutte + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..47b2060 --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +![Logo](https://user-images.githubusercontent.com/45887963/202138974-7c1be1b3-a329-40b9-b9c7-ccd3a4be5dea.png) + +This is a mod for **Outer Wilds** which predicts and visualizes future trajectories of player, ship, and scout, in map view. + +![Screenshot](https://user-images.githubusercontent.com/45887963/202139195-cc38666c-2c16-4875-ad94-7cadea7cc804.jpg) + +## Installation +- [Install OWML](https://github.com/amazingalek/owml#installation) +- [Download the latest release](https://github.com/SkutteOleg/TrajectoryPrediction/releases/latest) +- Extract `OlegSkutte.TrajectoryPrediction` directory to `OWML/Mods` directory +- Run `OWML.Launcher.exe` to start the game + +## Settings +![Settings](https://user-images.githubusercontent.com/45887963/202139282-0c378a33-0c12-4907-99e4-c98604dcabe1.jpg) +##### Simulation Settings +- **Seconds To Predict** - Determines how far into the future trajectories are predicted in seconds. Higher values take longer to compute. +- **High Precision Mode** - Toggles simulation time step between 1 second and `Time.fixedDeltaTime`. High Precision Mode takes 60 times longer to compute. +- **Predict GravityVolume Intersections** - Future trajectories may escape or enter gravity volumes of different celestial bodies. This setting toggles detection of which gravity volumes will be active at any given future time step. Takes longer to compute and allocates more memory. +##### Customization Settings +- **Player Trajectory Color** - Hex RGBA color of trajectory line of the player. +- **Ship Trajectory Color** - Hex RGBA color of trajectory line of the ship. +- **Scout Trajectory Color** - Hex RGBA color of trajectory line of the scout. +##### Performance Settings +- **Multithreading** - Shifts computations to a separate thread. +- **RAM To Allocate (Megabytes)** - Allocates roughly specified amount of RAM to reduce the frequency of GC lag spikes. +##### Experimental Settings +- **Parallelization** - Runs simulation of all celestial bodies in parallel. Speeds up computation but makes results inaccurate. + +## Limitations / Things to Improve +- This mod doesn't account for atmospheric drag. I assume it would've been computationally expensive and pretty useless in practice. +- Simulation allocates a lot of memory, causing periodic GC lag spikes. This could be helped by improving the memory footprint or by finding a way to enable Unity's incremental GC. +- To predict future trajectory of a celestial body, this mod first predicts future trajectories of celestial bodies whose gravity affects said body. Because of that, it doesn't work too well with The Hourglass Twins, since they both affect each other causing a mutual recursion. + +*Besides occasional bugfixes, I'm not planning to update this mod further. But if you have a pull request with improvements I'll merge it.* diff --git a/TrajectoryPrediction.sln b/TrajectoryPrediction.sln new file mode 100644 index 0000000..16ded42 --- /dev/null +++ b/TrajectoryPrediction.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrajectoryPrediction", "TrajectoryPrediction\TrajectoryPrediction.csproj", "{87B8CD34-2D94-4320-A706-CAD585DD5000}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {87B8CD34-2D94-4320-A706-CAD585DD5000}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {87B8CD34-2D94-4320-A706-CAD585DD5000}.Debug|Any CPU.Build.0 = Debug|Any CPU + {87B8CD34-2D94-4320-A706-CAD585DD5000}.Release|Any CPU.ActiveCfg = Release|Any CPU + {87B8CD34-2D94-4320-A706-CAD585DD5000}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/TrajectoryPrediction/AstroObjectPatch.cs b/TrajectoryPrediction/AstroObjectPatch.cs new file mode 100644 index 0000000..022daa6 --- /dev/null +++ b/TrajectoryPrediction/AstroObjectPatch.cs @@ -0,0 +1,15 @@ +using HarmonyLib; + +namespace TrajectoryPrediction; + +[HarmonyPatch(typeof(AstroObject))] +public class AstroObjectPatch +{ + [HarmonyPostfix] + [HarmonyPatch("Awake")] + // ReSharper disable once InconsistentNaming + private static void AstroObject_Awake(AstroObject __instance) + { + __instance.gameObject.AddComponent();; + } +} \ No newline at end of file diff --git a/TrajectoryPrediction/AstroObjectTrajectory.cs b/TrajectoryPrediction/AstroObjectTrajectory.cs new file mode 100644 index 0000000..10feebf --- /dev/null +++ b/TrajectoryPrediction/AstroObjectTrajectory.cs @@ -0,0 +1,124 @@ +using System; +using UnityEngine; + +namespace TrajectoryPrediction; + +public class AstroObjectTrajectory : MonoBehaviour +{ + private AstroObject _astroObject; + private OWRigidbody _body; + private GravityVolume _gravityVolume; + private float _triggerRadius; + private bool _updatedThisFrame; + private Vector3[] _trajectory; + private Vector3[] _trajectoryCache; + private Vector3 _framePosition; + private Vector3 _frameVelocity; + + private void Start() + { + _astroObject = GetComponent(); + _body = _astroObject.GetOWRigidbody(); + _gravityVolume = _astroObject.GetGravityVolume(); + TrajectoryPrediction.AstroObjectToTrajectoryMap[_astroObject] = this; + if (_gravityVolume) + { + if (_gravityVolume.GetOWTriggerVolume().GetShape()) + _triggerRadius = _gravityVolume.GetOWTriggerVolume().GetShape().localBounds.radius; + if (_gravityVolume.GetOWTriggerVolume().GetOWCollider()) + _triggerRadius = ((SphereCollider)_gravityVolume.GetOWTriggerVolume().GetOWCollider().GetCollider()).radius; + + TrajectoryPrediction.GravityVolumeToTrajectoryMap[_gravityVolume] = this; + TrajectoryPrediction.AddTrajectory(this); + } + + ApplyConfig(); + + TrajectoryPrediction.OnConfigUpdate += ApplyConfig; + TrajectoryPrediction.OnBeginFrame += BeginFrame; + TrajectoryPrediction.OnEndFrame += EndFrame; + } + + private void OnDestroy() + { + TrajectoryPrediction.AstroObjectToTrajectoryMap.Remove(_astroObject); + if (_gravityVolume) + { + TrajectoryPrediction.GravityVolumeToTrajectoryMap.Remove(_gravityVolume); + TrajectoryPrediction.RemoveTrajectory(this); + } + + TrajectoryPrediction.OnConfigUpdate -= ApplyConfig; + TrajectoryPrediction.OnBeginFrame -= BeginFrame; + TrajectoryPrediction.OnEndFrame -= EndFrame; + } + + private void ApplyConfig() + { + _trajectory = new Vector3[TrajectoryPrediction.StepsToSimulate]; + _trajectoryCache = new Vector3[TrajectoryPrediction.StepsToSimulate]; + } + + private void BeginFrame() + { + _updatedThisFrame = false; + if (_body) + { + _framePosition = _body.GetPosition(); + _frameVelocity = _body.GetVelocity(); + } + else + _framePosition = _astroObject.transform.position; + } + + private void EndFrame() + { + _trajectory.CopyTo(_trajectoryCache); + } + + internal void UpdateTrajectory() + { + if (_updatedThisFrame) + return; + + _updatedThisFrame = true; + + if (_body == null || _body.GetAttachedForceDetector() == null) + { + for (var i = 0; i < _trajectory.Length; i++) + _trajectory[i] = _astroObject.transform.position; + } + else + { + if (TrajectoryPrediction.Parallelization) + TrajectoryPrediction.SimulateTrajectoryMultiThreaded(_body, _framePosition, _frameVelocity, _trajectory); + else + TrajectoryPrediction.SimulateTrajectory(_body, _framePosition, _frameVelocity, _trajectory); + } + } + + public Vector3 GetFuturePosition(int step) + { + return _trajectory[Math.Max(step, 0)]; + } + + public Vector3 GetFuturePositionCached(int step) + { + return _trajectoryCache[Math.Max(step, 0)]; + } + + public GravityVolume GetGravityVolume() + { + return _gravityVolume; + } + + public float GetTriggerRadius() + { + return _triggerRadius; + } + + public Vector3 GetFramePosition() + { + return _framePosition; + } +} \ No newline at end of file diff --git a/TrajectoryPrediction/CanvasMapMarkerPatch.cs b/TrajectoryPrediction/CanvasMapMarkerPatch.cs new file mode 100644 index 0000000..f7ba623 --- /dev/null +++ b/TrajectoryPrediction/CanvasMapMarkerPatch.cs @@ -0,0 +1,16 @@ +using HarmonyLib; + +namespace TrajectoryPrediction; + +[HarmonyPatch(typeof(MapMarker))] +public class CanvasMapMarkerPatch +{ + [HarmonyPostfix] + [HarmonyPatch("InitMarker")] + // ReSharper disable once InconsistentNaming + private static void MapMarker_InitMarker(MapMarker __instance) + { + if (__instance._markerType is MapMarker.MarkerType.Ship or MapMarker.MarkerType.Probe or MapMarker.MarkerType.Player) + __instance._canvasMarker.gameObject.AddComponent().SetMarkerType(__instance._markerType); + } +} \ No newline at end of file diff --git a/TrajectoryPrediction/Extensions.cs b/TrajectoryPrediction/Extensions.cs new file mode 100644 index 0000000..b90d0f7 --- /dev/null +++ b/TrajectoryPrediction/Extensions.cs @@ -0,0 +1,24 @@ +using UnityEngine; + +namespace TrajectoryPrediction; + +public static class Extensions +{ + public static Vector3 CalculateForceAccelerationAtFuturePoint(this GravityVolume gravityVolume, Vector3 worldPoint, int step) + { + var delta = gravityVolume.GetTrajectory().GetFuturePosition(step) - worldPoint; + float distance = delta.magnitude; + float gravityMagnitude = gravityVolume.CalculateGravityMagnitude(distance); + return delta / distance * gravityMagnitude; + } + + public static AstroObjectTrajectory GetTrajectory(this GravityVolume gravityVolume) + { + return TrajectoryPrediction.GravityVolumeToTrajectoryMap.ContainsKey(gravityVolume) ? TrajectoryPrediction.GravityVolumeToTrajectoryMap[gravityVolume] : null; + } + + public static AstroObjectTrajectory GetTrajectory(this AstroObject astroObject) + { + return TrajectoryPrediction.AstroObjectToTrajectoryMap.ContainsKey(astroObject) ? TrajectoryPrediction.AstroObjectToTrajectoryMap[astroObject] : null; + } +} \ No newline at end of file diff --git a/TrajectoryPrediction/TrajectoryPrediction.cs b/TrajectoryPrediction/TrajectoryPrediction.cs new file mode 100644 index 0000000..3e2e8c6 --- /dev/null +++ b/TrajectoryPrediction/TrajectoryPrediction.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using HarmonyLib; +using OWML.Common; +using OWML.ModHelper; +using UnityEngine; +using UnityEngine.Scripting; + +namespace TrajectoryPrediction; + +public class TrajectoryPrediction : ModBehaviour +{ + public static int SecondsToPredict { get; private set; } + public static int StepsToSimulate { get; private set; } + public static bool HighPrecisionMode { get; private set; } + public static bool PredictGravityVolumeIntersections { get; private set; } + public static Color PlayerTrajectoryColor { get; private set; } + public static Color ShipTrajectoryColor { get; private set; } + public static Color ScoutTrajectoryColor { get; private set; } + public static bool Multithreading { get; private set; } + public static int RAMToAllocate { get; private set; } + public static bool Parallelization { get; private set; } + + public static event Action OnConfigUpdate; + public static event Action OnBeginFrame; + public static event Action OnEndFrame; + + internal static readonly Dictionary AstroObjectToTrajectoryMap = new(); + internal static readonly Dictionary GravityVolumeToTrajectoryMap = new(); + private static readonly List AstroObjectTrajectories = new(); + private static readonly List TrajectoryVisualizers = new(); + private static readonly List BusyBodies = new(); + private const float MemoryFootprint = 0.3f; + private static bool _active; + private static int _gcHack; + + private void Awake() + { + Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly()); + GlobalMessenger.AddListener("EnterMapView", OnEnterMapView); + GlobalMessenger.AddListener("ExitMapView", OnExitMapView); + } + + private void OnDestroy() + { + GlobalMessenger.RemoveListener("EnterMapView", OnEnterMapView); + GlobalMessenger.RemoveListener("ExitMapView", OnExitMapView); + } + + private static void OnEnterMapView() + { + _active = true; + } + + private static void OnExitMapView() + { + _active = false; + } + + public override void Configure(IModConfig config) + { + SecondsToPredict = Math.Max(config.GetSettingsValue("Seconds To Predict"), 0); + HighPrecisionMode = config.GetSettingsValue("High Precision Mode"); + StepsToSimulate = HighPrecisionMode ? (int)(SecondsToPredict / Time.fixedDeltaTime) : SecondsToPredict; + PredictGravityVolumeIntersections = config.GetSettingsValue("Predict GravityVolume Intersections"); + PlayerTrajectoryColor = ColorUtility.TryParseHtmlString(config.GetSettingsValue("Player Trajectory Color"), out var playerTrajectoryColor) ? playerTrajectoryColor : Color.cyan; + ShipTrajectoryColor = ColorUtility.TryParseHtmlString(config.GetSettingsValue("Ship Trajectory Color"), out var shipTrajectoryColor) ? shipTrajectoryColor : Color.yellow; + ScoutTrajectoryColor = ColorUtility.TryParseHtmlString(config.GetSettingsValue("Scout Trajectory Color"), out var scoutTrajectoryColor) ? scoutTrajectoryColor : Color.white; + Multithreading = config.GetSettingsValue("Multithreading"); + RAMToAllocate = Math.Max(config.GetSettingsValue("RAM To Allocate (Megabytes)"), 0); + Parallelization = config.GetSettingsValue("Parallelization"); + + BusyBodies.Clear(); + OnConfigUpdate?.Invoke(); + BeginFrame(); + } + + private void FixedUpdate() + { + if (!_active) + return; + + if (Multithreading) + { + if (TrajectoryVisualizers.Any(visualizer => visualizer.Busy)) + return; + + EndFrame(); + BeginFrame(); + foreach (var visualizer in TrajectoryVisualizers) + visualizer.Visualize(); + } + else + { + BeginFrame(); + foreach (var visualizer in TrajectoryVisualizers) + visualizer.Visualize(); + EndFrame(); + } + } + + private static void BeginFrame() + { + OnBeginFrame?.Invoke(); + + if (_gcHack < RAMToAllocate / MemoryFootprint) + GarbageCollector.GCMode = GarbageCollector.Mode.Disabled; + } + + private static void EndFrame() + { + OnEndFrame?.Invoke(); + + if (_gcHack < RAMToAllocate / MemoryFootprint) + _gcHack++; + else if (GarbageCollector.GCMode == GarbageCollector.Mode.Disabled) + GarbageCollector.GCMode = GarbageCollector.Mode.Enabled; + } + + public static void SimulateTrajectoryMultiThreaded(OWRigidbody body, Vector3 startingPosition, Vector3 startingVelocity, Vector3[] trajectory, AstroObject referenceAstroObject = null, bool stopOnCollision = false, bool predictVolumeIntersections = false, Action onExit = null) + { + if (BusyBodies.Contains(body)) + { + onExit?.Invoke(); + return; + } + + BusyBodies.Add(body); + + new Thread(() => + { + SimulateTrajectory(body, startingPosition, startingVelocity, trajectory, referenceAstroObject, stopOnCollision, predictVolumeIntersections); + + lock (BusyBodies) + BusyBodies.Remove(body); + + onExit?.Invoke(); + }).Start(); + } + + public static void SimulateTrajectory(OWRigidbody body, Vector3 startingPosition, Vector3 startingVelocity, Vector3[] trajectory, AstroObject referenceAstroObject = null, bool stopOnCollision = false, bool predictVolumeIntersections = false) + { + var position = startingPosition; + var velocity = HighPrecisionMode ? startingVelocity * Time.fixedDeltaTime : startingVelocity; + var forceDetector = body.GetAttachedForceDetector(); + var inheritedDetector = forceDetector._activeInheritedDetector; + GravityVolume[] activeVolumes = forceDetector._activeVolumes.Select(volume => volume as GravityVolume).ToArray(); + + if (referenceAstroObject) + referenceAstroObject.GetTrajectory().UpdateTrajectory(); + + if (predictVolumeIntersections) + { + foreach (var astroObject in AstroObjectTrajectories) + astroObject.UpdateTrajectory(); + } + else + { + foreach (var volume in activeVolumes) + volume.GetTrajectory().UpdateTrajectory(); + + if (inheritedDetector != null) + inheritedDetector._attachedBody.GetReferenceFrame().GetAstroObject().GetTrajectory().UpdateTrajectory(); + } + + trajectory[0] = startingPosition; + + var collision = false; + + for (var step = 1; step < trajectory.Length; step++) + { + if (stopOnCollision && collision) + { + trajectory[step] = Vector3.zero; + continue; + } + + if (predictVolumeIntersections) + activeVolumes = AstroObjectTrajectories.Where(astroObject => Vector3.Distance(position, astroObject.GetFuturePosition(step)) < astroObject.GetTriggerRadius()).Select(astroObject => astroObject.GetGravityVolume()).ToArray(); + + var acceleration = GetAccelerationAtFutureWorldPoint(activeVolumes, position, step, forceDetector._fieldMultiplier, () => collision = true); + + if (inheritedDetector != null) + { + GravityVolume[] inheritedDetectorActiveVolumes = inheritedDetector._activeVolumes.Select(volume => volume as GravityVolume).ToArray(); + var inheritedDetectorFuturePosition = inheritedDetector._attachedBody.GetReferenceFrame().GetAstroObject().GetTrajectory().GetFuturePosition(step); + acceleration += GetAccelerationAtFutureWorldPoint(inheritedDetectorActiveVolumes, inheritedDetectorFuturePosition, step, inheritedDetector._fieldMultiplier); + } + + if (HighPrecisionMode) + acceleration *= Time.fixedDeltaTime * Time.fixedDeltaTime; + + velocity += acceleration; + position += velocity; + trajectory[step] = position; + } + } + + private static Vector3 GetAccelerationAtFutureWorldPoint(IEnumerable gravityVolumes, Vector3 worldPoint, int step, float multiplier, Action collisionCallback = null) + { + var acceleration = Vector3.zero; + foreach (var gravityVolume in gravityVolumes) + { + if (Vector3.Distance(worldPoint, gravityVolume.GetTrajectory().GetFuturePosition(step - 1)) < gravityVolume._upperSurfaceRadius) + collisionCallback?.Invoke(); + + acceleration += gravityVolume.CalculateForceAccelerationAtFuturePoint(worldPoint, step) * multiplier; + } + + return acceleration; + } + + public static void AddTrajectory(AstroObjectTrajectory astroObject) + { + AstroObjectTrajectories.Add(astroObject); + } + + public static void RemoveTrajectory(AstroObjectTrajectory astroObject) + { + AstroObjectTrajectories.Remove(astroObject); + } + + public static void AddVisualizer(TrajectoryVisualizer visualizer) + { + TrajectoryVisualizers.Add(visualizer); + } + + public static void RemoveVisualizer(TrajectoryVisualizer visualizer) + { + TrajectoryVisualizers.Remove(visualizer); + } +} \ No newline at end of file diff --git a/TrajectoryPrediction/TrajectoryPrediction.csproj b/TrajectoryPrediction/TrajectoryPrediction.csproj new file mode 100644 index 0000000..76622f8 --- /dev/null +++ b/TrajectoryPrediction/TrajectoryPrediction.csproj @@ -0,0 +1,30 @@ + + + net48 + latest + Copyright © 2022 Oleg Skutte + true + false + false + false + MSB3270 + + + none + + + + + + + + PreserveNewest + + + PreserveNewest + + + + + + diff --git a/TrajectoryPrediction/TrajectoryVisualizer.cs b/TrajectoryPrediction/TrajectoryVisualizer.cs new file mode 100644 index 0000000..0f8653c --- /dev/null +++ b/TrajectoryPrediction/TrajectoryVisualizer.cs @@ -0,0 +1,196 @@ +using System; +using System.Linq; +using System.Threading; +using UnityEngine; + +namespace TrajectoryPrediction; + +public class TrajectoryVisualizer : MonoBehaviour +{ + public bool Busy { get; private set; } + private CanvasMapMarker _marker; + private MapMarker.MarkerType _markerType; + private LineRenderer _lineRenderer; + private OWRigidbody _body; + private ForceDetector _forceDetector; + private Vector3[] _trajectory; + private Vector3[] _trajectoryCached; + private float _timeSinceUpdate; + private Vector3 _framePosition; + private Vector3 _frameVelocity; + + private void Start() + { + _marker = GetComponent(); + _body = _marker._rigidbodyTarget; + _forceDetector = _body.GetAttachedForceDetector(); + _lineRenderer = gameObject.AddComponent(); + _lineRenderer.useWorldSpace = true; + var mapSatelliteLine = FindObjectOfType().GetComponent(); + _lineRenderer.material = mapSatelliteLine.material; + _lineRenderer.textureMode = mapSatelliteLine.textureMode; + TrajectoryPrediction.AddVisualizer(this); + ApplyConfig(); + + GlobalMessenger.AddListener("EnterMapView", OnEnterMapView); + GlobalMessenger.AddListener("ExitMapView", OnExitMapView); + TrajectoryPrediction.OnConfigUpdate += ApplyConfig; + TrajectoryPrediction.OnBeginFrame += BeginFrame; + TrajectoryPrediction.OnEndFrame += EndFrame; + _marker.OnMarkerChangeVisibility += SetVisibility; + + if (_markerType == MapMarker.MarkerType.Player) + { + GlobalMessenger.AddListener("EnterShip", OnEnterShip); + GlobalMessenger.AddListener("ExitShip", OnExitShip); + OnEnterShip(); + } + } + + private void OnDestroy() + { + TrajectoryPrediction.RemoveVisualizer(this); + GlobalMessenger.RemoveListener("EnterMapView", OnEnterMapView); + GlobalMessenger.RemoveListener("ExitMapView", OnExitMapView); + TrajectoryPrediction.OnConfigUpdate -= ApplyConfig; + TrajectoryPrediction.OnBeginFrame -= BeginFrame; + TrajectoryPrediction.OnEndFrame -= EndFrame; + _marker.OnMarkerChangeVisibility -= SetVisibility; + + if (_markerType == MapMarker.MarkerType.Player) + { + GlobalMessenger.RemoveListener("EnterShip", OnEnterShip); + GlobalMessenger.RemoveListener("ExitShip", OnExitShip); + } + } + + internal void SetMarkerType(MapMarker.MarkerType markerType) + { + _markerType = markerType; + } + + private void SetVisibility(bool value) + { + _lineRenderer.enabled = value; + } + + private void ApplyConfig() + { + _trajectory = new Vector3[TrajectoryPrediction.StepsToSimulate]; + _trajectoryCached = new Vector3[TrajectoryPrediction.StepsToSimulate]; + _lineRenderer.positionCount = TrajectoryPrediction.SecondsToPredict; + Busy = false; + switch (_markerType) + { + case MapMarker.MarkerType.Player: + _lineRenderer.startColor = TrajectoryPrediction.PlayerTrajectoryColor; + _lineRenderer.endColor = new Color(TrajectoryPrediction.PlayerTrajectoryColor.r, TrajectoryPrediction.PlayerTrajectoryColor.g, TrajectoryPrediction.PlayerTrajectoryColor.b, 0); + break; + case MapMarker.MarkerType.Ship: + _lineRenderer.startColor = TrajectoryPrediction.ShipTrajectoryColor; + _lineRenderer.endColor = new Color(TrajectoryPrediction.ShipTrajectoryColor.r, TrajectoryPrediction.ShipTrajectoryColor.g, TrajectoryPrediction.ShipTrajectoryColor.b, 0); + break; + case MapMarker.MarkerType.Probe: + _lineRenderer.startColor = TrajectoryPrediction.ScoutTrajectoryColor; + _lineRenderer.endColor = new Color(TrajectoryPrediction.ScoutTrajectoryColor.r, TrajectoryPrediction.ScoutTrajectoryColor.g, TrajectoryPrediction.ScoutTrajectoryColor.b, 0); + break; + default: + throw new ArgumentOutOfRangeException(nameof(_markerType)); + } + } + + private void BeginFrame() + { + _framePosition = _body.GetPosition(); + _frameVelocity = _body.GetVelocity(); + _timeSinceUpdate = 0; + } + + private void EndFrame() + { + _trajectory.CopyTo(_trajectoryCached); + } + + private void OnEnterMapView() + { + _lineRenderer.enabled = _marker.IsVisible(); + Busy = false; + } + + private void OnExitMapView() + { + _lineRenderer.enabled = false; + } + + private void OnEnterShip() + { + _body = Locator.GetShipBody(); + _forceDetector = _body.GetAttachedForceDetector(); + } + + private void OnExitShip() + { + _body = Locator.GetPlayerBody(); + _forceDetector = _body.GetAttachedForceDetector(); + } + + public void Visualize() + { + if (!_lineRenderer.enabled) + return; + + if (_forceDetector._activeVolumes.Count == 0 || _forceDetector._activeVolumes.All(volume => volume is not GravityVolume)) + { + if (TrajectoryPrediction.Multithreading) + { + Busy = true; + new Thread(() => + { + for (var i = 0; i < _trajectory.Length; i++) + _trajectory[i] = Vector3.zero; + + Busy = false; + }).Start(); + } + else + for (var i = 0; i < _trajectory.Length; i++) + _trajectory[i] = _framePosition; + } + else + { + if (TrajectoryPrediction.Multithreading) + { + Busy = true; + TrajectoryPrediction.SimulateTrajectoryMultiThreaded(_body, _framePosition, _frameVelocity, _trajectory, Locator.GetReferenceFrame()?.GetAstroObject(), true, TrajectoryPrediction.PredictGravityVolumeIntersections, () => Busy = false); + } + else + TrajectoryPrediction.SimulateTrajectory(_body, _framePosition, _frameVelocity, _trajectory, Locator.GetReferenceFrame()?.GetAstroObject(), true, TrajectoryPrediction.PredictGravityVolumeIntersections); + } + } + + private void Update() + { + if (!_lineRenderer.enabled) + return; + + _timeSinceUpdate += Time.deltaTime; + + var referenceFrame = Locator.GetReferenceFrame()?.GetAstroObject() ?? Locator._sun.GetOWRigidbody().GetReferenceFrame().GetAstroObject(); + + for (var i = 0; i < _lineRenderer.positionCount; i++) + { + int step = TrajectoryPrediction.HighPrecisionMode ? Mathf.Min((int)((i + _timeSinceUpdate) / Time.fixedDeltaTime), _trajectoryCached.Length - 1) : i; + + if (_trajectoryCached[step] == Vector3.zero) + { + _lineRenderer.SetPosition(i, _lineRenderer.GetPosition(Mathf.Max(0, i - 1))); + continue; + } + + var position = _trajectoryCached[step] - referenceFrame.GetTrajectory().GetFuturePositionCached(step) + referenceFrame.GetOWRigidbody().GetPosition(); + _lineRenderer.SetPosition(i, position); + } + + _lineRenderer.widthMultiplier = Vector3.Distance(_body.GetPosition(), Locator.GetActiveCamera().transform.position) / 500f; + } +} \ No newline at end of file diff --git a/TrajectoryPrediction/default-config.json b/TrajectoryPrediction/default-config.json new file mode 100644 index 0000000..a15b18b --- /dev/null +++ b/TrajectoryPrediction/default-config.json @@ -0,0 +1,26 @@ +{ + "enabled": true, + "settings": { + "Simulation Settings": { + "type": "separator" + }, + "Seconds To Predict": 120, + "High Precision Mode": true, + "Predict GravityVolume Intersections": true, + "Customization Settings": { + "type": "separator" + }, + "Player Trajectory Color": "#00FFFFFF", + "Ship Trajectory Color": "#FFFF00FF", + "Scout Trajectory Color": "#FFFFFFFF", + "Performance Settings": { + "type": "separator" + }, + "Multithreading": true, + "RAM To Allocate (Megabytes)": 256, + "Experimental Settings": { + "type": "separator" + }, + "Parallelization": false + } +} \ No newline at end of file diff --git a/TrajectoryPrediction/manifest.json b/TrajectoryPrediction/manifest.json new file mode 100644 index 0000000..645456c --- /dev/null +++ b/TrajectoryPrediction/manifest.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://raw.githubusercontent.com/ow-mods/owml/master/schemas/manifest_schema.json", + "filename": "TrajectoryPrediction.dll", + "author": "Oleg Skutte", + "name": "Trajectory Prediction", + "uniqueName": "OlegSkutte.TrajectoryPrediction", + "version": "0.6.0", + "owmlVersion": "2.7.3", + "dependencies": [] +}