Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions Config/WheelConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using BepInEx.Configuration;

namespace com.drowhunter.NewStarGPTelemetryMod
{
/// <summary>
/// Holds all BepInEx ConfigEntry bindings for wheel / FFB integration.
/// All values are live-adjustable at runtime via the BepInEx config UI.
/// </summary>
internal class WheelConfig
{
// ── Wheel / General ──────────────────────────────────────────────────

public ConfigEntry<bool> Enabled { get; }
public ConfigEntry<string> Backend { get; }
public ConfigEntry<bool> RequireX64 { get; }

// ── Wheel / FFB ──────────────────────────────────────────────────────

public ConfigEntry<float> OverallStrength { get; }
public ConfigEntry<float> MaxTorque { get; }
public ConfigEntry<float> BumpThreshold { get; }
public ConfigEntry<float> BumpScale { get; }
public ConfigEntry<int> BumpCooldownMs { get; }
public ConfigEntry<float> CForceDampingScale { get; }
public ConfigEntry<float> SmoothingAlpha { get; }

// ── Wheel / Input ────────────────────────────────────────────────────

public ConfigEntry<float> SteerDeadzone { get; }
public ConfigEntry<float> SteerSaturation { get; }

public WheelConfig(ConfigFile config)
{
// Wheel / General
Enabled = config.Bind(
"Wheel/General", "Enabled", false,
"Enable wheel integration (Moza FFB + ViGEm virtual gamepad). REQUIRES MOZA Pithouse software running!");

Backend = config.Bind(
"Wheel/General", "Backend", "Moza",
"Wheel backend to use. Supported: Moza.");

RequireX64 = config.Bind(
"Wheel/General", "RequireX64", true,
"Abort wheel init if the process is not 64-bit (required for Moza native DLLs).");

// Wheel / FFB
OverallStrength = config.Bind(
"Wheel/FFB", "OverallStrength", 1.0f,
new ConfigDescription("Overall FFB strength multiplier.", new AcceptableValueRange<float>(0f, 2f)));

MaxTorque = config.Bind(
"Wheel/FFB", "MaxTorque", 1.0f,
new ConfigDescription("Maximum torque clamp (0..1 maps to DirectInput 0..10000).", new AcceptableValueRange<float>(0f, 1f)));

BumpThreshold = config.Bind(
"Wheel/FFB", "BumpThreshold", 0.15f,
new ConfigDescription("Minimum normalised tire delta required to trigger a bump impulse.", new AcceptableValueRange<float>(0f, 1f)));

BumpScale = config.Bind(
"Wheel/FFB", "BumpScale", 0.35f,
new ConfigDescription("Scales the lateral tire delta into a bump impulse magnitude.", new AcceptableValueRange<float>(0f, 2f)));

BumpCooldownMs = config.Bind(
"Wheel/FFB", "BumpCooldownMs", 60,
new ConfigDescription("Minimum milliseconds between successive bump impulses.", new AcceptableValueRange<int>(0, 500)));

CForceDampingScale = config.Bind(
"Wheel/FFB", "CForceDampingScale", 0.4f,
new ConfigDescription("Scales centripetal force into damper coefficient.", new AcceptableValueRange<float>(0f, 2f)));

SmoothingAlpha = config.Bind(
"Wheel/FFB", "SmoothingAlpha", 0.25f,
new ConfigDescription("Exponential smoothing factor for FFB output (0 = no update, 1 = no smoothing).", new AcceptableValueRange<float>(0f, 1f)));

// Wheel / Input
SteerDeadzone = config.Bind(
"Wheel/Input", "SteerDeadzone", 0.05f,
new ConfigDescription("Normalised steer axis deadzone (0..1).", new AcceptableValueRange<float>(0f, 0.5f)));

SteerSaturation = config.Bind(
"Wheel/Input", "SteerSaturation", 1.0f,
new ConfigDescription("Normalised steer axis saturation point (0..1). Values above this map to ±1.", new AcceptableValueRange<float>(0.1f, 1f)));
}
}
}
17 changes: 17 additions & 0 deletions Contracts/IVirtualGamepadOutput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace com.drowhunter.NewStarGPTelemetryMod
{
/// <summary>
/// Writes virtual gamepad state to the OS (e.g. via ViGEm).
/// </summary>
internal interface IVirtualGamepadOutput
{
/// <summary>Initialise and connect the virtual gamepad. Returns true on success.</summary>
bool Initialize();

/// <summary>Submit a new gamepad state to the virtual device.</summary>
void Submit(VirtualPadState state);

/// <summary>Disconnect and release the virtual gamepad.</summary>
void Dispose();
}
}
17 changes: 17 additions & 0 deletions Contracts/IWheelFfbProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace com.drowhunter.NewStarGPTelemetryMod
{
/// <summary>
/// Sends force-feedback commands to the physical wheel.
/// </summary>
internal interface IWheelFfbProvider
{
/// <summary>Initialise the FFB provider. Returns true on success.</summary>
bool Initialize();

/// <summary>Apply the given FFB command to the wheel hardware.</summary>
void SendFfb(FfbCommand command);

/// <summary>Stop all active FFB effects and release resources.</summary>
void Dispose();
}
}
17 changes: 17 additions & 0 deletions Contracts/IWheelInputProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace com.drowhunter.NewStarGPTelemetryMod
{
/// <summary>
/// Provides physical wheel input state each frame.
/// </summary>
internal interface IWheelInputProvider
{
/// <summary>Initialise the input provider. Returns true on success.</summary>
bool Initialize();

/// <summary>Poll and return the latest wheel input state.</summary>
WheelInputState Poll();

/// <summary>Release all resources held by this provider.</summary>
void Dispose();
}
}
21 changes: 18 additions & 3 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -1,26 +1,41 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<!--<GameDir>$(APPDATA)\raicuparta\rai-pal\data\installed-mods\8148729079362356270\bepinex\BepInEx\plugins\</GameDir>-->
<GameDir>Y:\SteamLibrary\steamapps\common\New Star GP\release\BepInEx\plugins\</GameDir>
<GameDir>$(APPDATA)\raicuparta\rai-pal\data\installed-mods\8148729079362356270\bepinex\BepInEx\plugins\</GameDir>
<!--<GameDir>Y:\SteamLibrary\steamapps\common\New Star GP\release\BepInEx\plugins\</GameDir>-->
<!--<GameDir>C:\Program Files (x86)\Steam\steamapps\common\New Star GP\release\BepInEx\plugins\</GameDir>-->
</PropertyGroup>

