1
0

feat: add import/export

This commit is contained in:
Kacie 2023-01-25 00:05:30 +01:00
parent 82a7e10aa8
commit 1282abae67
5 changed files with 217 additions and 92 deletions

View File

@ -13,7 +13,7 @@ using Dalamud.Game.Network;
using Dalamud.Game.Command; using Dalamud.Game.Command;
using Dalamud.Game.ClientState; using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
// FFXIV_Vibe_Plugin libs // FFXIV_Vibe_Plugin libs
@ -35,10 +35,10 @@ namespace FFXIV_Vibe_Plugin {
private ObjectTable GameObjects { get; init; } private ObjectTable GameObjects { get; init; }
private DalamudPluginInterface PluginInterface { get; init; } private DalamudPluginInterface PluginInterface { get; init; }
// Custom variables from Kacie // Custom variables from Kacie
private readonly Plugin Plugin; private readonly Plugin Plugin;
private readonly bool wasInit = false; private readonly bool wasInit = false;
public readonly string CommandName = ""; public readonly string CommandName = "";
public PluginUI PluginUi { get; init; } public PluginUI PluginUi { get; init; }
private readonly string ShortName = ""; private readonly string ShortName = "";
private bool _firstUpdated = false; private bool _firstUpdated = false;
@ -205,12 +205,12 @@ namespace FFXIV_Vibe_Plugin {
} }
} }
private void DisplayUI() { private void DisplayUI() {
this.Plugin.DrawConfigUI(); this.Plugin.DrawConfigUI();
} }
private void DisplayConfigUI() { private void DisplayConfigUI() {
this.Plugin.DrawConfigUI(); this.Plugin.DrawConfigUI();
} }

View File

