From 90018f10085cdc1913114e56b49d69e5fd770661 Mon Sep 17 00:00:00 2001
From: Graicc <33105645+Graicc@users.noreply.github.com>
Date: Fri, 11 Jun 2021 19:33:47 -0400
Subject: [PATCH] Initial commit
---
.github/workflows/main.yml | 36 +++
.gitignore | 388 +++++++++++++++++++++++++
LICENSE | 21 ++
README.md | 45 +++
SampleProject/App.xaml | 9 +
SampleProject/App.xaml.cs | 17 ++
SampleProject/AssemblyInfo.cs | 10 +
SampleProject/MainWindow.xaml | 14 +
SampleProject/MainWindow.xaml.cs | 113 +++++++
SampleProject/SampleProject.csproj | 13 +
WriteAPI.sln | 35 +++
WriteAPI/CameraTransform.cs | 26 ++
WriteAPI/ConfigurationManager.cs | 73 +++++
WriteAPI/GameInterface.cs | 104 +++++++
WriteAPI/Listener.cs | 110 +++++++
WriteAPI/Program.cs | 29 ++
WriteAPI/QuaternionContractResolver.cs | 22 ++
WriteAPI/WriteAPI.csproj | 20 ++
WriteAPI/app.manifest | 76 +++++
WriteAPI/config.json | 4 +
20 files changed, 1165 insertions(+)
create mode 100644 .github/workflows/main.yml
create mode 100644 .gitignore
create mode 100644 LICENSE
create mode 100644 README.md
create mode 100644 SampleProject/App.xaml
create mode 100644 SampleProject/App.xaml.cs
create mode 100644 SampleProject/AssemblyInfo.cs
create mode 100644 SampleProject/MainWindow.xaml
create mode 100644 SampleProject/MainWindow.xaml.cs
create mode 100644 SampleProject/SampleProject.csproj
create mode 100644 WriteAPI.sln
create mode 100644 WriteAPI/CameraTransform.cs
create mode 100644 WriteAPI/ConfigurationManager.cs
create mode 100644 WriteAPI/GameInterface.cs
create mode 100644 WriteAPI/Listener.cs
create mode 100644 WriteAPI/Program.cs
create mode 100644 WriteAPI/QuaternionContractResolver.cs
create mode 100644 WriteAPI/WriteAPI.csproj
create mode 100644 WriteAPI/app.manifest
create mode 100644 WriteAPI/config.json
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..ac36e58
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,36 @@
+name: CD
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+ branches: [master]
+
+ workflow_dispatch:
+
+jobs:
+ build:
+ runs-on: windows-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup .NET Core
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: '3.1.x'
+ - name: Install dependencies
+ run: dotnet restore
+ - name: Build
+ run: |
+ dotnet publish -c Release
+ copy LICENSE WriteAPI\bin\Release\netcoreapp3.1\publish\
+ - name: Upload Write API
+ uses: actions/upload-artifact@v2
+ with:
+ name: Write API
+ path: WriteAPI/bin/Release/netcoreapp3.1/publish/
+ - name: Upload Sample Project
+ uses: actions/upload-artifact@v2
+ with:
+ name: Sample Project
+ path: SampleProject/bin/Release/netcoreapp3.1/publish/
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..34c8dee
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,388 @@
+## 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/master/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
+
+# Nuget personal access tokens and Credentials
+nuget.config
+
+# 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 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/
+
+# 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
+.idea/
+*.sln.iml
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..2f61bc0
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Graic
+
+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..adadc04
--- /dev/null
+++ b/README.md
@@ -0,0 +1,45 @@
+# WriteAPI
+
+A tool that provides an API for external programs to modify the location of the spectator camera in Echo VR.
+
+## How It Works
+
+WriteAPI works by directly writing values to Echo's memory using [memory.dll](https://github.com/erfg12/memory.dll/). The offsets for the multi-level pointers used are stored in the `config.json` file.
+
+## Usage
+
+This program does nothing on its own, but instead provides a way for other programs to easily modify the in game camera.
+
+For camera writing to work, Echo must be in "Free Cam" mode, activated by pressing `c` while in the 2d spectator view.
+
+The camera transform consists of a vector for position and a quaternion for rotation.
+The position `(0,0,0)` is the middle of the arena, with +Y being up and +Z being towards the orange goal.
+The rotation `(0,0,0,1)` points towards the orange goal, in the +Z direction.
+
+In order to update the camera transform, send a POST request to `127.0.0.1:6723` with a json body following the following format:
+
+```
+{
+ "position": {
+ "X":0.0,
+ "Y":5.68,
+ "Z":29
+ },
+ "rotation": {
+ "X":0.0,
+ "Y":-0.98,
+ "Z":0.18,
+ "W":0.0
+ }
+}
+```
+
+In order to get the camera transform, send a GET request to `127.0.0.1:6723`. The response will be json in the same format as the above.
+
+## Sample Project
+
+The repository includes a sample project that makes use of the API to move the camera. It is a .NET Core WPF project that interpolates the camera over a user defined path.
+
+## Disclaimer
+
+The use of this software may violate the End User License Agreement for Echo VR, available [here](http://www.readyatdawn.com/eula-echo-vr/). Per the `LICENSE`, I am not responsible for any consequences of the use of this software.
diff --git a/SampleProject/App.xaml b/SampleProject/App.xaml
new file mode 100644
index 0000000..5153bc3
--- /dev/null
+++ b/SampleProject/App.xaml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/SampleProject/App.xaml.cs b/SampleProject/App.xaml.cs
new file mode 100644
index 0000000..020667e
--- /dev/null
+++ b/SampleProject/App.xaml.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Data;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace SampleProject
+{
+ ///
+ /// Interaction logic for App.xaml
+ ///
+ public partial class App : Application
+ {
+ }
+}
diff --git a/SampleProject/AssemblyInfo.cs b/SampleProject/AssemblyInfo.cs
new file mode 100644
index 0000000..27503ab
--- /dev/null
+++ b/SampleProject/AssemblyInfo.cs
@@ -0,0 +1,10 @@
+using System.Windows;
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
diff --git a/SampleProject/MainWindow.xaml b/SampleProject/MainWindow.xaml
new file mode 100644
index 0000000..04e4e1a
--- /dev/null
+++ b/SampleProject/MainWindow.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/SampleProject/MainWindow.xaml.cs b/SampleProject/MainWindow.xaml.cs
new file mode 100644
index 0000000..0af706c
--- /dev/null
+++ b/SampleProject/MainWindow.xaml.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Net.Http;
+using System.Numerics;
+using System.Threading;
+using System.Windows;
+using Newtonsoft.Json;
+
+namespace SampleProject
+{
+ ///
+ /// Interaction logic for MainWindow.xaml
+ ///
+ public partial class MainWindow : Window
+ {
+ const string Endpoint = "http://127.0.0.1:6723/";
+
+ static readonly HttpClient client = new HttpClient();
+
+ public class CameraTransform
+ {
+ public Vector3 position = Vector3.Zero;
+ public Quaternion rotation = Quaternion.Identity;
+
+ public CameraTransform() { }
+
+ public CameraTransform(Vector3 pos, Quaternion rot)
+ {
+ position = pos;
+ rotation = rot;
+ }
+ }
+
+ static CameraTransform start = new CameraTransform();
+ static CameraTransform end = new CameraTransform();
+
+ static Thread followPathThread = new Thread(FollowPathThread);
+
+ public MainWindow()
+ {
+ InitializeComponent();
+
+ Closed += new EventHandler(MainWindow_Closed);
+ }
+
+ void MainWindow_Closed(object sender, EventArgs e)
+ {
+ client.Dispose();
+ }
+
+ private void SetStart_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ HttpResponseMessage response = client.GetAsync(Endpoint).Result;
+ string data = response.Content.ReadAsStringAsync().Result;
+ start = JsonConvert.DeserializeObject(data);
+ } catch (Exception E)
+ {
+ Console.WriteLine(E.Message);
+ }
+ }
+
+ private void SetEnd_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ HttpResponseMessage response = client.GetAsync(Endpoint).Result;
+ string data = response.Content.ReadAsStringAsync().Result;
+ end = JsonConvert.DeserializeObject(data);
+ } catch (Exception E)
+ {
+ Console.WriteLine(E.Message);
+ }
+ }
+
+ private void FollowPath_Click(object sender, RoutedEventArgs e)
+ {
+ if (!followPathThread.IsAlive)
+ {
+ followPathThread = new Thread(FollowPathThread);
+ followPathThread.Start();
+ }
+ }
+
+ static void FollowPathThread()
+ {
+ float duration = 3;
+
+ DateTime startTime = DateTime.Now;
+ DateTime currentTime = startTime;
+ float t = 0;
+ while (t < 1)
+ {
+ currentTime = DateTime.Now;
+ float elapsed = (currentTime.Ticks - startTime.Ticks) / 10000000f;
+ t = elapsed / duration;
+
+ Vector3 newPos = Vector3.Lerp(start.position, end.position, t);
+ Quaternion newRot = Quaternion.Slerp(start.rotation, end.rotation, t);
+
+ CameraTransform newTransform = new CameraTransform(newPos, newRot);
+
+ string data = JsonConvert.SerializeObject(newTransform);
+
+ using (HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("POST"), Endpoint))
+ {
+ request.Content = new StringContent(data);
+ client.SendAsync(request).Wait();
+ }
+ }
+ }
+ }
+}
diff --git a/SampleProject/SampleProject.csproj b/SampleProject/SampleProject.csproj
new file mode 100644
index 0000000..c25d6e4
--- /dev/null
+++ b/SampleProject/SampleProject.csproj
@@ -0,0 +1,13 @@
+
+
+
+ WinExe
+ netcoreapp3.1
+ true
+
+
+
+
+
+
+
diff --git a/WriteAPI.sln b/WriteAPI.sln
new file mode 100644
index 0000000..83ecec4
--- /dev/null
+++ b/WriteAPI.sln
@@ -0,0 +1,35 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.30804.86
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WriteAPI", "WriteAPI\WriteAPI.csproj", "{21639BC0-E56E-4B2C-BE01-D81FBF0D7E93}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleProject", "SampleProject\SampleProject.csproj", "{A53DBF3B-2AF4-4592-BD43-2CF470CC70DF}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {21639BC0-E56E-4B2C-BE01-D81FBF0D7E93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {21639BC0-E56E-4B2C-BE01-D81FBF0D7E93}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {21639BC0-E56E-4B2C-BE01-D81FBF0D7E93}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {21639BC0-E56E-4B2C-BE01-D81FBF0D7E93}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8D81F390-8FAB-49BB-BECB-96F6D26E4BFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8D81F390-8FAB-49BB-BECB-96F6D26E4BFB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8D81F390-8FAB-49BB-BECB-96F6D26E4BFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8D81F390-8FAB-49BB-BECB-96F6D26E4BFB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A53DBF3B-2AF4-4592-BD43-2CF470CC70DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A53DBF3B-2AF4-4592-BD43-2CF470CC70DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A53DBF3B-2AF4-4592-BD43-2CF470CC70DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A53DBF3B-2AF4-4592-BD43-2CF470CC70DF}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {F7C12DD1-5496-4AFE-81BB-BE945BF38138}
+ EndGlobalSection
+EndGlobal
diff --git a/WriteAPI/CameraTransform.cs b/WriteAPI/CameraTransform.cs
new file mode 100644
index 0000000..af325f8
--- /dev/null
+++ b/WriteAPI/CameraTransform.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Numerics;
+
+namespace WriteAPI
+{
+ public class CameraTransform
+ {
+ public Vector3 position = Vector3.Zero;
+ public Quaternion rotation = Quaternion.Identity;
+
+ public CameraTransform() { }
+
+ public CameraTransform(Vector3 pos, Quaternion rot)
+ {
+ position = pos;
+ rotation = rot;
+ }
+
+ public override string ToString()
+ {
+ return $"CameraTransform: {position} {rotation}";
+ }
+ }
+}
diff --git a/WriteAPI/ConfigurationManager.cs b/WriteAPI/ConfigurationManager.cs
new file mode 100644
index 0000000..15e3fec
--- /dev/null
+++ b/WriteAPI/ConfigurationManager.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Newtonsoft.Json;
+using System.IO;
+using System.Net.Http;
+
+namespace WriteAPI
+{
+ public static class ConfigurationManager
+ {
+ const string RemoteConfigPath = @"https://raw.githubusercontent.com/Graicc/WriteAPI/master/WriteAPI/config.json";
+ const string ConfigPath = "config.json";
+
+ public struct Config
+ {
+ public string cameraPositionAddress;
+ public string cameraRotationAddress;
+ }
+
+ public static Config config;
+
+ public static void UpdateConfig(bool updateConfig = true)
+ {
+ if (updateConfig)
+ {
+ TryUpdateConfigFile();
+ }
+
+ config = ReadLocalConfig();
+ }
+
+ static void TryUpdateConfigFile()
+ {
+ Console.WriteLine("Checking for updated config file...");
+ using (var httpClient = new HttpClient()) {
+ var response = httpClient.GetAsync(RemoteConfigPath).Result;
+ if (response.IsSuccessStatusCode)
+ {
+ using (Stream output = File.OpenWrite(ConfigPath))
+ {
+ response.Content.CopyToAsync(output).Wait();
+ }
+ Console.WriteLine("Updated config file");
+ } else
+ {
+ Console.WriteLine("Could not get remote config file");
+ }
+ }
+ }
+
+ static Config ReadLocalConfig()
+ {
+#if DEBUG
+ Console.WriteLine("Reading config");
+#endif
+ string data;
+
+ using (StreamReader reader = File.OpenText(ConfigPath))
+ {
+ data = reader.ReadToEnd();
+ }
+
+ Config localConfig = JsonConvert.DeserializeObject(data);
+
+#if DEBUG
+ Console.WriteLine($"Position address: {localConfig.cameraPositionAddress} | Rotation address: {localConfig.cameraRotationAddress}");
+#endif
+
+ return localConfig;
+ }
+ }
+}
diff --git a/WriteAPI/GameInterface.cs b/WriteAPI/GameInterface.cs
new file mode 100644
index 0000000..9c81111
--- /dev/null
+++ b/WriteAPI/GameInterface.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Numerics;
+using Memory;
+
+namespace WriteAPI
+{
+ public static class GameInterface
+ {
+ const string GameName = "echovr.exe";
+
+ public static CameraTransform CameraTransform
+ {
+ get
+ {
+ return GetTransform();
+ }
+ set
+ {
+ UpdateTransform(value);
+ }
+ }
+
+ static Mem mem = new Mem();
+
+ public static void Hook()
+ {
+ Console.WriteLine("Hooking Echo VR...");
+
+ bool hooked;
+ do
+ {
+ hooked = mem.OpenProcess(GameName);
+ if (!hooked)
+ {
+ Console.WriteLine("Could not find Echo VR process, make sure Echo VR is running");
+ Console.WriteLine("Trying again in 5 seconds...");
+ System.Threading.Thread.Sleep(5000);
+ }
+ } while (!hooked);
+
+ Console.WriteLine("Hooked Echo VR");
+ }
+
+ static void UpdateTransform(CameraTransform transform)
+ {
+ transform.rotation = Quaternion.Normalize(transform.rotation);
+ WriteVector(ConfigurationManager.config.cameraPositionAddress, transform.position);
+ WriteQuaternion(ConfigurationManager.config.cameraRotationAddress, transform.rotation);
+#if DEBUG
+ Console.WriteLine($"Wrote new camera transform: {transform}");
+#endif
+ }
+
+ static CameraTransform GetTransform()
+ {
+ Vector3 position = ReadVector(ConfigurationManager.config.cameraPositionAddress);
+ Quaternion rotation = ReadQuaternion(ConfigurationManager.config.cameraRotationAddress);
+ return new CameraTransform(position, rotation);
+ }
+
+ static string AddressWithOffset(string address, int offset)
+ {
+ string baseString = address.Substring(0, address.Length - 3);
+ string end = address.Substring(address.Length - 2);
+
+ int preoffset = Convert.ToInt32($"0x{end}", 16);
+ return baseString + (preoffset + offset).ToString("X2");
+ }
+
+ static void WriteVector(string address, Vector3 vector)
+ {
+ mem.WriteMemory(AddressWithOffset(address, 0), "float", vector.X.ToString());
+ mem.WriteMemory(AddressWithOffset(address, 4), "float", vector.Y.ToString());
+ mem.WriteMemory(AddressWithOffset(address, 8), "float", vector.Z.ToString());
+ }
+
+ static void WriteQuaternion(string address, Quaternion quaternion)
+ {
+ mem.WriteMemory(AddressWithOffset(address, 0), "float", quaternion.X.ToString());
+ mem.WriteMemory(AddressWithOffset(address, 4), "float", quaternion.Y.ToString());
+ mem.WriteMemory(AddressWithOffset(address, 8), "float", quaternion.Z.ToString());
+ mem.WriteMemory(AddressWithOffset(address, 12), "float", quaternion.W.ToString());
+ }
+
+ static Vector3 ReadVector(string address)
+ {
+ float x = mem.ReadFloat(AddressWithOffset(address, 0));
+ float y = mem.ReadFloat(AddressWithOffset(address, 4));
+ float z = mem.ReadFloat(AddressWithOffset(address, 8));
+ return new Vector3(x, y, z);
+ }
+
+ static Quaternion ReadQuaternion(string address)
+ {
+ float x = mem.ReadFloat(AddressWithOffset(address, 0));
+ float y = mem.ReadFloat(AddressWithOffset(address, 4));
+ float z = mem.ReadFloat(AddressWithOffset(address, 8));
+ float w = mem.ReadFloat(AddressWithOffset(address, 12));
+ return new Quaternion(x, y, z, w);
+ }
+ }
+}
diff --git a/WriteAPI/Listener.cs b/WriteAPI/Listener.cs
new file mode 100644
index 0000000..a8e8e3f
--- /dev/null
+++ b/WriteAPI/Listener.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Net;
+using Newtonsoft.Json;
+using System.Threading;
+using System.IO;
+
+namespace WriteAPI
+{
+ public static class Listener
+ {
+ // Spark already uses port 6722 for discord OAuth, so we use the next available port
+ public static readonly string[] Prefixes = { "http://127.0.0.1:6723/", "http://localhost:6723/" };
+
+ public static void Start()
+ {
+ Thread listenerThread = new Thread(ListenerThread);
+ listenerThread.Start();
+ }
+
+ static void ListenerThread()
+ {
+ HttpListener listener = new HttpListener();
+
+ foreach (string prefix in Prefixes)
+ {
+ listener.Prefixes.Add(prefix);
+ }
+
+ listener.Start();
+
+ Console.WriteLine($"Listening on {string.Join(", ", Prefixes)}");
+
+ while (true)
+ {
+ HttpListenerContext context = listener.GetContext();
+ HttpListenerRequest request = context.Request;
+
+ if (request.HttpMethod == "GET")
+ {
+ ProcessGet(context);
+ }
+ else if (request.HttpMethod == "POST")
+ {
+ ProcessPost(context);
+ }
+ }
+ }
+
+ static void ProcessGet(HttpListenerContext context)
+ {
+ CameraTransform transform = GameInterface.CameraTransform;
+ JsonSerializerSettings settings = new JsonSerializerSettings()
+ {
+ ContractResolver = QuaternionContractResolver.Instance
+ };
+
+ string data = JsonConvert.SerializeObject(transform, settings) + "\n";
+
+ HttpListenerResponse response = context.Response;
+
+ byte[] buffer = Encoding.UTF8.GetBytes(data);
+ response.ContentLength64 = buffer.Length;
+
+ response.OutputStream.Write(buffer, 0, buffer.Length);
+ response.OutputStream.Close();
+ }
+
+ static void ProcessPost(HttpListenerContext context)
+ {
+ string data;
+ using (StreamReader reader = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding))
+ {
+ data = reader.ReadToEnd();
+ }
+
+ CameraTransform transform;
+ HttpListenerResponse response = context.Response;
+
+ try
+ {
+ transform = JsonConvert.DeserializeObject(data);
+ }
+ catch (JsonException E)
+ {
+ Console.WriteLine("Invalid POST request");
+#if DEBUG
+ Console.WriteLine(E.Message);
+#endif
+ response.StatusCode = 400;
+ response.OutputStream.Close();
+ return;
+ }
+
+ if (transform == null)
+ {
+ Console.WriteLine("Invalid POST request");
+ response.StatusCode = 400;
+ response.OutputStream.Close();
+ return;
+ }
+
+ GameInterface.CameraTransform = transform;
+
+ ProcessGet(context);
+ return;
+ }
+ }
+}
diff --git a/WriteAPI/Program.cs b/WriteAPI/Program.cs
new file mode 100644
index 0000000..cf4b55e
--- /dev/null
+++ b/WriteAPI/Program.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Threading;
+
+namespace WriteAPI
+{
+ public static class Program
+ {
+ static void Main(string[] args)
+ {
+ // --noupdateconfig prevents config from being updated
+ bool updateConfig = true;
+ if (args.Length > 0)
+ {
+ if (args[0] == "--noupdateconfig")
+ {
+ updateConfig = false;
+ }
+ }
+
+ ConfigurationManager.UpdateConfig(updateConfig);
+ Console.WriteLine();
+
+ GameInterface.Hook();
+ Console.WriteLine();
+
+ Listener.Start();
+ }
+ }
+}
diff --git a/WriteAPI/QuaternionContractResolver.cs b/WriteAPI/QuaternionContractResolver.cs
new file mode 100644
index 0000000..a167b8b
--- /dev/null
+++ b/WriteAPI/QuaternionContractResolver.cs
@@ -0,0 +1,22 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
+using System.Reflection;
+using System.Numerics;
+
+namespace WriteAPI
+{
+ public class QuaternionContractResolver : DefaultContractResolver
+ {
+ public static QuaternionContractResolver Instance { get; } = new QuaternionContractResolver();
+
+ protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
+ {
+ JsonProperty property = base.CreateProperty(member, memberSerialization);
+ if (typeof(Quaternion).IsAssignableFrom(member.DeclaringType) && member.Name == nameof(Quaternion.IsIdentity))
+ {
+ property.Ignored = true;
+ }
+ return property;
+ }
+ }
+}
diff --git a/WriteAPI/WriteAPI.csproj b/WriteAPI/WriteAPI.csproj
new file mode 100644
index 0000000..4c5d699
--- /dev/null
+++ b/WriteAPI/WriteAPI.csproj
@@ -0,0 +1,20 @@
+
+
+
+ Exe
+ netcoreapp3.1
+ app.manifest
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/WriteAPI/app.manifest b/WriteAPI/app.manifest
new file mode 100644
index 0000000..47e8f8e
--- /dev/null
+++ b/WriteAPI/app.manifest
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/WriteAPI/config.json b/WriteAPI/config.json
new file mode 100644
index 0000000..7bd2dca
--- /dev/null
+++ b/WriteAPI/config.json
@@ -0,0 +1,4 @@
+{
+ "cameraPositionAddress": "echovr.exe+0x23210E8,0x7688,0x0,0x60",
+ "cameraRotationAddress": "echovr.exe+0x23210E8,0x7688,0x0,0x50"
+}
\ No newline at end of file