Skip to content

Commit

Permalink
support streamdeck+ dials
Browse files Browse the repository at this point in the history
  • Loading branch information
mhwlng committed Dec 9, 2022
1 parent 65c0f12 commit 0283a75
Show file tree
Hide file tree
Showing 17 changed files with 2,881 additions and 701 deletions.
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

![Elgato Stream Deck button plugin for Star Citizen](https://i.imgur.com/FSHsXRG.png)

**At least streamdeck software version 6 is required.**

This plugin gets the key bindings from the Star Citizen game files.

The bindings in the streamdeck plugin are automatically updated when changing bindings in Star Citizen options screen.
Expand Down Expand Up @@ -38,7 +40,26 @@ You can then set up different images for each toggle state.
The disadvantage is: that if you would press e.g. the gear up/down toggle button while the ship is still on the ground/powered off,
then the button image would be out of sync.

After you install the plugin in the streamdeck software, then there will be a new button type in the streamdeck software.
The plugin also has a Dial button for use with the 4 dials on the Streamdeck+ model.

There are 5 bindings (They must be keyboard bindings, you can't bind the mouse wheel!) :

- Dial Clockwise
- Dial Counter-Clockwise
- Dial Press
- Touch screen press
- Touch screen long press

When a dial is rotated, the 'key down' event is sent to the keyboard once.
When you let go of the dial for at least 100ms : the 'key up' event is sent to the keyboard.

When a dial button is pushed, the 'key down' event is sent to the keyboard.
When a dial button is released, the 'key up' event is sent to the keyboard.

When the touch screen is pressed or long-pressed, the behaviour is like the multi-action button :
The 'key down' event is sent to the keyboard. After a user-definable delay (default = 40 ms) the 'key up' event is sent to the keyboard.

After you install the plugin in the streamdeck software, then there will be new button types in the streamdeck software.

Choose a button in the streamdeck software (drag and drop), then choose a Star Citizen function for that button
(that must have a keyboard binding in Star Citizen. **A mouse, gamepad or joystick binding won't work!**)
Expand All @@ -48,7 +69,7 @@ Add an image to a button in this way:

![Button Image](https://i.imgur.com/xkgy7uZ.png)

Animated gif files are supported.
Animated gif files are supported. Dial images can be wider.

When the plugin is first started, it finds and opens the game file :

Expand Down
334 changes: 334 additions & 0 deletions starcitizen/Buttons/Dial.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
using System;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Input;
using WindowsInput.Native;
using BarRaider.SdTools;
using BarRaider.SdTools.Payloads;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Threading;
using System.Diagnostics;

// ReSharper disable StringLiteralTypo

namespace starcitizen.Buttons
{

[PluginActionId("com.mhwlng.starcitizen.dial")]
public class Dial : StarCitizenDialBase
{
protected class PluginSettings
{
public static PluginSettings CreateDefaultSettings()
{
var instance = new PluginSettings
{
FunctionCw = string.Empty,
FunctionCcw = string.Empty,
FunctionPress = string.Empty,
FunctionTouchLongPress = string.Empty,
FunctionTouchPress = string.Empty
};

return instance;
}

[JsonProperty(PropertyName = "functioncw")]
public string FunctionCw { get; set; }

[JsonProperty(PropertyName = "functionccw")]
public string FunctionCcw { get; set; }

[JsonProperty(PropertyName = "delay")]
public string Delay { get; set; }

[JsonProperty(PropertyName = "functionpress")]
public string FunctionPress { get; set; }

[JsonProperty(PropertyName = "functiontouchpress")]
public string FunctionTouchPress { get; set; }

[JsonProperty(PropertyName = "functiontouchlongpress")]
public string FunctionTouchLongPress { get; set; }
}

PluginSettings settings;
private int? _delay = null;

private bool ccwIsDown;
private bool cwIsDown;

private int ccwPending;
private int cwPending;

private DateTime? lastDialTime = null;

private Thread dialWatcherThread = null;
/// <summary>
/// Token to signal that we are no longer watching
/// </summary>
private CancellationTokenSource cancellationTokenSource;


public Dial(SDConnection connection, InitialPayload payload) : base(connection, payload)
{
if (payload.Settings == null || payload.Settings.Count == 0)
{
//Logger.Instance.LogMessage(TracingLevel.DEBUG, "Repeating Static Constructor #1");

settings = PluginSettings.CreateDefaultSettings();
Connection.SetSettingsAsync(JObject.FromObject(settings)).Wait();

}
else
{
//Logger.Instance.LogMessage(TracingLevel.DEBUG, "Repeating Static Constructor #2");

settings = payload.Settings.ToObject<PluginSettings>();
HandleFileNames();
}

cancellationTokenSource = new CancellationTokenSource();

dialWatcherThread = new Thread(state =>
{
while (true)
{
if (Program.dpReader == null || cancellationTokenSource.IsCancellationRequested)
{
StreamDeckCommon.ForceStop = true;

return;
}

var timeDiff = DateTime.Now - (lastDialTime ?? DateTime.Now);

if ((ccwIsDown || cwIsDown) && timeDiff.TotalMilliseconds >= 100)
{
//Logger.Instance.LogMessage(TracingLevel.INFO, $"DialRotate Released");

ReleaseCcw();

ReleaseCw();

lastDialTime = DateTime.Now;

}

Thread.Sleep(100);

}

})
{
Name = "Dial Watcher",
IsBackground = true
};
dialWatcherThread.Start();

}

public override void Dispose()
{
if (cancellationTokenSource != null)
cancellationTokenSource.Cancel();

if (dialWatcherThread != null)
dialWatcherThread.Join();

base.Dispose();

//Logger.Instance.LogMessage(TracingLevel.DEBUG, "Destructor called #1");

}

public override void TouchPress(TouchpadPressPayload payload)
{
if (Program.dpReader == null)
{
StreamDeckCommon.ForceStop = true;
return;
}

StreamDeckCommon.ForceStop = false;

if (payload.IsLongPress)
{
//Logger.Instance.LogMessage(TracingLevel.INFO, $"TouchPress: LongPress");

var action = Program.dpReader.GetBinding(settings.FunctionTouchLongPress);
if (action != null)
{
Logger.Instance.LogMessage(TracingLevel.INFO, CommandTools.ConvertKeyString(action.Keyboard));

StreamDeckCommon.SendKeypress(CommandTools.ConvertKeyString(action.Keyboard), _delay ?? 40);
}
}
else
{
//Logger.Instance.LogMessage(TracingLevel.INFO, $"TouchPress: Press");

var action = Program.dpReader.GetBinding(settings.FunctionTouchPress);
if (action != null)
{
Logger.Instance.LogMessage(TracingLevel.INFO, CommandTools.ConvertKeyString(action.Keyboard));

StreamDeckCommon.SendKeypress(CommandTools.ConvertKeyString(action.Keyboard), _delay ?? 40);
}
}
}

public override void DialPress(DialPressPayload payload)
{

if (Program.dpReader == null)
{
StreamDeckCommon.ForceStop = true;
return;
}

StreamDeckCommon.ForceStop = false;

if (payload.IsDialPressed)
{
//Logger.Instance.LogMessage(TracingLevel.INFO, $"DialPress: Press");
var action = Program.dpReader.GetBinding(settings.FunctionPress);
if (action != null)
{
Logger.Instance.LogMessage(TracingLevel.INFO, CommandTools.ConvertKeyString(action.Keyboard));

StreamDeckCommon.SendKeypressDown(CommandTools.ConvertKeyString(action.Keyboard));
}
}
else
{
//Logger.Instance.LogMessage(TracingLevel.INFO, $"DialPress: Release");
var action = Program.dpReader.GetBinding(settings.FunctionPress);
if (action != null)
{
Logger.Instance.LogMessage(TracingLevel.INFO, CommandTools.ConvertKeyString(action.Keyboard));

StreamDeckCommon.SendKeypressUp(CommandTools.ConvertKeyString(action.Keyboard));
}
}
}

private void ReleaseCw()
{
if (cwIsDown)
{
var action = Program.dpReader.GetBinding(settings.FunctionCw);
if (action != null)
{
Logger.Instance.LogMessage(TracingLevel.INFO, CommandTools.ConvertKeyString(action.Keyboard));

StreamDeckCommon.SendKeypressUp(CommandTools.ConvertKeyString(action.Keyboard));
Thread.Sleep(100);
cwIsDown = false;
cwPending = 0;
}
}
}

private void ReleaseCcw()
{
if (ccwIsDown)
{
var action = Program.dpReader.GetBinding(settings.FunctionCcw);
if (action != null)
{
Logger.Instance.LogMessage(TracingLevel.INFO, CommandTools.ConvertKeyString(action.Keyboard));

StreamDeckCommon.SendKeypressUp(CommandTools.ConvertKeyString(action.Keyboard));
Thread.Sleep(100);
ccwIsDown = false;
ccwPending = 0;
}
}
}
public override void DialRotate(DialRotatePayload payload)
{

if (Program.dpReader == null)
{
StreamDeckCommon.ForceStop = true;
return;
}

StreamDeckCommon.ForceStop = false;

lastDialTime = DateTime.Now;

if (payload.Ticks > 0)
{
ReleaseCcw();

//Logger.Instance.LogMessage(TracingLevel.INFO, $"DialRotate CW: {payload.Ticks}");

if (!cwIsDown)
{
var action = Program.dpReader.GetBinding(settings.FunctionCw);
if (action != null)
{
Logger.Instance.LogMessage(TracingLevel.INFO,
CommandTools.ConvertKeyString(action.Keyboard));

StreamDeckCommon.SendKeypressDown(CommandTools.ConvertKeyString(action.Keyboard));
cwIsDown = true;
cwPending += payload.Ticks;
}
}
}
else if (payload.Ticks < 0)
{
ReleaseCw();

//Logger.Instance.LogMessage(TracingLevel.INFO, $"DialRotate CCW: {payload.Ticks}");

if (!ccwIsDown)
{
var action = Program.dpReader.GetBinding(settings.FunctionCcw);
if (action != null)
{

Logger.Instance.LogMessage(TracingLevel.INFO,
CommandTools.ConvertKeyString(action.Keyboard));

StreamDeckCommon.SendKeypressDown(CommandTools.ConvertKeyString(action.Keyboard));
ccwIsDown = true;
ccwPending += -payload.Ticks;
}
}
}

}



public override void ReceivedSettings(ReceivedSettingsPayload payload)
{
//Logger.Instance.LogMessage(TracingLevel.DEBUG, "ReceivedSettings");

// New in StreamDeck-Tools v2.0:
BarRaider.SdTools.Tools.AutoPopulateSettings(settings, payload.Settings);
HandleFileNames();
}

private void HandleFileNames()
{
_delay = null;

if (!string.IsNullOrEmpty(settings.Delay))
{
var ok = int.TryParse(settings.Delay, out var delay);
if (ok && (delay > 0))
{
_delay = delay;
}
}

Connection.SetSettingsAsync(JObject.FromObject(settings)).Wait();
}
}
}
Loading

0 comments on commit 0283a75

Please sign in to comment.