<Target Name="CreateAndCopyToGameDir" AfterTargets="Build" Condition="Exists('$(GameDir)')">
<PropertyGroup>
<GamePluginDir>$(GameDir)$(TargetName)</GamePluginDir>
</PropertyGroup>

<PropertyGroup>
<MozaNativeDir>$(MSBuildThisFileDirectory)libs\moza\x64\</MozaNativeDir>
</PropertyGroup>

<ItemGroup>
<!-- Managed assemblies → plugin dir -->
<FilesToCopy Include="$(TargetPath)" />
<FilesToCopy Include="$(TargetDir)TelemetryLib.dll" Condition="Exists('$(TargetDir)TelemetryLib.dll')" />
<FilesToCopy Include="$(TargetDir)MOZA_API_CSharp.dll" Condition="Exists('$(TargetDir)MOZA_API_CSharp.dll')" />
<FilesToCopy Include="$(TargetDir)Nefarius.ViGEm.Client.dll" Condition="Exists('$(TargetDir)Nefarius.ViGEm.Client.dll')" />
<!-- PDBs (Debug only) -->
<FilesToCopy Include="$(TargetDir)$(TargetName).pdb" Condition="'$(Configuration)' == 'Debug' and Exists('$(TargetDir)$(TargetName).pdb')" />
<FilesToCopy Include="$(TargetDir)TelemetryLib.pdb" Condition="'$(Configuration)' == 'Debug' and Exists('$(TargetDir)TelemetryLib.pdb')" />
<PdbFilesToDelete Include="$(GamePluginDir)\*.pdb" Condition="'$(Configuration)' == 'Release'" />
<!-- Moza native x64 DLLs → plugin dir\x64\ -->
<MozaNativeToCopy Include="$(MozaNativeDir)MOZA_SDK.dll" Condition="Exists('$(MozaNativeDir)MOZA_SDK.dll')" />
<MozaNativeToCopy Include="$(MozaNativeDir)MOZA_API_C.dll" Condition="Exists('$(MozaNativeDir)MOZA_API_C.dll')" />
</ItemGroup>