@ -1,6 +1,7 @@
using Dalamud.Configuration; using Dalamud.Configuration;
using Dalamud.Plugin; using Dalamud.Plugin;
using System; using System;
using System.IO;
using System.Collections.Generic; using System.Collections.Generic;
using FFXIV_Vibe_Plugin.Triggers; using FFXIV_Vibe_Plugin.Triggers;
@ -27,7 +28,8 @@ namespace FFXIV_Vibe_Plugin {
public bool AUTO_OPEN { get; set; } = false; public bool AUTO_OPEN { get; set; } = false;
public List<Pattern> PatternList = new(); public List<Pattern> PatternList = new();
public string BUTTPLUG_SERVER_HOST { get; set; } = "127.0.0.1"; public string BUTTPLUG_SERVER_HOST { get; set; } = "127.0.0.1";
public int BUTTPLUG_SERVER_PORT { get; set; } = 12345; public int BUTTPLUG_SERVER_PORT { get; set; } = 12345;
public string EXPORT_DIR = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)+"\\FFXIV_Vibe_Plugin";
public List<Triggers.Trigger> TRIGGERS { get; set; } = new(); public List<Triggers.Trigger> TRIGGERS { get; set; } = new();
public Dictionary<string, FFXIV_Vibe_Plugin.Device.Device> VISITED_DEVICES = new(); public Dictionary<string, FFXIV_Vibe_Plugin.Device.Device> VISITED_DEVICES = new();
@ -36,7 +38,12 @@ namespace FFXIV_Vibe_Plugin {
[NonSerialized] [NonSerialized]
private DalamudPluginInterface? pluginInterface; private DalamudPluginInterface? pluginInterface;
public void Initialize(DalamudPluginInterface pluginInterface) { public void Initialize(DalamudPluginInterface pluginInterface) {
this.pluginInterface = pluginInterface; this.pluginInterface = pluginInterface;
try {
Directory.CreateDirectory(this.EXPORT_DIR);
} catch {
// pass
}
} }
public void Save() { public void Save() {
this.pluginInterface!.SavePluginConfig(this); this.pluginInterface!.SavePluginConfig(this);
@ -119,7 +126,9 @@ namespace FFXIV_Vibe_Plugin {
public List<Pattern> PatternList = new(); public List<Pattern> PatternList = new();
public string BUTTPLUG_SERVER_HOST { get; set; } = "127.0.0.1"; public string BUTTPLUG_SERVER_HOST { get; set; } = "127.0.0.1";
public int BUTTPLUG_SERVER_PORT { get; set; } = 12345; public int BUTTPLUG_SERVER_PORT { get; set; } = 12345;
public string EXPORT_DIR = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "\\FFXIV_Vibe_Plugin";
public List<Triggers.Trigger> TRIGGERS { get; set; } = new(); public List<Triggers.Trigger> TRIGGERS { get; set; } = new();

View File

@ -3,8 +3,10 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Security.Cryptography;
using FFXIV_Vibe_Plugin.Device; using FFXIV_Vibe_Plugin.Device;
using System.Xml.Linq;
namespace FFXIV_Vibe_Plugin.Triggers { namespace FFXIV_Vibe_Plugin.Triggers {
public enum KIND { public enum KIND {
@ -20,7 +22,7 @@ namespace FFXIV_Vibe_Plugin.Triggers {
Self Self
} }
public class Trigger : IComparable<Trigger> { public class Trigger : IComparable<Trigger>, IEquatable<Trigger> {
private static readonly int _initAmountMinValue = -1; private static readonly int _initAmountMinValue = -1;
private static readonly int _initAmountMaxValue = 10000000; private static readonly int _initAmountMaxValue = 10000000;
@ -49,8 +51,10 @@ namespace FFXIV_Vibe_Plugin.Triggers {
public List<TriggerDevice> Devices = new(); public List<TriggerDevice> Devices = new();
public Trigger(string name) { public Trigger(string name) {
this.Id = Guid.NewGuid().ToString();
this.Name = name; this.Name = name;
byte[] textBytes = System.Text.Encoding.UTF8.GetBytes(name);
byte[] hashed = SHA256.Create().ComputeHash(textBytes);
this.Id = BitConverter.ToString(hashed).Replace("-", String.Empty);
} }
public override string ToString() { public override string ToString() {
@ -59,13 +63,12 @@ namespace FFXIV_Vibe_Plugin.Triggers {
public int CompareTo(Trigger? other) { public int CompareTo(Trigger? other) {
if(other == null) { return 1; } if(other == null) { return 1; }
if(this.SortOder < other.SortOder) { return other.Name.CompareTo(this.Name);
return 1; }
} else if(this.SortOder > other.SortOder) {
return -1; public bool Equals(Trigger? other) {
} else { if (other == null) return false;
return 0; return this.Name.Equals(other.Name);
}
} }
public string GetShortID() { public string GetShortID() {

View File

@ -3,17 +3,23 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
// Dalamud libs
using ImGuiNET; // Dalamud libs
using ImGuiNET;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Components; using Dalamud.Interface.Components;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
// FFXIV_Vibe_Plugin libs // FFXIV_Vibe_Plugin libs
using FFXIV_Vibe_Plugin.Commons; using FFXIV_Vibe_Plugin.Commons;
using FFXIV_Vibe_Plugin.Triggers;
// Json libs
using Newtonsoft.Json;
using System.Text.Json;
@ -74,10 +80,11 @@ namespace FFXIV_Vibe_Plugin {
// Trigger // Trigger
private Triggers.Trigger? SelectedTrigger = null; private Triggers.Trigger? SelectedTrigger = null;
private string triggersViewMode = "default"; // default|edit|delete; private string triggersViewMode = "default"; // default|edit|delete;
string _tmp_exportPatternResponse = "";
/** Constructor */
/** Constructor */
public PluginUI( public PluginUI(
App currentPlugin, App currentPlugin,
Logger logger, Logger logger,
@ -87,16 +94,16 @@ namespace FFXIV_Vibe_Plugin {
Device.DevicesController deviceController, Device.DevicesController deviceController,
Triggers.TriggersController triggersController, Triggers.TriggersController triggersController,
Patterns Patterns Patterns Patterns
) : base( ) : base(
"FFXIV_Vibe_Plugin_UI", "FFXIV_Vibe_Plugin_UI",
ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollbar |
ImGuiWindowFlags.NoScrollWithMouse) { ImGuiWindowFlags.NoScrollWithMouse) {
this.Size = new Vector2(this.WIDTH, this.HEIGHT); this.Size = new Vector2(this.WIDTH, this.HEIGHT);
ImGui.SetNextWindowPos(new Vector2(100, 100), ImGuiCond.Appearing); ImGui.SetNextWindowPos(new Vector2(100, 100), ImGuiCond.Appearing);
//if(ImGui.Begin("FFXIV Vibe Plugin", ref this.visible, ImGuiWindowFlags.None)) { //if(ImGui.Begin("FFXIV Vibe Plugin", ref this.visible, ImGuiWindowFlags.None)) {
this.Logger = logger; this.Logger = logger;
this.Configuration = configuration; this.Configuration = configuration;
this.ConfigurationProfile = profile; this.ConfigurationProfile = profile;
@ -146,47 +153,47 @@ namespace FFXIV_Vibe_Plugin {
} }
public void DrawMainWindow() { public void DrawMainWindow() {
if (!this._expandedOnce) { if (!this._expandedOnce) {
ImGui.SetNextWindowCollapsed(false); ImGui.SetNextWindowCollapsed(false);
this._expandedOnce = true; this._expandedOnce = true;
} }
ImGui.Spacing(); ImGui.Spacing();
FFXIV_Vibe_Plugin.UI.UIBanner.Draw(this.frameCounter, this.Logger, this.loadedImages["icon.png"], this.DonationLink, this.DevicesController); FFXIV_Vibe_Plugin.UI.UIBanner.Draw(this.frameCounter, this.Logger, this.loadedImages["icon.png"], this.DonationLink, this.DevicesController);
// Back to on column // Back to on column
ImGui.Columns(1); ImGui.Columns(1);
// Tab header // Tab header
if (ImGui.BeginTabBar("##ConfigTabBar", ImGuiTabBarFlags.None)) { if (ImGui.BeginTabBar("##ConfigTabBar", ImGuiTabBarFlags.None)) {
if (ImGui.BeginTabItem("Connect")) { if (ImGui.BeginTabItem("Connect")) {
FFXIV_Vibe_Plugin.UI.UIConnect.Draw(this.Configuration, this.ConfigurationProfile, this.app, this.DevicesController); FFXIV_Vibe_Plugin.UI.UIConnect.Draw(this.Configuration, this.ConfigurationProfile, this.app, this.DevicesController);
ImGui.EndTabItem(); ImGui.EndTabItem();
} }
if (ImGui.BeginTabItem("Options")) { if (ImGui.BeginTabItem("Options")) {
this.DrawOptionsTab(); this.DrawOptionsTab();
ImGui.EndTabItem(); ImGui.EndTabItem();
} }
if (ImGui.BeginTabItem("Devices")) { if (ImGui.BeginTabItem("Devices")) {
this.DrawDevicesTab(); this.DrawDevicesTab();
ImGui.EndTabItem(); ImGui.EndTabItem();
} }
if (ImGui.BeginTabItem("Triggers")) { if (ImGui.BeginTabItem("Triggers")) {
this.DrawTriggersTab(); this.DrawTriggersTab();
ImGui.EndTabItem(); ImGui.EndTabItem();
} }
if (ImGui.BeginTabItem("Patterns")) { if (ImGui.BeginTabItem("Patterns")) {
this.DrawPatternsTab(); this.DrawPatternsTab();
ImGui.EndTabItem(); ImGui.EndTabItem();
} }
if (ImGui.BeginTabItem("Help")) { if (ImGui.BeginTabItem("Help")) {
this.DrawHelpTab(); this.DrawHelpTab();
ImGui.EndTabItem(); ImGui.EndTabItem();
} }
} }
} }
@ -329,6 +336,39 @@ namespace FFXIV_Vibe_Plugin {
ImGui.EndTable(); ImGui.EndTable();
} }
ImGui.EndChild();
ImGui.TextColored(ImGuiColors.DalamudViolet, "Trigger Import/Export Settings");
ImGui.BeginChild("###EXPORT_OPTIONS_ZONE", new Vector2(-1, 100f), true);
{
// Init table
ImGui.BeginTable("###EXPORT_OPTIONS_TABLE", 2);
ImGui.TableSetupColumn("###EXPORT_OPTIONS_TABLE_COL1", ImGuiTableColumnFlags.WidthFixed, 250);
ImGui.TableSetupColumn("###EXPORT_OPTIONS_TABLE_COL2", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableNextColumn();
ImGui.Text("Trigger Import/Export Directory:");
ImGui.TableNextColumn();
if (ImGui.InputText("###EXPORT_DIRECTORY_INPUT", ref this.ConfigurationProfile.EXPORT_DIR, 200)) {
this.Configuration.EXPORT_DIR = this.ConfigurationProfile.EXPORT_DIR;
this.Configuration.Save();
}
ImGui.TableNextRow();
ImGui.TableNextColumn();
if (ImGui.Button("Clear Import/Export Directory")) {
if (!this.ConfigurationProfile.EXPORT_DIR.Equals("")) {
try {
foreach (var filename in Directory.GetFiles(this.ConfigurationProfile.EXPORT_DIR)) {
File.Delete(filename);
}
} catch { }
}
}
ImGui.SameLine();
ImGuiComponents.HelpMarker("Deletes ALL files in the Import/Export Directory.");
ImGui.EndTable();
}
ImGui.EndChild(); ImGui.EndChild();
if (this.ConfigurationProfile.VERBOSE_CHAT || this.ConfigurationProfile.VERBOSE_SPELL) { if (this.ConfigurationProfile.VERBOSE_CHAT || this.ConfigurationProfile.VERBOSE_SPELL) {
@ -428,8 +468,8 @@ namespace FFXIV_Vibe_Plugin {
if (ImGui.BeginChild("###TriggersSelector", new Vector2(ImGui.GetWindowContentRegionMax().X / 3, -ImGui.GetFrameHeightWithSpacing()), true)) { if (ImGui.BeginChild("###TriggersSelector", new Vector2(ImGui.GetWindowContentRegionMax().X / 3, -ImGui.GetFrameHeightWithSpacing()), true)) {
ImGui.SetNextItemWidth(185); ImGui.SetNextItemWidth(185);
ImGui.InputText("###TriggersSelector_SearchBar", ref this.CURRENT_TRIGGER_SELECTOR_SEARCHBAR, 200); ImGui.InputText("###TriggersSelector_SearchBar", ref this.CURRENT_TRIGGER_SELECTOR_SEARCHBAR, 200);
ImGui.Spacing(); ImGui.Spacing();
for (int triggerIndex = 0; triggerIndex < triggers.Count; triggerIndex++) { for (int triggerIndex = 0; triggerIndex < triggers.Count; triggerIndex++) {
Triggers.Trigger trigger = triggers[triggerIndex]; Triggers.Trigger trigger = triggers[triggerIndex];
if (trigger != null) { if (trigger != null) {
@ -442,8 +482,8 @@ namespace FFXIV_Vibe_Plugin {
string triggerNameWithId = $"{triggerName}###{trigger.Id}"; string triggerNameWithId = $"{triggerName}###{trigger.Id}";
if (!Helpers.RegExpMatch(this.Logger, triggerName, this.CURRENT_TRIGGER_SELECTOR_SEARCHBAR)) { if (!Helpers.RegExpMatch(this.Logger, triggerName, this.CURRENT_TRIGGER_SELECTOR_SEARCHBAR)) {
continue; continue;
} }
if (ImGui.Selectable($"{triggerNameWithId}", selectedId == trigger.Id)) { // We don't want to show the ID if (ImGui.Selectable($"{triggerNameWithId}", selectedId == trigger.Id)) { // We don't want to show the ID
this.SelectedTrigger = trigger; this.SelectedTrigger = trigger;
this.triggersViewMode = "edit"; this.triggersViewMode = "edit";
@ -720,15 +760,15 @@ namespace FFXIV_Vibe_Plugin {
if ( if (
this.SelectedTrigger.ActionEffectType == (int)Structures.ActionEffectType.Damage || this.SelectedTrigger.ActionEffectType == (int)Structures.ActionEffectType.Damage ||
this.SelectedTrigger.ActionEffectType == (int)Structures.ActionEffectType.Heal this.SelectedTrigger.ActionEffectType == (int)Structures.ActionEffectType.Heal
|| ||
this.SelectedTrigger.Kind == (int)Triggers.KIND.HPChange) { this.SelectedTrigger.Kind == (int)Triggers.KIND.HPChange) {
// Min/Max amount values // Min/Max amount values
string type = ""; string type = "";
if (this.SelectedTrigger.ActionEffectType == (int)Structures.ActionEffectType.Damage) { type = "damage"; } if (this.SelectedTrigger.ActionEffectType == (int)Structures.ActionEffectType.Damage) { type = "damage"; }
if (this.SelectedTrigger.ActionEffectType == (int)Structures.ActionEffectType.Heal) { type = "heal"; } if (this.SelectedTrigger.ActionEffectType == (int)Structures.ActionEffectType.Heal) { type = "heal"; }
if (this.SelectedTrigger.Kind == (int)Triggers.KIND.HPChange) { type = "health"; } if (this.SelectedTrigger.Kind == (int)Triggers.KIND.HPChange) { type = "health"; }
// TRIGGER AMOUNT IN PERCENTAGE // TRIGGER AMOUNT IN PERCENTAGE
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.Text("Amount in percentage?"); ImGui.Text("Amount in percentage?");
ImGui.TableNextColumn(); ImGui.TableNextColumn();
@ -736,11 +776,11 @@ namespace FFXIV_Vibe_Plugin {
this.SelectedTrigger.AmountMinValue = 0; this.SelectedTrigger.AmountMinValue = 0;
this.SelectedTrigger.AmountMaxValue = 100; this.SelectedTrigger.AmountMaxValue = 100;
this.Configuration.Save(); this.Configuration.Save();
} }
// TRIGGER MIN_VALUE // TRIGGER MIN_VALUE
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.Text($"Min {type} value:"); ImGui.Text($"Min {type} value:");
ImGui.TableNextColumn(); ImGui.TableNextColumn();
@ -770,7 +810,17 @@ namespace FFXIV_Vibe_Plugin {
} }
ImGui.TableNextRow(); ImGui.TableNextRow();
} }
ImGui.EndTable(); ImGui.EndTable();
ImGui.Separator();
if (ImGui.Button("Export")) {
this._tmp_exportPatternResponse = export_trigger(SelectedTrigger);
}
ImGui.SameLine();
ImGuiComponents.HelpMarker("Writes this trigger to your export directory.");
ImGui.SameLine();
ImGui.Text($"{this._tmp_exportPatternResponse}");
ImGui.Separator();
ImGui.TextColored(ImGuiColors.DalamudViolet, "Actions & Devices"); ImGui.TextColored(ImGuiColors.DalamudViolet, "Actions & Devices");
ImGui.Separator(); ImGui.Separator();
@ -963,8 +1013,13 @@ namespace FFXIV_Vibe_Plugin {
ImGui.EndChild(); ImGui.EndChild();
} }
if (ImGui.Button("Add")) { if (ImGui.Button("Add")) {
Triggers.Trigger trigger = new("New Trigger"); int index = 0;
Triggers.Trigger trigger = new($"New Trigger {index}");
while (this.TriggerController.GetTriggers().Contains(trigger)) {
index++;
trigger = new($"New Trigger {index}");
}
this.TriggerController.AddTrigger(trigger); this.TriggerController.AddTrigger(trigger);
this.SelectedTrigger = trigger; this.SelectedTrigger = trigger;
this.triggersViewMode = "edit"; this.triggersViewMode = "edit";
@ -973,8 +1028,29 @@ namespace FFXIV_Vibe_Plugin {
ImGui.SameLine(); ImGui.SameLine();
if (ImGui.Button("Delete")) { if (ImGui.Button("Delete")) {
this.triggersViewMode = "delete"; this.triggersViewMode = "delete";
}
ImGui.SameLine();
if (ImGui.Button("Import Triggers")) {
if (!this.ConfigurationProfile.EXPORT_DIR.Equals("")) {
try {
foreach (var filename in Directory.GetFiles(this.ConfigurationProfile.EXPORT_DIR)) {
Trigger t = JsonConvert.DeserializeObject<Trigger>(File.ReadAllText(filename));
// Remove any triggers with the same name due to .Equals override
this.TriggerController.RemoveTrigger(t);
// Import the new trigger
this.TriggerController.AddTrigger(t);
}
} catch { }
}
}
ImGui.SameLine();
if (ImGui.Button("Export All")) {
if (!this.ConfigurationProfile.EXPORT_DIR.Equals("")) {
foreach (Trigger t in this.TriggerController.GetTriggers()) {
export_trigger(t);
}
}
} }
} }
public void DrawPatternsTab() { public void DrawPatternsTab() {
@ -1119,6 +1195,22 @@ namespace FFXIV_Vibe_Plugin {
this._tmp_void = "50:1000|100:2000"; this._tmp_void = "50:1000|100:2000";
ImGui.InputText("###HELP_PATTERN_EXAMPLE", ref this._tmp_void, 50); ImGui.InputText("###HELP_PATTERN_EXAMPLE", ref this._tmp_void, 50);
} }
public string export_trigger(Trigger trigger) {
if (this.ConfigurationProfile.EXPORT_DIR.Equals("")) {
return "No export directory has been set! Set one in Options.";
} else {
try {
File.WriteAllText(
Path.Join(this.ConfigurationProfile.EXPORT_DIR, $"{trigger.Name}.json"),
JsonConvert.SerializeObject(trigger, Formatting.Indented)
);
return "Successfully exported trigger!";
} catch {
return "Something went wrong while exporting!";
}
}
}
}
}
} }

View File

@ -29,6 +29,7 @@ A plugin for FFXIV that will let you vibe your controller or toys.
- Custom patterns per motor (save, with easy import, export). - Custom patterns per motor (save, with easy import, export).
- Vibe or trigger a pattern on HP Changed - Vibe or trigger a pattern on HP Changed
- HP Changed can have custom min/max values or percentages - HP Changed can have custom min/max values or percentages
- Export/Import triggers
## Prerequisites ## Prerequisites
- [FFXIV QuickLauncher](https://github.com/goatcorp/FFXIVQuickLauncher). - [FFXIV QuickLauncher](https://github.com/goatcorp/FFXIVQuickLauncher).
@ -64,7 +65,27 @@ not make a living from it.
## Build yourself ## Build yourself
You can build yourself, instructions are here: [Build yourself](./Docs/BUILD.md) You can build yourself, instructions are here: [Build yourself](./Docs/BUILD.md)o
## Import triggers
1. Start the game and make sure the plugin is working.
2. On your computer, go to your `%userprofile%` folder (eg: C:\\Users\\<yourname>) folder.
3. The go in the `FFXIV\_Vibe\_Plugin` folder (or create if it does not exists)
4. Add the triggers file you want to import (eg: `MyTrigger.json`)
5. In the plugin go to the `Triggers` tab and click on `Import Triggers` at the bottom.
That's it. It should load all of the triggers.
Note: you can define a custom directory to read/write in the `Options` tab.
## Export triggers
1. On your computre, go to your `%userprofile%` folder (eg: C:\\Users\\<yourname>) folder.
2. Go to the `FFXIV\_Vibe\_Plugin` folder (or create if it does not exists)
3. In the plugin, go to the `Triggers` tab.
4. Create your triggers (if they does not exist).
5. Select the trigger you want to export and click on the `Export` button.
That's it. You should see them in your userprofile directory.
Note: you can define a custom directory to read/write in the `Options` tab.
## USB Dongle vs Lovense Dongle vs Other ## USB Dongle vs Lovense Dongle vs Other
We recommend you to use a bluetooth dongle. Here is the one we are using: [TP-Link Nano USB Dongle Bluetooth 5.0](https://www.amazon.fr/gp/product/B09C25VRXD/ref=as_li_tl?ie=UTF8&camp=1642&creative=6746&creativeASIN=B09C25VRXD&linkCode=as2&tag=kaciexx-21&linkId=8b6c8c6e693ab549216c2dacad34e03b) We recommend you to use a bluetooth dongle. Here is the one we are using: [TP-Link Nano USB Dongle Bluetooth 5.0](https://www.amazon.fr/gp/product/B09C25VRXD/ref=as_li_tl?ie=UTF8&camp=1642&creative=6746&creativeASIN=B09C25VRXD&linkCode=as2&tag=kaciexx-21&linkId=8b6c8c6e693ab549216c2dacad34e03b)