1
0
Fork 0

fix: moving to App wrapper

master
Kacie 2023-01-24 22:50:56 +01:00
parent 38d7991097
commit 1a0790ffd4
29 changed files with 4184 additions and 303 deletions

View File

@ -1,138 +1,225 @@
# Supprimer la ligne ci-dessous si vous voulez hériter les paramètres .editorconfig des répertoires supérieurs
root = true
# top-most EditorConfig file
[*]
charset = utf-8
# Fichiers C#
[*.cs]
end_of_line = lf
insert_final_newline = true
#### Options EditorConfig principales ####
# 4 space indentation
# Indentation et espacement
indent_size = 2
indent_style = space
indent_size = 4
tab_width = 2
# disable redundant style warnings
# Préférences de nouvelle ligne
end_of_line = crlf
insert_final_newline = false
# Microsoft .NET properties
#### Conventions de codage .NET ####
# Organiser les instructions Using
dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = true
file_header_template = unset
# Préférences de this. et Me.
dotnet_style_qualification_for_event = false
dotnet_style_qualification_for_field = false
dotnet_style_qualification_for_method = false
dotnet_style_qualification_for_property = false
# Préférences des mots clés de langage par rapport aux types BCL
dotnet_style_predefined_type_for_locals_parameters_members = true
dotnet_style_predefined_type_for_member_access = true
# Préférences de parenthèses
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_operators = never_if_unnecessary
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
# Préférences de modificateur
dotnet_style_require_accessibility_modifiers = for_non_interface_members
# Préférences de niveau expression
dotnet_style_coalesce_expression = true
dotnet_style_collection_initializer = true
dotnet_style_explicit_tuple_names = true
dotnet_style_namespace_match_folder = true
dotnet_style_null_propagation = true
dotnet_style_object_initializer = true
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_prefer_auto_properties = true
dotnet_style_prefer_compound_assignment = true
dotnet_style_prefer_conditional_expression_over_assignment = true
dotnet_style_prefer_conditional_expression_over_return = true
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
dotnet_style_prefer_inferred_anonymous_type_member_names = true
dotnet_style_prefer_inferred_tuple_names = true
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
dotnet_style_prefer_simplified_boolean_expressions = true
dotnet_style_prefer_simplified_interpolation = true
# Préférences de champ
dotnet_style_readonly_field = true
# Préférences de paramètre
dotnet_code_quality_unused_parameters = all
# Préférences de suppression
dotnet_remove_unnecessary_suppression_exclusions = none
# Préférences de nouvelle ligne
dotnet_style_allow_multiple_blank_lines_experimental = true
dotnet_style_allow_statement_immediately_after_block_experimental = true
#### Conventions de codage C# ####
# Préférences de var
csharp_style_var_elsewhere = false
csharp_style_var_for_built_in_types = false
csharp_style_var_when_type_is_apparent = false
# Membres expression-bodied
csharp_style_expression_bodied_accessors = true
csharp_style_expression_bodied_constructors = false
csharp_style_expression_bodied_indexers = true
csharp_style_expression_bodied_lambdas = true
csharp_style_expression_bodied_local_functions = false
csharp_style_expression_bodied_methods = false
csharp_style_expression_bodied_operators = false
csharp_style_expression_bodied_properties = true
# Préférences correspondants au modèle
csharp_style_pattern_matching_over_as_with_null_check = true
csharp_style_pattern_matching_over_is_with_cast_check = true
csharp_style_prefer_extended_property_pattern = true
csharp_style_prefer_not_pattern = true
csharp_style_prefer_pattern_matching = true
csharp_style_prefer_switch_expression = true
# Préférences de vérification de valeur Null
csharp_style_conditional_delegate_call = true
# Préférences de modificateur
csharp_prefer_static_local_function = true
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
# Préférences de bloc de code
csharp_prefer_braces = true
csharp_prefer_simple_using_statement = true
csharp_style_namespace_declarations = block_scoped
csharp_style_prefer_method_group_conversion = true
csharp_style_prefer_top_level_statements = true
# Préférences de niveau expression
csharp_prefer_simple_default_expression = true
csharp_style_deconstructed_variable_declaration = true
csharp_style_implicit_object_creation_when_type_is_apparent = true
csharp_style_inlined_variable_declaration = true
csharp_style_prefer_index_operator = true
csharp_style_prefer_local_over_anonymous_function = true
csharp_style_prefer_null_check_over_type_check = true
csharp_style_prefer_range_operator = true
csharp_style_prefer_tuple_swap = true
csharp_style_prefer_utf8_string_literals = true
csharp_style_throw_expression = true
csharp_style_unused_value_assignment_preference = discard_variable
csharp_style_unused_value_expression_statement_preference = discard_variable
# Préférences pour la directive 'using'
csharp_using_directive_placement = outside_namespace
# Préférences de nouvelle ligne
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true
csharp_style_allow_embedded_statements_on_same_line_experimental = true
#### Règles de formatage C# ####
# Préférences de nouvelle ligne
csharp_new_line_before_catch = false
csharp_new_line_before_else = false
csharp_new_line_before_finally = false
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = none
csharp_new_line_between_query_expression_clauses = true
# Préférences de mise en retrait
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = false
csharp_new_line_before_open_brace = all
csharp_preferred_modifier_order = public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion
csharp_style_var_elsewhere = true:suggestion
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
dotnet_code_quality_unused_parameters = non_public
dotnet_naming_rule.event_rule.severity = warning
dotnet_naming_rule.event_rule.style = on_upper_camel_case_style
dotnet_naming_rule.event_rule.symbols = event_symbols
dotnet_naming_rule.private_constants_rule.severity = warning
dotnet_naming_rule.private_constants_rule.style = upper_camel_case_style
dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols
dotnet_naming_rule.private_instance_fields_rule.severity = warning
dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style
dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols
dotnet_naming_rule.private_static_fields_rule.severity = warning
dotnet_naming_rule.private_static_fields_rule.style = upper_camel_case_style
dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols
dotnet_naming_rule.private_static_readonly_rule.severity = warning
dotnet_naming_rule.private_static_readonly_rule.style = upper_camel_case_style
dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols
dotnet_naming_style.lower_camel_case_style.capitalization = camel_case
dotnet_naming_style.on_upper_camel_case_style.capitalization = pascal_case
dotnet_naming_style.on_upper_camel_case_style.required_prefix = On
dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case
dotnet_naming_symbols.event_symbols.applicable_accessibilities = *
dotnet_naming_symbols.event_symbols.applicable_kinds = event
dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private
dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field
dotnet_naming_symbols.private_constants_symbols.required_modifiers = const
dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private
dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field
dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private
dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field
dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static
dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private
dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field
dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly
dotnet_style_parentheses_in_arithmetic_binary_operators =always_for_clarity:suggestion
dotnet_style_parentheses_in_other_binary_operators =always_for_clarity:suggestion
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
dotnet_style_parentheses_in_other_operators=always_for_clarity:silent
dotnet_style_object_initializer = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
# Préférences d'espace
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_empty_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_before_open_square_brackets = false
csharp_space_before_comma = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_comma = true
csharp_space_after_cast = false
csharp_space_around_binary_operators = before_and_after
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = none
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# ReSharper properties
resharper_align_linq_query = true
resharper_align_multiline_argument = true
resharper_align_multiline_calls_chain = true
resharper_align_multiline_expression = true
resharper_align_multiline_extends_list = true
resharper_align_multiline_for_stmt = true
resharper_align_multline_type_parameter_constrains = true
resharper_align_multline_type_parameter_list = true
resharper_apply_on_completion = true
resharper_auto_property_can_be_made_get_only_global_highlighting = none
resharper_auto_property_can_be_made_get_only_local_highlighting = none
resharper_autodetect_indent_settings = true
resharper_braces_for_ifelse = required_for_multiline
resharper_can_use_global_alias = false
resharper_csharp_align_multiline_parameter = true
resharper_csharp_align_multiple_declaration = true
resharper_csharp_empty_block_style = together_same_line
resharper_csharp_int_align_comments = true
resharper_csharp_new_line_before_while = true
resharper_csharp_wrap_after_declaration_lpar = true
resharper_enforce_line_ending_style = true
resharper_member_can_be_private_global_highlighting = none
resharper_member_can_be_private_local_highlighting = none
resharper_new_line_before_finally = false
resharper_place_accessorholder_attribute_on_same_line = false
resharper_place_field_attribute_on_same_line = false
resharper_show_autodetect_configure_formatting_tip = false
resharper_use_indent_from_vs = false
# Préférences d'enveloppement
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
# ReSharper inspection severities
resharper_arrange_missing_parentheses_highlighting = hint
resharper_arrange_redundant_parentheses_highlighting = hint
resharper_arrange_this_qualifier_highlighting = none
resharper_arrange_type_member_modifiers_highlighting = hint
resharper_arrange_type_modifiers_highlighting = hint
resharper_built_in_type_reference_style_for_member_access_highlighting = hint
resharper_built_in_type_reference_style_highlighting = none
resharper_foreach_can_be_converted_to_query_using_another_get_enumerator_highlighting = none
resharper_foreach_can_be_partly_converted_to_query_using_another_get_enumerator_highlighting = none
resharper_invert_if_highlighting = none
resharper_loop_can_be_converted_to_query_highlighting = none
resharper_method_has_async_overload_highlighting = none
resharper_private_field_can_be_converted_to_local_variable_highlighting = none
resharper_redundant_base_qualifier_highlighting = none
resharper_suggest_var_or_type_built_in_types_highlighting = hint
resharper_suggest_var_or_type_elsewhere_highlighting = hint
resharper_suggest_var_or_type_simple_types_highlighting = hint
resharper_unused_auto_property_accessor_global_highlighting = none
csharp_style_deconstructed_variable_declaration=true:silent
#### Styles de nommage ####
[*.{appxmanifest,asax,ascx,aspx,axaml,axml,build,c,c++,cc,cginc,compute,config,cp,cpp,cs,cshtml,csproj,css,cu,cuh,cxx,dbml,discomap,dtd,h,hh,hlsl,hlsli,hlslinc,hpp,htm,html,hxx,inc,inl,ino,ipp,js,json,jsproj,jsx,lsproj,master,mpp,mq4,mq5,mqh,njsproj,nuspec,paml,proj,props,proto,razor,resjson,resw,resx,skin,StyleCop,targets,tasks,tpp,ts,tsx,usf,ush,vb,vbproj,xaml,xamlx,xml,xoml,xsd}]
indent_style = space
indent_size = 4
tab_width = 4
dotnet_style_parentheses_in_other_operators=always_for_clarity:silent
# Règles de nommage
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Spécifications de symboles
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Styles de nommage
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case

View File