<MakeDir Directories="$(GamePluginDir)" Condition="!Exists('$(GamePluginDir)')" />
<MakeDir Directories="$(GamePluginDir)\x64" Condition="@(MozaNativeToCopy->Count()) &gt; 0 and !Exists('$(GamePluginDir)\x64')" />
<Delete Files="@(PdbFilesToDelete)" Condition="'$(Configuration)' == 'Release'" />
<Copy SourceFiles="@(FilesToCopy)" DestinationFolder="$(GamePluginDir)" />
<Message Text="[$(ProjectName)] Copied @(FilesToCopy, ', ') to $(GamePluginDir)" Importance="High" />
<Copy SourceFiles="@(MozaNativeToCopy)" DestinationFolder="$(GamePluginDir)\x64" />
<Message Text="[$(ProjectName)] Copied managed → $(GamePluginDir)" Importance="High" />
<Message Text="[$(ProjectName)] Copied native → $(GamePluginDir)\x64" Importance="High" Condition="@(MozaNativeToCopy->Count()) &gt; 0" />
</Target>
<Target Name="WarnGameDirNotFound" AfterTargets="Build" Condition="!Exists('$(GameDir)')">
<Message Text="[$(ProjectName)] GameDir not found. Looked for: $(GameDir)" Importance="High" />
Expand Down
84 changes: 84 additions & 0 deletions Ffb/FfbModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System;
using System.Diagnostics;
using BepInEx.Logging;

