├── FH4RP.csproj ├── Helpers ├── Logger.cs └── VehicleDB.cs ├── FH4RP.sln ├── DataStructs ├── Vehicle.cs └── TelemetryData.cs ├── Networking ├── DiscordRPC.cs └── TelemetryServer.cs ├── Program.cs ├── LICENSE ├── readme.md └── .gitignore /FH4RP.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Helpers/Logger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace FH4RP.Helpers 6 | { 7 | public static class Logger 8 | { 9 | public static void Log(string msg) 10 | { 11 | Console.Clear(); 12 | Console.WriteLine(msg); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /FH4RP.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29201.188 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FH4RP", "FH4RP.csproj", "{DA21B4BE-7AAA-4CD9-AA35-963C281B2D9F}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {DA21B4BE-7AAA-4CD9-AA35-963C281B2D9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {DA21B4BE-7AAA-4CD9-AA35-963C281B2D9F}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {DA21B4BE-7AAA-4CD9-AA35-963C281B2D9F}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {DA21B4BE-7AAA-4CD9-AA35-963C281B2D9F}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {6F6515E8-D679-4386-98B5-B1FE5BABDB2D} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /DataStructs/Vehicle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace FH4RP.DataStructs 6 | { 7 | public struct Vehicle 8 | { 9 | 10 | public enum DrivetrainType 11 | { 12 | FWD = 0, 13 | RWD = 1, 14 | AWD = 2 15 | } 16 | 17 | public enum CarClass 18 | { 19 | D = 0, 20 | C = 1, 21 | B = 2, 22 | A = 3, 23 | S1 = 4, 24 | S2 = 5, 25 | X = 6 26 | } 27 | 28 | public int ID { get; set; } 29 | 30 | /// 31 | /// Vehicle Manufacturer 32 | /// 33 | public string Make { get; set; } 34 | 35 | /// 36 | /// Vehicle Model 37 | /// 38 | public string Model { get; set; } 39 | 40 | /// 41 | /// Vehicle Year 42 | /// 43 | public int Year { get; set; } 44 | 45 | public string GetVehicleInfo () 46 | { 47 | return $"{(Year == 0 ? 2019 : Year)} {(Make == "" ? "Unknown" : Make)} {(Model == "" ? "Unknown" : Model)}"; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Networking/DiscordRPC.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using DiscordRPC; 5 | using DiscordRPC.Logging; 6 | using FH4RP.DataStructs; 7 | using FH4RP.Helpers; 8 | 9 | namespace FH4RP.Networking 10 | { 11 | public static class DiscordRPC 12 | { 13 | public static DiscordRpcClient discordClient; 14 | 15 | public static void Initialize() 16 | { 17 | discordClient = new DiscordRpcClient("619736536333811722"); 18 | discordClient.Logger = new ConsoleLogger() { Level = LogLevel.Warning }; 19 | discordClient.OnReady += (sender, e) => { Console.WriteLine($"[Discord] Received ready from {e.User.Username}!"); }; 20 | discordClient.OnPresenceUpdate += (sender, e) => { /*Console.WriteLine("[Discord] Received presence update!");*/ }; 21 | discordClient.Initialize(); 22 | } 23 | 24 | public static void SetPresence(TelemetryData data) 25 | { 26 | discordClient.SetPresence(new RichPresence() 27 | { 28 | Details = VehicleDB.Instance.GetVehicle(data.CarOrdinal).GetVehicleInfo(), 29 | State = $"[{data.CarClass.ToString()} | {data.CarPI}] - {data.GetMPH()} mph", 30 | Assets = new Assets() 31 | { 32 | LargeImageKey = "fm" 33 | } 34 | }); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Timers; 3 | using FH4RP.Helpers; 4 | using FH4RP.Networking; 5 | 6 | namespace FH4RP 7 | { 8 | class Program 9 | { 10 | private static int ServerPort = 9909; 11 | public static TelemetryServer Server { get; private set; } 12 | 13 | static void Main(string[] args) 14 | { 15 | 16 | Console.WriteLine("Forza Horizon 4 | Discord Rich Presence App"); 17 | Console.WriteLine("Developed by digital#0001 | https://dgtl.dev"); 18 | Console.WriteLine(); 19 | Console.WriteLine("If you haven't already, open a command prompt as admin"); 20 | Console.WriteLine("and run the following command to allow Forza Horizon 4's"); 21 | Console.WriteLine("Telemetry Data Out to send to the local PC:"); 22 | Console.WriteLine(); 23 | Console.WriteLine("> CheckNetIsolation.exe LoopbackExempt -a -n=Microsoft.SunriseBaseGame_8wekyb3d8bbwe"); 24 | Console.WriteLine(); 25 | Server = new TelemetryServer(ServerPort); 26 | Server.Start(); 27 | 28 | Networking.DiscordRPC.Initialize(); 29 | 30 | var timer = new Timer(1250); 31 | timer.Elapsed += (sender, a) => { Networking.DiscordRPC.SetPresence(Server.LastUpdate); }; 32 | timer.Start(); 33 | 34 | // Wait for keypress to close 35 | Console.Read(); 36 | timer.Stop(); 37 | Networking.DiscordRPC.discordClient.Dispose(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, jaiden-d 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Helpers/VehicleDB.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.IO; 5 | using FH4RP.DataStructs; 6 | using Newtonsoft.Json; 7 | using System.Net; 8 | 9 | namespace FH4RP.Helpers 10 | { 11 | public class VehicleDB 12 | { 13 | 14 | public struct VehicleDBEntry 15 | { 16 | public int id; 17 | public int ordinal; 18 | public string make; 19 | public string model; 20 | public int year; 21 | public string game; 22 | [JsonProperty("class")] public string _class; 23 | public int pi; 24 | public string drivetrain; 25 | public int power; 26 | public int torque; 27 | public int weight; 28 | public int weight_percent; 29 | public int displacement; 30 | public string description; 31 | public int added_by; 32 | public int updated_by; 33 | public string created_at; 34 | public string updated_at; 35 | } 36 | 37 | public static VehicleDB Instance { get; private set; } 38 | 39 | public VehicleDB() 40 | { 41 | if (Instance == null) Instance = this; 42 | else return; 43 | } 44 | 45 | public Vehicle GetVehicle(int vehicleId) 46 | { 47 | Vehicle v = default; 48 | VehicleDBEntry entry = GetVehicleEntry(vehicleId); 49 | v.ID = entry.ordinal; 50 | v.Year = entry.year; 51 | v.Make = entry.make; 52 | v.Model = entry.model; 53 | 54 | string carInfo = $"[{vehicleId}] {v.GetVehicleInfo()}"; 55 | Logger.Log(carInfo + (v.Make == "Unknown" ? "\n\nThis vehicle does not exist in the database!\nPlease consider adding it by going to https://forzadb.dgtl.dev!" : "")); 56 | return v; 57 | } 58 | 59 | private string HttpGet(string uri) 60 | { 61 | string retStr = ""; 62 | HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri); 63 | request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; 64 | try 65 | { 66 | using(HttpWebResponse response = (HttpWebResponse)request.GetResponse()) 67 | using(Stream stream = response.GetResponseStream()) 68 | using(StreamReader reader = new StreamReader(stream)) 69 | { 70 | retStr = reader.ReadToEnd(); 71 | } 72 | } 73 | catch (Exception e) { Logger.Log($"[API] Exception: {e.ToString()}"); } 74 | return retStr; 75 | } 76 | 77 | private VehicleDBEntry GetVehicleEntry(int vehicleId) 78 | { 79 | return JsonConvert.DeserializeObject(HttpGet("https://forzadb.dgtl.dev/api/vehicles/" + vehicleId)); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Discord Rich Presence for Forza Horizon 4 2 | 3 | ## What does this do? 4 | 5 | This program will allow you to display [Discord Rich Presence](https://discordapp.com/developers/docs/rich-presence/how-to#so-what-is-it) information about your current car, performance index, and speed in Discord. 6 | 7 | ## How does it work? 8 | 9 | Forza Horizon 4 has the ability to send car telemetry data out to a server. This is commonly used for racing simulation devices such as dashboards and hydraulic-powered racing seats. This program uses the same telemetry data to show car information in Discord. 10 | 11 | ## Can I use this for Forza Horizon 5? 12 | 13 | Technically yes, but I recommend using my [other project for it](https://github.com/jaiden-d/FH5RP) as I have some other future plans for it. 14 | 15 | ## Running the program 16 | 17 | --- 18 | 19 | **Note:** Due to limitations with Universal Windows Platform (UWP) apps, you ***must*** run the following command in an elevated command prompt window (CMD as admin) if you want to run this program on the same machine that you're playing Forza Horizon 4 on! 20 | 21 | Make sure Forza Horizon 4 is closed before running this command! 22 | 23 | `CheckNetIsolation.exe LoopbackExempt -a -n=Microsoft.SunriseBaseGame_8wekyb3d8bbwe` 24 | 25 | --- 26 | 27 | Steps to configure Rich Presence: 28 | 29 | 1. Download the [latest release](https://github.com/zackdevine/FH4RP/releases) and extract it somewhere on your desktop. 30 | 2. Run the command above (only need to run it once per installation of Forza Horizon 4). 31 | 3. In Forza Horizon 4, load into the game world, then go to `Settings > HUD and Gameplay` and scroll down to the bottom. 32 | - Set `Data Out` to `On` 33 | - Set `Data Out IP Address` to `127.0.0.1` (or whatever local IP address is running the application) 34 | - Set `Data Out IP Port` to `9909` 35 | 4. Start the application! 36 | 37 | You should see some console output in the window that appears. Once you see the message stating that Discord is ready, you're good to go! 38 | 39 | ## Contributing 40 | 41 | The info that shows the vehicle manufacturer and model is pulled from a separate [web API](https://github.com/zackdevine/forzadb) that I created. Recording this data is a very time consuming task, as the only vehicle metadata that Forza returns is a unique ordinal ID for each vehicle. As such, every vehicle needs to be manually entered into the API database to be retrieved from the application. 42 | 43 | If you would like to help out (it would mean a lot!), then please feel free to download the debug version of the application and run it to get the ID of the vehicle you're currently using. New vehicles can be submitted at the [ForzaDB](https://forzadb.dgtl.dev) website. 44 | 45 | If you would like to contribute to this project, feel free to send PRs with updates! 46 | 47 | ## References 48 | 49 | This project could not have been possible without the Turn 10 community and [Lachee's Discord RPC](https://github.com/Lachee/discord-rpc-csharp) package. 50 | 51 | Forza logo from [Forza Horizon 4 Icon Pack - Windows 10 by tfphoenix](https://www.deviantart.com/tfphoenix/art/Forza-Horizon-4-Icon-Pack-Windows-10-767768186) on DeviantArt. 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | 33 | # Visual Studio 2015/2017 cache/options directory 34 | .vs/ 35 | # Uncomment if you have tasks that create the project's static files in wwwroot 36 | #wwwroot/ 37 | 38 | # Visual Studio 2017 auto generated files 39 | Generated\ Files/ 40 | 41 | # MSTest test Results 42 | [Tt]est[Rr]esult*/ 43 | [Bb]uild[Ll]og.* 44 | 45 | # NUnit 46 | *.VisualState.xml 47 | TestResult.xml 48 | nunit-*.xml 49 | 50 | # Build Results of an ATL Project 51 | [Dd]ebugPS/ 52 | [Rr]eleasePS/ 53 | dlldata.c 54 | 55 | # Benchmark Results 56 | BenchmarkDotNet.Artifacts/ 57 | 58 | # .NET Core 59 | project.lock.json 60 | project.fragment.lock.json 61 | artifacts/ 62 | 63 | # StyleCop 64 | StyleCopReport.xml 65 | 66 | # Files built by Visual Studio 67 | *_i.c 68 | *_p.c 69 | *_h.h 70 | *.ilk 71 | *.meta 72 | *.obj 73 | *.iobj 74 | *.pch 75 | *.pdb 76 | *.ipdb 77 | *.pgc 78 | *.pgd 79 | *.rsp 80 | *.sbr 81 | *.tlb 82 | *.tli 83 | *.tlh 84 | *.tmp 85 | *.tmp_proj 86 | *_wpftmp.csproj 87 | *.log 88 | *.vspscc 89 | *.vssscc 90 | .builds 91 | *.pidb 92 | *.svclog 93 | *.scc 94 | 95 | # Chutzpah Test files 96 | _Chutzpah* 97 | 98 | # Visual C++ cache files 99 | ipch/ 100 | *.aps 101 | *.ncb 102 | *.opendb 103 | *.opensdf 104 | *.sdf 105 | *.cachefile 106 | *.VC.db 107 | *.VC.VC.opendb 108 | 109 | # Visual Studio profiler 110 | *.psess 111 | *.vsp 112 | *.vspx 113 | *.sap 114 | 115 | # Visual Studio Trace Files 116 | *.e2e 117 | 118 | # TFS 2012 Local Workspace 119 | $tf/ 120 | 121 | # Guidance Automation Toolkit 122 | *.gpState 123 | 124 | # ReSharper is a .NET coding add-in 125 | _ReSharper*/ 126 | *.[Rr]e[Ss]harper 127 | *.DotSettings.user 128 | 129 | # JustCode is a .NET coding add-in 130 | .JustCode 131 | 132 | # TeamCity is a build add-in 133 | _TeamCity* 134 | 135 | # DotCover is a Code Coverage Tool 136 | *.dotCover 137 | 138 | # AxoCover is a Code Coverage Tool 139 | .axoCover/* 140 | !.axoCover/settings.json 141 | 142 | # Visual Studio code coverage results 143 | *.coverage 144 | *.coveragexml 145 | 146 | # NCrunch 147 | _NCrunch_* 148 | .*crunch*.local.xml 149 | nCrunchTemp_* 150 | 151 | # MightyMoose 152 | *.mm.* 153 | AutoTest.Net/ 154 | 155 | # Web workbench (sass) 156 | .sass-cache/ 157 | 158 | # Installshield output folder 159 | [Ee]xpress/ 160 | 161 | # DocProject is a documentation generator add-in 162 | DocProject/buildhelp/ 163 | DocProject/Help/*.HxT 164 | DocProject/Help/*.HxC 165 | DocProject/Help/*.hhc 166 | DocProject/Help/*.hhk 167 | DocProject/Help/*.hhp 168 | DocProject/Help/Html2 169 | DocProject/Help/html 170 | 171 | # Click-Once directory 172 | publish/ 173 | 174 | # Publish Web Output 175 | *.[Pp]ublish.xml 176 | *.azurePubxml 177 | # Note: Comment the next line if you want to checkin your web deploy settings, 178 | # but database connection strings (with potential passwords) will be unencrypted 179 | *.pubxml 180 | *.publishproj 181 | 182 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 183 | # checkin your Azure Web App publish settings, but sensitive information contained 184 | # in these scripts will be unencrypted 185 | PublishScripts/ 186 | 187 | # NuGet Packages 188 | *.nupkg 189 | # NuGet Symbol Packages 190 | *.snupkg 191 | # The packages folder can be ignored because of Package Restore 192 | **/[Pp]ackages/* 193 | # except build/, which is used as an MSBuild target. 194 | !**/[Pp]ackages/build/ 195 | # Uncomment if necessary however generally it will be regenerated when needed 196 | #!**/[Pp]ackages/repositories.config 197 | # NuGet v3's project.json files produces more ignorable files 198 | *.nuget.props 199 | *.nuget.targets 200 | 201 | # Microsoft Azure Build Output 202 | csx/ 203 | *.build.csdef 204 | 205 | # Microsoft Azure Emulator 206 | ecf/ 207 | rcf/ 208 | 209 | # Windows Store app package directories and files 210 | AppPackages/ 211 | BundleArtifacts/ 212 | Package.StoreAssociation.xml 213 | _pkginfo.txt 214 | *.appx 215 | *.appxbundle 216 | *.appxupload 217 | 218 | # Visual Studio cache files 219 | # files ending in .cache can be ignored 220 | *.[Cc]ache 221 | # but keep track of directories ending in .cache 222 | !?*.[Cc]ache/ 223 | 224 | # Others 225 | ClientBin/ 226 | ~$* 227 | *~ 228 | *.dbmdl 229 | *.dbproj.schemaview 230 | *.jfm 231 | *.pfx 232 | *.publishsettings 233 | orleans.codegen.cs 234 | 235 | # Including strong name files can present a security risk 236 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 237 | #*.snk 238 | 239 | # Since there are multiple workflows, uncomment next line to ignore bower_components 240 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 241 | #bower_components/ 242 | 243 | # RIA/Silverlight projects 244 | Generated_Code/ 245 | 246 | # Backup & report files from converting an old project file 247 | # to a newer Visual Studio version. Backup files are not needed, 248 | # because we have git ;-) 249 | _UpgradeReport_Files/ 250 | Backup*/ 251 | UpgradeLog*.XML 252 | UpgradeLog*.htm 253 | ServiceFabricBackup/ 254 | *.rptproj.bak 255 | 256 | # SQL Server files 257 | *.mdf 258 | *.ldf 259 | *.ndf 260 | 261 | # Business Intelligence projects 262 | *.rdl.data 263 | *.bim.layout 264 | *.bim_*.settings 265 | *.rptproj.rsuser 266 | *- [Bb]ackup.rdl 267 | *- [Bb]ackup ([0-9]).rdl 268 | *- [Bb]ackup ([0-9][0-9]).rdl 269 | 270 | # Microsoft Fakes 271 | FakesAssemblies/ 272 | 273 | # GhostDoc plugin setting file 274 | *.GhostDoc.xml 275 | 276 | # Node.js Tools for Visual Studio 277 | .ntvs_analysis.dat 278 | node_modules/ 279 | 280 | # Visual Studio 6 build log 281 | *.plg 282 | 283 | # Visual Studio 6 workspace options file 284 | *.opt 285 | 286 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 287 | *.vbw 288 | 289 | # Visual Studio LightSwitch build output 290 | **/*.HTMLClient/GeneratedArtifacts 291 | **/*.DesktopClient/GeneratedArtifacts 292 | **/*.DesktopClient/ModelManifest.xml 293 | **/*.Server/GeneratedArtifacts 294 | **/*.Server/ModelManifest.xml 295 | _Pvt_Extensions 296 | 297 | # Paket dependency manager 298 | .paket/paket.exe 299 | paket-files/ 300 | 301 | # FAKE - F# Make 302 | .fake/ 303 | 304 | # CodeRush personal settings 305 | .cr/personal 306 | 307 | # Python Tools for Visual Studio (PTVS) 308 | __pycache__/ 309 | *.pyc 310 | 311 | # Cake - Uncomment if you are using it 312 | # tools/** 313 | # !tools/packages.config 314 | 315 | # Tabs Studio 316 | *.tss 317 | 318 | # Telerik's JustMock configuration file 319 | *.jmconfig 320 | 321 | # BizTalk build output 322 | *.btp.cs 323 | *.btm.cs 324 | *.odx.cs 325 | *.xsd.cs 326 | 327 | # OpenCover UI analysis results 328 | OpenCover/ 329 | 330 | # Azure Stream Analytics local run output 331 | ASALocalRun/ 332 | 333 | # MSBuild Binary and Structured Log 334 | *.binlog 335 | 336 | # NVidia Nsight GPU debugger configuration file 337 | *.nvuser 338 | 339 | # MFractors (Xamarin productivity tool) working folder 340 | .mfractor/ 341 | 342 | # Local History for Visual Studio 343 | .localhistory/ 344 | 345 | # BeatPulse healthcheck temp database 346 | healthchecksdb 347 | 348 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 349 | MigrationBackup/ 350 | 351 | # Ionide (cross platform F# VS Code tools) working folder 352 | .ionide/ 353 | -------------------------------------------------------------------------------- /Networking/TelemetryServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Net; 5 | using System.Net.Sockets; 6 | using FH4RP.DataStructs; 7 | using FH4RP.Helpers; 8 | 9 | namespace FH4RP.Networking 10 | { 11 | public class TelemetryServer 12 | { 13 | private UdpClient udpClient; 14 | private IPEndPoint ep; 15 | public TelemetryData LastUpdate { get; private set; } 16 | 17 | public TelemetryServer(int port = 9909) 18 | { 19 | try 20 | { 21 | ep = new IPEndPoint(IPAddress.Loopback, port); 22 | udpClient = new UdpClient(ep); 23 | udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); 24 | } 25 | catch (Exception e) { Console.WriteLine($"Error: {e.ToString()}"); } 26 | new VehicleDB(); 27 | } 28 | 29 | public void Start() 30 | { 31 | Console.WriteLine($"Started telemetry server on {ep.Address.ToString()}:{ep.Port}."); 32 | udpClient.BeginReceive(new AsyncCallback(OnUdpReceive), null); 33 | } 34 | 35 | private void OnUdpReceive(IAsyncResult ar) 36 | { 37 | byte[] data = udpClient.EndReceive(ar, ref ep); 38 | TelemetryData telemetryData = CreateDataStruct(data); 39 | if (telemetryData.CarOrdinal != 0) LastUpdate = telemetryData; 40 | #if DEBUG 41 | // System.IO.File.WriteAllText("output.json", Newtonsoft.Json.JsonConvert.SerializeObject(LastUpdate, Newtonsoft.Json.Formatting.Indented)); 42 | #endif 43 | udpClient.BeginReceive(new AsyncCallback(OnUdpReceive), null); 44 | } 45 | 46 | private TelemetryData CreateDataStruct(byte[] data) 47 | { 48 | return new TelemetryData 49 | { 50 | IsRaceOn = BitConverter.ToInt32(data, 0), 51 | TimestampMS = BitConverter.ToUInt32(data, 4), 52 | 53 | EngineMaxRPM = BitConverter.ToSingle(data, 8), 54 | EngineIdleRPM = BitConverter.ToSingle(data, 12), 55 | EngineCurrentRPM = BitConverter.ToSingle(data, 16), 56 | 57 | AccelerationX = BitConverter.ToSingle(data, 20), 58 | AccelerationY = BitConverter.ToSingle(data, 24), 59 | AccelerationZ = BitConverter.ToSingle(data, 28), 60 | 61 | VelocityX = BitConverter.ToSingle(data, 32), 62 | VelocityY = BitConverter.ToSingle(data, 36), 63 | VelocityZ = BitConverter.ToSingle(data, 40), 64 | 65 | AngularVelocityX = BitConverter.ToSingle(data, 44), 66 | AngularVelocityY = BitConverter.ToSingle(data, 48), 67 | AngularVelocityZ = BitConverter.ToSingle(data, 52), 68 | 69 | Yaw = BitConverter.ToSingle(data, 56), 70 | Pitch = BitConverter.ToSingle(data, 60), 71 | Roll = BitConverter.ToSingle(data, 64), 72 | 73 | NormalizedSuspensionTravelFrontLeft = BitConverter.ToSingle(data, 68), 74 | NormalizedSuspensionTravelFrontRight = BitConverter.ToSingle(data, 72), 75 | NormalizedSuspensionTravelRearLeft = BitConverter.ToSingle(data, 76), 76 | NormalizedSuspensionTravelRearRight = BitConverter.ToSingle(data, 80), 77 | 78 | TireSlipRatioFrontLeft = BitConverter.ToSingle(data, 84), 79 | TireSlipRatioFrontRight = BitConverter.ToSingle(data, 88), 80 | TireSlipRatioRearLeft = BitConverter.ToSingle(data, 92), 81 | TireSlipRatioRearRight = BitConverter.ToSingle(data, 96), 82 | 83 | WheelRotationSpeedFrontLeft = BitConverter.ToSingle(data, 100), 84 | WheelRotationSpeedFrontRight = BitConverter.ToSingle(data, 104), 85 | WheelRotationSpeedRearLeft = BitConverter.ToSingle(data, 108), 86 | WheelRotationSpeedRearRight = BitConverter.ToSingle(data, 112), 87 | 88 | WheelOnRumbleStripFrontLeft = BitConverter.ToInt32(data, 116), 89 | WheelOnRumbleStripFrontRight = BitConverter.ToInt32(data, 120), 90 | WheelOnRumbleStripRearLeft = BitConverter.ToInt32(data, 124), 91 | WheelOnRumbleStripRearRight = BitConverter.ToInt32(data, 128), 92 | 93 | WheelInPuddleDepthFrontLeft = BitConverter.ToSingle(data, 132), 94 | WheelInPuddleDepthFrontRight = BitConverter.ToSingle(data, 136), 95 | WheelInPuddleDepthRearLeft = BitConverter.ToSingle(data, 140), 96 | WheelInPuddleDepthRearRight = BitConverter.ToSingle(data, 144), 97 | 98 | SurfaceRumbleFrontLeft = BitConverter.ToSingle(data, 148), 99 | SurfaceRumbleFrontRight = BitConverter.ToSingle(data, 152), 100 | SurfaceRumbleRearLeft = BitConverter.ToSingle(data, 156), 101 | SurfaceRumbleRearRight = BitConverter.ToSingle(data, 160), 102 | 103 | TireSlipAngleFrontLeft = BitConverter.ToSingle(data, 164), 104 | TireSlipAngleFrontRight = BitConverter.ToSingle(data, 168), 105 | TireSlipAngleRearLeft = BitConverter.ToSingle(data, 172), 106 | TireSlipAngleRearRight = BitConverter.ToSingle(data, 176), 107 | 108 | TireCombinedSlipFrontLeft = BitConverter.ToSingle(data, 180), 109 | TireCombinedSlipFrontRight = BitConverter.ToSingle(data, 184), 110 | TireCombinedSlipRearLeft = BitConverter.ToSingle(data, 188), 111 | TireCombinedSlipRearRight = BitConverter.ToSingle(data, 192), 112 | 113 | SuspensionTravelFrontLeft = BitConverter.ToSingle(data, 196), 114 | SuspensionTravelFrontRight = BitConverter.ToSingle(data, 200), 115 | SuspensionTravelRearLeft = BitConverter.ToSingle(data, 204), 116 | SuspensionTravelRearRight = BitConverter.ToSingle(data, 208), 117 | 118 | CarOrdinal = BitConverter.ToInt32(data, 212), 119 | 120 | CarClass = (Vehicle.CarClass)BitConverter.ToInt32(data, 216), 121 | CarPI = BitConverter.ToInt32(data, 220), 122 | DrivetrainType = (Vehicle.DrivetrainType)BitConverter.ToInt32(data, 224), 123 | NumCylinders = BitConverter.ToInt32(data, 228), 124 | 125 | PositionX = BitConverter.ToSingle(data, 232), 126 | PositionY = BitConverter.ToSingle(data, 236), 127 | PositionZ = BitConverter.ToSingle(data, 240), 128 | 129 | Speed = BitConverter.ToSingle(data, 244), 130 | Power = BitConverter.ToSingle(data, 248), 131 | Torque = BitConverter.ToSingle(data, 252), 132 | 133 | TireTempFrontLeft = BitConverter.ToSingle(data, 256), 134 | TireTempFrontRight = BitConverter.ToSingle(data, 260), 135 | TireTempRearLeft = BitConverter.ToSingle(data, 264), 136 | TireTempRearRight = BitConverter.ToSingle(data, 268), 137 | 138 | Boost = BitConverter.ToSingle(data, 272), 139 | Fuel = BitConverter.ToSingle(data, 276), 140 | DistanceTraveled = BitConverter.ToSingle(data, 280), 141 | 142 | BestLap = BitConverter.ToSingle(data, 284), 143 | LastLap = BitConverter.ToSingle(data, 288), 144 | CurrentLap = BitConverter.ToSingle(data, 292), 145 | CurrentRaceTime = BitConverter.ToSingle(data, 296), 146 | LapNumber = BitConverter.ToUInt16(data, 300), 147 | 148 | RacePosition = data[302], 149 | Accel = data[303], 150 | Brake = data[304], 151 | Clutch = data[305], 152 | Handbrake = data[306], 153 | Gear = data[307], 154 | Steer = (sbyte)data[308], 155 | NormalizedDrivingLine = (sbyte)data[309], 156 | NormalizedAIBrakeDifference = (sbyte)data[310] 157 | }; 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /DataStructs/TelemetryData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace FH4RP.DataStructs 6 | { 7 | public struct TelemetryData 8 | { 9 | /// 10 | /// Returns 1 when in a race, 0 when in menus or otherwise 11 | /// 12 | public int IsRaceOn { get; set; } 13 | 14 | /// 15 | /// Timestamp in MS, can overflow to 0 eventually 16 | /// 17 | public uint TimestampMS { get; set; } 18 | 19 | 20 | 21 | public float EngineMaxRPM { get; set; } 22 | public float EngineIdleRPM { get; set; } 23 | public float EngineCurrentRPM { get; set; } 24 | 25 | 26 | 27 | /// 28 | /// Right (x) position axis in local space 29 | /// 30 | public float AccelerationX { get; set; } 31 | 32 | /// 33 | /// Up (y) position axis in local space 34 | /// 35 | public float AccelerationY { get; set; } 36 | 37 | /// 38 | /// Forward (z) position axis in local space 39 | /// 40 | public float AccelerationZ { get; set; } 41 | 42 | 43 | 44 | /// 45 | /// Right (x) velocity axis in local space 46 | /// 47 | public float VelocityX { get; set; } 48 | 49 | /// 50 | /// Up (y) velocity axis in local space 51 | /// 52 | public float VelocityY { get; set; } 53 | 54 | /// 55 | /// Forward (z) velocity axis in local space 56 | /// 57 | public float VelocityZ { get; set; } 58 | 59 | 60 | 61 | /// 62 | /// Pitch (x) velocity in local space 63 | /// 64 | public float AngularVelocityX { get; set; } 65 | 66 | /// 67 | /// Yaw (y) velocity in local space 68 | /// 69 | public float AngularVelocityY { get; set; } 70 | 71 | /// 72 | /// Roll (z) velocity in local space 73 | /// 74 | public float AngularVelocityZ { get; set; } 75 | 76 | 77 | 78 | public float Yaw { get; set; } 79 | public float Pitch { get; set; } 80 | public float Roll { get; set; } 81 | 82 | 83 | 84 | /// 85 | /// Normalized suspension travel (Front Left Tire) - 0.0 = max stretch | 1.0 = max compression 86 | /// 87 | public float NormalizedSuspensionTravelFrontLeft { get; set; } 88 | 89 | /// 90 | /// Normalized suspension travel (Front Right Tire) - 0.0 = max stretch | 1.0 = max compression 91 | /// 92 | public float NormalizedSuspensionTravelFrontRight { get; set; } 93 | 94 | /// 95 | /// Normalized suspension travel (Rear Left Tire) - 0.0 = max stretch | 1.0 = max compression 96 | /// 97 | public float NormalizedSuspensionTravelRearLeft { get; set; } 98 | 99 | /// 100 | /// Normalized suspension travel (Rear Right Tire) - 0.0 = max stretch | 1.0 = max compression 101 | /// 102 | public float NormalizedSuspensionTravelRearRight { get; set; } 103 | 104 | 105 | 106 | /// 107 | /// Normalized tire slip ratio (Front Left Tire) - 0.0 = 100% grip and |ratio| > 1.0 means loss of grip 108 | /// 109 | public float TireSlipRatioFrontLeft { get; set; } 110 | 111 | /// 112 | /// Normalized tire slip ratio (Front Right Tire) - 0.0 = 100% grip and |ratio| > 1.0 means loss of grip 113 | /// 114 | public float TireSlipRatioFrontRight { get; set; } 115 | 116 | /// 117 | /// Normalized tire slip ratio (Rear Left Tire) - 0.0 = 100% grip and |ratio| > 1.0 means loss of grip 118 | /// 119 | public float TireSlipRatioRearLeft { get; set; } 120 | 121 | /// 122 | /// Normalized tire slip ratio (Rear Right Tire) - 0.0 = 100% grip and |ratio| > 1.0 means loss of grip 123 | /// 124 | public float TireSlipRatioRearRight { get; set; } 125 | 126 | 127 | 128 | /// 129 | /// Wheel rotation speed in radians/sec - Front Left Tire 130 | /// 131 | public float WheelRotationSpeedFrontLeft { get; set; } 132 | 133 | /// 134 | /// Wheel rotation speed in radians/sec - Front Right Tire 135 | /// 136 | public float WheelRotationSpeedFrontRight { get; set; } 137 | 138 | /// 139 | /// Wheel rotation speed in radians/sec - Rear Left Tire 140 | /// 141 | public float WheelRotationSpeedRearLeft { get; set; } 142 | 143 | /// 144 | /// Wheel rotation speed in radians/sec - Rear Right Tire 145 | /// 146 | public float WheelRotationSpeedRearRight { get; set; } 147 | 148 | 149 | 150 | /// 151 | /// Returns 1 when the front left wheel is on a rumble strip, 0 when not 152 | /// 153 | public int WheelOnRumbleStripFrontLeft { get; set; } 154 | 155 | /// 156 | /// Returns 1 when the front right wheel is on a rumble strip, 0 when not 157 | /// 158 | public int WheelOnRumbleStripFrontRight { get; set; } 159 | 160 | /// 161 | /// Returns 1 when the rear left wheel is on a rumble strip, 0 when not 162 | /// 163 | public int WheelOnRumbleStripRearLeft { get; set; } 164 | 165 | /// 166 | /// Returns 1 when the rear right wheel is on a rumble strip, 0 when not 167 | /// 168 | public int WheelOnRumbleStripRearRight { get; set; } 169 | 170 | 171 | 172 | // From 0 to 1, where 1 is the deepest puddle 173 | public float WheelInPuddleDepthFrontLeft { get; set; } 174 | public float WheelInPuddleDepthFrontRight { get; set; } 175 | public float WheelInPuddleDepthRearLeft { get; set; } 176 | public float WheelInPuddleDepthRearRight { get; set; } 177 | 178 | // Non-dimensional surface rumble values passed to controller force feedback 179 | public float SurfaceRumbleFrontLeft { get; set; } 180 | public float SurfaceRumbleFrontRight { get; set; } 181 | public float SurfaceRumbleRearLeft { get; set; } 182 | public float SurfaceRumbleRearRight { get; set; } 183 | 184 | // Tire normalized slip angle, = 0 means 100% grip and |angle| > 1.0 means loss of grip. 185 | public float TireSlipAngleFrontLeft { get; set; } 186 | public float TireSlipAngleFrontRight { get; set; } 187 | public float TireSlipAngleRearLeft { get; set; } 188 | public float TireSlipAngleRearRight { get; set; } 189 | 190 | // Tire normalized combined slip, = 0 means 100% grip and |slip| > 1.0 means loss of grip. 191 | public float TireCombinedSlipFrontLeft { get; set; } 192 | public float TireCombinedSlipFrontRight { get; set; } 193 | public float TireCombinedSlipRearLeft { get; set; } 194 | public float TireCombinedSlipRearRight { get; set; } 195 | 196 | // Actual suspension travel in meters 197 | public float SuspensionTravelFrontLeft { get; set; } 198 | public float SuspensionTravelFrontRight { get; set; } 199 | public float SuspensionTravelRearLeft { get; set; } 200 | public float SuspensionTravelRearRight { get; set; } 201 | 202 | /// 203 | /// The Car ID 204 | /// 205 | public int CarOrdinal { get; set; } 206 | 207 | /// 208 | /// The car class - see 209 | /// 210 | public Vehicle.CarClass CarClass { get; set; } 211 | 212 | /// 213 | /// The performance index value 214 | /// 215 | public int CarPI { get; set; } 216 | 217 | /// 218 | /// The current drivetrain type - see 219 | /// 220 | public Vehicle.DrivetrainType DrivetrainType { get; set; } 221 | 222 | /// 223 | /// The number of cylinders 224 | /// 225 | public int NumCylinders { get; set; } 226 | 227 | 228 | 229 | /// 230 | /// X position in meters 231 | /// 232 | public float PositionX { get; set; } 233 | 234 | /// 235 | /// Y position in meters 236 | /// 237 | public float PositionY { get; set; } 238 | 239 | /// 240 | /// Z position in meters 241 | /// 242 | public float PositionZ { get; set; } 243 | 244 | /// 245 | /// Current speed in meters per second 246 | /// 247 | public float Speed { get; set; } 248 | 249 | /// 250 | /// The amount of power in watts 251 | /// 252 | public float Power { get; set; } 253 | 254 | /// 255 | /// The amount of torque in newton-meters 256 | /// 257 | public float Torque { get; set; } 258 | 259 | // Tire temperature (celcius) 260 | public float TireTempFrontLeft { get; set; } 261 | public float TireTempFrontRight { get; set; } 262 | public float TireTempRearLeft { get; set; } 263 | public float TireTempRearRight { get; set; } 264 | 265 | /// 266 | /// Boost amount 267 | /// 268 | public float Boost { get; set; } 269 | 270 | /// 271 | /// Fuel amount 272 | /// 273 | public float Fuel { get; set; } 274 | 275 | /// 276 | /// Distance traveled (meters) 277 | /// 278 | public float DistanceTraveled { get; set; } 279 | 280 | /// 281 | /// Best lap time 282 | /// 283 | public float BestLap { get; set; } 284 | 285 | /// 286 | /// Last lap time 287 | /// 288 | public float LastLap { get; set; } 289 | 290 | /// 291 | /// Current lap time 292 | /// 293 | public float CurrentLap { get; set; } 294 | 295 | /// 296 | /// Current race (total) time 297 | /// 298 | public float CurrentRaceTime { get; set; } 299 | 300 | /// 301 | /// Current lap number 302 | /// 303 | public UInt16 LapNumber { get; set; } 304 | 305 | /// 306 | /// Current race position 307 | /// 308 | public byte RacePosition { get; set; } 309 | 310 | /// 311 | /// Acceleration amount 312 | /// 313 | public byte Accel { get; set; } 314 | 315 | /// 316 | /// Brake amount 317 | /// 318 | public byte Brake { get; set; } 319 | 320 | /// 321 | /// Clutch amount 322 | /// 323 | public byte Clutch { get; set; } 324 | 325 | /// 326 | /// Handbrake amount 327 | /// 328 | public byte Handbrake { get; set; } 329 | 330 | /// 331 | /// Current gear 332 | /// 333 | public byte Gear { get; set; } 334 | 335 | /// 336 | /// Current steering amount 337 | /// 338 | public sbyte Steer { get; set; } 339 | 340 | public sbyte NormalizedDrivingLine { get; set; } 341 | public sbyte NormalizedAIBrakeDifference { get; set; } 342 | 343 | public int GetMPH() 344 | { 345 | return (int)(VelocityZ * 2.23694f); 346 | } 347 | } 348 | } 349 | --------------------------------------------------------------------------------