@ -0,0 +1,339 @@
using System.Collections.Generic;
using System;
using System.Threading;
// Dalamud libs
using Dalamud.IoC;
using Dalamud.Data;
using Dalamud.Plugin;
using Dalamud.Game;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Network;
using Dalamud.Game.Command;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Objects;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
// FFXIV_Vibe_Plugin libs
using FFXIV_Vibe_Plugin.Commons;
using FFXIV_Vibe_Plugin.Triggers;
using FFXIV_Vibe_Plugin.Hooks;
using FFXIV_Vibe_Plugin.Experimental;
using FFXIV_Vibe_Plugin.Migrations;
namespace FFXIV_Vibe_Plugin {
internal class App {
private Dalamud.Game.Gui.ChatGui? DalamudChat { get; init; }
public Configuration Configuration { get; init; }
private GameNetwork GameNetwork { get; init; }
private DataManager DataManager { get; init; }
private ClientState ClientState { get; init; }
private SigScanner Scanner { get; init; }
private ObjectTable GameObjects { get; init; }
private DalamudPluginInterface PluginInterface { get; init; }
// Custom variables from Kacie
private readonly bool wasInit = false;
public readonly string CommandName = "";
private readonly string ShortName = "";
private bool _firstUpdated = false;
private readonly PlayerStats PlayerStats;
private PluginUI PluginUi { get; init; }
private ConfigurationProfile ConfigurationProfile;
private readonly Logger Logger;
private readonly ActionEffect hook_ActionEffect;
private readonly Device.DevicesController DeviceController;
private readonly TriggersController TriggersController;
private readonly Patterns Patterns;
// Experiments
private readonly NetworkCapture experiment_networkCapture;
public App(string commandName, string shortName, GameNetwork gameNetwork, ClientState clientState, DataManager dataManager, Dalamud.Game.Gui.ChatGui? dalamudChat, Configuration configuration, SigScanner scanner, ObjectTable gameObjects, DalamudPluginInterface pluginInterface) {
return;
this.CommandName = commandName;
this.ShortName = shortName;
this.GameNetwork = gameNetwork;
this.ClientState = clientState;
this.DataManager = dataManager;
this.DalamudChat = dalamudChat;
this.Configuration = configuration;
this.GameObjects = gameObjects;
this.Scanner = scanner;
this.PluginInterface = pluginInterface;
if (DalamudChat != null) {
DalamudChat.ChatMessage += ChatWasTriggered;
}
this.Logger = new Logger(this.DalamudChat, ShortName, Logger.LogLevel.VERBOSE);
// Migrations
Migration migration = new(Configuration, Logger);
migration.Patch_0_2_0_to_1_0_0_config_profile();
// Configuration Profile
this.ConfigurationProfile = this.Configuration.GetDefaultProfile();
// Patterns
this.Patterns = new Patterns();
this.Patterns.SetCustomPatterns(this.ConfigurationProfile.PatternList);
// Initialize the devices Controller
/* TODO: this.DeviceController = new Device.DevicesController(this.Logger, this.Configuration, this.ConfigurationProfile, this.Patterns);*/
this.DeviceController = null;
if (this.ConfigurationProfile.AUTO_CONNECT) {
Thread t = new(delegate () {
Thread.Sleep(2000);
this.Command_DeviceController_Connect();
});
t.Start();
}
// Initialize Hook ActionEffect
this.hook_ActionEffect = new(this.DataManager, this.Logger, this.Scanner, clientState, gameObjects);
this.hook_ActionEffect.ReceivedEvent += SpellWasTriggered;
// Init the login event.
this.ClientState.Login += this.ClientState_LoginEvent;
// Initialize player stats monitoring.
this.PlayerStats = new PlayerStats(this.Logger, this.ClientState);
PlayerStats.Event_CurrentHpChanged += this.PlayerCurrentHPChanged;
PlayerStats.Event_MaxHpChanged += this.PlayerCurrentHPChanged;
// Triggers
this.TriggersController = new Triggers.TriggersController(this.Logger, this.PlayerStats, this.ConfigurationProfile);
// UI
this.PluginUi = new PluginUI(this, this.Logger, this.PluginInterface, this.Configuration, this.ConfigurationProfile, this.DeviceController, this.TriggersController, this.Patterns);
// Experimental
this.experiment_networkCapture = new NetworkCapture(this.Logger, this.GameNetwork);
// Make sure we set the current profile everywhere.
this.SetProfile(this.Configuration.CurrentProfileName);
// Set the init variable
this.wasInit = true;
}
public void Dispose() {
if (!this.wasInit) { return; }
this.Logger.Debug("Disposing plugin...");
// Cleaning device controller.
if (this.DeviceController != null) {
this.DeviceController.Dispose();
}
// Cleaning chat triggers.
if (DalamudChat != null) {
DalamudChat.ChatMessage -= ChatWasTriggered;
}
// Cleaning hooks
this.hook_ActionEffect.Dispose();
// Cleaning experimentations
this.experiment_networkCapture.Dispose();
this.PluginUi.Dispose();
this.Logger.Debug("Plugin disposed!");
}
public static string GetHelp(string command) {
string helpMessage = $@"Usage:
{command} config
{command} connect
{command} disconnect
{command} send <0-100> # Send vibe intensity to all toys
{command} stop
";
return helpMessage;
}
public void OnCommand(string command, string args) {
if (args.Length == 0) {
this.DisplayUI();
} else {
if (args.StartsWith("help")) {
this.Logger.Chat(App.GetHelp($"/{ShortName}"));
} else if (args.StartsWith("config")) {
this.DisplayConfigUI();
} else if (args.StartsWith("connect")) {
this.Command_DeviceController_Connect();
} else if (args.StartsWith("disconnect")) {
this.Command_DeviceController_Disconnect();
} else if (args.StartsWith("send")) {
this.Command_SendIntensity(args);
} else if (args.StartsWith("stop")) {
this.DeviceController.SendVibeToAll(0);
}
// Experimental
else if (args.StartsWith("exp_network_start")) {
this.experiment_networkCapture.StartNetworkCapture();
} else if (args.StartsWith("exp_network_stop")) {
this.experiment_networkCapture.StopNetworkCapture();
} else {
this.Logger.Chat($"Unknown subcommand: {args}");
}
}
}
private void FirstUpdated() {
this.Logger.Debug("First updated");
if (this.ConfigurationProfile != null && this.ConfigurationProfile.AUTO_OPEN) {
this.DisplayUI();
}
}
private void DisplayUI() {
if (this.PluginUi != null) {
this.PluginUi.Display();
}
}
private void DisplayConfigUI() {
this.PluginUi.Display();
}
public void DrawUI() {
if(this.PluginUi == null) {
return;
}
this.PluginUi.Draw();
if (this.ClientState.IsLoggedIn) {
this.PlayerStats.Update(this.ClientState);
}
// Trigger first updated method
if (!this._firstUpdated) {
this.FirstUpdated();
this._firstUpdated = true;
}
}
public void Command_DeviceController_Connect() {
if (this.DeviceController == null) {
this.Logger.Error("No device controller available to connect.");
return;
}
if (this.ConfigurationProfile != null) {
string host = this.ConfigurationProfile.BUTTPLUG_SERVER_HOST;
int port = this.ConfigurationProfile.BUTTPLUG_SERVER_PORT;
this.DeviceController.Connect(host, port);
}
}
private void Command_DeviceController_Disconnect() {
if (this.DeviceController == null) {
this.Logger.Error("No device controller available to disconnect.");
return;
}
this.DeviceController.Disconnect();
}
private void Command_SendIntensity(string args) {
string[] blafuckcsharp;
int intensity;
try {
blafuckcsharp = args.Split(" ", 2);
intensity = int.Parse(blafuckcsharp[1]);
this.Logger.Chat($"Command Send intensity {intensity}");
} catch (Exception e) when (e is FormatException or IndexOutOfRangeException) {
this.Logger.Error($"Malformed arguments for send [intensity].", e);
return;
}
if (this.DeviceController == null) {
this.Logger.Error("No device controller available to send intensity.");
return;
}
this.DeviceController.SendVibeToAll(intensity);
}
/************************************
* LISTEN TO EVENTS *
************************************/
private void SpellWasTriggered(object? sender, HookActionEffects_ReceivedEventArgs args) {
if (this.TriggersController == null) {
this.Logger.Warn("SpellWasTriggered: TriggersController not init yet, ignoring spell...");
return;
}
Structures.Spell spell = args.Spell;
if (this.ConfigurationProfile != null && this.ConfigurationProfile.VERBOSE_SPELL) {
this.Logger.Debug($"VERBOSE_SPELL: {spell}");
}
List<Trigger>? triggers = this.TriggersController.CheckTrigger_Spell(spell);
foreach (Trigger trigger in triggers) {
this.DeviceController.SendTrigger(trigger);
}
}
private void ChatWasTriggered(XivChatType chatType, uint senderId, ref SeString _sender, ref SeString _message, ref bool isHandled) {
if (this.TriggersController == null) {
this.Logger.Warn("ChatWasTriggered: TriggersController not init yet, ignoring chat...");
return;
}
string fromPlayerName = _sender.ToString();
if (this.ConfigurationProfile != null && this.ConfigurationProfile.VERBOSE_CHAT) {
string XivChatTypeName = ((XivChatType)chatType).ToString();
this.Logger.Debug($"VERBOSE_CHAT: {fromPlayerName} type={XivChatTypeName}: {_message}");
}
List<Trigger> triggers = this.TriggersController.CheckTrigger_Chat(chatType, fromPlayerName, _message.TextValue);
foreach (Trigger trigger in triggers) {
this.DeviceController.SendTrigger(trigger);
}
}
public bool SetProfile(string profileName) {
bool result = this.Configuration.SetCurrentProfile(profileName);
if (!result) {
this.Logger.Warn($"You are trying to use profile {profileName} which can't be found");
return false;
}
ConfigurationProfile? configProfileToCheck = this.Configuration.GetProfile(profileName);
if (configProfileToCheck != null) {
this.ConfigurationProfile = configProfileToCheck;
this.PluginUi.SetProfile(this.ConfigurationProfile);
this.DeviceController.SetProfile(this.ConfigurationProfile);
this.TriggersController.SetProfile(this.ConfigurationProfile);
}
return true;
}
private void ClientState_LoginEvent(object? send, EventArgs e) {
this.PlayerStats.Update(this.ClientState);
}
private void PlayerCurrentHPChanged(object? send, EventArgs e) {
float currentHP = this.PlayerStats.GetCurrentHP();
float maxHP = this.PlayerStats.GetMaxHP();
if (this.TriggersController == null) {
this.Logger.Warn("PlayerCurrentHPChanged: TriggersController not init yet, ignoring HP change...");
return;
}
float percentageHP = currentHP / maxHP * 100f;
List<Trigger> triggers = TriggersController.CheckTrigger_HPChanged((int)currentHP, (float)percentageHP);
this.Logger.Debug($"Player HPChanged {currentHP}/{maxHP} {percentageHP}%");
// Overwrites the threshold for every motors
foreach (Trigger trigger in triggers) {
this.DeviceController.SendTrigger(trigger);
}
}
}
}

View File

@ -0,0 +1,50 @@
using System;
using System.Text.RegularExpressions;
namespace FFXIV_Vibe_Plugin.Commons {
internal class Helpers {
/** Get number of milliseconds (unix timestamp) */
public static int GetUnix() {
return (int)DateTimeOffset.Now.ToUnixTimeMilliseconds();
}
public static int ClampInt(int value, int min, int max) {
if(value < min) { return min; } else if( value > max) { return max; }
return value;
}
public static float ClampFloat(float value, float min, float max) {
if(value < min) { return min; } else if(value > max) { return max; }
return value;
}
public static int ClampIntensity(int intensity, int threshold) {
intensity = ClampInt(intensity, 0, 100);
return (int)(intensity / (100.0f / threshold));
}
/** Check if a regexp matches the given text */
public static bool RegExpMatch(Logger Logger, string text, string regexp) {
bool found = false;
if(regexp.Trim() == "") {
found = true;
} else {
string patternCheck = String.Concat(@"", regexp);
try {
System.Text.RegularExpressions.Match m = Regex.Match(text, patternCheck, RegexOptions.IgnoreCase);
if(m.Success) {
found = true;
}
} catch(Exception) {
Logger.Error($"Probably a wrong REGEXP for {regexp}");
}
}
return found;
}
}
}

View File

@ -0,0 +1,132 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Dalamud.Plugin;
using Dalamud.IoC;
namespace FFXIV_Vibe_Plugin.Commons {
internal class Logger {
// Initialize the Dalamud.Gui system.
private readonly Dalamud.Game.Gui.ChatGui? DalamudChatGui;
// Logger name.
private readonly string name = "";
// Current log level.
private readonly LogLevel log_level = LogLevel.DEBUG;
// The prefix symbol of the log message.
private readonly string prefix = ">";
// Available log levels.
public enum LogLevel {
VERBOSE, DEBUG, LOG, INFO, WARN, ERROR, FATAL,
}
/** Constructor */
public Logger(Dalamud.Game.Gui.ChatGui? DalamudChatGui, string name, LogLevel log_level) {
this.DalamudChatGui = DalamudChatGui;
this.name = name;
this.log_level = log_level;
}
/** Printing in the chat gui a message. */
public void Chat(string msg) {
if(DalamudChatGui != null) {
string m = this.FormatMessage(LogLevel.LOG, msg);
DalamudChatGui.Print(m);
} else {
Dalamud.Logging.PluginLog.LogError("No gui chat");
}
}
/** Printing in the chat gui an error message. */
public void ChatError(string msg) {
string m = this.FormatMessage(LogLevel.ERROR, msg);
DalamudChatGui?.PrintError(m);
this.Error(msg);
}
/** Printing in the chat gui an error message with an exception. */
public void ChatError(string msg, Exception e) {
string m = this.FormatMessage(LogLevel.ERROR, msg, e);
DalamudChatGui?.PrintError(m);
this.Error(m);
}
/** Log message as 'debug' to logs. */
public void Verbose(string msg) {
if(this.log_level > LogLevel.VERBOSE) { return; }
string m = this.FormatMessage(LogLevel.VERBOSE, msg);
Dalamud.Logging.PluginLog.LogVerbose(m);
}
/** Log message as 'debug' to logs. */
public void Debug(string msg) {
if(this.log_level > LogLevel.DEBUG) { return; }
string m = this.FormatMessage(LogLevel.DEBUG, msg);
Dalamud.Logging.PluginLog.LogDebug(m);
}
/** Log message as 'log' to logs. */
public void Log(string msg) {
if(this.log_level > LogLevel.LOG) { return; }
string m = this.FormatMessage(LogLevel.LOG, msg);
Dalamud.Logging.PluginLog.Log(m);
}
/** Log message as 'info' to logs. */
public void Info(string msg) {
if(this.log_level > LogLevel.INFO) { return; }
string m = this.FormatMessage(LogLevel.INFO, msg);
Dalamud.Logging.PluginLog.Information(m);
}
/** Log message as 'warning' to logs. */
public void Warn(string msg) {
if(this.log_level > LogLevel.WARN) { return; }
string m = this.FormatMessage(LogLevel.WARN, msg);
Dalamud.Logging.PluginLog.Warning(m);
}
/** Log message as 'error' to logs. */
public void Error(string msg) {
if(this.log_level > LogLevel.ERROR) { return; }
string m = this.FormatMessage(LogLevel.ERROR, msg);
Dalamud.Logging.PluginLog.Error(m);
}
/** Log message as 'error' to logs with an exception. */
public void Error(string msg, Exception e) {
if(this.log_level > LogLevel.ERROR) { return; }
string m = this.FormatMessage(LogLevel.ERROR, msg, e);
Dalamud.Logging.PluginLog.Error(m);
}
/** Log message as 'fatal' to logs. */
public void Fatal(string msg) {
if(this.log_level > LogLevel.FATAL) { return; }
string m = this.FormatMessage(LogLevel.FATAL, msg);
Dalamud.Logging.PluginLog.Fatal(m);
}
/** Log message as 'fatal' to logs with an exception. */
public void Fatal(string msg, Exception e) {
if(this.log_level > LogLevel.FATAL) { return; }
string m = this.FormatMessage(LogLevel.FATAL, msg, e);
Dalamud.Logging.PluginLog.Fatal(m);
}
private string FormatMessage(LogLevel type, string msg) {
return $"{(name != "" ? name + " " : "")}{type} {this.prefix} {msg}";
}
private string FormatMessage(LogLevel type, string msg, Exception e) {
return $"{(name != "" ? name+" " : "")}{type} {this.prefix} {e.Message}\\n{msg}";
}
}
}

View File