namespace com.drowhunter.NewStarGPTelemetryMod
{
/// <summary>
/// Converts enhanced telemetry data into an <see cref="FfbCommand"/> each frame.
///
/// Implemented effects (v1):
/// 1. CForce damper – centripetal force → lateral damper coefficient.
/// 2. Tire impulses – front left/right wheel delta → short ConstantForce burst.
/// 3. Global smoothing – exponential smoothing on damper coefficient.
/// 4. Threshold + cooldown on impulses.
/// </summary>
internal class FfbModel
{
private readonly WheelConfig _cfg;
private readonly ManualLogSource _log;

// Smoothing state
private float _smoothedDamper;

// Impulse cooldown: tracks elapsed ms since last impulse using a Stopwatch.
private readonly Stopwatch _impulseStopwatch = new Stopwatch();

public FfbModel(WheelConfig cfg, ManualLogSource log)
{
_cfg = cfg;
_log = log;
}

/// <summary>
/// Compute the FFB command for the current physics frame.
/// </summary>
public FfbCommand Compute(in NewStarTelemetryData data)
{
// ── 1. CForce-based damper ────────────────────────────────────────
// cForce is the centripetal acceleration (m/s² or g depending on extractor).
// We scale and clamp it to produce a lateral resistance coefficient.
float rawDamper = data.cForce * _cfg.CForceDampingScale.Value
* _cfg.OverallStrength.Value;
rawDamper = Clamp(rawDamper, -_cfg.MaxTorque.Value, _cfg.MaxTorque.Value);

// ── 3. Exponential smoothing ──────────────────────────────────────
float alpha = _cfg.SmoothingAlpha.Value;
_smoothedDamper = _smoothedDamper + alpha * (rawDamper - _smoothedDamper);

// ── 2. Tire impulse ───────────────────────────────────────────────
// Lateral impulse = FR delta minus FL delta → positive = right-side hit.
float lateralDelta = data.TireFR - data.TireFL;
float impulse = lateralDelta * _cfg.BumpScale.Value
* _cfg.OverallStrength.Value;
impulse = Clamp(impulse, -_cfg.MaxTorque.Value, _cfg.MaxTorque.Value);

bool hasImpulse = false;
float impulseMagnitude = 0f;

if (Math.Abs(impulse) >= _cfg.BumpThreshold.Value)
{
long elapsedMs = _impulseStopwatch.IsRunning
? _impulseStopwatch.ElapsedMilliseconds
: long.MaxValue;

if (elapsedMs >= _cfg.BumpCooldownMs.Value)
{
hasImpulse = true;
impulseMagnitude = impulse;
_impulseStopwatch.Restart();
}
}

return new FfbCommand
{
DamperCoefficient = _smoothedDamper,
HasImpulse = hasImpulse,
ImpulseMagnitude = impulseMagnitude,
};
}

private static float Clamp(float value, float min, float max)
=> value < min ? min : value > max ? max : value;
}
}
27 changes: 27 additions & 0 deletions Models/FfbCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace com.drowhunter.NewStarGPTelemetryMod
{
/// <summary>
/// The computed force-feedback command for one physics frame.
/// </summary>
internal struct FfbCommand
{
/// <summary>
/// Lateral damper coefficient derived from centripetal force. Range -1..1.
/// Positive values push the wheel right; negative push left.
/// Sent to the ETDamper effect as positive/negative coefficient.
/// </summary>
public float DamperCoefficient;

/// <summary>
/// Whether a short bump impulse should fire this frame.
/// </summary>
public bool HasImpulse;

/// <summary>
/// Magnitude of the bump impulse. Range -1..1.
/// Positive = right-side bump, negative = left-side bump.
/// Sent to the ETConstantForce effect.
/// </summary>
public float ImpulseMagnitude;
}
}
29 changes: 29 additions & 0 deletions Models/VirtualPadState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace com.drowhunter.NewStarGPTelemetryMod
{
/// <summary>
/// Represents the desired state of the virtual Xbox 360 gamepad to be submitted to ViGEm.
/// </summary>
internal struct VirtualPadState
{
/// <summary>Left stick X axis: -1..1 (left/right). Mapped from wheel steer angle.</summary>
public float LeftStickX;

/// <summary>Left stick Y axis: -1..1 (down/up). Reserved for future use.</summary>
public float LeftStickY;

/// <summary>Right stick X axis: -1..1. Reserved for future use.</summary>
public float RightStickX;

/// <summary>Right stick Y axis: -1..1. Reserved for future use.</summary>
public float RightStickY;

/// <summary>Left trigger (gas/throttle): 0..1.</summary>
public float LeftTrigger;

/// <summary>Right trigger (brake): 0..1.</summary>
public float RightTrigger;

/// <summary>Xbox button bitmask. See Nefarius.ViGEm.Client.Targets.Xbox360.Xbox360Button.</summary>
public ushort Buttons;
}
}
26 changes: 26 additions & 0 deletions Models/WheelInputState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace com.drowhunter.NewStarGPTelemetryMod
{
/// <summary>
/// Snapshot of the physical wheel's current input state, normalised to -1..1 or 0..1 where applicable.
/// </summary>
internal struct WheelInputState
{
/// <summary>Steering angle normalised to -1..1 (left to right).</summary>
public float SteerAngle;

/// <summary>Steering velocity in normalised units per second.</summary>
public float SteerVelocity;

/// <summary>Throttle axis 0..1 (0 = released, 1 = fully pressed).</summary>
public float Throttle;

/// <summary>Brake axis 0..1.</summary>
public float Brake;

/// <summary>Clutch axis 0..1.</summary>
public float Clutch;

/// <summary>Raw 128-element button state array from the HID report. May be null if no data available.</summary>
public bool[] Buttons;
}
}
Loading