diff --git a/KayakVRMiragePlugin/Config.cs b/KayakVRMiragePlugin/Config.cs new file mode 100644 index 0000000..dbf1468 --- /dev/null +++ b/KayakVRMiragePlugin/Config.cs @@ -0,0 +1,14 @@ +using YawGLAPI; + +namespace KayakVRMiragePlugin +{ + public struct Config + { + [Info(Description = "UDP port the game is using")] + public int Port = 33001; + + public Config() + { + } + } +} diff --git a/KayakVRMiragePlugin/KayakVRMiragePlugin.cs b/KayakVRMiragePlugin/KayakVRMiragePlugin.cs new file mode 100644 index 0000000..0543840 --- /dev/null +++ b/KayakVRMiragePlugin/KayakVRMiragePlugin.cs @@ -0,0 +1,107 @@ +using SharedLib; +using SharedLib.TelemetryHelper; + +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using System.Threading; + +using YawGLAPI; + +namespace KayakVRMiragePlugin +{ + [Export(typeof(Game))] + [ExportMetadata("Name", "Kayak VR: Mirage")] + [ExportMetadata("Version", "1.0")] + public class KayakVRMiragePlugin : Game + { + #region Standard Properties + + public int STEAM_ID => 1683340; + public string PROCESS_NAME => "KayakVR-Win64-Shipping"; + public bool PATCH_AVAILABLE => false; + public string AUTHOR => "YawVR"; + + public string Description => ResourceHelper.Description; + public Stream Logo => ResourceHelper.Logo; + public Stream SmallLogo => ResourceHelper.SmallLogo; + public Stream Background => ResourceHelper.Background; + + public List DefaultProfile() => dispatcher.JsonToComponents(ResourceHelper.DefaultProfile); + + public string[] GetInputData() => InputHelper.GetValues(default).Keys(); + + public LedEffect DefaultLED() => new(EFFECT_TYPE.KNIGHT_RIDER, 0, [YawColor.WHITE], 0); + + #endregion + + private Config settings; + private UdpTelemetry telemetry; + private Thread readThread; + private IMainFormDispatcher dispatcher; + private IProfileManager controller; + private volatile bool running = false; + + public void SetReferences(IProfileManager controller, IMainFormDispatcher dispatcher) + { + this.dispatcher = dispatcher; + this.controller = controller; + } + + public void Init() + { + settings = dispatcher.GetConfigObject(); + running = true; + readThread = new Thread(new ThreadStart(ReadThread)); + readThread.IsBackground = true; + readThread.Start(); + } + + private void ReadThread() + { + try + { + telemetry = new UdpTelemetry(new UdpTelemetryConfig + { + ReceiveAddress = new IPEndPoint(IPAddress.Any, settings.Port) + }, new SRSPacketConverter()); + } + catch (Exception x) + { + dispatcher.ShowNotification(NotificationType.ERROR, x.Message); + Exit(); + return; + } + + while (running) + { + try + { + var data = telemetry.Receive(); + + foreach (var (i, _, value) in InputHelper.GetValues(data).WithIndex()) + { + controller.SetInput(i, value); + } + } + catch (SocketException) { } + } + } + + public void Exit() + { + running = false; + telemetry?.Dispose(); + } + + public void PatchGame() { } + + public Dictionary GetFeatures() => null; + + public Type GetConfigBody() => typeof(Config); + } +} diff --git a/KayakVRMiragePlugin/KayakVRMiragePlugin.csproj b/KayakVRMiragePlugin/KayakVRMiragePlugin.csproj new file mode 100644 index 0000000..17db5ee --- /dev/null +++ b/KayakVRMiragePlugin/KayakVRMiragePlugin.csproj @@ -0,0 +1,35 @@ + + + net8.0 + Library + false + All;Debug;Release + KayakVRMiragePlugin + KayakVRMiragePlugin + Copyright © 2024 + 1.0.0.0 + 1.0.0.0 + 1.0.0 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/KayakVRMiragePlugin/Resources/Default.yawglprofile b/KayakVRMiragePlugin/Resources/Default.yawglprofile new file mode 100644 index 0000000..ea4df13 --- /dev/null +++ b/KayakVRMiragePlugin/Resources/Default.yawglprofile @@ -0,0 +1,82 @@ +{ + "GameName": "Kayak VR: Mirage", + "Name": "Default", + "Components": [ + { + "Constant": false, + "Input_index": 3, + "Output_index": 0, + "MultiplierPos": 1, + "MultiplierNeg": 1, + "Offset": 0, + "Inverse": false, + "Limit": -1, + "Smoothing": 1, + "Enabled": true, + "Spikeflatter": { + "Enabled": false, + "Limit": 100, + "Strength": 0.5 + }, + "Deadzone": 0, + "Type": 0, + "Condition": [], + "Math": [] + }, + { + "Constant": false, + "Input_index": 4, + "Output_index": 1, + "MultiplierPos": 1, + "MultiplierNeg": 1, + "Offset": 0, + "Inverse": false, + "Limit": -1, + "Smoothing": 1, + "Enabled": true, + "Spikeflatter": { + "Enabled": false, + "Limit": 100, + "Strength": 0.5 + }, + "Deadzone": 0, + "Type": 0, + "Condition": [], + "Math": [] + }, + { + "Constant": false, + "Input_index": 5, + "Output_index": 2, + "MultiplierPos": 1, + "MultiplierNeg": 1, + "Offset": 0, + "Inverse": false, + "Limit": -1, + "Smoothing": 1, + "Enabled": true, + "Spikeflatter": { + "Enabled": false, + "Limit": 100, + "Strength": 0.5 + }, + "Deadzone": 0, + "Type": 0, + "Condition": [], + "Math": [] + } + ], + "Effects": { + "EffectID": 0, + "InputID": 0, + "Multiplier": 0, + "Colors": [ + { + "R": 0, + "G": 0, + "B": 0 + } + ] + }, + "Functions": [] +} diff --git a/KayakVRMiragePlugin/Resources/description.html b/KayakVRMiragePlugin/Resources/description.html new file mode 100644 index 0000000..5d1a5ae --- /dev/null +++ b/KayakVRMiragePlugin/Resources/description.html @@ -0,0 +1,39 @@ + + + + + + Kayak VR: Mirage + + + +

Kayak VR: Mirage

+

+ Kayak VR: Mirage outputs motion data natively over UDP on port 33001 using the + SRS (SimRacingStudio) Motion Output Format. No patching or additional software is required. +

+

Setup

+
    +
  1. Launch Kayak VR: Mirage.
  2. +
  3. Start the plugin in GameLink – it will automatically receive telemetry on port 33001.
  4. +
+

Data Channels

+
    +
  • Heave – Vertical acceleration
  • +
  • Sway – Lateral acceleration
  • +
  • Surge – Longitudinal acceleration
  • +
  • Yaw – Rotation around the vertical axis
  • +
  • Pitch – Rotation around the lateral axis
  • +
  • Roll – Rotation around the longitudinal axis
  • +
  • Extra1–Extra6 – Additional game-specific channels
  • +
+ Version 1.0.0 + + diff --git a/KayakVRMiragePlugin/Resources/logo.png b/KayakVRMiragePlugin/Resources/logo.png new file mode 100644 index 0000000..98852cd Binary files /dev/null and b/KayakVRMiragePlugin/Resources/logo.png differ diff --git a/KayakVRMiragePlugin/Resources/recent.png b/KayakVRMiragePlugin/Resources/recent.png new file mode 100644 index 0000000..856dbd6 Binary files /dev/null and b/KayakVRMiragePlugin/Resources/recent.png differ diff --git a/KayakVRMiragePlugin/Resources/wide.png b/KayakVRMiragePlugin/Resources/wide.png new file mode 100644 index 0000000..18a82d0 Binary files /dev/null and b/KayakVRMiragePlugin/Resources/wide.png differ diff --git a/KayakVRMiragePlugin/SRSPacket.cs b/KayakVRMiragePlugin/SRSPacket.cs new file mode 100644 index 0000000..a18d058 --- /dev/null +++ b/KayakVRMiragePlugin/SRSPacket.cs @@ -0,0 +1,52 @@ +using System; + +using SharedLib.TelemetryHelper; + +namespace KayakVRMiragePlugin +{ + /// + /// SRS (SimRacingStudio) Motion Output Format. + /// The packet has a variable-length metadata header followed by the motion block. + /// The motion block is always the last 32 bytes of the packet (8 × IEEE 754 float). + /// + internal struct SRSPacket + { + public float Heave; + public float Sway; + public float Surge; + public float Yaw; + public float Pitch; + public float Roll; + public float Extra1; + public float Extra2; + } + + /// + /// Extracts the SRS motion block from the last 32 bytes of the raw UDP payload. + /// + internal class SRSPacketConverter : IByteConverter + { + private const int MotionBlockSize = 32; // 8 floats × 4 bytes + + public SRSPacket FromBytes(byte[] data) + { + if (data.Length < MotionBlockSize) + return default; + + int offset = data.Length - MotionBlockSize; + return new SRSPacket + { + Heave = BitConverter.ToSingle(data, offset + 0), + Sway = BitConverter.ToSingle(data, offset + 4), + Surge = BitConverter.ToSingle(data, offset + 8), + Yaw = BitConverter.ToSingle(data, offset + 12), + Pitch = BitConverter.ToSingle(data, offset + 16), + Roll = BitConverter.ToSingle(data, offset + 20), + Extra1 = BitConverter.ToSingle(data, offset + 24), + Extra2 = BitConverter.ToSingle(data, offset + 28), + }; + } + + public byte[] ToBytes(SRSPacket data) => Array.Empty(); + } +} diff --git a/Plugins.sln b/Plugins.sln index 6752ecf..6e3d0d3 100644 --- a/Plugins.sln +++ b/Plugins.sln @@ -59,6 +59,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "iRacingPlugin", "iRacingPlu EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KartKraftPlugin", "KartKraftPlugin\KartKraftPlugin.csproj", "{EA0C82A9-58B4-468E-9973-52A206BDF502}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KayakVRMiragePlugin", "KayakVRMiragePlugin\KayakVRMiragePlugin.csproj", "{6A8B2C3D-4E5F-6A7B-8C9D-0E1F2A3B4C5D}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LFSPlugin", "LFSPlugin\LFSPlugin.csproj", "{563D0FC7-AD70-40BD-87AB-F46816CCD32D}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ManiaPlanetPlugin", "ManiaPlanetPlugin\ManiaPlanetPlugin.csproj", "{BF097056-6F2A-41E6-812A-0D991749609D}" @@ -259,6 +261,10 @@ Global {EA0C82A9-58B4-468E-9973-52A206BDF502}.Debug|Any CPU.Build.0 = Debug|Any CPU {EA0C82A9-58B4-468E-9973-52A206BDF502}.Release|Any CPU.ActiveCfg = Release|Any CPU {EA0C82A9-58B4-468E-9973-52A206BDF502}.Release|Any CPU.Build.0 = Release|Any CPU + {6A8B2C3D-4E5F-6A7B-8C9D-0E1F2A3B4C5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6A8B2C3D-4E5F-6A7B-8C9D-0E1F2A3B4C5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6A8B2C3D-4E5F-6A7B-8C9D-0E1F2A3B4C5D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6A8B2C3D-4E5F-6A7B-8C9D-0E1F2A3B4C5D}.Release|Any CPU.Build.0 = Release|Any CPU {563D0FC7-AD70-40BD-87AB-F46816CCD32D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {563D0FC7-AD70-40BD-87AB-F46816CCD32D}.Debug|Any CPU.Build.0 = Debug|Any CPU {563D0FC7-AD70-40BD-87AB-F46816CCD32D}.Release|Any CPU.ActiveCfg = Release|Any CPU