@ -0,0 +1,251 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FFXIV_Vibe_Plugin {
internal class OpCodes {
////////////////////////////////////////////////////////////////////////////////
/// Lobby Connection IPC Codes
/**
* Server IPC Lobby Type Codes.
*/
public enum ServerLobbyIpcType : ushort {
LobbyError = 0x0002,
LobbyServiceAccountList = 0x000C,
LobbyCharList = 0x000D,
LobbyCharCreate = 0x000E,
LobbyEnterWorld = 0x000F,
LobbyServerList = 0x0015,
LobbyRetainerList = 0x0017,
};
/**
* Client IPC Lobby Type Codes.
*/
public enum ClientLobbyIpcType : ushort {
ReqCharList = 0x0003,
ReqEnterWorld = 0x0004,
ClientVersionInfo = 0x0005,
ReqCharDelete = 0x000A,
ReqCharCreate = 0x000B,
};
////////////////////////////////////////////////////////////////////////////////
/// Zone Connection IPC Codes
/**
* Server IPC Zone Type Codes.
*/
public enum ServerZoneIpcType : ushort {
PlayerSetup = 0x008B, // updated 6.0
UpdateHpMpTp = 0x0296,// updated 6.0
PlayerStats = 0x038D,// updated 6.0
ActorControl = 0x017E,// updated 6.0
ActorControlSelf = 0x02E6,// updated 6.0
ActorControlTarget = 0x0168,// updated 6.0
Playtime = 0x03C3,// updated 6.0
Examine = 0x011B,// updated 6.0
MarketBoardSearchResult = 0x0201,// updated 6.0
MarketBoardItemListingCount = 0x023C,// updated 6.0
MarketBoardItemListingHistory = 0x0192,// updated 6.0
MarketBoardItemListing = 0x0323,// updated 6.0
MarketBoardPurchase = 0x009D,// updated 6.0
ActorMove = 0x0235,// updated 6.0
ResultDialog = 0x00AF,// updated 6.0
RetainerInformation = 0x0129,// updated 6.0
NpcSpawn = 0x032E,// updated 6.0
ItemMarketBoardInfo = 0x008A,// updated 6.0
PlayerSpawn = 0x0133,// updated 6.0
ContainerInfo = 0x00EE,// updated 6.0
ItemInfo = 0x0173,// updated 6.0
UpdateClassInfo = 0x03A5,// updated 6.0
ActorCast = 0x0108,// updated 6.0
CurrencyCrystalInfo = 0x0258,// updated 6.0
InitZone = 0x02C4,// updated 6.0
EffectResult = 0x0196,// updated 6.0
EventStart = 0x0334,// updated 6.0
EventFinish = 0x01B8,// updated 6.0
SomeDirectorUnk4 = 0x0164,// updated 6.0
UpdateInventorySlot = 0x02B6,// updated 6.0
DesynthResult = 0x02D5,// updated 6.0
InventoryActionAck = 0x00FC,// updated 6.0
InventoryTransaction = 0x008F,// updated 6.0
InventoryTransactionFinish = 0x039B,// updated 6.0
CFNotify = 0x0317,// updated 6.0
PrepareZoning = 0x0090,// updated 6.0
ActorSetPos = 0x0199,// updated 6.0
PlaceFieldMarker = 0x037D,// updated 6.0
PlaceFieldMarkerPreset = 0x01CF,// updated 6.0
ObjectSpawn = 0x0319,// updated 6.0
Effect = 0x035A,// updated 6.0
StatusEffectList = 0x02C5,// updated 6.0
ActorGauge = 0x0283,// updated 6.0
FreeCompanyInfo = 0x031C,// updated 6.0
FreeCompanyDialog = 0x036E,// updated 6.0
AirshipTimers = 0x00ED,// updated 6.0
SubmarineTimers = 0x00F5,// updated 6.0
AirshipStatusList = 0x023F,// updated 6.0
AirshipStatus = 0x01E3,// updated 6.0
AirshipExplorationResult = 0x00B4,// updated 6.0
SubmarineProgressionStatus = 0x030B,// updated 6.0
SubmarineStatusList = 0x02F4,// updated 6.0
SubmarineExplorationResult = 0x0183,// updated 6.0
EventPlay = 0x00A5, // Updated 6.0
EventPlay4 = 0x022E, // Updated 6.0
EventPlay8 = 0x18B, // Updated 6.0
EventPlay16 = 0x1F4, // Updated 6.0
EventPlay32 = 0x65, // Updated 6.0
EventPlay64 = 0x3A8, // Updated 6.0
EventPlay128 = 0x16E, // Updated 6.0
EventPlay255 = 0x366, // Updated 6.0
WeatherChange = 0x01FD, // Updated 6.0
Logout = 0x02EC, // updated 6.0 hotfix
//HousingWardInfo = 0x012A, // updated 5.58 hotfix
};
/**
* Client IPC Zone Type Codes.
*/
public enum ClientZoneIpcType : ushort {
UpdatePositionHandler = 0x0346,// updated 6.0
ClientTrigger = 0x03AC,// updated 6.0
ChatHandler = 0x01CC,// updated 6.0
SetSearchInfoHandler = 0x03B1,// updated 6.0
MarketBoardPurchaseHandler = 0x00DC,// updated 6.0
InventoryModifyHandler = 0x00A3,// updated 6.0 (Base offset: 0x00AA)
UpdatePositionInstance = 0x0163,// updated 6.0
//PingHandler = 0x02CD, // updated 5.58 hotfix
//InitHandler = 0x01AA, // updated 5.58 hotfix
//FinishLoadingHandler = 0x02DA, // updated 5.58 hotfix
//CFCommenceHandler = 0x0092, // updated 5.58 hotfix
//CFRegisterDuty = 0x03C7, // updated 5.58 hotfix
//CFRegisterRoulette = 0x00C2, // updated 5.58 hotfix
//PlayTimeHandler = 0x00B0, // updated 5.58 hotfix
//LogoutHandler = 0x0178, // updated 5.58 hotfix
//CancelLogout = 0x01F9, // updated 5.58 hotfix
//CFDutyInfoHandler = 0x0092, // updated 5.58 hotfix
//SocialReqSendHandler = 0x023A, // updated 5.58 hotfix
//CreateCrossWorldLS = 0x0336, // updated 5.58 hotfix
//SocialListHandler = 0x0187, // updated 5.58 hotfix
//ReqSearchInfoHandler = 0x022C, // updated 5.58 hotfix
//ReqExamineSearchCommentHandler = 0x0315, // updated 5.58 hotfix
//ReqRemovePlayerFromBlacklist = 0x0145, // updated 5.58 hotfix
//BlackListHandler = 0x0161, // updated 5.58 hotfix
//PlayerSearchHandler = 0x02FF, // updated 5.58 hotfix
//LinkshellListHandler = 0x023B, // updated 5.58 hotfix
//MarketBoardRequestItemListingInfo = 0x0189, // updated 5.58 hotfix
//MarketBoardRequestItemListings = 0x0092, // updated 5.58 hotfix
//MarketBoardSearch = 0x02F9, // updated 5.58 hotfix
//ReqExamineFcInfo = 0x0136, // updated 5.58 hotfix
//FcInfoReqHandler = 0x0234, // updated 5.58 hotfix
//FreeCompanyUpdateShortMessageHandler = 0x0123, // added 5.0
//ReqMarketWishList = 0x0306, // updated 5.58 hotfix
//ReqJoinNoviceNetwork = 0x01D5, // updated 5.58 hotfix
//ReqCountdownInitiate = 0x00C2, // updated 5.58 hotfix
//ReqCountdownCancel = 0x00E6, // updated 5.58 hotfix
//ZoneLineHandler = 0x03CC, // updated 5.58 hotfix
//DiscoveryHandler = 0x023A, // updated 5.58 hotfix
//PlaceFieldMarker = 0x02AF, // updated 5.58 hotfix
//PlaceFieldMarkerPreset = 0x018E, // updated 5.58 hotfix
//SkillHandler = 0x0244, // updated 5.58 hotfix
//GMCommand1 = 0x018A, // updated 5.58 hotfix
//GMCommand2 = 0x02FD, // updated 5.58 hotfix
//AoESkillHandler = 0x01F1, // updated 5.58 hotfix
//InventoryEquipRecommendedItems = 0x0109, // updated 5.58 hotfix
//ReqPlaceHousingItem = 0x0352, // updated 5.58 hotfix
//BuildPresetHandler = 0x024E, // updated 5.58 hotfix
//TalkEventHandler = 0x0305, // updated 5.58 hotfix
//EmoteEventHandler = 0x03A7, // updated 5.58 hotfix
//WithinRangeEventHandler = 0x02EE, // updated 5.58 hotfix
//OutOfRangeEventHandler = 0x00EE, // updated 5.58 hotfix
//EnterTeriEventHandler = 0x0389, // updated 5.58 hotfix
//ReturnEventHandler = 0x03B4, // updated 5.58 hotfix
//TradeReturnEventHandler = 0x0216, // updated 5.58 hotfix
//LinkshellEventHandler = 0x0239, // updated 5.58 hotfix
//LinkshellEventHandler1 = 0x0239, // updated 5.58 hotfix
//ReqEquipDisplayFlagsChange = 0x01F6, // updated 5.58 hotfix
//LandRenameHandler = 0x018C, // updated 5.58 hotfix
//HousingUpdateHouseGreeting = 0x02F4, // updated 5.58 hotfix
//HousingUpdateObjectPosition = 0x02CB, // updated 5.58 hotfix
//SetSharedEstateSettings = 0x0179, // updated 5.58 hotfix
//PerformNoteHandler = 0x016E, // updated 5.58 hotfix
};
////////////////////////////////////////////////////////////////////////////////
/// Chat Connection IPC Codes
/**
* Server IPC Chat Type Codes.
*/
public enum ServerChatIpcType : ushort {
//Tell = 0x0064, // updated for sb
//TellErrNotFound = 0x0066,
//FreeCompanyEvent = 0x012C, // added 5.0
};
/**
* Client IPC Chat Type Codes.
*/
public enum ClientChatIpcType : ushort {
//TellReq = 0x0064,
};
public static string? GetName(ushort opCode) {
string? name = "?Unknow?";
if(Enum.IsDefined(typeof(OpCodes.ServerLobbyIpcType), opCode)) {
name = "ServerLobbyIpcType-" + Enum.GetName(typeof(OpCodes.ServerLobbyIpcType), opCode);
}
if(Enum.IsDefined(typeof(OpCodes.ClientLobbyIpcType), opCode)) {
name = "ClientLobbyIpcType-" + Enum.GetName(typeof(OpCodes.ClientLobbyIpcType), opCode);
}
if(Enum.IsDefined(typeof(OpCodes.ServerZoneIpcType), opCode)) {
name = "ServerZoneIpcType-" + Enum.GetName(typeof(OpCodes.ServerZoneIpcType), opCode);
}
if(Enum.IsDefined(typeof(OpCodes.ClientZoneIpcType), opCode)) {
name = "ClientZoneIpcType-" + Enum.GetName(typeof(OpCodes.ClientZoneIpcType), opCode);
}
if(Enum.IsDefined(typeof(OpCodes.ServerChatIpcType), opCode)) {
name = "ServerChatIpcType-" + Enum.GetName(typeof(OpCodes.ServerChatIpcType), opCode);
}
if(Enum.IsDefined(typeof(OpCodes.ClientChatIpcType), opCode)) {
name = "ClientChatIpcType-"+Enum.GetName(typeof(OpCodes.ClientChatIpcType), opCode);
}
return name;
}
}
}

View File

@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FFXIV_Vibe_Plugin {
public class Patterns {
private readonly List<Pattern> BuiltinPatterns = new();
private List<Pattern> CustomPatterns = new();
/**
* Pattern is: [intensity]:[duration in ms]
*/
public Patterns() {
this.AddBuiltinPattern(new Pattern("intensity", "100:0")); // Don't change this one.
this.AddBuiltinPattern(new Pattern("ramp", "10:150|20:150|30:150|40:150|50:150|60:150|70:150|80:150|90:150|100:250|0:0"));
this.AddBuiltinPattern(new Pattern("bump", "10:150|20:150|30:150|40:150|50:150|60:150|70:150|80:150|90:150|100:250|50:250|100:500|0:0"));
this.AddBuiltinPattern(new Pattern("square", "100:800|50:800|0:200|100:1000|0:0"));
this.AddBuiltinPattern(new Pattern("shake", "100:500|20:200|100:500|80:500|100:200|90:100|100:200|90:200|100:800|0:0"));
this.AddBuiltinPattern(new Pattern("sos", "100:500|50:200|100:500|50:200|100:500|50:200|100:1000|30:200|100:1000|30:200|100:1000|30:200|100:500|50:200|100:500|50:200|100:500|0:0"));
this.AddBuiltinPattern(new Pattern("xenoWave", "10:650|15:500|20:400|30:400|45:350|60:300|75:300|95:250|100:200|90:250|75:300|60:300|45:350|30:400|20:400|15:500|10:650|5:750|0:0"));
this.AddBuiltinPattern(new Pattern("slowVibe", "10:1000|20:1000|10:1000|50:1000|0:0"));
this.AddBuiltinPattern(new Pattern("Vel: Poke Release 25s", "100:3000|50:500|60:500|70:500|80:500|90:500|100:3000|50:500|60:500|70:500|80:500|90:500|100:3000|50:500|60:500|70:500|80:500|90:500|100:3000|50:500|60:500|70:500|80:500|90:500|100:3000|0:0"));
this.AddBuiltinPattern(new Pattern("Vel: Stop to gentle 14.6s", "40:200|50:200|60:200|70:200|80:200|90:200|100:3000|90:600|80:800|70:1000|60:1200|50:1400|40:1600|30:1800|25:2000|0:0"));
this.AddBuiltinPattern(new Pattern("Vel: Teleport 7s", "20:500|30:500|40:500|50:500|60:500|70:500|80:500|90:500|100:1000|90:200|80:200|70:200|60:200|50:200|40:200|30:200|20:200|10:200|5:200|0:0"));
this.AddBuiltinPattern(new Pattern("Vel: Paralysis 7s", "100:200|0:200|100:200|0:200|100:200|0:500|100:200|0:200|100:200|0:200|100:200|0:500|100:200|0:200|100:200|0:200|100:200|0:500|200:100|200:0|200:100|200:0|200:100|500:0|200:100|200:0|200:100|200:0|200:100|0:0"));
this.AddBuiltinPattern(new Pattern("Vel: Paralysis (longer) 11.3s", "50:200|0:200|100:500|0:200|40:200|0:700|20:200|0:500|100:200|0:200|60:200|0:400|80:200|0:300|90:200|0:200|35:200|0:500|55:200|0:200|40:200|0:700|20:200|0:200|100:900|0:200|60:200|0:200|30:200|0:300|70:200|0:200|100:500|0:200|50:200|0:200|100:400|0:200|100:200|0:0"));
this.AddBuiltinPattern(new Pattern("Vel: Simple vibe 12s", "50:2000|100:2000|0:200|100:200|0:200|100:200|0:200|100:200|0:200|100:200|0:200|100:200|0:200|100:200|0:200|100:200|0:200|100:200|0:200|100:200|0:200|100:200|0:200|100:200|0:200|100:200|0:200|100:200|0:200|100:200|0:200|100:200|0:200|100:200|0:200|100:200|0:200|100:200|0:200|100:200|0:200|100:200|0:0"));
this.AddBuiltinPattern(new Pattern("Vel: Impact 1.25s", "100:500|90:100|80:100|70:100|60:100|50:100|40:100|30:100|20:100|10:100|0:0"));
this.AddBuiltinPattern(new Pattern("Vel: Skyshard 5.65s", "100:100|20:100|40:100|60:100|20:2500|40:150|20:150|80:150|100:2000|80:150|40:150|0:0"));
this.AddBuiltinPattern(new Pattern("Vel: Sprint 20s", "100:400|50:400|100:400|50:400|100:400|50:400|100:400|50:400|100:400|50:400|100:400|50:400|100:400|50:400|100:400|50:400|100:400|50:400|100:400|50:400|100:400|50:400|100:400|50:400|100:400|50:400|100:400|50:400|100:400|50:400|100:400|50:400|100:400|50:400|100:400|50:400|100:400|50:400|400:100|400:50|400:100|400:50|400:100|400:50|400:100|400:50|400:100|400:50|400:100|400:50"));
this.AddBuiltinPattern(new Pattern("Vel: Heartbeat (fast) 8.3s", "50:200|0:200|70:200|0:1000|50:200|0:200|70:200|0:1000|50:200|0:200|70:200|0:1000|50:200|0:200|70:200|0:1000|0:0"));
this.AddBuiltinPattern(new Pattern("FF Victory Jingle 4.95sec", "20:150|0:150|70:150|0:150|0:500|60:150|0:150|40:150|0:150|0:500|30:150|0:150|80:150|0:150|0:500|100:150|0:150|20:150|0:150|0:500|30:150|0:150|40:150|0:150|0:500|90:150|0:150|80:150|0:150|0:500|20:150|0:150|10:150|0:150|0:500|90:150|0:150|80:150|0:150|0:0"));
}
public List<Pattern> GetAllPatterns() {
return this.BuiltinPatterns.Concat(this.CustomPatterns).ToList();
}
public List<Pattern> GetBuiltinPatterns() {
return this.BuiltinPatterns;
}
/** Returns a copy of the list to avoid error if any modification happens */
public List<Pattern> GetCustomPatterns() {
List<Pattern> newList = new();
foreach(Pattern pattern in this.CustomPatterns) {
newList.Add(pattern);
}
return newList;
}
public void SetCustomPatterns(List<Pattern> customPatterns) {
this.CustomPatterns = customPatterns;
}
public Pattern GetPatternById(int index) {
return this.GetAllPatterns()[index];
}
public void AddBuiltinPattern(Pattern pattern) {
BuiltinPatterns.Add(pattern);
}
public void AddCustomPattern(Pattern pattern) {
Pattern? foundPattern = CustomPatterns.FirstOrDefault<Pattern>(p => p.Name == pattern.Name );
if(foundPattern != null) {
foundPattern.Name = pattern.Name;
foundPattern.Value = pattern.Value;
} else {
CustomPatterns.Add(pattern);
}
}
public bool RemoveCustomPattern(Pattern pattern) {
int index = CustomPatterns.IndexOf(pattern);
if(index > -1) {
this.CustomPatterns.RemoveAt(index);
return true;
}
return false;
}
}
public class Pattern {
public int Index = -1;
public string Name = "pattern";
public string Value = "10:1000";
public Pattern(string name="pattern", string value="10:1000") {
this.Name = name;
this.Value = value;
}
}
}

