6 |
7 |
8 | One Microsoft Way
9 | Redmond, WA 98052-6399
10 | P:
11 | 425.555.0100
12 |
13 |
14 |
15 | Support:Support@example.com
16 | Marketing:Marketing@example.com
17 |
18 |
--------------------------------------------------------------------------------
/.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 | *.suo
8 | *.user
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # Visual Studio cache/options directory
13 | .vs/
14 |
15 | # C# Dev Kit
16 | .mono/
17 |
18 | # MSTest test Results
19 | [Tt]est[Rr]esult*/
20 | [Bb]uild[Ll]og.*
21 |
22 | # .NET Artifacts
23 | artifacts/
24 |
--------------------------------------------------------------------------------
/Web/ForzaData.Web.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | forzadata-web
5 | net8.0
6 | false
7 | false
8 | false
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/Core/SampleStruct.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 |
3 | namespace ForzaData.Core;
4 |
5 | ///
6 | /// Samples structure
7 | ///
8 | [StructLayout(LayoutKind.Auto)]
9 | public struct SampleStruct
10 | {
11 | ///
12 | /// Ticks elapsed since the record's beginning
13 | ///
14 | public long Elapsed;
15 |
16 | ///
17 | /// Length of
18 | ///
19 | public int Length;
20 |
21 | ///
22 | /// Data chunk
23 | ///
24 | public byte[] Data;
25 | }
26 |
--------------------------------------------------------------------------------
/Web/wwwroot/lib/bootstrap/dist/js/npm.js:
--------------------------------------------------------------------------------
1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment.
2 | require('../../js/transition.js')
3 | require('../../js/alert.js')
4 | require('../../js/button.js')
5 | require('../../js/carousel.js')
6 | require('../../js/collapse.js')
7 | require('../../js/dropdown.js')
8 | require('../../js/modal.js')
9 | require('../../js/tooltip.js')
10 | require('../../js/popover.js')
11 | require('../../js/scrollspy.js')
12 | require('../../js/tab.js')
13 | require('../../js/affix.js')
--------------------------------------------------------------------------------
/Web/wwwroot/lib/jquery-validation-unobtrusive/.bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jquery-validation-unobtrusive",
3 | "homepage": "https://github.com/aspnet/jquery-validation-unobtrusive",
4 | "version": "3.2.9",
5 | "_release": "3.2.9",
6 | "_resolution": {
7 | "type": "version",
8 | "tag": "v3.2.9",
9 | "commit": "a91f5401898e125f10771c5f5f0909d8c4c82396"
10 | },
11 | "_source": "https://github.com/aspnet/jquery-validation-unobtrusive.git",
12 | "_target": "^3.2.9",
13 | "_originalSource": "jquery-validation-unobtrusive",
14 | "_direct": true
15 | }
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to this project
2 |
3 | Unfortunately I don't have enough time in my life to provide guidance, reviews of pull-requests or debate about any contribution you could make to this project.
4 |
5 | Your pull request has so an high chance of not being merged, and I'm sorry for that.
6 |
7 | **Be sure I still appreciate any feedback !**
8 |
9 | This project will remain experimental on my side.
10 |
11 | I will follow next Forza Horizon/Motorsport game releases with interest to see how I can support them after all.
12 |
13 | **Thanks for reading me and for having forked my code !**
14 |
--------------------------------------------------------------------------------
/SampleFix/ForzaData.SampleFix.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | forzadata-samplefix
5 | Exe
6 | net8.0
7 | partial
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/SampleRecorder/ForzaData.SampleRecorder.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | forzadata-samplerecorder
5 | Exe
6 | net8.0
7 | partial
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/Core/ForzaMotorsportExtrasDataStruct.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 |
3 | namespace ForzaData.Core;
4 |
5 | ///
6 | /// Motorsport (2023) only extras data structure
7 | ///
8 | ///
9 | /// https://support.forzamotorsport.net/hc/en-us/articles/21742934024211-Forza-Motorsport-Data-Out-Documentation
10 | ///
11 | [StructLayout(LayoutKind.Auto)]
12 | public struct ForzaMotorsportExtrasDataStruct
13 | {
14 | public float TireWearFrontLeft;
15 | public float TireWearFrontRight;
16 | public float TireWearRearLeft;
17 | public float TireWearRearRight;
18 |
19 | // ID for track
20 | public int TrackOrdinal;
21 | }
22 |
--------------------------------------------------------------------------------
/Web/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) .NET Foundation. All rights reserved.
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use
4 | these files except in compliance with the License. You may obtain a copy of the
5 | License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software distributed
10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the
12 | specific language governing permissions and limitations under the License.
13 |
--------------------------------------------------------------------------------
/Console/ForzaData.Console.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | forzadata-console
5 | Exe
6 | net8.0
7 | partial
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/Web/wwwroot/lib/jquery/.bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jquery",
3 | "main": "dist/jquery.js",
4 | "license": "MIT",
5 | "ignore": [
6 | "package.json"
7 | ],
8 | "keywords": [
9 | "jquery",
10 | "javascript",
11 | "browser",
12 | "library"
13 | ],
14 | "homepage": "https://github.com/jquery/jquery-dist",
15 | "version": "3.3.1",
16 | "_release": "3.3.1",
17 | "_resolution": {
18 | "type": "version",
19 | "tag": "3.3.1",
20 | "commit": "9e8ec3d10fad04748176144f108d7355662ae75e"
21 | },
22 | "_source": "https://github.com/jquery/jquery-dist.git",
23 | "_target": "^3.3.1",
24 | "_originalSource": "jquery",
25 | "_direct": true
26 | }
--------------------------------------------------------------------------------
/Core/UdpStreamObserver.cs:
--------------------------------------------------------------------------------
1 | namespace ForzaData.Core;
2 |
3 | public abstract class UdpStreamObserver : IObserver
4 | {
5 | private IDisposable? _unsubscriber;
6 |
7 | protected UdpStreamObserver()
8 | {
9 |
10 | }
11 |
12 | public void Subscribe(IObservable listener)
13 | {
14 | _unsubscriber = listener.Subscribe(this);
15 | }
16 |
17 | public void Unsubscribe()
18 | {
19 | if (_unsubscriber != null)
20 | {
21 | _unsubscriber.Dispose();
22 | _unsubscriber = null;
23 | }
24 | }
25 |
26 | public abstract void OnCompleted();
27 |
28 | public abstract void OnError(Exception error);
29 |
30 | public abstract void OnNext(byte[] value);
31 | }
32 |
--------------------------------------------------------------------------------
/Core/ForzaDataVersion.cs:
--------------------------------------------------------------------------------
1 | namespace ForzaData.Core;
2 |
3 | ///
4 | /// Internal forza data versions
5 | ///
6 | public enum ForzaDataVersion : byte
7 | {
8 | ///
9 | /// Unknown version
10 | ///
11 | Unknown = 0,
12 |
13 | ///
14 | /// Sled-only data (Forza Motorsport 7 and Forza Motorsport (2023))
15 | ///
16 | Sled = 1,
17 |
18 | ///
19 | /// Car dash V1 (Forza Motorsport 7)
20 | ///
21 | CarDashV1 = 2,
22 |
23 | ///
24 | /// Car dash V2 (Forza Horizon 4 and 5)
25 | ///
26 | CarDashV2 = 3,
27 |
28 | ///
29 | /// Car dash V3 (Forza Motorsport (2023))
30 | ///
31 | CarDashV3 = 4
32 | }
33 |
--------------------------------------------------------------------------------
/Core/ForzaDataObserver.cs:
--------------------------------------------------------------------------------
1 | namespace ForzaData.Core;
2 |
3 | public abstract class ForzaDataObserver : IObserver
4 | {
5 | private IDisposable? _unsubscriber;
6 |
7 | protected ForzaDataObserver()
8 | {
9 |
10 | }
11 |
12 | public void Subscribe(IObservable listener)
13 | {
14 | _unsubscriber = listener.Subscribe(this);
15 | }
16 |
17 | public void Unsubscribe()
18 | {
19 | if (_unsubscriber != null)
20 | {
21 | _unsubscriber.Dispose();
22 | _unsubscriber = null;
23 | }
24 | }
25 |
26 | public abstract void OnCompleted();
27 |
28 | public abstract void OnError(ForzaDataException error);
29 |
30 | public abstract void OnError(Exception error);
31 |
32 | public abstract void OnNext(ForzaDataStruct value);
33 | }
34 |
--------------------------------------------------------------------------------
/Core/ForzaDataStruct.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 |
3 | namespace ForzaData.Core;
4 |
5 | ///
6 | /// Forza Data Out structure
7 | ///
8 | [StructLayout(LayoutKind.Auto)]
9 | public struct ForzaDataStruct
10 | {
11 | ///
12 | /// Protocol version
13 | ///
14 | public ForzaDataVersion Version;
15 |
16 | ///
17 | /// Sled data
18 | ///
19 | public ForzaSledDataStruct Sled;
20 |
21 | ///
22 | /// Car dash data
23 | ///
24 | public ForzaCarDashDataStruct? CarDash;
25 |
26 | ///
27 | /// Horizon extras data
28 | ///
29 | public ForzaHorizonExtrasDataStruct? HorizonExtras;
30 |
31 | ///
32 | /// Motorsport extras data
33 | ///
34 | public ForzaMotorsportExtrasDataStruct? MotorsportExtras;
35 | }
36 |
--------------------------------------------------------------------------------
/Web/Controllers/HomeController.cs:
--------------------------------------------------------------------------------
1 | using ForzaData.Web.Models;
2 | using Microsoft.AspNetCore.Mvc;
3 | using System.Diagnostics;
4 |
5 | namespace ForzaData.Web.Controllers;
6 |
7 | public class HomeController : Controller
8 | {
9 | public IActionResult Index()
10 | {
11 | return View();
12 | }
13 |
14 | public IActionResult About()
15 | {
16 | ViewData["Message"] = "Your application description page.";
17 |
18 | return View();
19 | }
20 |
21 | public IActionResult Contact()
22 | {
23 | ViewData["Message"] = "Your contact page.";
24 |
25 | return View();
26 | }
27 |
28 | public IActionResult Privacy()
29 | {
30 | return View();
31 | }
32 |
33 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
34 | public IActionResult Error()
35 | {
36 | return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Web/Views/Shared/Error.cshtml:
--------------------------------------------------------------------------------
1 | @model ErrorViewModel
2 | @{
3 | ViewData["Title"] = "Error";
4 | }
5 |
6 |
Error.
7 |
An error occurred while processing your request.
8 |
9 | @if (Model.ShowRequestId)
10 | {
11 |
12 | Request ID:@Model.RequestId
13 |
14 | }
15 |
16 |
Development Mode
17 |
18 | Swapping to Development environment will display more detailed information about the error that occurred.
19 |
20 |
21 | Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application.
22 |
23 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | updates:
4 |
5 | # GitHub Actions
6 | - package-ecosystem: "github-actions"
7 | directory: "/"
8 | schedule:
9 | interval: weekly
10 |
11 | # devcontainers
12 | - package-ecosystem: "devcontainers"
13 | directory: "/"
14 | schedule:
15 | interval: weekly
16 |
17 | # NuGet
18 | - package-ecosystem: "nuget"
19 | directory: "/"
20 | schedule:
21 | interval: daily
22 | ignore:
23 | - dependency-name: "*"
24 | update-types:
25 | - "version-update:semver-major"
26 | groups:
27 | mstest:
28 | applies-to: version-updates
29 | patterns:
30 | - "Microsoft.NET.Test.Sdk"
31 | - "Microsoft.CodeCoverage"
32 | - "Microsoft.Testing.*"
33 | - "Microsoft.TestPlatform.*"
34 | - "MSTest.*"
35 | spectre:
36 | applies-to: version-updates
37 | patterns:
38 | - "Spectre.Console"
39 | - "Spectre.Console.*"
40 |
--------------------------------------------------------------------------------
/Web/wwwroot/css/site.css:
--------------------------------------------------------------------------------
1 | /* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification\
2 | for details on configuring this project to bundle and minify static web assets. */
3 | body {
4 | padding-top: 50px;
5 | padding-bottom: 20px;
6 | }
7 |
8 | /* Wrapping element */
9 | /* Set some basic padding to keep content from hitting the edges */
10 | .body-content {
11 | padding-left: 15px;
12 | padding-right: 15px;
13 | }
14 |
15 | /* Carousel */
16 | .carousel-caption p {
17 | font-size: 20px;
18 | line-height: 1.4;
19 | }
20 |
21 | /* Make .svg files in the carousel display properly in older browsers */
22 | .carousel-inner .item img[src$=".svg"] {
23 | width: 100%;
24 | }
25 |
26 | /* QR code generator */
27 | #qrCode {
28 | margin: 15px;
29 | }
30 |
31 | /* Hide/rearrange for smaller screens */
32 | @media screen and (max-width: 767px) {
33 | /* Hide captions */
34 | .carousel-caption {
35 | display: none;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Console/DefaultCommandSettings.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel;
2 | using System.Net;
3 |
4 | namespace ForzaData.Console;
5 |
6 | public sealed class DefaultCommandSettings : CommandSettings
7 | {
8 | [Description("IP of server (your PC or console running the game) to listen to")]
9 | [CommandOption("-s|--server ")]
10 | [TypeConverter(typeof(IPAddressTypeConverter))]
11 | public IPAddress? Server { get; set; }
12 |
13 | [Description("Local network port to listen on")]
14 | [CommandOption("-p|--port ")]
15 | public ushort? Port { get; set; }
16 |
17 | public override ValidationResult Validate()
18 | {
19 | if (Server is null)
20 | return ValidationResult.Error("The server IP address is required");
21 | if (Port is null)
22 | return ValidationResult.Error("The local network port number is required");
23 | if (Port is < 1024 or > 65535)
24 | return ValidationResult.Error("The local network port number must be in 1024-65535 range");
25 |
26 | return ValidationResult.Success();
27 | }
28 | }
--------------------------------------------------------------------------------
/Core/ForzaDataReader.HorizonExtras.cs:
--------------------------------------------------------------------------------
1 | namespace ForzaData.Core;
2 |
3 | public partial class ForzaDataReader
4 | {
5 | ///
6 | /// Map of all horizon extras values
7 | ///
8 | ///
9 | /// See for info.
10 | ///
11 | internal static class HorizonExtrasMap
12 | {
13 | // People assumptions, since there's no documentation:
14 | // https://forums.forza.net/t/data-out-telemetry-variables-and-structure/535984/5
15 | internal static readonly Range CarCategory = 0..4;
16 | internal static readonly Range UnknownField1 = 4..8;
17 | internal static readonly Range UnknownField2 = 8..12;
18 | }
19 |
20 | private static ForzaHorizonExtrasDataStruct ReadHorizonExtrasData(in ReadOnlySpan data) => new()
21 | {
22 | CarCategory = BitConverter.ToInt32(data[HorizonExtrasMap.CarCategory]),
23 | UnknownField1 = BitConverter.ToInt32(data[HorizonExtrasMap.UnknownField1]),
24 | UnknownField2 = BitConverter.ToInt32(data[HorizonExtrasMap.UnknownField2])
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/Core/IPAddressTypeConverter.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel;
2 | using System.Diagnostics.CodeAnalysis;
3 | using System.Globalization;
4 | using System.Net;
5 |
6 | namespace ForzaData.Core
7 | {
8 | public class IPAddressTypeConverter : TypeConverter
9 | {
10 | public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
11 | {
12 | return sourceType == typeof(string);
13 | }
14 |
15 | public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType)
16 | {
17 | return destinationType == typeof(string);
18 | }
19 |
20 | public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
21 | {
22 | if (value is not string)
23 | return null;
24 |
25 | return IPAddress.Parse((string)value);
26 | }
27 |
28 | public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
29 | {
30 | if (value is not IPAddress)
31 | return null;
32 |
33 | return ((IPAddress)value).ToString();
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Web/wwwroot/lib/jquery-validation/.bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jquery-validation",
3 | "homepage": "https://jqueryvalidation.org/",
4 | "repository": {
5 | "type": "git",
6 | "url": "git://github.com/jquery-validation/jquery-validation.git"
7 | },
8 | "authors": [
9 | "Jörn Zaefferer "
10 | ],
11 | "description": "Form validation made easy",
12 | "main": "dist/jquery.validate.js",
13 | "keywords": [
14 | "forms",
15 | "validation",
16 | "validate"
17 | ],
18 | "license": "MIT",
19 | "ignore": [
20 | "**/.*",
21 | "node_modules",
22 | "bower_components",
23 | "test",
24 | "demo",
25 | "lib"
26 | ],
27 | "dependencies": {
28 | "jquery": ">= 1.7.2"
29 | },
30 | "version": "1.17.0",
31 | "_release": "1.17.0",
32 | "_resolution": {
33 | "type": "version",
34 | "tag": "1.17.0",
35 | "commit": "fc9b12d3bfaa2d0c04605855b896edb2934c0772"
36 | },
37 | "_source": "https://github.com/jzaefferer/jquery-validation.git",
38 | "_target": "^1.17.0",
39 | "_originalSource": "jquery-validation",
40 | "_direct": true
41 | }
--------------------------------------------------------------------------------
/Web/wwwroot/lib/bootstrap/.bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bootstrap",
3 | "description": "The most popular front-end framework for developing responsive, mobile first projects on the web.",
4 | "keywords": [
5 | "css",
6 | "js",
7 | "less",
8 | "mobile-first",
9 | "responsive",
10 | "front-end",
11 | "framework",
12 | "web"
13 | ],
14 | "homepage": "http://getbootstrap.com",
15 | "license": "MIT",
16 | "moduleType": "globals",
17 | "main": [
18 | "less/bootstrap.less",
19 | "dist/js/bootstrap.js"
20 | ],
21 | "ignore": [
22 | "/.*",
23 | "_config.yml",
24 | "CNAME",
25 | "composer.json",
26 | "CONTRIBUTING.md",
27 | "docs",
28 | "js/tests",
29 | "test-infra"
30 | ],
31 | "dependencies": {
32 | "jquery": "1.9.1 - 3"
33 | },
34 | "version": "3.3.7",
35 | "_release": "3.3.7",
36 | "_resolution": {
37 | "type": "version",
38 | "tag": "v3.3.7",
39 | "commit": "0b9c4a4007c44201dce9a6cc1a38407005c26c86"
40 | },
41 | "_source": "https://github.com/twbs/bootstrap.git",
42 | "_target": "v3.3.7",
43 | "_originalSource": "bootstrap",
44 | "_direct": true
45 | }
--------------------------------------------------------------------------------
/Web/wwwroot/lib/bootstrap/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2011-2016 Twitter, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Core/ForzaCarDashDataStruct.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 |
3 | namespace ForzaData.Core;
4 |
5 | ///
6 | /// "Car dash" data structure
7 | ///
8 | [StructLayout(LayoutKind.Auto)]
9 | public struct ForzaCarDashDataStruct
10 | {
11 | //Position (meters)
12 | public float PositionX;
13 | public float PositionY;
14 | public float PositionZ;
15 |
16 | public float Speed; // meters per second
17 | public float Power; // watts
18 | public float Torque; // newton meter
19 |
20 | public float TireTempFrontLeft;
21 | public float TireTempFrontRight;
22 | public float TireTempRearLeft;
23 | public float TireTempRearRight;
24 |
25 | public float Boost;
26 | public float Fuel;
27 | public float DistanceTraveled;
28 | public float BestLap;
29 | public float LastLap;
30 | public float CurrentLap;
31 | public float CurrentRaceTime;
32 |
33 | public ushort LapNumber;
34 | public byte RacePosition;
35 |
36 | public byte Accel;
37 | public byte Brake;
38 | public byte Clutch;
39 | public byte HandBrake;
40 | public byte Gear;
41 | public sbyte Steer;
42 |
43 | public sbyte NormalizedDrivingLine;
44 | public sbyte NormalizedAIBrakeDifference;
45 | }
46 |
--------------------------------------------------------------------------------
/Console/DefaultCommand.cs:
--------------------------------------------------------------------------------
1 | namespace ForzaData.Console;
2 |
3 | public sealed class DefaultCommand : Command
4 | {
5 | public override int Execute([NotNull] CommandContext context, [NotNull] DefaultCommandSettings settings)
6 | {
7 | using var cancellationTokenSource = new CancellationTokenSource();
8 | using var listener = new ForzaDataListener((int)settings.Port!, settings.Server!);
9 |
10 | // cancellation provided by CTRL + C / CTRL + break
11 | System.Console.CancelKeyPress += (sender, e) =>
12 | {
13 | e.Cancel = true;
14 | cancellationTokenSource.Cancel();
15 | };
16 |
17 | // forza data observer
18 | var console = new ForzaDataConsole();
19 | console.Subscribe(listener);
20 |
21 | try
22 | {
23 | System.Console.WriteLine($"Listening for data from {settings.Server} to local port {settings.Port}...");
24 | System.Console.WriteLine($"Please press CTRL+C to stop listening");
25 |
26 | // forza data observable
27 | listener.Listen(cancellationTokenSource.Token);
28 | }
29 | catch (OperationCanceledException)
30 | {
31 | // user cancellation requested
32 | }
33 |
34 | console.Unsubscribe();
35 |
36 | return 0;
37 | }
38 | }
--------------------------------------------------------------------------------
/Web/wwwroot/lib/jquery-validation/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | =====================
3 |
4 | Copyright Jörn Zaefferer
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in
14 | all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 | workflow_dispatch:
11 |
12 | concurrency:
13 | group: ${{ github.workflow }}-${{ github.ref }}
14 | cancel-in-progress: true
15 |
16 | jobs:
17 | build:
18 | runs-on: ${{ matrix.os }}
19 | strategy:
20 | matrix:
21 | os:
22 | - ubuntu-latest
23 | - windows-latest
24 | steps:
25 | - uses: actions/checkout@v6
26 | - name: Setup .NET
27 | uses: actions/setup-dotnet@v5
28 | with:
29 | global-json-file: global.json
30 | cache: true
31 | cache-dependency-path: '**/packages.lock.json'
32 | - name: Restore dependencies
33 | run: dotnet restore --use-current-runtime
34 | - name: Build
35 | run: dotnet build --no-restore --use-current-runtime
36 | - name: Test
37 | run: dotnet test --no-build --verbosity normal --logger html --logger trx
38 | - name: Test results artifacts
39 | uses: actions/upload-artifact@v6
40 | with:
41 | name: test-results-${{ matrix.os }}
42 | path: '*/TestResults'
43 | if: ${{ always() }}
44 |
--------------------------------------------------------------------------------
/Web/Startup.cs:
--------------------------------------------------------------------------------
1 | using ForzaData.Web.Configuration;
2 |
3 | namespace ForzaData.Web;
4 |
5 | public class Startup(IConfiguration configuration)
6 | {
7 | public IConfiguration Configuration { get; } = configuration;
8 |
9 | public void ConfigureServices(IServiceCollection services)
10 | {
11 | services.Configure(options =>
12 | {
13 | options.CheckConsentNeeded = context => true;
14 | options.MinimumSameSitePolicy = SameSiteMode.None;
15 | });
16 |
17 | services.Configure("App", Configuration);
18 |
19 | services.AddControllersWithViews();
20 | services.AddRazorPages();
21 | }
22 |
23 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
24 | {
25 | if (env.IsDevelopment())
26 | {
27 | app.UseDeveloperExceptionPage();
28 | }
29 | else
30 | {
31 | app.UseExceptionHandler("/Home/Error");
32 | app.UseHsts();
33 | }
34 |
35 | app.UseHttpsRedirection();
36 | app.UseStaticFiles();
37 | app.UseCookiePolicy();
38 |
39 | app.UseRouting();
40 |
41 | app.UseEndpoints(endpoints =>
42 | {
43 | endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
44 | endpoints.MapRazorPages();
45 | });
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Core/packages.lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "dependencies": {
4 | "net8.0": {
5 | "Microsoft.Extensions.Logging.Abstractions": {
6 | "type": "Direct",
7 | "requested": "[8.0.3, )",
8 | "resolved": "8.0.3",
9 | "contentHash": "dL0QGToTxggRLMYY4ZYX5AMwBb+byQBd/5dMiZE07Nv73o6I5Are3C7eQTh7K2+A4ct0PVISSr7TZANbiNb2yQ==",
10 | "dependencies": {
11 | "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
12 | }
13 | },
14 | "Microsoft.NET.ILLink.Tasks": {
15 | "type": "Direct",
16 | "requested": "[8.0.12, )",
17 | "resolved": "8.0.12",
18 | "contentHash": "FV4HnQ3JI15PHnJ5PGTbz+rYvrih42oLi/7UMIshNwCwUZhTq13UzrggtXk4ygrcMcN+4jsS6hhshx2p/Zd0ig=="
19 | },
20 | "Microsoft.Extensions.DependencyInjection.Abstractions": {
21 | "type": "Transitive",
22 | "resolved": "8.0.2",
23 | "contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg=="
24 | }
25 | },
26 | "net8.0/linux-arm64": {},
27 | "net8.0/linux-x64": {},
28 | "net8.0/osx-arm64": {},
29 | "net8.0/osx-x64": {},
30 | "net8.0/win-arm64": {},
31 | "net8.0/win-x64": {}
32 | }
33 | }
--------------------------------------------------------------------------------
/Web/Views/Shared/_ValidationScriptsPartial.cshtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/Core/ForzaDataReader.MotorsportExtras.cs:
--------------------------------------------------------------------------------
1 | namespace ForzaData.Core;
2 |
3 | public partial class ForzaDataReader
4 | {
5 | ///
6 | /// Map of all motorsport extras values
7 | ///
8 | ///
9 | /// See for info.
10 | ///
11 | internal static class MotorsportExtrasMap
12 | {
13 | internal static readonly Range TireWearFrontLeft = 0..4;
14 | internal static readonly Range TireWearFrontRight = 4..8;
15 | internal static readonly Range TireWearRearLeft = 8..12;
16 | internal static readonly Range TireWearRearRight = 12..16;
17 | internal static readonly Range TrackOrdinal = 16..20;
18 | }
19 |
20 | private static ForzaMotorsportExtrasDataStruct ReadMotorsportExtrasData(in ReadOnlySpan data) => new()
21 | {
22 | TireWearFrontLeft = BitConverter.ToSingle(data[MotorsportExtrasMap.TireWearFrontLeft]),
23 | TireWearFrontRight = BitConverter.ToSingle(data[MotorsportExtrasMap.TireWearFrontRight]),
24 | TireWearRearLeft = BitConverter.ToSingle(data[MotorsportExtrasMap.TireWearRearLeft]),
25 | TireWearRearRight = BitConverter.ToSingle(data[MotorsportExtrasMap.TireWearRearRight]),
26 | TrackOrdinal = BitConverter.ToInt32(data[MotorsportExtrasMap.TrackOrdinal])
27 | };
28 | }
29 |
--------------------------------------------------------------------------------
/Web/packages.lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "dependencies": {
4 | "net8.0": {
5 | "Microsoft.NET.ILLink.Tasks": {
6 | "type": "Direct",
7 | "requested": "[8.0.12, )",
8 | "resolved": "8.0.12",
9 | "contentHash": "FV4HnQ3JI15PHnJ5PGTbz+rYvrih42oLi/7UMIshNwCwUZhTq13UzrggtXk4ygrcMcN+4jsS6hhshx2p/Zd0ig=="
10 | },
11 | "Microsoft.Extensions.DependencyInjection.Abstractions": {
12 | "type": "Transitive",
13 | "resolved": "8.0.2",
14 | "contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg=="
15 | },
16 | "Microsoft.Extensions.Logging.Abstractions": {
17 | "type": "Transitive",
18 | "resolved": "8.0.3",
19 | "contentHash": "dL0QGToTxggRLMYY4ZYX5AMwBb+byQBd/5dMiZE07Nv73o6I5Are3C7eQTh7K2+A4ct0PVISSr7TZANbiNb2yQ==",
20 | "dependencies": {
21 | "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
22 | }
23 | },
24 | "forzadata-core": {
25 | "type": "Project",
26 | "dependencies": {
27 | "Microsoft.Extensions.Logging.Abstractions": "[8.0.3, )"
28 | }
29 | }
30 | },
31 | "net8.0/linux-arm64": {},
32 | "net8.0/linux-x64": {},
33 | "net8.0/osx-arm64": {},
34 | "net8.0/osx-x64": {},
35 | "net8.0/win-arm64": {},
36 | "net8.0/win-x64": {}
37 | }
38 | }
--------------------------------------------------------------------------------
/Core/SampleWriter.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | namespace ForzaData.Core;
4 |
5 | public class SampleWriter(Stream output) : IDisposable
6 | {
7 | private readonly BinaryWriter _writer = new(output, Encoding.UTF8, true);
8 |
9 | private bool _isDisposed = false;
10 |
11 | public int Writes { get; private set; } = 0;
12 |
13 | public void Flush() => _writer.Flush();
14 |
15 | public void Dispose()
16 | {
17 | Dispose(true);
18 | GC.SuppressFinalize(this);
19 | }
20 |
21 | protected virtual void Dispose(bool isDisposing)
22 | {
23 | if (!_isDisposed)
24 | {
25 | if (isDisposing)
26 | {
27 | _writer.Dispose();
28 | }
29 |
30 | _isDisposed = true;
31 | }
32 | }
33 |
34 | public void Write(SampleStruct chunk)
35 | {
36 | if (chunk.Elapsed < 0)
37 | throw new SampleException("Chunk has an invalid elapsed value");
38 | else if (chunk.Length <= 0)
39 | throw new SampleException("Chunk has an invalid length");
40 | else if (chunk.Data == null || chunk.Data.Length == 0)
41 | throw new SampleException("Chunk has no data");
42 | else if (chunk.Length != chunk.Data.Length)
43 | throw new SampleException("Chunk has an invalid data length");
44 |
45 | try
46 | {
47 | _writer.Write(chunk.Elapsed);
48 | _writer.Write(chunk.Length);
49 | _writer.Write(chunk.Data);
50 | Writes++;
51 | }
52 | catch (Exception ex)
53 | {
54 | throw new SampleException("Unexpected sample write error", ex);
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/SampleRecorder/UdpStreamSampleRecorder.cs:
--------------------------------------------------------------------------------
1 | using ForzaData.Core;
2 | using System.Diagnostics;
3 | using System.Text;
4 |
5 | namespace ForzaData.SampleRecorder;
6 |
7 | public class UdpStreamSampleRecorder(Stream output) : UdpStreamObserver, IDisposable
8 | {
9 | private readonly BinaryWriter _writer = new(output, Encoding.UTF8, true);
10 | private readonly Stopwatch _stopwatch = new();
11 |
12 | private bool _isDisposed = false;
13 |
14 | public long BytesRead { get; private set; }
15 | public long BytesWritten { get; private set; }
16 |
17 | public override void OnCompleted()
18 | {
19 | _writer.Flush();
20 | }
21 |
22 | public override void OnError(Exception error)
23 | {
24 | Debug.WriteLine(error);
25 | }
26 |
27 | public override void OnNext(byte[] value)
28 | {
29 | long elapsed = _stopwatch.ElapsedTicks;
30 |
31 | if (!_stopwatch.IsRunning)
32 | {
33 | _stopwatch.Start();
34 | }
35 |
36 | BytesRead += value.Length;
37 |
38 | _writer.Write(elapsed); BytesWritten += 8; // 8 bytes
39 | _writer.Write(value.Length); BytesWritten += 4; // 4 bytes
40 | _writer.Write(value); BytesWritten += value.Length; // n bytes
41 | }
42 |
43 | protected virtual void Dispose(bool isDisposing)
44 | {
45 | if (!_isDisposed)
46 | {
47 | if (isDisposing)
48 | {
49 | _writer.Close();
50 | _writer.Dispose();
51 | }
52 |
53 | _isDisposed = true;
54 | }
55 | }
56 |
57 | public void Dispose()
58 | {
59 | Dispose(true);
60 | GC.SuppressFinalize(this);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Core/SampleReader.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | namespace ForzaData.Core;
4 |
5 | public class SampleReader(Stream input) : IDisposable
6 | {
7 | private readonly BinaryReader _reader = new(input, Encoding.UTF8, true);
8 |
9 | private bool _isDisposed = false;
10 |
11 | public int Reads { get; private set; } = 0;
12 | public bool EndOfStream { get; private set; } = false;
13 |
14 | public void Dispose()
15 | {
16 | Dispose(true);
17 | GC.SuppressFinalize(this);
18 | }
19 |
20 | protected virtual void Dispose(bool isDisposing)
21 | {
22 | if (!_isDisposed)
23 | {
24 | if (isDisposing)
25 | {
26 | _reader.Dispose();
27 | }
28 |
29 | _isDisposed = true;
30 | }
31 | }
32 |
33 | public bool TryRead(out SampleStruct chunk)
34 | {
35 | EndOfStream = false;
36 |
37 | try
38 | {
39 | long elapsed = _reader.ReadInt64();
40 | int length = _reader.ReadInt32();
41 |
42 | if (elapsed >= 0 && length > 0)
43 | {
44 | byte[] data = _reader.ReadBytes(length);
45 |
46 | if (length == data.Length)
47 | {
48 | chunk = new SampleStruct
49 | {
50 | Elapsed = elapsed,
51 | Length = length,
52 | Data = data
53 | };
54 | Reads++;
55 | return true;
56 | }
57 | }
58 |
59 | chunk = default;
60 | return false;
61 | }
62 | catch (EndOfStreamException)
63 | {
64 | EndOfStream = true;
65 | chunk = default;
66 | return false;
67 | }
68 | catch (Exception ex)
69 | {
70 | throw new SampleException("Unexpected sample read error", ex);
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Tests/ForzaData.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | forzadata-tests
5 | net8.0
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | PreserveNewest
26 |
27 |
28 | PreserveNewest
29 |
30 |
31 | PreserveNewest
32 |
33 |
34 | PreserveNewest
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 0.8
5 | Geoffrey Vancoetsem
6 |
7 | git
8 | https://github.com/geeooff/forza-data-web
9 | ForzaDataWeb
10 | true
11 | LICENSE
12 | README.md
13 |
14 |
15 |
16 | 12.0
17 | enable
18 | enable
19 |
20 |
21 |
22 | true
23 | true
24 | win-x64;osx-x64;linux-x64;win-arm64;osx-arm64;linux-arm64
25 | true
26 | true
27 | true
28 | true
29 | true
30 | true
31 | embedded
32 | true
33 |
34 |
35 |
36 | true
37 | true
38 |
39 |
40 |
--------------------------------------------------------------------------------
/SampleRecorder/DefaultCommandSettings.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel;
2 | using System.Net;
3 |
4 | namespace ForzaData.SampleRecorder;
5 |
6 | public sealed class DefaultCommandSettings : CommandSettings
7 | {
8 | [Description("IP of server (your PC or console running the game) to listen to")]
9 | [CommandOption("-s|--server ")]
10 | [TypeConverter(typeof(IPAddressTypeConverter))]
11 | public IPAddress? Server { get; set; }
12 |
13 | [Description("Local network port to listen on")]
14 | [CommandOption("-p|--port ")]
15 | public ushort? Port { get; set; }
16 |
17 | [Description("Output file")]
18 | [CommandOption("-o|--output")]
19 | public string? Output { get; set; }
20 |
21 | public override ValidationResult Validate()
22 | {
23 | if (Server is null)
24 | return ValidationResult.Error("The server IP address is required");
25 | if (Port is null)
26 | return ValidationResult.Error("The local network port number is required");
27 | if (Port is < 1024 or > 65535)
28 | return ValidationResult.Error("The local network port number must be in 1024-65535 range");
29 | if (string.IsNullOrEmpty(Output))
30 | return ValidationResult.Error("An output file must be specified");
31 |
32 | var outputFile = new FileInfo(Output);
33 |
34 | if (outputFile.Directory?.Exists ?? false)
35 | {
36 | try
37 | {
38 | outputFile.Directory.Create();
39 | }
40 | catch (Exception ex)
41 | {
42 | return ValidationResult.Error($"An error occured while creating output file directory: {ex.Message}");
43 | }
44 | }
45 |
46 | return ValidationResult.Success();
47 | }
48 | }
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // https://code.visualstudio.com/docs/csharp/debugger-settings
3 | "version": "0.2.0",
4 | "configurations": [
5 | {
6 | "name": "console (external)",
7 | "type": "coreclr",
8 | "request": "launch",
9 | "preLaunchTask": "build",
10 | "program": "${workspaceFolder}/artifacts/bin/ForzaData.Console/debug/forzadata-console.dll",
11 | "args": [
12 | "--server", "${input:serverIpAddress}",
13 | "--port", "${input:clientPortNumber}"
14 | ],
15 | "cwd": "${workspaceFolder}/Console",
16 | "console": "externalTerminal",
17 | "stopAtEntry": false
18 | },
19 | {
20 | "name": "console (integrated)",
21 | "type": "coreclr",
22 | "request": "launch",
23 | "preLaunchTask": "build",
24 | "program": "${workspaceFolder}/artifacts/bin/ForzaData.Console/debug/forzadata-console.dll",
25 | "args": [
26 | "--server", "${input:serverIpAddress}",
27 | "--port", "${input:clientPortNumber}"
28 | ],
29 | "cwd": "${workspaceFolder}/Console",
30 | "console": "integratedTerminal",
31 | "stopAtEntry": false
32 | }
33 | ],
34 | "inputs": [
35 | {
36 | "id": "serverIpAddress",
37 | "description": "Please enter the server (eg. Xbox) IP Address that will emit data:",
38 | "type": "promptString",
39 | "default": "192.168.0.100"
40 | },
41 | {
42 | "id": "clientPortNumber",
43 | "description": "Please enter the client port number om which the data will be received:",
44 | "type": "promptString",
45 | "default": "7777"
46 | }
47 | ]
48 | }
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | ref:
7 | description: 'Commit ref'
8 | required: true
9 | type: string
10 | default: 'main'
11 | tag:
12 | description: 'Tag'
13 | required: true
14 | type: string
15 | desc:
16 | description: 'Release descriptin'
17 | required: true
18 | type: string
19 |
20 | concurrency:
21 | group: ${{ github.workflow }}-${{ github.ref }}
22 | cancel-in-progress: true
23 |
24 | jobs:
25 |
26 | build:
27 | runs-on: ${{ matrix.os }}
28 | strategy:
29 | matrix:
30 | os:
31 | - ubuntu-latest
32 | - windows-latest
33 | - macos-latest
34 | steps:
35 | - uses: actions/checkout@v6
36 | - name: Setup .NET
37 | uses: actions/setup-dotnet@v5
38 | with:
39 | global-json-file: global.json
40 | cache: true
41 | cache-dependency-path: '**/packages.lock.json'
42 | - name: Restore dependencies
43 | run: dotnet restore --use-current-runtime
44 | - name: Publish
45 | run: dotnet publish --no-restore --use-current-runtime
46 | - name: Upload artifacts
47 | uses: actions/upload-artifact@v6
48 | with:
49 | name: artifacts-${{ matrix.os }}
50 | path: 'artifacts/publish/*/release'
51 | retention-days: 1
52 | if-no-files-found: error
53 |
54 | # create-release:
55 | # permissions:
56 | # contents: write
57 | # needs: build
58 | # runs-on: ubuntu-latest
59 | # steps:
60 | # - name: Download artifacts
61 | # uses: actions/download-artifact@v4
62 |
63 |
--------------------------------------------------------------------------------
/Web/wwwroot/lib/jquery/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright JS Foundation and other contributors, https://js.foundation/
2 |
3 | This software consists of voluntary contributions made by many
4 | individuals. For exact contribution history, see the revision history
5 | available at https://github.com/jquery/jquery
6 |
7 | The following license applies to all parts of this software except as
8 | documented below:
9 |
10 | ====
11 |
12 | Permission is hereby granted, free of charge, to any person obtaining
13 | a copy of this software and associated documentation files (the
14 | "Software"), to deal in the Software without restriction, including
15 | without limitation the rights to use, copy, modify, merge, publish,
16 | distribute, sublicense, and/or sell copies of the Software, and to
17 | permit persons to whom the Software is furnished to do so, subject to
18 | the following conditions:
19 |
20 | The above copyright notice and this permission notice shall be
21 | included in all copies or substantial portions of the Software.
22 |
23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
30 |
31 | ====
32 |
33 | All files located in the node_modules and external directories are
34 | externally maintained libraries used by this software which have their
35 | own licenses; we recommend you read them, as their terms may differ from
36 | the terms above.
37 |
--------------------------------------------------------------------------------
/Core/ForzaDataListener.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 |
3 | namespace ForzaData.Core;
4 |
5 | public class ForzaDataListener(int port, IPAddress serverIpAddress)
6 | : UdpStreamListener(port, serverIpAddress), IObservable
7 | {
8 | private readonly ICollection> _observers = [];
9 | private readonly ForzaDataReader _reader = new();
10 |
11 | private ForzaDataStruct? _lastData = null;
12 |
13 | public IDisposable Subscribe(IObserver observer)
14 | {
15 | if (!_observers.Contains(observer))
16 | {
17 | _observers.Add(observer);
18 |
19 | if (_lastData != null)
20 | {
21 | observer.OnNext(_lastData.Value);
22 | }
23 | }
24 | return new ForzaDataUnsubscriber(_observers, observer);
25 | }
26 |
27 | protected override void NotifyData(byte[] data)
28 | {
29 | base.NotifyData(data);
30 |
31 | ForzaDataStruct forzaData;
32 |
33 | try
34 | {
35 | forzaData = _reader.Read(data);
36 |
37 | // success reading
38 | NotifyData(forzaData);
39 | }
40 | catch (Exception ex)
41 | {
42 | // read exception: notified to observers
43 | NotifyError(new ForzaDataException("An error occured while trying to read data", ex));
44 | }
45 | }
46 |
47 | protected void NotifyData(ForzaDataStruct data)
48 | {
49 | foreach (var observer in _observers)
50 | {
51 | observer.OnNext(data);
52 | }
53 | }
54 |
55 | protected void NotifyError(ForzaDataException error)
56 | {
57 | foreach (var observer in _observers)
58 | {
59 | observer.OnError(error);
60 | }
61 | }
62 |
63 | protected override void NotifyCompletion()
64 | {
65 | base.NotifyCompletion();
66 |
67 | foreach (var observer in _observers)
68 | {
69 | observer.OnCompleted();
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/SampleRecorder/DefaultCommand.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 |
3 | namespace ForzaData.SampleRecorder;
4 |
5 | public sealed class DefaultCommand : Command
6 | {
7 | public override int Execute([NotNull] CommandContext context, [NotNull] DefaultCommandSettings settings)
8 | {
9 | var serverIpAddress = settings.Server!;
10 | var outputFile = new FileInfo(settings.Output!);
11 |
12 | using (var cancellationTokenSource = new CancellationTokenSource())
13 | using (var listener = new UdpStreamListener((int)settings.Port!, serverIpAddress))
14 | {
15 | // cancellation provided by CTRL + C / CTRL + break
16 | System.Console.CancelKeyPress += (sender, e) =>
17 | {
18 | e.Cancel = true;
19 | cancellationTokenSource.Cancel();
20 | };
21 |
22 | // udp packet observer to output file
23 | using var output = new FileStream(outputFile.FullName, FileMode.Create, FileAccess.Write);
24 | using var sampleRecorder = new UdpStreamSampleRecorder(output);
25 |
26 | sampleRecorder.Subscribe(listener);
27 |
28 | Console.Out.WriteLine($"Listening to {serverIpAddress}...");
29 |
30 | try
31 | {
32 | // forza data observable
33 | listener.Listen(cancellationTokenSource.Token);
34 | }
35 | catch (OperationCanceledException)
36 | {
37 | // user cancellation requested
38 | }
39 |
40 | sampleRecorder.Unsubscribe();
41 |
42 | Console.Out.Write("Listening stopped. ");
43 |
44 | if (output.Position > 0L)
45 | Console.Out.WriteLine($"{sampleRecorder.BytesRead:N0} bytes read, {sampleRecorder.BytesWritten:N0} bytes written");
46 | else
47 | Console.Out.WriteLine("Nothing was read.");
48 | }
49 |
50 | outputFile.Refresh();
51 |
52 | // empty file deletion
53 | if (outputFile.Length == 0L)
54 | {
55 | outputFile.Delete();
56 | }
57 |
58 | return 0;
59 | }
60 | }
--------------------------------------------------------------------------------
/SampleFix/packages.lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "dependencies": {
4 | "net8.0": {
5 | "Microsoft.NET.ILLink.Tasks": {
6 | "type": "Direct",
7 | "requested": "[8.0.12, )",
8 | "resolved": "8.0.12",
9 | "contentHash": "FV4HnQ3JI15PHnJ5PGTbz+rYvrih42oLi/7UMIshNwCwUZhTq13UzrggtXk4ygrcMcN+4jsS6hhshx2p/Zd0ig=="
10 | },
11 | "Spectre.Console.Cli": {
12 | "type": "Direct",
13 | "requested": "[0.49.1, )",
14 | "resolved": "0.49.1",
15 | "contentHash": "wBZzyEbKqfPFFUPhV5E7/k4Kwy4UDO42IVzvzk0C4Pkjjw+NSd0EOBkIutYET4vJY4X81pD9ooQO9gfBGXj4+g==",
16 | "dependencies": {
17 | "Spectre.Console": "0.49.1"
18 | }
19 | },
20 | "Microsoft.Extensions.DependencyInjection.Abstractions": {
21 | "type": "Transitive",
22 | "resolved": "8.0.2",
23 | "contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg=="
24 | },
25 | "Microsoft.Extensions.Logging.Abstractions": {
26 | "type": "Transitive",
27 | "resolved": "8.0.3",
28 | "contentHash": "dL0QGToTxggRLMYY4ZYX5AMwBb+byQBd/5dMiZE07Nv73o6I5Are3C7eQTh7K2+A4ct0PVISSr7TZANbiNb2yQ==",
29 | "dependencies": {
30 | "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
31 | }
32 | },
33 | "Spectre.Console": {
34 | "type": "Transitive",
35 | "resolved": "0.49.1",
36 | "contentHash": "USV+pdu49OJ3nCjxNuw1K9Zw/c1HCBbwbjXZp0EOn6wM99tFdAtN34KEBZUMyRuJuXlUMDqhd8Yq9obW2MslYA=="
37 | },
38 | "forzadata-core": {
39 | "type": "Project",
40 | "dependencies": {
41 | "Microsoft.Extensions.Logging.Abstractions": "[8.0.3, )"
42 | }
43 | }
44 | },
45 | "net8.0/linux-arm64": {},
46 | "net8.0/linux-x64": {},
47 | "net8.0/osx-arm64": {},
48 | "net8.0/osx-x64": {},
49 | "net8.0/win-arm64": {},
50 | "net8.0/win-x64": {}
51 | }
52 | }
--------------------------------------------------------------------------------
/SampleRecorder/packages.lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "dependencies": {
4 | "net8.0": {
5 | "Microsoft.NET.ILLink.Tasks": {
6 | "type": "Direct",
7 | "requested": "[8.0.12, )",
8 | "resolved": "8.0.12",
9 | "contentHash": "FV4HnQ3JI15PHnJ5PGTbz+rYvrih42oLi/7UMIshNwCwUZhTq13UzrggtXk4ygrcMcN+4jsS6hhshx2p/Zd0ig=="
10 | },
11 | "Spectre.Console.Cli": {
12 | "type": "Direct",
13 | "requested": "[0.49.1, )",
14 | "resolved": "0.49.1",
15 | "contentHash": "wBZzyEbKqfPFFUPhV5E7/k4Kwy4UDO42IVzvzk0C4Pkjjw+NSd0EOBkIutYET4vJY4X81pD9ooQO9gfBGXj4+g==",
16 | "dependencies": {
17 | "Spectre.Console": "0.49.1"
18 | }
19 | },
20 | "Microsoft.Extensions.DependencyInjection.Abstractions": {
21 | "type": "Transitive",
22 | "resolved": "8.0.2",
23 | "contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg=="
24 | },
25 | "Microsoft.Extensions.Logging.Abstractions": {
26 | "type": "Transitive",
27 | "resolved": "8.0.3",
28 | "contentHash": "dL0QGToTxggRLMYY4ZYX5AMwBb+byQBd/5dMiZE07Nv73o6I5Are3C7eQTh7K2+A4ct0PVISSr7TZANbiNb2yQ==",
29 | "dependencies": {
30 | "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
31 | }
32 | },
33 | "Spectre.Console": {
34 | "type": "Transitive",
35 | "resolved": "0.49.1",
36 | "contentHash": "USV+pdu49OJ3nCjxNuw1K9Zw/c1HCBbwbjXZp0EOn6wM99tFdAtN34KEBZUMyRuJuXlUMDqhd8Yq9obW2MslYA=="
37 | },
38 | "forzadata-core": {
39 | "type": "Project",
40 | "dependencies": {
41 | "Microsoft.Extensions.Logging.Abstractions": "[8.0.3, )"
42 | }
43 | }
44 | },
45 | "net8.0/linux-arm64": {},
46 | "net8.0/linux-x64": {},
47 | "net8.0/osx-arm64": {},
48 | "net8.0/osx-x64": {},
49 | "net8.0/win-arm64": {},
50 | "net8.0/win-x64": {}
51 | }
52 | }
--------------------------------------------------------------------------------
/Console/packages.lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "dependencies": {
4 | "net8.0": {
5 | "Microsoft.NET.ILLink.Tasks": {
6 | "type": "Direct",
7 | "requested": "[8.0.12, )",
8 | "resolved": "8.0.12",
9 | "contentHash": "FV4HnQ3JI15PHnJ5PGTbz+rYvrih42oLi/7UMIshNwCwUZhTq13UzrggtXk4ygrcMcN+4jsS6hhshx2p/Zd0ig=="
10 | },
11 | "Spectre.Console": {
12 | "type": "Direct",
13 | "requested": "[0.49.1, )",
14 | "resolved": "0.49.1",
15 | "contentHash": "USV+pdu49OJ3nCjxNuw1K9Zw/c1HCBbwbjXZp0EOn6wM99tFdAtN34KEBZUMyRuJuXlUMDqhd8Yq9obW2MslYA=="
16 | },
17 | "Spectre.Console.Cli": {
18 | "type": "Direct",
19 | "requested": "[0.49.1, )",
20 | "resolved": "0.49.1",
21 | "contentHash": "wBZzyEbKqfPFFUPhV5E7/k4Kwy4UDO42IVzvzk0C4Pkjjw+NSd0EOBkIutYET4vJY4X81pD9ooQO9gfBGXj4+g==",
22 | "dependencies": {
23 | "Spectre.Console": "0.49.1"
24 | }
25 | },
26 | "Microsoft.Extensions.DependencyInjection.Abstractions": {
27 | "type": "Transitive",
28 | "resolved": "8.0.2",
29 | "contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg=="
30 | },
31 | "Microsoft.Extensions.Logging.Abstractions": {
32 | "type": "Transitive",
33 | "resolved": "8.0.3",
34 | "contentHash": "dL0QGToTxggRLMYY4ZYX5AMwBb+byQBd/5dMiZE07Nv73o6I5Are3C7eQTh7K2+A4ct0PVISSr7TZANbiNb2yQ==",
35 | "dependencies": {
36 | "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
37 | }
38 | },
39 | "forzadata-core": {
40 | "type": "Project",
41 | "dependencies": {
42 | "Microsoft.Extensions.Logging.Abstractions": "[8.0.3, )"
43 | }
44 | }
45 | },
46 | "net8.0/linux-arm64": {},
47 | "net8.0/linux-x64": {},
48 | "net8.0/osx-arm64": {},
49 | "net8.0/osx-x64": {},
50 | "net8.0/win-arm64": {},
51 | "net8.0/win-x64": {}
52 | }
53 | }
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": ".NET 8 SDK",
3 |
4 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
5 | "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
6 |
7 | // Features to add to the dev container. More info: https://containers.dev/features.
8 | // .NET SDK version must be aligned with global.json for locked-mode restores to work properly
9 | "features": {
10 | "ghcr.io/devcontainers/features/dotnet:2": {
11 | "version": "8.0.405"
12 | },
13 | "ghcr.io/devcontainers/features/git-lfs:1": {}
14 | },
15 |
16 | // UDP listening port
17 | "appPort": "7777:7777/udp",
18 |
19 | // TCP listening ports
20 | "forwardPorts": [
21 | 5000,
22 | 5001
23 | ],
24 |
25 | // The UDP port needs to be forwarded to the devcontainer
26 | "portsAttributes": {
27 | "5000": {
28 | "label": "Web",
29 | "protocol": "http"
30 | },
31 | "5001": {
32 | "label": "Web",
33 | "protocol": "https"
34 | },
35 | "7777": {
36 | "label": "UDP Listening Port"
37 | }
38 | },
39 |
40 | "onCreateCommand": "sudo dotnet workload update",
41 | "postCreateCommand": "dotnet restore",
42 |
43 | // Configure tool-specific properties.
44 | "customizations": {
45 | "vscode": {
46 | "extensions": [
47 | "ms-dotnettools.csdevkit",
48 | "redhat.vscode-yaml",
49 | "redhat.vscode-xml",
50 | "ms-dotnettools.vscodeintellicode-csharp",
51 | "EditorConfig.EditorConfig",
52 | "yzhang.markdown-all-in-one",
53 | "ms-dotnettools.csharp",
54 | "ms-dotnettools.vscode-dotnet-runtime",
55 | "ms-vscode.hexeditor"
56 | ]
57 | }
58 | },
59 |
60 | // environment variables
61 | "containerEnv": {
62 | "DOTNET_NOLOGO": "true",
63 | "DOTNET_CLI_TELEMETRY_OPTOUT": "true"
64 | }
65 |
66 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
67 | // "remoteUser": "root"
68 | }
69 |
--------------------------------------------------------------------------------
/Web/Views/Shared/_CookieConsentPartial.cshtml:
--------------------------------------------------------------------------------
1 | @using Microsoft.AspNetCore.Http.Features
2 |
3 | @{
4 | var consentFeature = Context.Features.Get();
5 | var showBanner = !consentFeature?.CanTrack ?? false;
6 | var cookieString = consentFeature?.CreateConsentCookie();
7 | }
8 |
9 | @if (showBanner)
10 | {
11 |
33 |
41 | }
--------------------------------------------------------------------------------
/SampleFix/DefaultCommandSettings.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel;
2 |
3 | namespace ForzaData.SampleFix;
4 |
5 | public sealed class DefaultCommandSettings : CommandSettings
6 | {
7 | [Description("Use constant 60 Hz to resynchronize sample")]
8 | [CommandOption("--use-static-frequency")]
9 | [DefaultValue(false)]
10 | public bool UseStaticFrequency { get; set; } = false;
11 |
12 | [Description("Use forza data's decoded timestamps to resynchronize (recommended)")]
13 | [CommandOption("--use-timestamp")]
14 | [DefaultValue(false)]
15 | public bool UseTimestamp { get; set; } = false;
16 |
17 | [Description("Ignores invalid chunks (not increasing / not parsable)")]
18 | [CommandOption("--ignore-invalid-chunks")]
19 | [DefaultValue(false)]
20 | public bool IgnoreInvalidChunks { get; set; } = false;
21 |
22 | [Description("Input file")]
23 | [CommandOption("-i|--input")]
24 | public string? Input { get; set; }
25 |
26 | [Description("Output file")]
27 | [CommandOption("-o|--output")]
28 | public string? Output { get; set; }
29 |
30 | public override ValidationResult Validate()
31 | {
32 | if (UseStaticFrequency == UseTimestamp)
33 | return ValidationResult.Error("You must specify which fix to use: timestamp OR frequency");
34 |
35 | if (string.IsNullOrEmpty(Input))
36 | return ValidationResult.Error("An input file must be specified");
37 |
38 | if (string.IsNullOrEmpty(Output))
39 | return ValidationResult.Error("An output file must be specified");
40 |
41 | var inputFile = new FileInfo(Input);
42 |
43 | if (!inputFile.Exists)
44 | return ValidationResult.Error($"Input file {inputFile.Name} is not found or its access is denied");
45 |
46 | var outputFile = new FileInfo(Output);
47 |
48 | if (outputFile.Directory?.Exists ?? false)
49 | {
50 | try
51 | {
52 | outputFile.Directory.Create();
53 | }
54 | catch (Exception ex)
55 | {
56 | return ValidationResult.Error($"An error occured while creating output file directory: {ex.Message}");
57 | }
58 | }
59 |
60 | return ValidationResult.Success();
61 | }
62 | }
--------------------------------------------------------------------------------
/Tests/ForzaDataReaderTests.cs:
--------------------------------------------------------------------------------
1 | using ForzaData.Core;
2 | using Microsoft.VisualStudio.TestTools.UnitTesting;
3 |
4 | namespace ForzaData.Tests;
5 |
6 | [TestClass]
7 | public class ForzaDataReaderTests
8 | {
9 | [TestMethod]
10 | [DataTestMethod]
11 | [DataRow("FM7_Sled_2019-04-25_Xbox.bin")]
12 | [DataRow("FM7_CarDash_2019-04-25_Xbox.bin")]
13 | public void ActualVsLegacy(string sampleFileName)
14 | {
15 | var sampleFolder = new DirectoryInfo("Samples");
16 | var sampleFile = new FileInfo(Path.Combine(sampleFolder.FullName, sampleFileName));
17 |
18 | using var sampleStream = sampleFile.OpenRead();
19 | using var sampleReader = new SampleReader(sampleStream);
20 |
21 | var actualDataReader = new ForzaDataReader();
22 |
23 | var sledFields = typeof(ForzaSledDataStruct).GetFields();
24 | var carDashFields = typeof(ForzaCarDashDataStruct).GetFields();
25 |
26 | // iterate every chunk
27 | while (sampleReader.TryRead(out SampleStruct chunk))
28 | {
29 | // read chunk with old and new readers
30 | var expectedForzaData = LegacyForzaDataReader.Read(chunk.Data);
31 | var actualForzaData = actualDataReader.Read(chunk.Data);
32 |
33 | // comparing read version
34 | Assert.AreEqual(
35 | expectedForzaData.Version,
36 | actualForzaData.Version,
37 | $"{nameof(ForzaDataStruct.Version)} comparison failed at chunk Elapsed = {chunk.Elapsed}"
38 | );
39 |
40 | // comparing each sled field
41 | foreach (var sledField in sledFields)
42 | {
43 | Assert.AreEqual(
44 | sledField.GetValue(expectedForzaData.Sled),
45 | sledField.GetValue(actualForzaData.Sled),
46 | $"{sledField.Name} comparison failed at chunk Elapsed = {chunk.Elapsed}"
47 | );
48 | }
49 |
50 | // comparing each car dash field
51 | foreach (var carDashField in carDashFields)
52 | {
53 | Assert.AreEqual(
54 | carDashField.GetValue(expectedForzaData.CarDash),
55 | carDashField.GetValue(actualForzaData.CarDash),
56 | $"{carDashField.Name} comparison failed at chunk Elapsed = {chunk.Elapsed}"
57 | );
58 | }
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "options": {
4 | "cwd": "${workspaceFolder}",
5 | },
6 | "presentation": {
7 | "echo": false,
8 | "focus": true,
9 | "panel": "dedicated",
10 | "showReuseMessage": false,
11 | "clear": true
12 | },
13 | "tasks": [
14 | {
15 | "label": "restore",
16 | "icon": {
17 | "id": "package"
18 | },
19 | "command": "dotnet",
20 | "type": "process",
21 | "args": [
22 | "restore",
23 | "--use-current-runtime",
24 | "/property:GenerateFullPaths=true",
25 | "/consoleloggerparameters:NoSummary"
26 | ],
27 | "problemMatcher": "$msCompile"
28 | },
29 | {
30 | "label": "build",
31 | "icon": {
32 | "id": "run"
33 | },
34 | "command": "dotnet",
35 | "type": "process",
36 | "args": [
37 | "build",
38 | "--use-current-runtime",
39 | "/property:GenerateFullPaths=true",
40 | "/consoleloggerparameters:NoSummary"
41 | ],
42 | "problemMatcher": "$msCompile",
43 | "group": {
44 | "kind": "build",
45 | "isDefault": true
46 | }
47 | },
48 | {
49 | "label": "test",
50 | "icon": {
51 | "id": "beaker"
52 | },
53 | "command": "dotnet",
54 | "type": "process",
55 | "args": [
56 | "test",
57 | "--results-directory", "TestResults",
58 | "--logger", "html;LogFileName=TestResult.html",
59 | "--logger", "trx;LogFileName=TestResult.trx",
60 | "/property:GenerateFullPaths=true",
61 | "/consoleloggerparameters:NoSummary"
62 | ],
63 | "problemMatcher": "$msCompile",
64 | "group": {
65 | "kind": "test",
66 | "isDefault": true
67 | }
68 | },
69 | {
70 | "label": "publish",
71 | "icon": {
72 | "id": "rocket"
73 | },
74 | "command": "dotnet",
75 | "type": "process",
76 | "args": [
77 | "publish",
78 | "--use-current-runtime",
79 | "/property:GenerateFullPaths=true",
80 | "/consoleloggerparameters:NoSummary"
81 | ],
82 | "problemMatcher": "$msCompile"
83 | },
84 | ]
85 | }
--------------------------------------------------------------------------------
/Console/CarClasses.cs:
--------------------------------------------------------------------------------
1 | namespace ForzaData.Console;
2 |
3 | public static class CarClasses
4 | {
5 | private static class Motorsport
6 | {
7 | internal static bool IsMotorsportCarClass(int carClass, int carPerformance) => carClass switch
8 | {
9 | 0 => carPerformance is >= 100 and <= 300, // E
10 | 1 => carPerformance is >= 301 and <= 400, // D
11 | 2 => carPerformance is >= 401 and <= 500, // C
12 | 3 => carPerformance is >= 501 and <= 600, // B
13 | 4 => carPerformance is >= 601 and <= 700, // A
14 | 5 => carPerformance is >= 701 and <= 800, // S
15 | 6 => carPerformance is >= 801 and <= 900, // R
16 | 7 => carPerformance is >= 901 and <= 998, // P
17 | 8 => carPerformance == 999, // X
18 | _ => false
19 | };
20 |
21 | internal static string? GetMotorsportCarClassCode(int carClass) => carClass switch
22 | {
23 | 0 => "E",
24 | 1 => "D",
25 | 2 => "C",
26 | 3 => "B",
27 | 4 => "A",
28 | 5 => "S",
29 | 6 => "R",
30 | 7 => "P",
31 | 8 => "X",
32 | _ => null
33 | };
34 | }
35 |
36 | private static class Horizon
37 | {
38 | internal static bool IsHorizonCarClass(int carClass, int carPerformance) => carClass switch
39 | {
40 | 0 => carPerformance is >= 100 and <= 500, // D
41 | 1 => carPerformance is >= 501 and <= 600, // C
42 | 2 => carPerformance is >= 601 and <= 700, // B
43 | 3 => carPerformance is >= 701 and <= 800, // A
44 | 4 => carPerformance is >= 801 and <= 900, // S1
45 | 5 => carPerformance is >= 901 and <= 998, // S2
46 | 6 => carPerformance == 999, // X
47 | _ => false
48 | };
49 |
50 | internal static string? GetHorizonCarClassCode(int carClass) => carClass switch
51 | {
52 | 0 => "D",
53 | 1 => "C",
54 | 2 => "B",
55 | 3 => "A",
56 | 4 => "S1",
57 | 5 => "S2",
58 | 6 => "X",
59 | _ => null
60 | };
61 | }
62 |
63 | public static string? GetCarClassCode(int carClass, int carPerformance)
64 | {
65 | string? carClassCode = null;
66 |
67 | if (Motorsport.IsMotorsportCarClass(carClass, carPerformance))
68 | carClassCode = Motorsport.GetMotorsportCarClassCode(carClass);
69 | else if (Horizon.IsHorizonCarClass(carClass, carPerformance))
70 | carClassCode = Horizon.GetHorizonCarClassCode(carClass);
71 |
72 | return carClassCode;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/SampleFix/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "FM7_CarDash_2019-04-25_Xbox_TS-fix": {
4 | "commandName": "Project",
5 | "commandLineArgs": "--use-timestamp --ignore-invalid-chunks --input FM7_CarDash_2019-04-25_Xbox.bin --output FM7_CarDash_2019-04-25_Xbox_TS-fix.bin",
6 | "workingDirectory": "$(SolutionDir)\\Samples"
7 | },
8 | "FM7_CarDash_2019-04-25_Xbox_60Hz-fix": {
9 | "commandName": "Project",
10 | "commandLineArgs": "--use-static-frequency --ignore-invalid-chunks --input FM7_CarDash_2019-04-25_Xbox.bin --output FM7_CarDash_2019-04-25_Xbox_60Hz-fix.bin",
11 | "workingDirectory": "$(SolutionDir)\\Samples"
12 | },
13 | "FH4_FreeRoam_Winter_2019-04-25_Xbox_TS-fix": {
14 | "commandName": "Project",
15 | "commandLineArgs": "--use-timestamp --ignore-invalid-chunks --input FH4_FreeRoam_Winter_2019-04-25_Xbox.bin --output FH4_FreeRoam_Winter_2019-04-25_Xbox_TS-fix.bin",
16 | "workingDirectory": "$(SolutionDir)\\Samples"
17 | },
18 | "FH4_FreeRoam_Winter_2019-04-25_Xbox_60Hz-fix": {
19 | "commandName": "Project",
20 | "commandLineArgs": "--use-static-frequency --ignore-invalid-chunks --input FH4_FreeRoam_Winter_2019-04-25_Xbox.bin --output FH4_FreeRoam_Winter_2019-04-25_Xbox_60Hz-fix.bin",
21 | "workingDirectory": "$(SolutionDir)\\Samples"
22 | },
23 | "FH4_Race_Winter_2019-04-25_Xbox_TS-fix": {
24 | "commandName": "Project",
25 | "commandLineArgs": "--use-timestamp --ignore-invalid-chunks --input FH4_Race_Winter_2019-04-25_Xbox.bin --output FH4_Race_Winter_2019-04-25_Xbox_TS-fix.bin",
26 | "workingDirectory": "$(SolutionDir)\\Samples"
27 | },
28 | "FH4_Race_Winter_2019-04-25_Xbox_60Hz-fix": {
29 | "commandName": "Project",
30 | "commandLineArgs": "--use-static-frequency --ignore-invalid-chunks --input FH4_Race_Winter_2019-04-25_Xbox.bin --output FH4_Race_Winter_2019-04-25_Xbox_60Hz-fix.bin",
31 | "workingDirectory": "$(SolutionDir)\\Samples"
32 | },
33 | "FM7_Sled_2019-04-25_Xbox_TS-fix": {
34 | "commandName": "Project",
35 | "commandLineArgs": "--use-timestamp --ignore-invalid-chunks --input FM7_Sled_2019-04-25_Xbox.bin --output FM7_Sled_2019-04-25_Xbox_TS-fix.bin",
36 | "workingDirectory": "$(SolutionDir)\\Samples"
37 | },
38 | "FM7_Sled_2019-04-25_Xbox_60Hz-fix.bin": {
39 | "commandName": "Project",
40 | "commandLineArgs": "--use-static-frequency --ignore-invalid-chunks --input FM7_Sled_2019-04-25_Xbox.bin --output FM7_Sled_2019-04-25_Xbox_60Hz-fix.bin",
41 | "workingDirectory": "$(SolutionDir)\\Samples"
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/Core/ForzaDataReader.cs:
--------------------------------------------------------------------------------
1 | namespace ForzaData.Core;
2 |
3 | public partial class ForzaDataReader
4 | {
5 | /// Forza Motorsport 7 and Forza Motorsport (2023) sled only
6 | internal const int SledPacketLength = 232;
7 |
8 | /// Forza Motorsport 7 sled + car dash
9 | internal const int CarDashV1PacketLength = 311;
10 |
11 | /// Forza Horizon 4/5 sled + car dash
12 | internal const int CarDashV2PacketLength = 324;
13 |
14 | /// Forza Motorsport (2023) sled + car dash
15 | internal const int CarDashV3PacketLength = 331;
16 |
17 | /// Range of sled data (for all games)
18 | internal static readonly Range SledRange = 0..232;
19 |
20 | /// Range of car dash data (for Forza Motorsport 7 and Forza Motorsport (2023))
21 | internal static readonly Range CarDashRange = 232..311;
22 |
23 | /// Range of Horizon extras data (for Forza Horizon 4 and 5)
24 | internal static readonly Range HorizonExtrasRange = 232..244;
25 |
26 | /// Range of car dash data (for Forza Horizon 4 and 5)
27 | internal static readonly Range CarDashAfterHorizonExtrasRange = 244..323;
28 |
29 | /// Range of Motorsport extras data (for Forza Motorsport (2023))
30 | internal static readonly Range MotorsportExtrasRange = 311..331;
31 |
32 | public ForzaDataReader()
33 | {
34 |
35 | }
36 |
37 | public bool TryRead(in ReadOnlySpan input, out ForzaDataStruct output)
38 | {
39 | try
40 | {
41 | output = Read(input);
42 | return true;
43 | }
44 | catch
45 | {
46 | output = new ForzaDataStruct()
47 | {
48 | Version = ForzaDataVersion.Unknown
49 | };
50 | return false;
51 | }
52 | }
53 |
54 | public ForzaDataStruct Read(in ReadOnlySpan input)
55 | {
56 | ForzaDataStruct output = new()
57 | {
58 | Version = GetVersion(input.Length)
59 | };
60 |
61 | if (output.Version != ForzaDataVersion.Unknown)
62 | {
63 | output.Sled = ReadSledData(input[SledRange]);
64 |
65 | if (output.Version == ForzaDataVersion.CarDashV1)
66 | {
67 | output.CarDash = ReadCarDashData(input[CarDashRange]);
68 | }
69 | else if (output.Version == ForzaDataVersion.CarDashV2)
70 | {
71 | output.HorizonExtras = ReadHorizonExtrasData(input[HorizonExtrasRange]);
72 | output.CarDash = ReadCarDashData(input[CarDashAfterHorizonExtrasRange]);
73 | }
74 | else if (output.Version == ForzaDataVersion.CarDashV3)
75 | {
76 | output.CarDash = ReadCarDashData(input[CarDashRange]);
77 | output.MotorsportExtras = ReadMotorsportExtrasData(input[MotorsportExtrasRange]);
78 | }
79 | }
80 |
81 | return output;
82 | }
83 |
84 | private static ForzaDataVersion GetVersion(int length) => length switch
85 | {
86 | SledPacketLength => ForzaDataVersion.Sled,
87 | CarDashV1PacketLength => ForzaDataVersion.CarDashV1,
88 | CarDashV2PacketLength => ForzaDataVersion.CarDashV2,
89 | CarDashV3PacketLength => ForzaDataVersion.CarDashV3,
90 | _ => ForzaDataVersion.Unknown,
91 | };
92 | }
93 |
--------------------------------------------------------------------------------
/Core/UdpStreamListener.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Net.Sockets;
3 |
4 | namespace ForzaData.Core;
5 |
6 | public class UdpStreamListener(int port, IPAddress serverIpAddress) : IDisposable, IObservable
7 | {
8 | private readonly UdpClient _udpClient = new(port);
9 | private readonly ICollection> _observers = [];
10 | private IPEndPoint _serverEndPoint = new(serverIpAddress, 0);
11 |
12 | private bool _isDisposed = false;
13 | private byte[]? _lastData = null;
14 |
15 | public void Close()
16 | {
17 | _udpClient.Close();
18 | }
19 |
20 | public void Dispose()
21 | {
22 | Dispose(true);
23 | GC.SuppressFinalize(this);
24 | }
25 |
26 | protected virtual void Dispose(bool isDisposing)
27 | {
28 | if (!_isDisposed)
29 | {
30 | if (isDisposing)
31 | {
32 | _udpClient.Dispose();
33 | }
34 |
35 | _isDisposed = true;
36 | }
37 | }
38 |
39 | public IDisposable Subscribe(IObserver observer)
40 | {
41 | if (!_observers.Contains(observer))
42 | {
43 | _observers.Add(observer);
44 |
45 | if (_lastData != null)
46 | {
47 | observer.OnNext(_lastData);
48 | }
49 | }
50 | return new UdpStreamUnsubscriber(_observers, observer);
51 | }
52 |
53 | protected virtual void NotifyData(byte[] data)
54 | {
55 | foreach (var observer in _observers)
56 | {
57 | observer.OnNext(data);
58 | }
59 | }
60 |
61 | protected virtual void NotifyError(Exception error)
62 | {
63 | foreach (var observer in _observers)
64 | {
65 | observer.OnError(error);
66 | }
67 | }
68 |
69 | protected virtual void NotifyCompletion()
70 | {
71 | foreach (var observer in _observers)
72 | {
73 | observer.OnCompleted();
74 | }
75 | }
76 |
77 | public void Listen(CancellationToken cancellation)
78 | {
79 | while (true)
80 | {
81 | IAsyncResult receiveResult = _udpClient.BeginReceive(ReceiveCallback, null);
82 |
83 | // wait for async receive or cancel requested
84 | WaitHandle.WaitAny([receiveResult.AsyncWaitHandle, cancellation.WaitHandle]);
85 |
86 | if (cancellation.IsCancellationRequested)
87 | {
88 | // don't have choice, UdpClient will trigger receiveCallback on UDP socket closing
89 | _udpClient.Close();
90 | break;
91 | }
92 | }
93 |
94 | // end of notifications
95 | NotifyCompletion();
96 |
97 | // notify end of operations if requested
98 | cancellation.ThrowIfCancellationRequested();
99 | }
100 |
101 | private void ReceiveCallback(IAsyncResult asyncResult)
102 | {
103 | byte[] data;
104 |
105 | try
106 | {
107 | _lastData = data = _udpClient.EndReceive(asyncResult, ref _serverEndPoint!);
108 | }
109 | catch (ObjectDisposedException)
110 | {
111 | // UDP client has been closed previously to abort connect/receive
112 | // no need to notify observers or throw exception
113 | return;
114 | }
115 | catch (Exception ex)
116 | {
117 | // fatal exception: notify observers
118 | NotifyError(ex);
119 | throw;
120 | }
121 |
122 | // success reading
123 | NotifyData(data);
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/Web/Views/Shared/_Layout.cshtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | @ViewData["Title"] - ForzaData.Web
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
20 |
40 |
41 |
42 |
43 |
44 | @RenderBody()
45 |
46 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
63 |
69 |
70 |
71 |
72 | @RenderSection("Scripts", required: false)
73 |
74 |
75 |
--------------------------------------------------------------------------------
/SampleFix/DefaultCommand.cs:
--------------------------------------------------------------------------------
1 | namespace ForzaData.SampleFix;
2 |
3 | public sealed class DefaultCommand : Command
4 | {
5 | private const double ChunkTime = 1000d / 60d; // 60 Hz = 16.667 ms elapsed per chunk
6 |
7 | internal enum ExitCodes
8 | {
9 | OK = 0,
10 | Help = 1,
11 | InputError = 2,
12 | NonIncreasingTimestamps = 3
13 | }
14 |
15 | public override int Execute([NotNull] CommandContext context, [NotNull] DefaultCommandSettings settings)
16 | {
17 | var inputFile = new FileInfo(settings.Input!);
18 | var outputFile = new FileInfo(settings.Output!);
19 |
20 | string typeOfFix = settings.UseTimestamp
21 | ? "timestamp"
22 | : settings.UseStaticFrequency
23 | ? "fixed frequency"
24 | : "unknown";
25 |
26 | Console.Out.WriteLine($"Input: {inputFile.Name}");
27 | Console.Out.WriteLine($"Output: {outputFile.Name}");
28 | Console.Out.WriteLine($"Fix: {typeOfFix}");
29 |
30 | // input read
31 | using var input = new FileStream(inputFile.FullName, FileMode.Open, FileAccess.Read);
32 | using var output = new FileStream(outputFile.FullName, FileMode.Create, FileAccess.Write);
33 | using var reader = new SampleReader(input);
34 | using var writer = new SampleWriter(output);
35 |
36 | ForzaDataReader dataReader = new();
37 | ForzaDataStruct? firstForzaData = null;
38 | ForzaDataStruct? prevForzaData = null;
39 | int chunkIndex;
40 |
41 | // iterate every chunk
42 | for (chunkIndex = 0; reader.TryRead(out SampleStruct chunk); chunkIndex++)
43 | {
44 | // decode forza data structure
45 | ForzaDataStruct forzaData = dataReader.Read(chunk.Data);
46 | if (!firstForzaData.HasValue)
47 | firstForzaData = forzaData;
48 |
49 | // if timestamps are used, validate them
50 | if (settings.UseTimestamp)
51 | {
52 | // non increasing timestamps
53 | if (prevForzaData?.Sled.TimestampMS > forzaData.Sled.TimestampMS)
54 | {
55 | if (settings.IgnoreInvalidChunks)
56 | {
57 | Console.Out.WriteLine($"Ignoring chunk #{chunkIndex}, {chunk.Length} bytes (non increasing timestamp)");
58 | continue;
59 | }
60 | else
61 | {
62 | Console.Error.WriteLine($"Non increasing timestamp at chunk #{chunkIndex}, {chunk.Length} bytes");
63 | return (int)ExitCodes.NonIncreasingTimestamps;
64 | }
65 | }
66 |
67 | // fixing elapsed ticks from previous chunk timestamp diff
68 | uint elapsedMilliseconds = forzaData.Sled.TimestampMS - firstForzaData.Value.Sled.TimestampMS;
69 | chunk.Elapsed = TimeSpan.FromMilliseconds(elapsedMilliseconds).Ticks;
70 | }
71 | else if (settings.UseStaticFrequency)
72 | {
73 | // fixing elapsed ticks with static chunk time
74 | uint elapsedMilliseconds = (uint)Math.Round(
75 | chunkIndex * ChunkTime,
76 | MidpointRounding.AwayFromZero
77 | );
78 | chunk.Elapsed = TimeSpan.FromMilliseconds(elapsedMilliseconds).Ticks;
79 | }
80 |
81 | // fixed chunk written to output
82 | writer.Write(chunk);
83 |
84 | prevForzaData = forzaData;
85 | }
86 |
87 | // warning if not on end of stream
88 | if (!reader.EndOfStream)
89 | {
90 | Console.Error.WriteLine($"Can't decode more chunks, the rest of the source is ignored");
91 | }
92 |
93 | writer.Flush();
94 | outputFile.Refresh();
95 |
96 | // final result
97 | Console.Out.WriteLine($"Chunks: {reader.Reads} read, {writer.Writes} written (diff: {writer.Writes - reader.Reads})");
98 | Console.Out.WriteLine($"Size: {inputFile.Length} bytes on input, {outputFile.Length} bytes on output, (diff: {outputFile.Length - inputFile.Length})");
99 |
100 | return (int)ExitCodes.OK;
101 | }
102 | }
--------------------------------------------------------------------------------
/Core/ForzaDataReader.CarDash.cs:
--------------------------------------------------------------------------------
1 | namespace ForzaData.Core;
2 |
3 | public partial class ForzaDataReader
4 | {
5 | ///
6 | /// Map of all car dash values
7 | ///
8 | ///
9 | /// Indexes are not absolute, they are relative to the data offset.
10 | /// See and for info.
11 | ///
12 | internal static class CarDashMap
13 | {
14 | internal static readonly Range PositionX = 0..4;
15 | internal static readonly Range PositionY = 4..8;
16 | internal static readonly Range PositionZ = 8..12;
17 |
18 | internal static readonly Range Speed = 12..16;
19 | internal static readonly Range Power = 16..20;
20 | internal static readonly Range Torque = 20..24;
21 |
22 | internal static readonly Range TireTempFrontLeft = 24..28;
23 | internal static readonly Range TireTempFrontRight = 28..32;
24 | internal static readonly Range TireTempRearLeft = 32..36;
25 | internal static readonly Range TireTempRearRight = 36..40;
26 |
27 | internal static readonly Range Boost = 40..44;
28 | internal static readonly Range Fuel = 44..48;
29 | internal static readonly Range DistanceTraveled = 48..52;
30 | internal static readonly Range BestLap = 52..56;
31 | internal static readonly Range LastLap = 56..60;
32 | internal static readonly Range CurrentLap = 60..64;
33 | internal static readonly Range CurrentRaceTime = 64..68;
34 |
35 | internal static readonly Range LapNumber = 68..70;
36 | internal static readonly Index RacePosition = 70;
37 |
38 | internal static readonly Index Accel = 71;
39 | internal static readonly Index Brake = 72;
40 | internal static readonly Index Clutch = 73;
41 | internal static readonly Index HandBrake = 74;
42 | internal static readonly Index Gear = 75;
43 | internal static readonly Index Steer = 76;
44 |
45 | internal static readonly Index NormalizedDrivingLine = 77;
46 | internal static readonly Index NormalizedAIBrakeDifference = 78;
47 | }
48 |
49 | private static ForzaCarDashDataStruct ReadCarDashData(in ReadOnlySpan data) => new()
50 | {
51 | PositionX = BitConverter.ToSingle(data[CarDashMap.PositionX]),
52 | PositionY = BitConverter.ToSingle(data[CarDashMap.PositionY]),
53 | PositionZ = BitConverter.ToSingle(data[CarDashMap.PositionZ]),
54 |
55 | Speed = BitConverter.ToSingle(data[CarDashMap.Speed]),
56 | Power = BitConverter.ToSingle(data[CarDashMap.Power]),
57 | Torque = BitConverter.ToSingle(data[CarDashMap.Torque]),
58 |
59 | TireTempFrontLeft = BitConverter.ToSingle(data[CarDashMap.TireTempFrontLeft]),
60 | TireTempFrontRight = BitConverter.ToSingle(data[CarDashMap.TireTempFrontRight]),
61 | TireTempRearLeft = BitConverter.ToSingle(data[CarDashMap.TireTempRearLeft]),
62 | TireTempRearRight = BitConverter.ToSingle(data[CarDashMap.TireTempRearRight]),
63 |
64 | Boost = BitConverter.ToSingle(data[CarDashMap.Boost]),
65 | Fuel = BitConverter.ToSingle(data[CarDashMap.Fuel]),
66 | DistanceTraveled = BitConverter.ToSingle(data[CarDashMap.DistanceTraveled]),
67 | BestLap = BitConverter.ToSingle(data[CarDashMap.BestLap]),
68 | LastLap = BitConverter.ToSingle(data[CarDashMap.LastLap]),
69 | CurrentLap = BitConverter.ToSingle(data[CarDashMap.CurrentLap]),
70 | CurrentRaceTime = BitConverter.ToSingle(data[CarDashMap.CurrentRaceTime]),
71 |
72 | LapNumber = BitConverter.ToUInt16(data[CarDashMap.LapNumber]),
73 | RacePosition = data[CarDashMap.RacePosition],
74 |
75 | Accel = data[CarDashMap.Accel],
76 | Brake = data[CarDashMap.Brake],
77 | Clutch = data[CarDashMap.Clutch],
78 | HandBrake = data[CarDashMap.HandBrake],
79 | Gear = data[CarDashMap.Gear],
80 | Steer = unchecked((sbyte)data[CarDashMap.Steer]),
81 |
82 | NormalizedDrivingLine = unchecked((sbyte)data[CarDashMap.NormalizedDrivingLine]),
83 | NormalizedAIBrakeDifference = unchecked((sbyte)data[CarDashMap.NormalizedAIBrakeDifference])
84 | };
85 | }
86 |
--------------------------------------------------------------------------------
/ForzaDataWeb.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.0.31521.260
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForzaData.Web", "Web\ForzaData.Web.csproj", "{E97262A1-C8CE-45A6-9F6E-DA4D07DD765D}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForzaData.Tests", "Tests\ForzaData.Tests.csproj", "{7A2718F7-7DA3-4015-85D2-E2244F71CE5A}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForzaData.Console", "Console\ForzaData.Console.csproj", "{5B142436-5225-4461-9CDD-04C885200230}"
11 | EndProject
12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForzaData.Core", "Core\ForzaData.Core.csproj", "{15424D1D-BB5E-417A-A8E0-4F97340B1ADC}"
13 | EndProject
14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9EDFB982-DFDF-4E51-9C77-0D34A23B0AF8}"
15 | ProjectSection(SolutionItems) = preProject
16 | .editorconfig = .editorconfig
17 | CONTRIBUTING.md = CONTRIBUTING.md
18 | LICENSE = LICENSE
19 | README.md = README.md
20 | EndProjectSection
21 | EndProject
22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForzaData.SampleRecorder", "SampleRecorder\ForzaData.SampleRecorder.csproj", "{E98526DE-487B-4DC9-B523-AC5EC2770F8C}"
23 | EndProject
24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForzaData.SampleFix", "SampleFix\ForzaData.SampleFix.csproj", "{83007329-2B0F-4BDC-BDA4-909AFA351BCD}"
25 | EndProject
26 | Global
27 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
28 | Debug|Any CPU = Debug|Any CPU
29 | Release|Any CPU = Release|Any CPU
30 | EndGlobalSection
31 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
32 | {E97262A1-C8CE-45A6-9F6E-DA4D07DD765D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33 | {E97262A1-C8CE-45A6-9F6E-DA4D07DD765D}.Debug|Any CPU.Build.0 = Debug|Any CPU
34 | {E97262A1-C8CE-45A6-9F6E-DA4D07DD765D}.Release|Any CPU.ActiveCfg = Release|Any CPU
35 | {E97262A1-C8CE-45A6-9F6E-DA4D07DD765D}.Release|Any CPU.Build.0 = Release|Any CPU
36 | {7A2718F7-7DA3-4015-85D2-E2244F71CE5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
37 | {7A2718F7-7DA3-4015-85D2-E2244F71CE5A}.Debug|Any CPU.Build.0 = Debug|Any CPU
38 | {7A2718F7-7DA3-4015-85D2-E2244F71CE5A}.Release|Any CPU.ActiveCfg = Release|Any CPU
39 | {7A2718F7-7DA3-4015-85D2-E2244F71CE5A}.Release|Any CPU.Build.0 = Release|Any CPU
40 | {5B142436-5225-4461-9CDD-04C885200230}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
41 | {5B142436-5225-4461-9CDD-04C885200230}.Debug|Any CPU.Build.0 = Debug|Any CPU
42 | {5B142436-5225-4461-9CDD-04C885200230}.Release|Any CPU.ActiveCfg = Release|Any CPU
43 | {5B142436-5225-4461-9CDD-04C885200230}.Release|Any CPU.Build.0 = Release|Any CPU
44 | {15424D1D-BB5E-417A-A8E0-4F97340B1ADC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
45 | {15424D1D-BB5E-417A-A8E0-4F97340B1ADC}.Debug|Any CPU.Build.0 = Debug|Any CPU
46 | {15424D1D-BB5E-417A-A8E0-4F97340B1ADC}.Release|Any CPU.ActiveCfg = Release|Any CPU
47 | {15424D1D-BB5E-417A-A8E0-4F97340B1ADC}.Release|Any CPU.Build.0 = Release|Any CPU
48 | {E98526DE-487B-4DC9-B523-AC5EC2770F8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
49 | {E98526DE-487B-4DC9-B523-AC5EC2770F8C}.Debug|Any CPU.Build.0 = Debug|Any CPU
50 | {E98526DE-487B-4DC9-B523-AC5EC2770F8C}.Release|Any CPU.ActiveCfg = Release|Any CPU
51 | {E98526DE-487B-4DC9-B523-AC5EC2770F8C}.Release|Any CPU.Build.0 = Release|Any CPU
52 | {83007329-2B0F-4BDC-BDA4-909AFA351BCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
53 | {83007329-2B0F-4BDC-BDA4-909AFA351BCD}.Debug|Any CPU.Build.0 = Debug|Any CPU
54 | {83007329-2B0F-4BDC-BDA4-909AFA351BCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
55 | {83007329-2B0F-4BDC-BDA4-909AFA351BCD}.Release|Any CPU.Build.0 = Release|Any CPU
56 | EndGlobalSection
57 | GlobalSection(SolutionProperties) = preSolution
58 | HideSolutionNode = FALSE
59 | EndGlobalSection
60 | GlobalSection(ExtensibilityGlobals) = postSolution
61 | SolutionGuid = {9E84FB8E-183F-4EE0-8C40-2CB661FCA554}
62 | EndGlobalSection
63 | EndGlobal
64 |
--------------------------------------------------------------------------------
/Core/ForzaSledDataStruct.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 |
3 | namespace ForzaData.Core;
4 |
5 | ///
6 | /// "Sled" data structure
7 | ///
8 | [StructLayout(LayoutKind.Auto)]
9 | public struct ForzaSledDataStruct
10 | {
11 | // = 1 when race is on. = 0 when in menus/race stopped …
12 | public int IsRaceOn;
13 |
14 | //Can overflow to 0 eventually
15 | public uint TimestampMS;
16 |
17 | public float EngineMaxRpm;
18 | public float EngineIdleRpm;
19 | public float CurrentEngineRpm;
20 |
21 | //In the car's local space; X = right, Y = up, Z = forward
22 | public float AccelerationX;
23 | public float AccelerationY;
24 | public float AccelerationZ;
25 |
26 | //In the car's local space; X = right, Y = up, Z = forward
27 | public float VelocityX;
28 | public float VelocityY;
29 | public float VelocityZ;
30 |
31 | //In the car's local space; X = pitch, Y = yaw, Z = roll
32 | public float AngularVelocityX;
33 | public float AngularVelocityY;
34 | public float AngularVelocityZ;
35 |
36 | public float Yaw;
37 | public float Pitch;
38 | public float Roll;
39 |
40 | // Suspension travel normalized: 0.0f = max stretch; 1.0 = max compression
41 | public float NormalizedSuspensionTravelFrontLeft;
42 | public float NormalizedSuspensionTravelFrontRight;
43 | public float NormalizedSuspensionTravelRearLeft;
44 | public float NormalizedSuspensionTravelRearRight;
45 |
46 | // Tire normalized slip ratio, = 0 means 100% grip and |ratio| > 1.0 means loss of grip.
47 | public float TireSlipRatioFrontLeft;
48 | public float TireSlipRatioFrontRight;
49 | public float TireSlipRatioRearLeft;
50 | public float TireSlipRatioRearRight;
51 |
52 | // Wheel rotation speed radians/sec.
53 | public float WheelRotationSpeedFrontLeft;
54 | public float WheelRotationSpeedFrontRight;
55 | public float WheelRotationSpeedRearLeft;
56 | public float WheelRotationSpeedRearRight;
57 |
58 | // = 1 when wheel is on rumble strip, = 0 when off.
59 | public int WheelOnRumbleStripFrontLeft;
60 | public int WheelOnRumbleStripFrontRight;
61 | public int WheelOnRumbleStripRearLeft;
62 | public int WheelOnRumbleStripRearRight;
63 |
64 | // = from 0 to 1, where 1 is the deepest puddle
65 | public float WheelInPuddleDepthFrontLeft;
66 | public float WheelInPuddleDepthFrontRight;
67 | public float WheelInPuddleDepthRearLeft;
68 | public float WheelInPuddleDepthRearRight;
69 |
70 | // Non-dimensional surface rumble values passed to controller force feedback
71 | public float SurfaceRumbleFrontLeft;
72 | public float SurfaceRumbleFrontRight;
73 | public float SurfaceRumbleRearLeft;
74 | public float SurfaceRumbleRearRight;
75 |
76 | // Tire normalized slip angle, = 0 means 100% grip and |angle| > 1.0 means loss of grip.
77 | public float TireSlipAngleFrontLeft;
78 | public float TireSlipAngleFrontRight;
79 | public float TireSlipAngleRearLeft;
80 | public float TireSlipAngleRearRight;
81 |
82 | // Tire normalized combined slip, = 0 means 100% grip and |slip| > 1.0 means loss of grip.
83 | public float TireCombinedSlipFrontLeft;
84 | public float TireCombinedSlipFrontRight;
85 | public float TireCombinedSlipRearLeft;
86 | public float TireCombinedSlipRearRight;
87 |
88 | // Actual suspension travel in meters
89 | public float SuspensionTravelMetersFrontLeft;
90 | public float SuspensionTravelMetersFrontRight;
91 | public float SuspensionTravelMetersRearLeft;
92 | public float SuspensionTravelMetersRearRight;
93 |
94 | ///
95 | /// Unique ID of the car make/model
96 | ///
97 | public int CarOrdinal;
98 |
99 | ///
100 | /// Between 0 (D -- worst cars) and 7 (X class -- best cars) inclusive
101 | ///
102 | public int CarClass;
103 |
104 | ///
105 | /// Between 100 (slowest car) and 999 (fastest car) inclusive
106 | ///
107 | public int CarPerformanceIndex;
108 |
109 | ///
110 | /// Corresponds to EDrivetrainType; 0 = FWD, 1 = RWD, 2 = AWD
111 | ///
112 | public int DrivetrainType;
113 |
114 | ///
115 | /// Number of cylinders in the engine
116 | ///
117 | public int NumCylinders;
118 | }
119 |
--------------------------------------------------------------------------------
/Web/Views/Home/Index.cshtml:
--------------------------------------------------------------------------------
1 | @{
2 | ViewData["Title"] = "Home Page";
3 | }
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Learn how to build ASP.NET apps that can run anywhere.
17 |
18 | Learn More
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | There are powerful new features in Visual Studio for building modern web apps.
28 |
29 | Learn More
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | Learn how Microsoft's Azure cloud platform allows you to build, deploy, and scale web apps.
39 |
40 | Learn More
41 |
42 |