View File

@ -0,0 +1,130 @@
using System;
using System.Collections.Generic;
namespace FFXIV_Vibe_Plugin.Commons {
internal class Structures {
public enum ActionEffectType : byte {
Any = 0,
Miss = 1,
FullResist = 2,
Damage = 3,
Heal = 4,
BlockedDamage = 5,
ParriedDamage = 6,
Invulnerable = 7,
NoEffectText = 8,
Unknown_0 = 9,
MpLoss = 10,
MpGain = 11,
TpLoss = 12,
TpGain = 13,
GpGain = 14,
ApplyStatusEffectTarget = 15,
ApplyStatusEffectSource = 16,
StatusNoEffect = 20,
Taunt = 24,
StartActionCombo = 27,
ComboSucceed = 28,
Knockback = 33,
Mount = 40,
MountJapaneseVersion = 240,
VFX = 59,
Transport = 60,
};
// Unused, should be usefull for HookActionEffects but don't know where this field is.
public enum DamageType {
Unknown = 0,
Slashing = 1,
Piercing = 2,
Blunt = 3,
Magic = 5,
Darkness = 6,
Physical = 7,
LimitBreak = 8,
}
/**
* Still testing: https://github.com/SapphireServer/Sapphire/blob/master/src/common/Network/PacketDef/Zone/ClientZoneDef.h#L73
*/
public struct EffectEntry
{
public ActionEffectType type = ActionEffectType.Any;
public byte param0 = 0;
public byte param1 = 0;
public byte param2 = 0;
public byte mult = 0;
public byte flags = 0;
public ushort value = 0;
public EffectEntry(ActionEffectType type, byte param0, byte param1, byte param2, byte mult, byte flags, ushort value) {
this.type = type;
this.param0 = param0;
this.param1 = param1;
this.param2 = param2;
this.mult = mult;
this.flags = flags;
this.value = value;
}
public override string ToString() {
return $"type: {this.type}, p0: {param0}, p1: {param1}, p2: {param2}, mult: {mult}, flags: {flags} | {Convert.ToString(flags, 2)}, value: {value}";
}
}
public struct Player {
public int Id;
public string Name;
public string? Info;
public Player(int id, string name, string? info=null) {
this.Id = id;
this.Name = name;
this.Info = info;
}
public override string ToString() {
if(this.Info != null) {
return $"{Name}({Id}) [info:{this.Info}]";
}
return $"{Name}({Id})";
}
}
public struct Spell {
public int Id;
public string Name = "Undefined_Spell_Name";
public Player Player;
public int[]? Amounts;
public float AmountAverage;
public List<Player>? Targets;
public DamageType DamageType = 0;
public ActionEffectType ActionEffectType = 0;
public Spell(int id, string name, Player player, int[]? amounts, float amountAverage, List<Player>? targets, DamageType damageType, ActionEffectType actionEffectType) {
Id = id;
Name = name;
Player = player;
Amounts = amounts;
AmountAverage = amountAverage;
Targets = targets;
DamageType = damageType;
ActionEffectType = actionEffectType;
}
public override string ToString() {
string targetsString = "";
if(Targets != null) {
if(Targets.Count > 0) {
targetsString = String.Join(",", this.Targets);
} else {
targetsString = "*no target*";
}
}
return $"{Player} casts {Name}#{ActionEffectType} on: {targetsString}. Avg: {AmountAverage}";
}
}
}
}

View File

@ -0,0 +1,131 @@
using Dalamud.Configuration;
using Dalamud.Plugin;
using System;
using System.Collections.Generic;
using FFXIV_Vibe_Plugin.Triggers;
namespace FFXIV_Vibe_Plugin {
[Serializable]
public class Configuration : IPluginConfiguration {
public int Version { get; set; } = 0;
public string CurrentProfileName = "Default";
public List<ConfigurationProfile> Profiles = new();
/**
* TODO: 2022.01.12
* LEGACY from version 2.0.0. Changed to presets in 2.1.0.
* This was moved to presets. It should be remove one day */
public bool VERBOSE_SPELL = false;
public bool VERBOSE_CHAT = false;
public bool VIBE_HP_TOGGLE { get; set; } = false;
public int VIBE_HP_MODE { get; set; } = 0;
public int MAX_VIBE_THRESHOLD { get; set; } = 100;
public bool AUTO_CONNECT { get; set; } = true;
public bool AUTO_OPEN { get; set; } = false;
public List<Pattern> PatternList = new();
public string BUTTPLUG_SERVER_HOST { get; set; } = "127.0.0.1";
public int BUTTPLUG_SERVER_PORT { get; set; } = 12345;
public List<Triggers.Trigger> TRIGGERS { get; set; } = new();
public Dictionary<string, FFXIV_Vibe_Plugin.Device.Device> VISITED_DEVICES = new();
// the below exist just to make saving less cumbersome
[NonSerialized]
private DalamudPluginInterface? pluginInterface;
public void Initialize(DalamudPluginInterface pluginInterface) {
this.pluginInterface = pluginInterface;
}
public void Save() {
this.pluginInterface!.SavePluginConfig(this);
}
/**
* Get the profile specified by name.
*/
public ConfigurationProfile? GetProfile(String name="") {
if(name == "") {
name = this.CurrentProfileName;
}
ConfigurationProfile? profile = this.Profiles.Find(i => i.Name == name);
return profile;
}
public ConfigurationProfile GetDefaultProfile() {
String defaultProfileName = "Default profile";
ConfigurationProfile? profileToCheck = this.GetProfile(this.CurrentProfileName);
if(profileToCheck == null) {
profileToCheck = this.GetProfile(defaultProfileName);
}
ConfigurationProfile profileToReturn = profileToCheck ?? (new());
if(profileToCheck == null) {
profileToReturn.Name = defaultProfileName;
this.Profiles.Add(profileToReturn);
this.CurrentProfileName = defaultProfileName;
this.Save();
}
return profileToReturn;
}
public ConfigurationProfile? GetFirstProfile() {
ConfigurationProfile? profile = null;
if(profile == null && this.Profiles.Count > 0) {
profile = this.Profiles[0];
}
return profile;
}
public void RemoveProfile(String name) {
ConfigurationProfile? profile = this.GetProfile(name);
if(profile != null) {
this.Profiles.Remove(profile);
}
}
public bool AddProfile(String name) {
ConfigurationProfile? profile = GetProfile(name);
if(profile == null) {
profile = new();
profile.Name = name;
this.Profiles.Add(profile);
return true;
}
return false;
}
public bool SetCurrentProfile(String name) {
ConfigurationProfile? profile = this.GetProfile(name);
if(profile != null) {
this.CurrentProfileName = profile.Name;
return true;
}
return false;
}
}
public class ConfigurationProfile{
public string Name = "Default";
public bool VERBOSE_SPELL = false;
public bool VERBOSE_CHAT = false;
public bool VIBE_HP_TOGGLE { get; set; } = false;
public int VIBE_HP_MODE { get; set; } = 0;
public int MAX_VIBE_THRESHOLD { get; set; } = 100;
public bool AUTO_CONNECT { get; set; } = true;
public bool AUTO_OPEN { get; set; } = false;
public List<Pattern> PatternList = new();
public string BUTTPLUG_SERVER_HOST { get; set; } = "127.0.0.1";
public int BUTTPLUG_SERVER_PORT { get; set; } = 12345;
public List<Triggers.Trigger> TRIGGERS { get; set; } = new();
public Dictionary<string, FFXIV_Vibe_Plugin.Device.Device> VISITED_DEVICES = new();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

View File

@ -0,0 +1,193 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FFXIV_Vibe_Plugin.Commons;
using Buttplug;
namespace FFXIV_Vibe_Plugin.Device {
public enum UsableCommand {
Vibrate,
Rotate,
Linear,
Stop
}
public class Device {
private readonly ButtplugClientDevice? ButtplugClientDevice;
public int Id = -1;
public string Name = "UnsetDevice";
public bool CanVibrate = false;
public int VibrateMotors = -1;
public uint[] VibrateSteps = Array.Empty<uint>();
public bool CanRotate = false;
public int RotateMotors = -1;
public uint[] RotateSteps = Array.Empty<uint>();
public bool CanLinear = false;
public int LinearMotors = -1;
public uint[] LinearSteps = Array.Empty<uint>();
public bool CanBattery = false;
public bool CanStop = false;
public bool IsConnected = false;
public double BatteryLevel = -1;
// TODO: use that ?
public List<UsableCommand> UsableCommands = new();
public int[] CurrentVibrateIntensity = Array.Empty<int>();
public int[] CurrentRotateIntensity = Array.Empty<int>();
public int[] CurrentLinearIntensity = Array.Empty<int>();
public Device(ButtplugClientDevice buttplugClientDevice) {
if(buttplugClientDevice != null) {
this.ButtplugClientDevice = buttplugClientDevice;
Id = (int)buttplugClientDevice.Index;
Name = buttplugClientDevice.Name;
this.SetCommands();
this.ResetMotors();
this.UpdateBatteryLevel();
}
}
public override string ToString() {
List<string> commands = this.GetCommandsInfo();
return $"Device: {Id}:{Name} (connected={IsConnected}, battery={GetBatteryPercentage()}, commands={String.Join(",", commands)})";
}
private void SetCommands() {
if(this.ButtplugClientDevice == null) { return; }
foreach(var cmd in this.ButtplugClientDevice.AllowedMessages) {
if(cmd.Key == ServerMessage.Types.MessageAttributeType.VibrateCmd) {
this.CanVibrate = true;
this.VibrateMotors = (int)cmd.Value.FeatureCount;
this.VibrateSteps = cmd.Value.StepCount;
this.UsableCommands.Add(UsableCommand.Vibrate);
} else if(cmd.Key == ServerMessage.Types.MessageAttributeType.RotateCmd) {
this.CanRotate = true;
this.RotateMotors = (int)cmd.Value.FeatureCount;
this.RotateSteps = cmd.Value.StepCount;
this.UsableCommands.Add(UsableCommand.Rotate);
} else if(cmd.Key == ServerMessage.Types.MessageAttributeType.LinearCmd) {
this.CanLinear = true;
this.LinearMotors = (int)cmd.Value.FeatureCount;
this.LinearSteps = cmd.Value.StepCount;
this.UsableCommands.Add(UsableCommand.Linear);
} else if(cmd.Key == ServerMessage.Types.MessageAttributeType.BatteryLevelCmd) {
this.CanBattery = true;
} else if(cmd.Key == ServerMessage.Types.MessageAttributeType.StopDeviceCmd) {
this.CanStop = true;
this.UsableCommands.Add(UsableCommand.Stop);
}
}
}
/** Init all current motors intensity and default to zero */
private void ResetMotors() {
if(this.CanVibrate) {
this.CurrentVibrateIntensity = new int[this.VibrateMotors];
for(int i=0; i<this.VibrateMotors; i++) { this.CurrentVibrateIntensity[i] = 0; };
}
if(this.CanRotate) {
this.CurrentRotateIntensity = new int[this.RotateMotors];
for(int i = 0; i < this.RotateMotors; i++) { this.CurrentRotateIntensity[i] = 0; };
}
if(this.CanLinear) {
this.CurrentLinearIntensity = new int[this.LinearMotors];
for(int i = 0; i < this.LinearMotors; i++) { this.CurrentLinearIntensity[i] = 0; };
}
}
public List<UsableCommand> GetUsableCommands() {
return this.UsableCommands;
}
public List<String> GetCommandsInfo() {
List<string> commands = new();
if(CanVibrate) {
commands.Add($"vibrate motors={VibrateMotors} steps={String.Join(",", VibrateSteps)}");
}
if(CanRotate) {
commands.Add($"rotate motors={RotateMotors} steps={String.Join(",", RotateSteps)}");
}
if(CanLinear) {
commands.Add($"rotate motors={LinearMotors} steps={String.Join(",", LinearSteps)}");
}
if(CanBattery) commands.Add("battery");
if(CanStop) commands.Add("stop");
return commands;
}
public double UpdateBatteryLevel() {
if(this.ButtplugClientDevice == null) { return 0; }
if(!CanBattery) {return -1; }
Task<double> batteryLevelTask = this.ButtplugClientDevice.SendBatteryLevelCmd();
batteryLevelTask.Wait();
this.BatteryLevel = batteryLevelTask.Result;
return this.BatteryLevel;
}
public string GetBatteryPercentage() {
return $"{this.BatteryLevel*100}%";
}
public void Stop() {
if(this.ButtplugClientDevice == null) { return; }
if(CanVibrate) {
this.ButtplugClientDevice.SendVibrateCmd(0);
}
if(CanRotate) {
this.ButtplugClientDevice.SendRotateCmd(0f, true);
}
if(CanStop) {
this.ButtplugClientDevice.SendStopDeviceCmd();
}
ResetMotors();
}
public void SendVibrate(int intensity, int motorId=-1, int threshold=100) {
if(this.ButtplugClientDevice == null) return;
if(this.ButtplugClientDevice == null) { return; }
if(!CanVibrate || !IsConnected) return;
Dictionary<uint, double> motorIntensity = new();
for(int i=0; i < this.VibrateMotors; i++) {
if(motorId == -1 || motorId == i) {
this.CurrentVibrateIntensity[i] = intensity;
motorIntensity.Add((uint)i, Helpers.ClampIntensity(intensity, threshold) / 100.0);
}
}
this.ButtplugClientDevice.SendVibrateCmd(motorIntensity);
}
public void SendRotate(int intensity, bool clockWise=true, int motorId=-1, int threshold = 100) {
if(this.ButtplugClientDevice == null) return;
if(!CanRotate || !IsConnected) return;
Dictionary<uint, (double, bool)> motorIntensity = new();
for(int i = 0; i < this.RotateMotors; i++) {
if(motorId == -1 || motorId == i) {
this.CurrentRotateIntensity[i] = intensity;
(double, bool) values = (Helpers.ClampIntensity(intensity, threshold) / 100.0, clockWise);
motorIntensity.Add((uint)i, values);
}
}
this.ButtplugClientDevice.SendRotateCmd(motorIntensity);
}
public void SendLinear(int intensity, int duration=500, int motorId = -1, int threshold = 100) {
if(this.ButtplugClientDevice == null) return;
if(!CanLinear || !IsConnected) return;
Dictionary<uint, (uint, double)> motorIntensity = new();
for(int i = 0; i < this.LinearMotors; i++) {
if(motorId == -1 || motorId == i) {
this.CurrentLinearIntensity[i] = intensity;
(uint, double) values = ((uint)duration, Helpers.ClampIntensity(intensity, threshold) / 100.0);
motorIntensity.Add((uint)i, values);
}
}
this.ButtplugClientDevice.SendLinearCmd(motorIntensity);
}
}
}

View File

@ -0,0 +1,435 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
#region FFXIV_Vibe_Plugin deps
using FFXIV_Vibe_Plugin.Commons;
#endregion
#region Other deps
using Buttplug;
#endregion
namespace FFXIV_Vibe_Plugin.Device {
internal class DevicesController {
private readonly Logger Logger;
private readonly Configuration Configuration;
private ConfigurationProfile Profile;
private readonly Patterns Patterns;
private Triggers.Trigger? CurrentPlayingTrigger;
/**
* State of the current device and motor when it started to play as a unix timestamp.
* This is used to detect if a thread that runs a pattern should stop
*/
private readonly Dictionary<string, int> CurrentDeviceAndMotorPlaying = new();
// Buttplug related
private ButtplugClient? BPClient;
private readonly List<Device> Devices = new();
private bool isScanning = false;
// Internal variables
private readonly static Mutex mut = new();
public DevicesController(Logger logger, Configuration configuration, ConfigurationProfile profile, Patterns patterns) {
this.Logger = logger;
this.Configuration = configuration;
this.Profile = profile;
this.Patterns = patterns;
}
public void Dispose() {
this.Disconnect();
}
public void SetProfile(ConfigurationProfile profile) {
this.Profile = profile;
}
public void Connect(String host, int port) {
if(this.IsConnected()) {
this.Logger.Debug("Disconnecting previous instance! Waiting 2sec...");
this.Disconnect();
Thread.Sleep(200);
}
try {
this.BPClient = new("bp-dalamud");
} catch(Exception e) {
this.Logger.Error($"Can't load bp.", e);
return;
}
this.BPClient.ServerDisconnect += BPClient_ServerDisconnected;
this.BPClient.DeviceAdded += BPClient_DeviceAdded;
this.BPClient.DeviceRemoved += BPClient_DeviceRemoved;
this.BPClient.ScanningFinished += BPClient_OnScanComplete;
string hostandport = host + ":" + port.ToString();
try {
var uri = new Uri($"ws://{hostandport}/buttplug");
var connector = new ButtplugWebsocketConnectorOptions(uri);
this.Logger.Log($"Connecting to {hostandport}.");
Task task = this.BPClient.ConnectAsync(connector);
task.Wait();
this.ScanDevice();
} catch(Exception e) {
this.Logger.Error($"Could not connect to {hostandport}.", e);
}
Thread.Sleep(200);
if(this.BPClient.Connected) {
this.Logger.Log($"FVP connected to Intiface!");
} else {
this.Logger.Error("Failed connecting (Intiface server is up?)");
return;
}
}
private void BPClient_ServerDisconnected(object? sender, EventArgs e) {
this.Logger.Debug("Server disconnected");
this.Disconnect();
}
public bool IsConnected() {
bool isConnected = false;
if(this.BPClient != null) {
isConnected = this.BPClient.Connected;
}
return isConnected;
}
public void ScanDevice() {
if(this.BPClient == null) { return; }
this.Logger.Debug("Scanning for devices...");
if(this.IsConnected()) {
try {
this.isScanning = true;
var task = this.BPClient.StartScanningAsync();
task.Wait();
} catch(Exception e) {
this.isScanning = false;
this.Logger.Error("Scanning issue. No 'Device Comm Managers' enabled on Intiface?");
this.Logger.Error(e.Message);
}
}
}
public bool IsScanning() {
return this.isScanning;
}
public void StopScanningDevice() {
if(this.BPClient != null && this.IsConnected()) {
try {
Task task = this.BPClient.StopScanningAsync();
task.Wait();
} catch(Exception) {
this.Logger.Debug("StopScanningDevice ignored: already stopped");
}
}
this.isScanning = false;
}
private void BPClient_OnScanComplete(object? sender, EventArgs e) {
this.Logger.Debug("Stop scanning...");
// FIXME: this is not working, bp client emit the trigger instantly. Let's ignore for the moment.
// this.isScanning = false;
}
private void BPClient_DeviceAdded(object? sender, DeviceAddedEventArgs arg) {
try {
mut.WaitOne();
ButtplugClientDevice BPClientDevice = arg.Device;
Device device = new(BPClientDevice);
device.IsConnected = true;
this.Logger.Log($"{arg.Device.Name}, {BPClientDevice.Name}");
this.Devices.Add(device);
if(!this.Profile.VISITED_DEVICES.ContainsKey(device.Name)) {
this.Profile.VISITED_DEVICES[device.Name] = device;
this.Configuration.Save();
this.Logger.Debug($"Adding device to visited list {device})");
}
this.Logger.Debug($"Added {device})");
} finally {
mut.ReleaseMutex();
}
}
private void BPClient_DeviceRemoved(object? sender, DeviceRemovedEventArgs e) {
try {
mut.WaitOne();
int index = this.Devices.FindIndex(device => device.Id == e.Device.Index);
if(index > -1) {
this.Logger.Debug($"Removed {Devices[index]}");
Device device = Devices[index];
this.Devices.RemoveAt(index);
device.IsConnected = false;
}
} finally {
mut.ReleaseMutex();
}
}
public void Disconnect() {
this.Devices.Clear();
if(this.BPClient == null || !this.IsConnected()) {
return;
}
try {
if(this.BPClient.IsScanning) {
var task = this.BPClient.StopScanningAsync();
task.Wait();
}
} catch(Exception e) {
this.Logger.Error("Couldn't stop scanning device... Unknown reason.");
this.Logger.Error(e.Message);
}
try {
for(int i = 0; i < this.BPClient.Devices.Length; i++) {
this.Logger.Log($"Disconnecting device {i} {this.BPClient.Devices[i].Name}");
this.BPClient.Devices[i].Dispose();
}
} catch(Exception e) {
this.Logger.Error("Error while disconnecting device", e);
}
try {
Thread.Sleep(1000);
if(this.BPClient != null) {
this.BPClient.DisconnectAsync();
this.Logger.Log("Disconnecting! Bye... Waiting 2sec...");
}
} catch(Exception e) {
// ignore exception, we are trying to do our best
this.Logger.Error("Error while disconnecting client", e);
}
this.BPClient = null;
}
public List<Device> GetDevices() {
return this.Devices;
}
public Dictionary<String, Device> GetVisitedDevices() {
return this.Profile.VISITED_DEVICES;
}
public void UpdateAllBatteryLevel() {
foreach(Device device in this.GetDevices()) {
device.UpdateBatteryLevel();
}
}
public void StopAll() {
foreach(Device device in this.GetDevices()) {
device.Stop();
}
}
public void SendTrigger(Triggers.Trigger trigger, int threshold=100) {
if(!this.IsConnected()) {
this.Logger.Debug($"Not connected, cannot send ${trigger}");
return;
}
this.Logger.Debug($"Sending trigger {trigger} (priority={trigger.Priority})");
// Check if the trigger has the priority
if(this.CurrentPlayingTrigger == null) {
this.CurrentPlayingTrigger = trigger;
}
if(trigger.Priority < this.CurrentPlayingTrigger.Priority) {
this.Logger.Debug($"Ignoring trigger because lower priority => {trigger} < {this.CurrentPlayingTrigger}");
return;
}
this.CurrentPlayingTrigger = trigger;
foreach(Triggers.TriggerDevice triggerDevice in trigger.Devices) {
Device? device = this.FindDevice(triggerDevice.Name);
if(device != null && triggerDevice != null) {
if(triggerDevice.ShouldVibrate) {
for(int motorId = 0; motorId < triggerDevice.VibrateSelectedMotors?.Length; motorId++) {
if(triggerDevice.VibrateSelectedMotors != null && triggerDevice.VibrateMotorsThreshold != null) {
bool motorEnabled = triggerDevice.VibrateSelectedMotors[motorId];
int motorThreshold = triggerDevice.VibrateMotorsThreshold[motorId] * threshold / 100;
int motorPatternId = triggerDevice.VibrateMotorsPattern[motorId];
float startAfter = trigger.StartAfter;
float stopAfter = trigger.StopAfter;
if(motorEnabled) {
this.Logger.Debug($"Sending {device.Name} vibration to motor: {motorId} patternId={motorPatternId} with threshold: {motorThreshold}!");
this.SendPattern("vibrate", device, motorThreshold, motorId, motorPatternId, startAfter, stopAfter);
}
}
}
}
if(triggerDevice.ShouldRotate) {
for(int motorId = 0; motorId < triggerDevice.RotateSelectedMotors?.Length; motorId++) {
if(triggerDevice.RotateSelectedMotors != null && triggerDevice.RotateMotorsThreshold != null) {
bool motorEnabled = triggerDevice.RotateSelectedMotors[motorId];
int motorThreshold = triggerDevice.RotateMotorsThreshold[motorId] * threshold / 100;
int motorPatternId = triggerDevice.RotateMotorsPattern[motorId];
float startAfter = trigger.StartAfter;
float stopAfter = trigger.StopAfter;
if(motorEnabled) {
this.Logger.Debug($"Sending {device.Name} rotation to motor: {motorId} patternId={motorPatternId} with threshold: {motorThreshold}!");
this.SendPattern("rotate", device, motorThreshold, motorId, motorPatternId, startAfter, stopAfter);
}
}
}
}
if(triggerDevice.ShouldLinear) {
for(int motorId = 0; motorId < triggerDevice.LinearSelectedMotors?.Length; motorId++) {
if(triggerDevice.LinearSelectedMotors != null && triggerDevice.LinearMotorsThreshold != null) {
bool motorEnabled = triggerDevice.LinearSelectedMotors[motorId];
int motorThreshold = triggerDevice.LinearMotorsThreshold[motorId] * threshold / 100;
int motorPatternId = triggerDevice.LinearMotorsPattern[motorId];
float startAfter = trigger.StartAfter;
float stopAfter = trigger.StopAfter;
if(motorEnabled) {
this.Logger.Debug($"Sending {device.Name} linear to motor: {motorId} patternId={motorPatternId} with threshold: {motorThreshold}!");
this.SendPattern("linear", device, motorThreshold, motorId, motorPatternId, startAfter, stopAfter);
}
}
}
}
if(triggerDevice.ShouldStop) {
this.Logger.Debug($"Sending stop to {device.Name}!");
DevicesController.SendStop(device);
}
}
}
}
/** Search for a device with the corresponding text */
public Device? FindDevice(string text) {
Device? foundDevice = null;
foreach(Device device in this.Devices) {
if(device.Name.Contains(text) && device != null) {
foundDevice = device;
}
}
return foundDevice;
}
/**
* Sends an itensity vibe to all of the devices
* @param {float} intensity
*/
public void SendVibeToAll(int intensity) {
if(this.IsConnected() && this.BPClient != null) {
foreach(Device device in this.Devices) {
device.SendVibrate(intensity, -1, this.Profile.MAX_VIBE_THRESHOLD);
device.SendRotate(intensity, true, -1, this.Profile.MAX_VIBE_THRESHOLD);
device.SendLinear(intensity, 500, -1, this.Profile.MAX_VIBE_THRESHOLD);
}
}
}
public void SendPattern(string command, Device device, int threshold, int motorId = -1, int patternId = 0, float StartAfter = 0, float StopAfter = 0) {
this.SaveCurrentMotorAndDevicePlayingState(device, motorId);
Pattern pattern = Patterns.GetPatternById(patternId);
string[] patternSegments = pattern.Value.Split("|");
this.Logger.Log($"SendPattern '{command}' pattern={pattern.Name} ({patternSegments.Length} segments) to {device} motor={motorId} startAfter={StartAfter} stopAfter={StopAfter} threshold={threshold}");
string deviceAndMotorId = $"{device.Name}:{motorId}";
int startedUnixTime = this.CurrentDeviceAndMotorPlaying[deviceAndMotorId];
// Make sure things stops if StopAfter is set by sending a zero.
// We make sure to send the zero to the correct device and if it is still running.
bool forceStop = false;
Thread tStopAfter = new(delegate () {
if(StopAfter == 0) { return; }
Thread.Sleep((int)StopAfter * 1000);
if(startedUnixTime == this.CurrentDeviceAndMotorPlaying[deviceAndMotorId]) {
forceStop = true;
this.SendCommand(command, device, 0, motorId);
this.Logger.Debug($"Force stopping {deviceAndMotorId} because of StopAfter={StopAfter}");
}
});
tStopAfter.Start();
Thread t = new(delegate () {
Thread.Sleep((int)StartAfter * 1000);
// Stop exectution if a new pattern is sent to the same device and motor.
if(startedUnixTime != this.CurrentDeviceAndMotorPlaying[deviceAndMotorId]) {
return;
}
// Experimental send a fake command to activate connection
this.SendCommand(command, device, 0, motorId);
Thread.Sleep(50); // Yield if necessary
for(int segIndex = 0; segIndex < patternSegments.Length; segIndex++) {
// Stop exectution if a new pattern is send to the same device and motor.
if(startedUnixTime != this.CurrentDeviceAndMotorPlaying[deviceAndMotorId]) {
break;
}
string patternSegment = patternSegments[segIndex];
string[] patternValues = patternSegment.Split(":");
int intensity = Helpers.ClampIntensity(Int32.Parse(patternValues[0]), threshold);
int duration = Int32.Parse(patternValues[1]);
//this.Logger.Debug($"SENDING SEGMENT: intensity={intensity} duration={duration}");
// Stop after and send 0 intensity
if(forceStop || (StopAfter > 0 && StopAfter * 1000 + startedUnixTime < Helpers.GetUnix())) {
this.SendCommand(command, device, 0, motorId, duration);
break;
}
// Send the command \o/
this.SendCommand(command, device, intensity, motorId, duration);
Thread.Sleep(duration);
}
// Make sure we clean the current playing trigger.
this.CurrentPlayingTrigger = null;
});
t.Start();
}
public void SendCommand(string command, Device device, int intensity, int motorId, int duration=500) {
if(command == "vibrate") {
this.SendVibrate(device, intensity, motorId);
} else if(command == "rotate") {
this.SendRotate(device, intensity, motorId);
} else if(command == "linear") {
this.SendLinear(device, intensity, motorId, duration);
}
}
public void SendVibrate(Device device, int intensity, int motorId = -1) {
device.SendVibrate(intensity, motorId, this.Profile.MAX_VIBE_THRESHOLD);
}
public void SendRotate(Device device, int intensity, int motorId = -1, bool clockwise = true) {
device.SendRotate(intensity, clockwise, motorId, this.Profile.MAX_VIBE_THRESHOLD);
}
public void SendLinear(Device device, int intensity, int motorId = -1, int duration = 500) {
device.SendLinear(intensity, duration, motorId, this.Profile.MAX_VIBE_THRESHOLD);
}
public static void SendStop(Device device) {
device.Stop();
}
private void SaveCurrentMotorAndDevicePlayingState(Device device, int motorId) {
string deviceAndMotorId = $"{device.Name}:{motorId}";
this.CurrentDeviceAndMotorPlaying[deviceAndMotorId] = Helpers.GetUnix();
}
}
}

View File

@ -0,0 +1,86 @@
using System;
using Dalamud.Game.Network;
using FFXIV_Vibe_Plugin.Commons;
namespace FFXIV_Vibe_Plugin.Experimental {
internal class NetworkCapture {
private readonly Logger Logger;
// NetworkCapture experiment
private readonly GameNetwork? GameNetwork;
private bool ExperimentalNetworkCaptureStarted = false;
/** Constructor */
public NetworkCapture(Logger logger, GameNetwork gameNetwork) {
this.Logger = logger;
this.GameNetwork = gameNetwork;
}
/** Dispose all experiments */
public void Dispose() {
this.StopNetworkCapture();
}
/** Monitor the network and caputre some information */
public void StartNetworkCapture() {
/*
this.Logger.Debug("STARTING EXPERIMENTAL");
this.ExperimentalNetworkCaptureStarted = true;
if(this.GameNetwork != null) {
this.GameNetwork.Enable();
this.GameNetwork.NetworkMessage += this.OnNetworkReceived;
}*/
}
/** Stops the network capture experiment. */
public void StopNetworkCapture() {
if(!this.ExperimentalNetworkCaptureStarted) { return; }
this.Logger.Debug("STOPPING EXPERIMENTAL");
if(this.GameNetwork != null) {
this.GameNetwork.NetworkMessage -= this.OnNetworkReceived;
}
this.ExperimentalNetworkCaptureStarted = false;
}
/**
* Analyze the network message when received.
* 1. We get the opCode
* 2. We could retrieve the name of the OpCode using our Common.OpCodes
* 3. If it is a ClientTrigger OpCode, we get the correct bytes using Sapphire structs
* 4. By analyzing a bit the behavior in the game, we can clearly see that the "param11"
* is going from 0 to 1 when the weapon is drawn.
*/
unsafe private void OnNetworkReceived(IntPtr dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction) {
int vOut = Convert.ToInt32(opCode);
string? name = OpCodes.GetName(opCode);
uint actionId = 111111111;
if(direction == NetworkMessageDirection.ZoneUp) {
actionId = *(uint*)(dataPtr + 0x4);
}
this.Logger.Log($"Hex: {vOut:X} Decimal: {opCode} ActionId: {actionId} SOURCE_ID: {sourceActorId} TARGET_ID: {targetActorId} DIRECTION: {direction} DATA_PTR: {dataPtr} NAME: {name}");
if(name == "ClientZoneIpcType-ClientTrigger") {
UInt16 commandId = *(UInt16*)(dataPtr);
byte unk_1 = *(byte*)(dataPtr + 0x2);
byte unk_2 = *(byte*)(dataPtr + 0x3);
uint param11 = *(uint*)(dataPtr + 0x4);
uint param12 = *(uint*)(dataPtr + 0x8);
uint param2 = *(uint*)(dataPtr + 0xC);
uint param4 = *(uint*)(dataPtr + 0x10);
uint param5 = *(uint*)(dataPtr + 0x14);
ulong param3 = *(ulong*)(dataPtr + 0x18);
string extra = "";
if(param11 == 0) {
extra += "WeaponIn";
} else if(param11 == 1) {
extra += "WeaponOut";
}
this.Logger.Log($"{name} {direction} {extra} {commandId} {unk_1} {unk_2} {param11} {param12} {param2} {param2} {param4} {param5} {param3}");
}
}
}
}

View File

@ -0,0 +1,229 @@
using System;
using System.Collections.Generic;
#region Dalamud deps
using Dalamud.Game;
using Dalamud.Hooking;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Data;
#endregion
#region FFXIV_Vibe_Plugin deps
using FFXIV_Vibe_Plugin.Commons;
#endregion
namespace FFXIV_Vibe_Plugin.Hooks {
internal class ActionEffect {
// Constructor params
private readonly DataManager? DataManager;
private readonly Logger Logger;
private readonly SigScanner Scanner;
private readonly ClientState ClientState;
private readonly ObjectTable GameObjects;
// Lumina excel sheet for actions.
private readonly Lumina.Excel.ExcelSheet<Lumina.Excel.GeneratedSheets.Action>? LuminaActionSheet;
// Hooks
private delegate void HOOK_ReceiveActionEffectDelegate(int sourceId, IntPtr sourceCharacter, IntPtr pos, IntPtr effectHeader, IntPtr effectArray, IntPtr effectTrail);
private Hook<HOOK_ReceiveActionEffectDelegate>? receiveActionEffectHook;
// Event to dispatch.
public event EventHandler<HookActionEffects_ReceivedEventArgs>? ReceivedEvent;
// Constructor
public ActionEffect(DataManager dataManager, Logger logger, SigScanner scanner, ClientState clientState, ObjectTable gameObjects) {
this.DataManager = dataManager;
this.Logger = logger;
this.Scanner = scanner;
this.ClientState = clientState;
this.GameObjects = gameObjects;
this.InitHook();
if(DataManager != null) {
this.LuminaActionSheet = DataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets.Action>();
}
}
/** Dispose the hook and disable it */
public void Dispose() {
receiveActionEffectHook?.Disable();
receiveActionEffectHook?.Dispose();
}
private void InitHook() {
try {
// Found on: https://github.com/lmcintyre/DamageInfoPlugin/blob/main/DamageInfoPlugin/DamageInfoPlugin.cs#L133
IntPtr receiveActionEffectFuncPtr = this.Scanner.ScanText("4C 89 44 24 ?? 55 56 41 54 41 55 41 56");
receiveActionEffectHook = new Hook<HOOK_ReceiveActionEffectDelegate>(receiveActionEffectFuncPtr, ReceiveActionEffect);
}
catch (Exception e) {
this.Dispose();
this.Logger.Warn($"Encountered an error loading HookActionEffect: {e.Message}. Disabling it...");
throw;
}
receiveActionEffectHook.Enable();
this.Logger.Log("HookActionEffect was correctly enabled!");
}
unsafe private void ReceiveActionEffect(int sourceId, IntPtr sourceCharacter, IntPtr pos, IntPtr effectHeader, IntPtr effectArray, IntPtr effectTrail) {
Structures.Spell spell = new();
try {
// Get data structure
uint ptr_id = *((uint*)effectHeader.ToPointer() + 0x2);
uint ptr_animId = *((ushort*)effectHeader.ToPointer() + 0xE);
ushort ptr_op = *((ushort*)effectHeader.ToPointer() - 0x7);
byte ptr_targetCount = *(byte*)(effectHeader + 0x21);
Structures.EffectEntry effect = *(Structures.EffectEntry*)(effectArray);
// Get more info from data structure
string playerName = GetCharacterNameFromSourceId(sourceId);
String spellName = this.GetSpellName(ptr_id, true);
int[] amounts = this.GetAmounts(ptr_targetCount, effectArray);
float amountAverage = ComputeAverageAmount(amounts);
List<Structures.Player> targets = this.GetAllTarget(ptr_targetCount, effectTrail, amounts);
// Spell definition
spell.Id = (int)ptr_id;
spell.Name = spellName;
spell.Player = new Structures.Player(sourceId, playerName);
spell.Amounts = amounts;
spell.AmountAverage = amountAverage;
spell.Targets = targets;
spell.DamageType = Structures.DamageType.Unknown;
// WARNING: if there is no target, some information will be wrong !
// It is needed to avoid effect type if there is no target.
if(targets.Count == 0) {
spell.ActionEffectType = Structures.ActionEffectType.Any;
} else {
spell.ActionEffectType = effect.type;
}
this.DispatchReceivedEvent(spell);
} catch(Exception e) {
this.Logger.Log($"{e.Message} {e.StackTrace}");
}
this.RestoreOriginalHook(sourceId, sourceCharacter, pos, effectHeader, effectArray, effectTrail);
}
private void RestoreOriginalHook(int sourceId, IntPtr sourceCharacter, IntPtr pos, IntPtr effectHeader, IntPtr effectArray, IntPtr effectTrail) {
if(receiveActionEffectHook != null) {
receiveActionEffectHook.Original(sourceId, sourceCharacter, pos, effectHeader, effectArray, effectTrail);
}
}
unsafe private int[] GetAmounts(byte count, IntPtr effectArray) {
int[] RESULT = new int[count];
int targetCount = (int)count;
int effectsEntries = 0;
// The packet size depends on the number of target.
if(targetCount == 0) {
effectsEntries = 0;
} else if(targetCount == 1) {
effectsEntries = 8;
} else if(targetCount <= 8) {
effectsEntries = 64;
} else if(targetCount <= 16) {
effectsEntries = 128;
} else if(targetCount <= 24) {
effectsEntries = 192;
} else if(targetCount <= 32) {
effectsEntries = 256;
}
// Creates a list of EffectEntry (the base binary structure of the effect).
List<Structures.EffectEntry> entries = new(effectsEntries);
for(int i = 0; i < effectsEntries; i++) {
entries.Add(*(Structures.EffectEntry*)(effectArray + i * 8));
}
// Sum all the damage.
int counterValueFound = 0;
for(int i = 0; i < entries.Count; i++) {
// DEBUG: Logger.Debug(entries[i].ToString());
if(i % 8 == 0) { // Value of dmg is located every 8
uint tDmg = entries[i].value;
if(entries[i].mult != 0) {
tDmg += ((uint)ushort.MaxValue + 1) * entries[i].mult;
}
// We add the value of the damage that we found.
if(counterValueFound < count) {
RESULT[counterValueFound] = (int)tDmg;
}
counterValueFound++;
}
}
return RESULT;
}
private static int ComputeAverageAmount(int[] amounts) {
var result = 0;
for(int i=0; i < amounts.Length; i++) {
result += amounts[i];
}
result = result != 0 ? result / amounts.Length : result;
return result;
}
unsafe private List<Structures.Player> GetAllTarget(byte count, IntPtr effectTrail, int[] amounts) {
List<Structures.Player> names = new();
if((int)count >= 1) {
ulong[] targets = new ulong[(int)count];
for(int i=0; i < count; i++) {
targets[i] = *(ulong*)(effectTrail + i * 8);
var targetId = (int)targets[i];
var targetName = this.GetCharacterNameFromSourceId(targetId);
var targetPlayer = new Structures.Player(targetId, targetName, $"{amounts[i]}");
names.Add(targetPlayer);
}
}
return names;
}
private string GetSpellName(uint actionId, bool withId) {
if(this.LuminaActionSheet == null) {
this.Logger.Warn("HookActionEffect.GetSpellName: LuminaActionSheet is null");
return "***LUMINA ACTION SHEET NOT LOADED***";
}
var row = this.LuminaActionSheet.GetRow(actionId);
var spellName = "";
if(row != null) {
if(withId) {
spellName = $"{row.RowId}:";
}
if(row.Name != null) {
spellName += $"{row.Name}";
}
} else {
spellName = "!Unknown Spell Name!";
}
return spellName;
}
private string GetCharacterNameFromSourceId(int sourceId) {
var character = this.GameObjects.SearchById((uint)sourceId);
var characterName = "";
if(character != null) {
characterName = character.Name.TextValue;
}
return characterName;
}
protected virtual void DispatchReceivedEvent(Structures.Spell spell) {
HookActionEffects_ReceivedEventArgs args = new();
args.Spell = spell;
ReceivedEvent?.Invoke(this, args);
}
}
// EventArgs data HookActionEffects_ReceivedEventArgs the 'Received' event is triggers.
internal class HookActionEffects_ReceivedEventArgs : EventArgs {
public Structures.Spell Spell { get; set; }
}
}

View File

@ -0,0 +1,84 @@
using System;
using Dalamud.Game.ClientState;
using FFXIV_Vibe_Plugin.Commons;
namespace FFXIV_Vibe_Plugin {
internal class PlayerStats {
private readonly Logger Logger;
// EVENTS
public event EventHandler? Event_CurrentHpChanged;
public event EventHandler? Event_MaxHpChanged;
// Stats of the player
private float _CurrentHp, _prevCurrentHp = -1;
private float _MaxHp, _prevMaxHp = -1;
public string PlayerName = "*unknown*";
public PlayerStats( Logger logger, ClientState clientState) {
this.Logger = logger;
this.UpdatePlayerState(clientState);
}
public void Update(ClientState clientState) {
if(clientState == null || clientState.LocalPlayer == null) { return; }
this.UpdatePlayerState(clientState);
this.UpdatePlayerName(clientState);
this.UpdateCurrentHp(clientState);
}
public void UpdatePlayerState(ClientState clientState) {
if(clientState != null && clientState.LocalPlayer != null) {
if(this._CurrentHp == -1 || this._MaxHp == -1) {
this.Logger.Debug($"UpdatePlayerState {this._CurrentHp} {this._MaxHp}");
this._CurrentHp = this._prevCurrentHp = clientState.LocalPlayer.CurrentHp;
this._MaxHp = this._prevMaxHp = clientState.LocalPlayer.MaxHp;
this.Logger.Debug($"UpdatePlayerState {this._CurrentHp} {this._MaxHp}");
}
}
}
public string UpdatePlayerName(ClientState clientState) {
if(clientState != null && clientState.LocalPlayer != null) {
this.PlayerName = clientState.LocalPlayer.Name.TextValue;
}
return this.PlayerName;
}
public string GetPlayerName() {
return this.PlayerName;
}
private void UpdateCurrentHp(ClientState clientState) {
// Updating current values
if(clientState != null && clientState.LocalPlayer != null) {
this._CurrentHp = clientState.LocalPlayer.CurrentHp;
this._MaxHp = clientState.LocalPlayer.MaxHp;
}
// Send events after all value updated
if(this._CurrentHp != this._prevCurrentHp) {
Event_CurrentHpChanged?.Invoke(this, EventArgs.Empty);
}
if(this._MaxHp != this._prevMaxHp) {
Event_MaxHpChanged?.Invoke(this, EventArgs.Empty);
}
// Save previous values
this._prevCurrentHp = this._CurrentHp;
this._prevMaxHp = this._MaxHp;
}
/***** PUBLIC API ******/
public float GetCurrentHP() {
return this._CurrentHp;
}
public float GetMaxHP() {
return this._MaxHp;
}
}
}

View File

@ -0,0 +1,27 @@
using System;
namespace FFXIV_Vibe_Plugin.Triggers {
[Serializable]
public class ChatTrigger : IComparable {
public ChatTrigger(int intensity, string text) {
Intensity = intensity;
Text = text;
}
public int Intensity { get; }
public string Text { get; }
public override string ToString() {
return $"Trigger(intensity: {Intensity}, text: '{Text}')";
}
public string ToConfigString() {
return $"{Intensity} {Text}";
}
public int CompareTo(object? obj) {
int thatintensity = obj is ChatTrigger that ? that.Intensity : 0;
return this.Intensity.CompareTo(thatintensity);
}
}
}

View File

@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FFXIV_Vibe_Plugin.Device;
namespace FFXIV_Vibe_Plugin.Triggers {
enum KIND {
Chat,
Spell,
HPChange
}
enum DIRECTION {
Any,
Outgoing,
Incoming,
Self
}
public class Trigger : IComparable<Trigger> {
private static readonly int _initAmountMinValue = -1;
private static readonly int _initAmountMaxValue = 10000000;
// General
public bool Enabled = true;
public int SortOder = -1;
public readonly string Id = "";
public string Name = "";
public string Description = "";
public int Kind = (int)KIND.Chat;
public int ActionEffectType = (int)FFXIV_Vibe_Plugin.Commons.Structures.ActionEffectType.Any;
public int Direction = (int)DIRECTION.Any;
public string ChatText = "hello world";
public string SpellText = "";
public int AmountMinValue = Trigger._initAmountMinValue;
public int AmountMaxValue = Trigger._initAmountMaxValue;
public bool AmountInPercentage = false;
public string FromPlayerName = "";
public string ToPlayerName = "";
public float StartAfter = 0;
public float StopAfter = 0;
public int Priority = 0;
public readonly List<int> AllowedChatTypes = new ();
// Devices associated with this trigger
public List<TriggerDevice> Devices = new();
public Trigger(string name) {
this.Id = Guid.NewGuid().ToString();
this.Name = name;
}
public override string ToString() {
return $"Trigger(name={this.Name}, id={this.GetShortID()})";
}
public int CompareTo(Trigger? other) {
if(other == null) { return 1; }
if(this.SortOder < other.SortOder) {
return 1;
} else if(this.SortOder > other.SortOder) {
return -1;
} else {
return 0;
}
}
public string GetShortID() {
return this.Id[..5];
}
public void Reset() {
this.AmountMaxValue = Trigger._initAmountMaxValue;
this.AmountMinValue = Trigger._initAmountMinValue;
}
}
public class TriggerDevice {
public string Name = "";
public bool IsEnabled = false;
public bool ShouldVibrate = false;
public bool ShouldRotate = false;
public bool ShouldLinear = false;
public bool ShouldStop = false;
public Device.Device? Device;
// Vibrate states per motor
public bool[] VibrateSelectedMotors;
public int[] VibrateMotorsThreshold;
public int[] VibrateMotorsPattern;
// Rotate states per motor
public bool[] RotateSelectedMotors;
public int[] RotateMotorsThreshold;
public int[] RotateMotorsPattern;
// Linear states per motor
public bool[] LinearSelectedMotors;
public int[] LinearMotorsThreshold;
public int[] LinearMotorsPattern;
public TriggerDevice(Device.Device device) {
this.Name = device.Name;
this.Device = device;
// Init vibration array
this.VibrateSelectedMotors = new bool[device.CanVibrate ? device.VibrateMotors : 0];
this.VibrateMotorsThreshold = new int[device.CanVibrate ? device.VibrateMotors : 0];
this.VibrateMotorsPattern = new int[device.CanVibrate ? device.VibrateMotors : 0];
// Init rotate array
this.RotateSelectedMotors = new bool[device.CanRotate ? device.RotateMotors : 0];
this.RotateMotorsThreshold = new int[device.CanRotate ? device.RotateMotors : 0];
this.RotateMotorsPattern = new int[device.CanRotate ? device.RotateMotors : 0];
// Init linear array
this.LinearSelectedMotors = new bool[device.CanLinear ? device.LinearMotors : 0];
this.LinearMotorsThreshold = new int[device.CanLinear ? device.LinearMotors : 0];
this.LinearMotorsPattern = new int[device.CanLinear ? device.LinearMotors : 0];
}
public override string ToString() {
return $"TRIGGER_DEVICE {this.Name}";
}
}
}

View File

@ -0,0 +1,158 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Dalamud.Game.ClientState;
using Dalamud.Game.Text;
using FFXIV_Vibe_Plugin.Triggers;
using FFXIV_Vibe_Plugin.Commons;
using System.Text.RegularExpressions;
namespace FFXIV_Vibe_Plugin.Triggers {
internal class TriggersController {
private readonly Logger Logger;
private readonly PlayerStats PlayerStats;
private ConfigurationProfile Profile;
private List<Triggers.Trigger> Triggers = new();
public TriggersController(Logger logger, PlayerStats playerStats, ConfigurationProfile profile) {
this.Logger = logger;
this.PlayerStats = playerStats;
this.Profile = profile;
}
public void SetProfile(ConfigurationProfile profile) {
this.Profile = profile;
this.Triggers = profile.TRIGGERS;
}
public List<Triggers.Trigger> GetTriggers() {
return this.Triggers;
}
public void AddTrigger(Trigger trigger) {
this.Triggers.Add(trigger);
}
public void RemoveTrigger(Trigger trigger) {
this.Triggers.Remove(trigger);
}
public List<Trigger> CheckTrigger_Chat(XivChatType chatType, string ChatFromPlayerName, string ChatMsg) {
List<Trigger> triggers = new();
ChatFromPlayerName = ChatFromPlayerName.Trim().ToLower();
for(int triggerIndex = 0; triggerIndex < this.Triggers.Count; triggerIndex++) {
Trigger trigger = this.Triggers[triggerIndex];
// Ignore if not enabled
if(!trigger.Enabled) { continue; }
// Ignore if the player name is not authorized when chat type is not "Echo"
if(chatType != XivChatType.Echo) {
if(!Helpers.RegExpMatch(this.Logger, ChatFromPlayerName, trigger.FromPlayerName)) { continue; }
if(trigger.AllowedChatTypes.Count > 0 && !trigger.AllowedChatTypes.Any(ct => ct == (int)chatType)) {
continue;
}
}
// Check if the KIND of the trigger is a chat and if it matches
if(trigger.Kind == (int)KIND.Chat) {
if(Helpers.RegExpMatch(this.Logger, ChatMsg, trigger.ChatText)){
if(this.Profile.VERBOSE_CHAT) {
this.Logger.Debug($"ChatTrigger matched {trigger.ChatText}<>{ChatMsg}, adding {trigger}");
}
triggers.Add(trigger);
}
}
}
return triggers;
}
public List<Trigger> CheckTrigger_Spell(Structures.Spell spell) {
List<Trigger> triggers = new();
string spellName = spell.Name != null ? spell.Name.Trim() : "";
for(int triggerIndex = 0; triggerIndex < this.Triggers.Count; triggerIndex++) {
Trigger trigger = this.Triggers[triggerIndex];
// Ignore if not enabled
if(!trigger.Enabled) { continue; }
// Ignore if the player name is not authorized
if(!Helpers.RegExpMatch(this.Logger, spell.Player.Name, trigger.FromPlayerName)) { continue; }
if(trigger.Kind == (int)KIND.Spell) {
if(!Helpers.RegExpMatch(this.Logger, spellName, trigger.SpellText)) { continue; }
if(trigger.ActionEffectType != (int)Structures.ActionEffectType.Any && trigger.ActionEffectType != (int)spell.ActionEffectType) {
continue;
}
if(trigger.ActionEffectType == (int)Structures.ActionEffectType.Damage || trigger.ActionEffectType == (int)Structures.ActionEffectType.Heal) {
if(trigger.AmountMinValue >= spell.AmountAverage) { continue; }
if(trigger.AmountMaxValue <= spell.AmountAverage) { continue; }
}
FFXIV_Vibe_Plugin.Triggers.DIRECTION direction = this.GetSpellDirection(spell);
if(trigger.Direction != (int)FFXIV_Vibe_Plugin.Triggers.DIRECTION.Any && (int)direction != trigger.Direction) { continue;}
if(this.Profile.VERBOSE_SPELL) {
this.Logger.Debug($"SpellTrigger matched {spell}, adding {trigger}");
}
triggers.Add(trigger);
}
}
return triggers;
}
public List<Trigger> CheckTrigger_HPChanged(int currentHP, float percentageHP) {
List<Trigger> triggers = new();
for(int triggerIndex = 0; triggerIndex < this.Triggers.Count; triggerIndex++) {
Trigger trigger = this.Triggers[triggerIndex];
// Ignore if not enabled
if(!trigger.Enabled) { continue; }
// Check if the amount is in percentage
if (trigger.AmountInPercentage) {
if (percentageHP < trigger.AmountMinValue) { continue; }
if (percentageHP > trigger.AmountMaxValue) { continue; }
this.Logger.Debug($"{percentageHP}, {trigger.AmountMinValue}, {trigger.AmountMaxValue}");
}
// If the amount is not in percentage check the amount value
else {
if (trigger.AmountMinValue >= currentHP) { continue; }
if (trigger.AmountMaxValue <= currentHP) { continue; }
}
if(trigger.Kind == (int)KIND.HPChange) {
triggers.Add(trigger);
}
}
return triggers;
}
public FFXIV_Vibe_Plugin.Triggers.DIRECTION GetSpellDirection(Structures.Spell spell) {
string myName = this.PlayerStats.GetPlayerName();
List<Structures.Player> targets = new();
if(spell.Targets != null) {
targets = spell.Targets;
}
if(targets.Count >= 1 && targets[0].Name != myName) {
return FFXIV_Vibe_Plugin.Triggers.DIRECTION.Outgoing;
}
if(spell.Player.Name != myName) {
return FFXIV_Vibe_Plugin.Triggers.DIRECTION.Incoming;
}
return FFXIV_Vibe_Plugin.Triggers.DIRECTION.Self;
}
}
}

View File

@ -0,0 +1,26 @@
using System;
using Dalamud.Interface.Components;
using System.Diagnostics;
using ImGuiNET;
using FFXIV_Vibe_Plugin.Commons;
namespace FFXIV_Vibe_Plugin.UI.Components {
internal class ButtonLink {
public static void Draw(string text, string link, Dalamud.Interface.FontAwesomeIcon Icon, Logger Logger) {
if(ImGuiComponents.IconButton(Icon)) {
try {
_ = Process.Start(new ProcessStartInfo() {
FileName = link,
UseShellExecute = true,
});
} catch(Exception e) {
Logger.Error($"Could not open repoUrl: {link}", e);
}
}
if(ImGui.IsItemHovered()) { ImGui.SetTooltip(text); }
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
using System;
using System.Linq;
using System.Numerics;
using ImGuiNET;
using Dalamud.Interface.Colors;
using FFXIV_Vibe_Plugin.Commons;
using FFXIV_Vibe_Plugin.Device;
namespace FFXIV_Vibe_Plugin.UI {
internal class UIBanner {
public static void Draw(int frameCounter, Logger logger, ImGuiScene.TextureWrap image, String donationLink, DevicesController devicesController) {
ImGui.Columns(2, "###main_header", false);
float logoScale = 0.2f;
ImGui.SetColumnWidth(0, (int)(image.Width * logoScale + 20));
ImGui.Image(image.ImGuiHandle, new Vector2(image.Width * logoScale, image.Height * logoScale));
ImGui.NextColumn();
if(devicesController.IsConnected()) {
int nbrDevices = devicesController.GetDevices().Count;
ImGui.TextColored(ImGuiColors.ParsedGreen, "Your are connected!");
ImGui.Text($"Number of device(s): {nbrDevices}");
} else {
ImGui.TextColored(ImGuiColors.ParsedGrey, "Your are not connected!");
}
if(frameCounter < 200) { // Make blink effect
ImGui.Text("Donations: ");
} else {
ImGui.Text(" ");
}
ImGui.SameLine();
ImGui.Text($"{donationLink}");
ImGui.SameLine();
UI.Components.ButtonLink.Draw("Thanks for the donation ;)", donationLink, Dalamud.Interface.FontAwesomeIcon.Pray, logger);
}
}
}

View File

@ -0,0 +1,61 @@
using System;
using System.Numerics;
using ImGuiNET;
using Dalamud.Interface.Colors;
using FFXIV_Vibe_Plugin.Commons;
using FFXIV_Vibe_Plugin.Device;
namespace FFXIV_Vibe_Plugin.UI {
internal class UIConnect {
public static void Draw(Configuration configuration, ConfigurationProfile configurationProfile, App plugin, DevicesController devicesController) {
ImGui.Spacing();
ImGui.TextColored(ImGuiColors.DalamudViolet, "Server address & port");
ImGui.BeginChild("###Server", new Vector2(-1, 40f), true);
{
// Connect/disconnect button
string config_BUTTPLUG_SERVER_HOST = configurationProfile.BUTTPLUG_SERVER_HOST;
ImGui.SetNextItemWidth(200);
if(ImGui.InputText("##serverHost", ref config_BUTTPLUG_SERVER_HOST, 99)) {
configurationProfile.BUTTPLUG_SERVER_HOST = config_BUTTPLUG_SERVER_HOST.Trim().ToLower();
configuration.Save();
}
ImGui.SameLine();
int config_BUTTPLUG_SERVER_PORT = configurationProfile.BUTTPLUG_SERVER_PORT;
ImGui.SetNextItemWidth(100);
if(ImGui.InputInt("##serverPort", ref config_BUTTPLUG_SERVER_PORT, 10)) {
configurationProfile.BUTTPLUG_SERVER_PORT = config_BUTTPLUG_SERVER_PORT;
configuration.Save();
}
}
ImGui.EndChild();
ImGui.Spacing();
ImGui.BeginChild("###Main_Connection", new Vector2(-1, 40f), true);
{
if(!devicesController.IsConnected()) {
if(ImGui.Button("Connect", new Vector2(100, 24))) {
plugin.Command_DeviceController_Connect();
}
} else {
if(ImGui.Button("Disconnect", new Vector2(100, 24))) {
devicesController.Disconnect();
}
}
// Checkbox AUTO_CONNECT
ImGui.SameLine();
bool config_AUTO_CONNECT = configurationProfile.AUTO_CONNECT;
if(ImGui.Checkbox("Automatically connects. ", ref config_AUTO_CONNECT)) {
configurationProfile.AUTO_CONNECT = config_AUTO_CONNECT;
configuration.Save();
}
}
ImGui.EndChild();
}
}
}

View File

@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FFXIV_Vibe_Plugin.Commons;
namespace FFXIV_Vibe_Plugin.Migrations {
internal class Migration {
private readonly Configuration configuration;
private readonly Logger logger;
public Migration(Configuration configuration, Logger logger) {
this.configuration = configuration;
this.logger = logger;
}
public bool Patch_0_2_0_to_1_0_0_config_profile() {
var VersionToApply = 0;
var configuration = this.configuration;
var logger = this.logger;
if(configuration.Version == VersionToApply && configuration != null) {
ConfigurationProfile preset = new() {
Name = "Default (auto-migration from v0.2.0 to v1.0.0)",
VERBOSE_SPELL = configuration.VERBOSE_SPELL,
VERBOSE_CHAT = configuration.VERBOSE_CHAT,
VIBE_HP_TOGGLE = configuration.VIBE_HP_TOGGLE,
VIBE_HP_MODE = configuration.VIBE_HP_MODE,
MAX_VIBE_THRESHOLD = configuration.MAX_VIBE_THRESHOLD,
AUTO_CONNECT = configuration.AUTO_CONNECT,
AUTO_OPEN = configuration.AUTO_OPEN,
PatternList = configuration.PatternList,
BUTTPLUG_SERVER_HOST = configuration.BUTTPLUG_SERVER_HOST,
BUTTPLUG_SERVER_PORT = configuration.BUTTPLUG_SERVER_PORT,
TRIGGERS = configuration.TRIGGERS,
VISITED_DEVICES = configuration.VISITED_DEVICES
};
configuration.Version = VersionToApply+1;
configuration.CurrentProfileName = preset.Name;
configuration.Profiles.Add(preset);
configuration.Save();
logger.Warn("Migration from 2.0.0 to 2.1.0 using profiles done successfully");
return true;
}
return false;
}
}
}

View File

@ -1,28 +0,0 @@
using Dalamud.Configuration;
using Dalamud.Plugin;
using System;
namespace FFXIV_Plugin_Vibe
{
[Serializable]
public class Configuration : IPluginConfiguration
{
public int Version { get; set; } = 0;
public bool SomePropertyToBeSavedAndWithADefault { get; set; } = true;
// the below exist just to make saving less cumbersome
[NonSerialized]
private DalamudPluginInterface? PluginInterface;
public void Initialize(DalamudPluginInterface pluginInterface)
{
this.PluginInterface = pluginInterface;
}
public void Save()
{
this.PluginInterface!.SavePluginConfig(this);
}
}
}

View File

@ -21,7 +21,7 @@
</PropertyGroup>
<ItemGroup>
<Content Include="..\Data\goat.png">
<Content Include="..\Data\logo.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Visible>false</Visible>
</Content>
@ -37,6 +37,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Buttplug" Version="2.0.6" />
<PackageReference Include="DalamudPackager" Version="2.1.10" />
<Reference Include="FFXIVClientStructs">
<HintPath>$(DalamudLibPath)FFXIVClientStructs.dll</HintPath>

View File

@ -1,14 +1,14 @@
{
"Author": "Kaciexx",
"Name": "FFXIV Vibe Plugin",
"Punchline": "Vibe controllers or toys",
"Description": "Plugin that let you vibe your controller or toys",
"RepoUrl": "https://github.com/kaciexx/FFXIV_Vibe_Plugin",
"InternalName": "FFXIV_Vibe_Plugin",
"ApplicableVersion": "any",
"Tags": [
"vibe",
"vibration",
"wave"
]
"Author": "Kaciexx",
"Name": "FFXIV Vibe Plugin",
"Punchline": "Vibe controllers or toys",
"Description": "Plugin that let you vibe your controller or toys",
"RepoUrl": "https://github.com/kaciexx/FFXIV_Vibe_Plugin",
"InternalName": "FFXIV_Vibe_Plugin",
"ApplicableVersion": "any",
"Tags": [
"vibe",
"vibration",
"wave"
]
}

View File

@ -1,69 +1,87 @@
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.Command;
using Dalamud.Game.Network;
using Dalamud.Interface.Windowing;
using Dalamud.IoC;
using Dalamud.Plugin;
using FFXIV_Vibe_Plugin.Windows;
using System.IO;
using System.Reflection;
using Dalamud.Interface.Windowing;
using FFXIV_Plugin_Vibe.Windows;
namespace FFXIV_Plugin_Vibe
{
public sealed class Plugin : IDalamudPlugin
{
public string Name => "Sample Plugin";
private const string CommandName = "/pmycommand";
namespace FFXIV_Vibe_Plugin {
public sealed class Plugin : IDalamudPlugin {
// Dalamud plugin definition
public string Name => "FFXIV Vibe Plugin";
public static readonly string ShortName = "FVP";
public readonly string CommandName = "/fvp";
private DalamudPluginInterface PluginInterface { get; init; }
private CommandManager CommandManager { get; init; }
public Configuration Configuration { get; init; }
public WindowSystem WindowSystem = new("SamplePlugin");
// Dalamud plugins
private Dalamud.Game.Gui.ChatGui? DalamudChat { get; init; }
private DalamudPluginInterface PluginInterface { get; init; }
private CommandManager CommandManager { get; init; }
public Configuration Configuration { get; init; }
public Plugin(
[RequiredVersion("1.0")] DalamudPluginInterface pluginInterface,
[RequiredVersion("1.0")] CommandManager commandManager)
{
this.PluginInterface = pluginInterface;
this.CommandManager = commandManager;
public WindowSystem WindowSystem = new("FFXIV_Vibe_Plugin");
this.Configuration = this.PluginInterface.GetPluginConfig() as Configuration ?? new Configuration();
this.Configuration.Initialize(this.PluginInterface);
// FFXIV_Vibe_Plugin definition
// TODO: private PluginUI PluginUi { get; init; }
private FFXIV_Vibe_Plugin.App app;
// you might normally want to embed resources and load them from the manifest stream
var imagePath = Path.Combine(PluginInterface.AssemblyLocation.Directory?.FullName!, "goat.png");
var goatImage = this.PluginInterface.UiBuilder.LoadImage(imagePath);
public Plugin(
[RequiredVersion("1.0")] DalamudPluginInterface pluginInterface,
[RequiredVersion("1.0")] CommandManager commandManager,
[RequiredVersion("1.0")] ClientState clientState,
[RequiredVersion("1.0")] GameNetwork gameNetwork,
[RequiredVersion("1.0")] SigScanner scanner,
[RequiredVersion("1.0")] ObjectTable gameObjects,
[RequiredVersion("1.0")] DataManager dataManager
) {
this.PluginInterface = pluginInterface;
this.CommandManager = commandManager;
WindowSystem.AddWindow(new ConfigWindow(this));
WindowSystem.AddWindow(new MainWindow(this, goatImage));
this.Configuration = this.PluginInterface.GetPluginConfig() as Configuration ?? new Configuration();
this.Configuration.Initialize(this.PluginInterface);
this.CommandManager.AddHandler(CommandName, new CommandInfo(OnCommand)
{
HelpMessage = "A useful message to display in /xlhelp"
});
// you might normally want to embed resources and load them from the manifest stream
var imagePath = Path.Combine(PluginInterface.AssemblyLocation.Directory?.FullName!, "logo.png");
var logoImage = this.PluginInterface.UiBuilder.LoadImage(imagePath);
this.PluginInterface.UiBuilder.Draw += DrawUI;
this.PluginInterface.UiBuilder.OpenConfigUi += DrawConfigUI;
}
WindowSystem.AddWindow(new ConfigWindow(this));
WindowSystem.AddWindow(new MainWindow(this, logoImage));
public void Dispose()
{
this.WindowSystem.RemoveAllWindows();
this.CommandManager.RemoveHandler(CommandName);
}
this.CommandManager.AddHandler(CommandName, new CommandInfo(OnCommand) {
HelpMessage = "A vibe plugin for fun..."
});
private void OnCommand(string command, string args)
{
// in response to the slash command, just display our main ui
WindowSystem.GetWindow("My Amazing Window").IsOpen = true;
}
this.PluginInterface.UiBuilder.Draw += DrawUI;
this.PluginInterface.UiBuilder.OpenConfigUi += DrawConfigUI;
private void DrawUI()
{
this.WindowSystem.Draw();
}
public void DrawConfigUI()
{
WindowSystem.GetWindow("A Wonderful Configuration Window").IsOpen = true;
}
// Init our own app
this.app = new FFXIV_Vibe_Plugin.App(CommandName, ShortName, gameNetwork, clientState, dataManager, DalamudChat, Configuration, scanner, gameObjects, pluginInterface);
}
public void Dispose() {
this.WindowSystem.RemoveAllWindows();
this.CommandManager.RemoveHandler(CommandName);
this.app.Dispose();
}
private void OnCommand(string command, string args) {
// in response to the slash command, just display our main ui
WindowSystem.GetWindow("My Amazing Window").IsOpen = true;
this.app.OnCommand(command, args);
}
private void DrawUI() {
this.WindowSystem.Draw();
this.app.DrawUI();
}
public void DrawConfigUI() {
WindowSystem.GetWindow("A Wonderful Configuration Window").IsOpen = true;
}
}
}

View File

@ -3,34 +3,30 @@ using System.Numerics;
using Dalamud.Interface.Windowing;
using ImGuiNET;
namespace FFXIV_Plugin_Vibe.Windows;
namespace FFXIV_Vibe_Plugin.Windows;
public class ConfigWindow : Window, IDisposable
{
private Configuration Configuration;
public class ConfigWindow : Window, IDisposable {
private Configuration Configuration;
public ConfigWindow(Plugin plugin) : base(
"A Wonderful Configuration Window",
ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollbar |
ImGuiWindowFlags.NoScrollWithMouse)
{
this.Size = new Vector2(232, 75);
this.SizeCondition = ImGuiCond.Always;
public ConfigWindow(Plugin plugin) : base(
"A Wonderful Configuration Window",
ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollbar |
ImGuiWindowFlags.NoScrollWithMouse) {
this.Size = new Vector2(232, 75);
this.SizeCondition = ImGuiCond.Always;
this.Configuration = plugin.Configuration;
}
this.Configuration = plugin.Configuration;
}
public void Dispose() { }
public void Dispose() { }
public override void Draw()
{
// can't ref a property, so use a local copy
var configValue = this.Configuration.SomePropertyToBeSavedAndWithADefault;
if (ImGui.Checkbox("Random Config Bool", ref configValue))
{
this.Configuration.SomePropertyToBeSavedAndWithADefault = configValue;
// can save immediately on change, if you don't want to provide a "Save and Close" button
this.Configuration.Save();
}
}
public override void Draw() {
// can't ref a property, so use a local copy
/*var configValue = this.Configuration.SomePropertyToBeSavedAndWithADefault;
if (ImGui.Checkbox("Random Config Bool", ref configValue)) {
this.Configuration.SomePropertyToBeSavedAndWithADefault = configValue;
// can save immediately on change, if you don't want to provide a "Save and Close" button
this.Configuration.Save();
}*/
}
}

View File

@ -4,45 +4,39 @@ using Dalamud.Interface.Windowing;
using ImGuiNET;
using ImGuiScene;
namespace FFXIV_Plugin_Vibe.Windows;
namespace FFXIV_Vibe_Plugin.Windows;
public class MainWindow : Window, IDisposable
{
private TextureWrap GoatImage;
private Plugin Plugin;
public class MainWindow : Window, IDisposable {
private TextureWrap GoatImage;
private Plugin Plugin;
public MainWindow(Plugin plugin, TextureWrap goatImage) : base(
"My Amazing Window", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)
{
this.SizeConstraints = new WindowSizeConstraints
{
MinimumSize = new Vector2(375, 330),
MaximumSize = new Vector2(float.MaxValue, float.MaxValue)
};
public MainWindow(Plugin plugin, TextureWrap goatImage) : base(
"My Amazing Window", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse) {
this.SizeConstraints = new WindowSizeConstraints {
MinimumSize = new Vector2(375, 330),
MaximumSize = new Vector2(float.MaxValue, float.MaxValue)
};
this.GoatImage = goatImage;
this.Plugin = plugin;
this.GoatImage = goatImage;
this.Plugin = plugin;
}
public void Dispose() {
this.GoatImage.Dispose();
}
public override void Draw() {
/*ImGui.Text($"The random config bool is {this.Plugin.Configuration.SomePropertyToBeSavedAndWithADefault}");*/
if (ImGui.Button("Show Settings")) {
this.Plugin.DrawConfigUI();
}
public void Dispose()
{
this.GoatImage.Dispose();
}
ImGui.Spacing();
public override void Draw()
{
ImGui.Text($"The random config bool is {this.Plugin.Configuration.SomePropertyToBeSavedAndWithADefault}");
if (ImGui.Button("Show Settings"))
{
this.Plugin.DrawConfigUI();
}
ImGui.Spacing();
ImGui.Text("Have a goat:");
ImGui.Indent(55);
ImGui.Image(this.GoatImage.ImGuiHandle, new Vector2(this.GoatImage.Width, this.GoatImage.Height));
ImGui.Unindent(55);
}
ImGui.Text("Have a goat:");
ImGui.Indent(55);
ImGui.Image(this.GoatImage.ImGuiHandle, new Vector2(this.GoatImage.Width, this.GoatImage.Height));
ImGui.Unindent(55);
}
}

View File

@ -2,11 +2,37 @@
"version": 1,
"dependencies": {
"net7.0-windows7.0": {
"Buttplug": {
"type": "Direct",
"requested": "[2.0.6, )",
"resolved": "2.0.6",
"contentHash": "UNusT8YfG+KMYN7ZG0IPjqGbgOTGDm83n0N6zpF11QxciwyBiAcs7NHJquImuJdrDI7ygVtzb2VepGVrIH5HDw==",
"dependencies": {
"ButtplugRustFFI": "2.0.5",
"Google.Protobuf": "3.19.1",
"System.Memory": "4.5.4"
}
},
"DalamudPackager": {
"type": "Direct",
"requested": "[2.1.10, )",
"resolved": "2.1.10",
"contentHash": "S6NrvvOnLgT4GDdgwuKVJjbFo+8ZEj+JsEYk9ojjOR/MMfv1dIFpT8aRJQfI24rtDcw1uF+GnSSMN4WW1yt7fw=="
},
"ButtplugRustFFI": {
"type": "Transitive",
"resolved": "2.0.5",
"contentHash": "1uwoNtiYysP5LvYJJYGYcOBSh6wLNQhpt8M1Mdeu4TEcM7LvMRQbjiIN6Bhd6W5Xpk1H+75kP3j/GFIsQyecUw=="
},
"Google.Protobuf": {
"type": "Transitive",
"resolved": "3.19.1",
"contentHash": "M6yun2BPdHkBjD3V14muZSt72azWHRJEx88ME2TyyH2+/ww6R3hIptjBFQQtO6pmkfLXW/NGQ4hADWSa9AmK2A=="
},
"System.Memory": {
"type": "Transitive",
"resolved": "4.5.4",
"contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw=="
}
}
}