diff --git a/.editorconfig b/.editorconfig index 1e47248..7b6b34d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,138 +1,225 @@ -root = true -# top-most EditorConfig file - -[*] -charset = utf-8 - -end_of_line = lf -insert_final_newline = true - -# 4 space indentation -indent_style = space -indent_size = 4 - -# disable redundant style warnings - -# Microsoft .NET properties -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_space_between_method_call_empty_parameter_list_parentheses = 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_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 - -# 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 - -[*.{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 +# Supprimer la ligne ci-dessous si vous voulez hériter les paramètres .editorconfig des répertoires supérieurs +root = true + +# Fichiers C# +[*.cs] + +#### Options EditorConfig principales #### + +# Indentation et espacement +indent_size = 2 +indent_style = space +tab_width = 2 + +# Préférences de nouvelle ligne +end_of_line = crlf +insert_final_newline = false + +#### 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_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_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Préférences d'enveloppement +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Styles de nommage #### + +# 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 diff --git a/FFXIV_Vibe_Plugin/App/App.cs b/FFXIV_Vibe_Plugin/App/App.cs new file mode 100644 index 0000000..3f0ca0f --- /dev/null +++ b/FFXIV_Vibe_Plugin/App/App.cs @@ -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? 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 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 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); + } + } + } +} diff --git a/FFXIV_Vibe_Plugin/App/Commons/Helpers.cs b/FFXIV_Vibe_Plugin/App/Commons/Helpers.cs new file mode 100644 index 0000000..3e89116 --- /dev/null +++ b/FFXIV_Vibe_Plugin/App/Commons/Helpers.cs @@ -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; + } + } +} diff --git a/FFXIV_Vibe_Plugin/App/Commons/Logger.cs b/FFXIV_Vibe_Plugin/App/Commons/Logger.cs new file mode 100644 index 0000000..26b61b5 --- /dev/null +++ b/FFXIV_Vibe_Plugin/App/Commons/Logger.cs @@ -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}"; + } + } +} diff --git a/FFXIV_Vibe_Plugin/App/Commons/OpCodes.cs b/FFXIV_Vibe_Plugin/App/Commons/OpCodes.cs new file mode 100644 index 0000000..a9ababe --- /dev/null +++ b/FFXIV_Vibe_Plugin/App/Commons/OpCodes.cs @@ -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; + } + } +} diff --git a/FFXIV_Vibe_Plugin/App/Commons/Patterns.cs b/FFXIV_Vibe_Plugin/App/Commons/Patterns.cs new file mode 100644 index 0000000..09aaaeb --- /dev/null +++ b/FFXIV_Vibe_Plugin/App/Commons/Patterns.cs @@ -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 BuiltinPatterns = new(); + private List 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 GetAllPatterns() { + return this.BuiltinPatterns.Concat(this.CustomPatterns).ToList(); + } + + public List GetBuiltinPatterns() { + return this.BuiltinPatterns; + } + + /** Returns a copy of the list to avoid error if any modification happens */ + public List GetCustomPatterns() { + List newList = new(); + foreach(Pattern pattern in this.CustomPatterns) { + newList.Add(pattern); + } + return newList; + } + + public void SetCustomPatterns(List 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(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; + } + } +} diff --git a/FFXIV_Vibe_Plugin/App/Commons/Structures.cs b/FFXIV_Vibe_Plugin/App/Commons/Structures.cs new file mode 100644 index 0000000..96be861 --- /dev/null +++ b/FFXIV_Vibe_Plugin/App/Commons/Structures.cs @@ -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? Targets; + public DamageType DamageType = 0; + public ActionEffectType ActionEffectType = 0; + + public Spell(int id, string name, Player player, int[]? amounts, float amountAverage, List? 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}"; + } + } + } + +} diff --git a/FFXIV_Vibe_Plugin/App/Configuration.cs b/FFXIV_Vibe_Plugin/App/Configuration.cs new file mode 100644 index 0000000..7fba94a --- /dev/null +++ b/FFXIV_Vibe_Plugin/App/Configuration.cs @@ -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 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 PatternList = new(); + public string BUTTPLUG_SERVER_HOST { get; set; } = "127.0.0.1"; + public int BUTTPLUG_SERVER_PORT { get; set; } = 12345; + public List TRIGGERS { get; set; } = new(); + public Dictionary 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 PatternList = new(); + + public string BUTTPLUG_SERVER_HOST { get; set; } = "127.0.0.1"; + public int BUTTPLUG_SERVER_PORT { get; set; } = 12345; + + public List TRIGGERS { get; set; } = new(); + + public Dictionary VISITED_DEVICES = new(); + + } + + +} diff --git a/FFXIV_Vibe_Plugin/App/Data/logo.png b/FFXIV_Vibe_Plugin/App/Data/logo.png new file mode 100644 index 0000000..27abd80 Binary files /dev/null and b/FFXIV_Vibe_Plugin/App/Data/logo.png differ diff --git a/FFXIV_Vibe_Plugin/App/Devices/Device.cs b/FFXIV_Vibe_Plugin/App/Devices/Device.cs new file mode 100644 index 0000000..ed25dbe --- /dev/null +++ b/FFXIV_Vibe_Plugin/App/Devices/Device.cs @@ -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(); + public bool CanRotate = false; + public int RotateMotors = -1; + public uint[] RotateSteps = Array.Empty(); + public bool CanLinear = false; + public int LinearMotors = -1; + public uint[] LinearSteps = Array.Empty(); + public bool CanBattery = false; + public bool CanStop = false; + public bool IsConnected = false; + public double BatteryLevel = -1; + // TODO: use that ? + public List UsableCommands = new(); + + public int[] CurrentVibrateIntensity = Array.Empty(); + public int[] CurrentRotateIntensity = Array.Empty(); + public int[] CurrentLinearIntensity = Array.Empty(); + + 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 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 GetUsableCommands() { + return this.UsableCommands; + } + + public List GetCommandsInfo() { + List 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 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 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 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 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); + } + } +} diff --git a/FFXIV_Vibe_Plugin/App/Devices/DevicesController.cs b/FFXIV_Vibe_Plugin/App/Devices/DevicesController.cs new file mode 100644 index 0000000..52c9d16 --- /dev/null +++ b/FFXIV_Vibe_Plugin/App/Devices/DevicesController.cs @@ -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 CurrentDeviceAndMotorPlaying = new(); + + // Buttplug related + private ButtplugClient? BPClient; + private readonly List 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 GetDevices() { + return this.Devices; + } + + public Dictionary 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(); + } + } +} diff --git a/FFXIV_Vibe_Plugin/App/Experimental/NetworkCapture.cs b/FFXIV_Vibe_Plugin/App/Experimental/NetworkCapture.cs new file mode 100644 index 0000000..ec2fef6 --- /dev/null +++ b/FFXIV_Vibe_Plugin/App/Experimental/NetworkCapture.cs @@ -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}"); + } + + } + } +} diff --git a/FFXIV_Vibe_Plugin/App/Hooks/ActionEffect.cs b/FFXIV_Vibe_Plugin/App/Hooks/ActionEffect.cs new file mode 100644 index 0000000..a158800 --- /dev/null +++ b/FFXIV_Vibe_Plugin/App/Hooks/ActionEffect.cs @@ -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? LuminaActionSheet; + + // Hooks + private delegate void HOOK_ReceiveActionEffectDelegate(int sourceId, IntPtr sourceCharacter, IntPtr pos, IntPtr effectHeader, IntPtr effectArray, IntPtr effectTrail); + private Hook? receiveActionEffectHook; + + // Event to dispatch. + public event EventHandler? 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(); + } + } + + /** 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(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 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 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 GetAllTarget(byte count, IntPtr effectTrail, int[] amounts) { + List 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; } + } +} diff --git a/FFXIV_Vibe_Plugin/App/PlayerStats.cs b/FFXIV_Vibe_Plugin/App/PlayerStats.cs new file mode 100644 index 0000000..2d603c3 --- /dev/null +++ b/FFXIV_Vibe_Plugin/App/PlayerStats.cs @@ -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; + } + } +} diff --git a/FFXIV_Vibe_Plugin/App/Triggers/ChatTrigger.cs b/FFXIV_Vibe_Plugin/App/Triggers/ChatTrigger.cs new file mode 100644 index 0000000..096eb7f --- /dev/null +++ b/FFXIV_Vibe_Plugin/App/Triggers/ChatTrigger.cs @@ -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); + } + } + +} diff --git a/FFXIV_Vibe_Plugin/App/Triggers/Trigger.cs b/FFXIV_Vibe_Plugin/App/Triggers/Trigger.cs new file mode 100644 index 0000000..8717f5d --- /dev/null +++ b/FFXIV_Vibe_Plugin/App/Triggers/Trigger.cs @@ -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 { + 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 AllowedChatTypes = new (); + + // Devices associated with this trigger + public List 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}"; + } + } +} diff --git a/FFXIV_Vibe_Plugin/App/Triggers/TriggersController.cs b/FFXIV_Vibe_Plugin/App/Triggers/TriggersController.cs new file mode 100644 index 0000000..f0d9388 --- /dev/null +++ b/FFXIV_Vibe_Plugin/App/Triggers/TriggersController.cs @@ -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 = 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 GetTriggers() { + return this.Triggers; + } + + public void AddTrigger(Trigger trigger) { + this.Triggers.Add(trigger); + } + + public void RemoveTrigger(Trigger trigger) { + this.Triggers.Remove(trigger); + } + + public List CheckTrigger_Chat(XivChatType chatType, string ChatFromPlayerName, string ChatMsg) { + List 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 CheckTrigger_Spell(Structures.Spell spell) { + List 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 CheckTrigger_HPChanged(int currentHP, float percentageHP) { + List 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 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; + } + } + + + +} diff --git a/FFXIV_Vibe_Plugin/App/UI/Components/ButtonLink.cs b/FFXIV_Vibe_Plugin/App/UI/Components/ButtonLink.cs new file mode 100644 index 0000000..13bb299 --- /dev/null +++ b/FFXIV_Vibe_Plugin/App/UI/Components/ButtonLink.cs @@ -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); } + } + } +} + diff --git a/FFXIV_Vibe_Plugin/App/UI/PluginUI.cs b/FFXIV_Vibe_Plugin/App/UI/PluginUI.cs new file mode 100644 index 0000000..2dc1221 --- /dev/null +++ b/FFXIV_Vibe_Plugin/App/UI/PluginUI.cs @@ -0,0 +1,1128 @@ +using Dalamud.Game.Text; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Components; +using Dalamud.Plugin; +using FFXIV_Vibe_Plugin.Commons; +using ImGuiNET; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; + + +namespace FFXIV_Vibe_Plugin { + + class PluginUI : IDisposable { + + private int frameCounter = 0; + + private readonly DalamudPluginInterface PluginInterface; + private readonly Configuration Configuration; + private ConfigurationProfile ConfigurationProfile; + private readonly Device.DevicesController DevicesController; + private readonly Triggers.TriggersController TriggerController; + private readonly App app; + private readonly Logger Logger; + + // Images + private readonly Dictionary loadedImages = new(); + + // Patterns + private readonly Patterns Patterns = new(); + + private readonly string DonationLink = "http://paypal.me/kaciedev"; + + // this extra bool exists for ImGui, since you can't ref a property + private bool visible = false; + public bool Visible { + get { return this.visible; } + set { this.visible = value; } + } + private bool _expandedOnce = false; + private readonly int WIDTH = 700; + private readonly int HEIGHT = 800; + private readonly int COLUMN0_WIDTH = 130; + + private string _tmp_void = ""; + + // The value to send as a test for vibes. + private int simulator_currentAllIntensity = 0; + + // Temporary UI values + private int TRIGGER_CURRENT_SELECTED_DEVICE = -1; + private string CURRENT_TRIGGER_SELECTOR_SEARCHBAR = ""; + private int _tmp_currentDraggingTriggerIndex = -1; + + // Custom Patterns + readonly string VALID_REGEXP_PATTERN = "^(\\d+:\\d+)+(\\|\\d+:\\d+)*$"; + string CURRENT_PATTERN_SEARCHBAR = ""; + string _tmp_currentPatternNameToAdd = ""; + string _tmp_currentPatternValueToAdd = ""; + string _tmp_currentPatternValueState = "unset"; // unset|valid|unvalid + + // Profile + string _tmp_currentProfileNameToAdd = ""; + string _tmp_currentProfile_ErrorMsg = ""; + + // Some limits + private readonly int TRIGGER_MIN_AFTER = 0; + private readonly int TRIGGER_MAX_AFTER = 120; + + + // Trigger + private Triggers.Trigger? SelectedTrigger = null; + private string triggersViewMode = "default"; // default|edit|delete; + + /** Constructor */ + public PluginUI( + App currentPlugin, + Logger logger, + DalamudPluginInterface pluginInterface, + Configuration configuration, + ConfigurationProfile profile, + Device.DevicesController deviceController, + Triggers.TriggersController triggersController, + Patterns Patterns + ) { + this.Logger = logger; + this.Configuration = configuration; + this.ConfigurationProfile = profile; + this.PluginInterface = pluginInterface; + this.app = currentPlugin; + this.DevicesController = deviceController; + this.TriggerController = triggersController; + this.Patterns = Patterns; + this.LoadImages(); + } + + public void Display() { + this.Visible = true; + this._expandedOnce = false; + } + + /** + * Function that will load all the images so that they are usable. + * Don't forget to add the image into the project file. + */ + private void LoadImages() { + List images = new(); + images.Add("logo.png"); + + string assemblyLocation = System.Reflection.Assembly.GetExecutingAssembly().Location; + foreach(string img in images) { + string imagePath = Path.Combine(Path.GetDirectoryName(assemblyLocation)!, $"Data\\Images\\{img}"); + this.loadedImages.Add(img, this.PluginInterface.UiBuilder.LoadImage(imagePath)); + } + } + + public void Dispose() { + // Dispose all loaded images. + foreach(KeyValuePair img in this.loadedImages) { + if(img.Value != null) img.Value.Dispose(); + } + } + + public void SetProfile(ConfigurationProfile profile) { + this.ConfigurationProfile = profile; + } + + public void Draw() { + // This is our only draw handler attached to UIBuilder, so it needs to be + // able to draw any windows we might have open. + // Each method checks its own visibility/state to ensure it only draws when + // it actually makes sense. + // There are other ways to do this, but it is generally best to keep the number of + // draw delegates as low as possible. + DrawMainWindow(); + frameCounter = (frameCounter+1) % 400; + + } + + public void DrawMainWindow() { + if(!Visible) { + return; + } + if(!this._expandedOnce) { + ImGui.SetNextWindowCollapsed(false); + this._expandedOnce = true; + } + + ImGui.SetNextWindowPos(new Vector2(100, 100), ImGuiCond.Appearing); + ImGui.SetNextWindowSize(new Vector2(this.WIDTH, this.HEIGHT), ImGuiCond.Appearing); + ImGui.SetNextWindowSizeConstraints(new Vector2(this.WIDTH, this.HEIGHT), new Vector2(float.MaxValue, float.MaxValue)); + if(ImGui.Begin("FFXIV Vibe Plugin", ref this.visible, ImGuiWindowFlags.None)) { + ImGui.Spacing(); + + FFXIV_Vibe_Plugin.UI.UIBanner.Draw(this.frameCounter, this.Logger, this.loadedImages["logo.png"], this.DonationLink, this.DevicesController); + + // Back to on column + ImGui.Columns(1); + + // Tab header + if(ImGui.BeginTabBar("##ConfigTabBar", ImGuiTabBarFlags.None)) { + if(ImGui.BeginTabItem("Connect")) { + FFXIV_Vibe_Plugin.UI.UIConnect.Draw(this.Configuration, this.ConfigurationProfile, this.app, this.DevicesController); + ImGui.EndTabItem(); + } + + if(ImGui.BeginTabItem("Options")) { + this.DrawOptionsTab(); + ImGui.EndTabItem(); + } + if(ImGui.BeginTabItem("Devices")) { + this.DrawDevicesTab(); + ImGui.EndTabItem(); + } + if(ImGui.BeginTabItem("Triggers")) { + this.DrawTriggersTab(); + ImGui.EndTabItem(); + } + if(ImGui.BeginTabItem("Patterns")) { + this.DrawPatternsTab(); + ImGui.EndTabItem(); + } + if(ImGui.BeginTabItem("Help")) { + this.DrawHelpTab(); + ImGui.EndTabItem(); + } + } + } + + ImGui.End(); + } + + public void DrawOptionsTab() { + ImGui.TextColored(ImGuiColors.DalamudViolet, "Profile settings"); + float CONFIG_PROFILE_ZONE_HEIGHT = this._tmp_currentProfile_ErrorMsg == "" ? 100f : 120f; + ImGui.BeginChild("###CONFIGURATION_PROFILE_ZONE", new Vector2(-1, CONFIG_PROFILE_ZONE_HEIGHT), true); + { + // Init table + ImGui.BeginTable("###CONFIGURATION_PROFILE_TABLE", 3); + ImGui.TableSetupColumn("###CONFIGURATION_PROFILE_TABLE_COL1", ImGuiTableColumnFlags.WidthFixed, 150); + ImGui.TableSetupColumn("###CONFIGURATION_PROFILE_TABLE_COL2", ImGuiTableColumnFlags.WidthFixed, 350); + ImGui.TableSetupColumn("###CONFIGURATION_PROFILE_TABLE_COL3", ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableNextColumn(); + ImGui.Text("Current profile:"); + ImGui.TableNextColumn(); + string[] PROFILES = this.Configuration.Profiles.Select(profile => profile.Name).ToArray(); + int currentProfileIndex = this.Configuration.Profiles.FindIndex(profile => profile.Name == this.Configuration.CurrentProfileName); + ImGui.SetNextItemWidth(350); + if(ImGui.Combo("###CONFIGURATION_CURRENT_PROFILE", ref currentProfileIndex, PROFILES, PROFILES.Length)) { + this.Configuration.CurrentProfileName = this.Configuration.Profiles[currentProfileIndex].Name; + this.app.SetProfile(this.Configuration.CurrentProfileName); + this.Logger.Debug($"New profile selected: {this.Configuration.CurrentProfileName}"); + this.Configuration.Save(); + } + ImGui.TableNextColumn(); + if(ImGuiComponents.IconButton(Dalamud.Interface.FontAwesomeIcon.Trash)) { + if(this.Configuration.Profiles.Count <= 1) { + string errorMsg = "You can't delete this profile. At least one profile should exists. Create another one before deleting."; + this.Logger.Error(errorMsg); + this._tmp_currentProfile_ErrorMsg = errorMsg; + } else { + this.Configuration.RemoveProfile(this.ConfigurationProfile.Name); + ConfigurationProfile? newProfileToUse = this.Configuration.GetFirstProfile(); + if(newProfileToUse != null) { + this.app.SetProfile(newProfileToUse.Name); + } + this.Configuration.Save(); + } + } + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text("Add new profile: "); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(350); + if(ImGui.InputText("###CONFIGURATION_NEW_PROFILE_NAME", ref _tmp_currentProfileNameToAdd, 150)) { + this._tmp_currentProfile_ErrorMsg = ""; + } + ImGui.TableNextColumn(); + if(this._tmp_currentProfileNameToAdd.Length > 0) { + if(ImGuiComponents.IconButton(Dalamud.Interface.FontAwesomeIcon.Plus)) { + if(this._tmp_currentProfileNameToAdd.Trim() != "") { + bool wasAdded = this.Configuration.AddProfile(this._tmp_currentProfileNameToAdd); + if(!wasAdded) { + string errorMsg = $"The current profile name '{this._tmp_currentProfileNameToAdd}' already exists!"; + this.Logger.Error(errorMsg); + this._tmp_currentProfile_ErrorMsg = errorMsg; + } else { + this.app.SetProfile(this._tmp_currentProfileNameToAdd); + this.Logger.Debug($"New profile added {_tmp_currentProfileNameToAdd}"); + this._tmp_currentProfileNameToAdd = ""; + this.Configuration.Save(); + } + } + } + } + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text("Rename current profile"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(350); + if(ImGui.InputText("###CONFIGURATION_CURRENT_PROFILE_RENAME", ref this.ConfigurationProfile.Name, 150)) { + this.Configuration.CurrentProfileName = this.ConfigurationProfile.Name; + this.Configuration.Save(); + } + ImGui.EndTable(); + + + if(this._tmp_currentProfile_ErrorMsg != "") { + ImGui.TextColored(ImGuiColors.DalamudRed, this._tmp_currentProfile_ErrorMsg); + } + }; + ImGui.EndChild(); + + + ImGui.TextColored(ImGuiColors.DalamudViolet, "General Settings"); + ImGui.BeginChild("###GENERAL_OPTIONS_ZONE", new Vector2(-1, 125f), true); + { + // Init table + ImGui.BeginTable("###GENERAL_OPTIONS_TABLE", 2); + ImGui.TableSetupColumn("###GENERAL_OPTIONS_TABLE_COL1", ImGuiTableColumnFlags.WidthFixed, 250); + ImGui.TableSetupColumn("###GENERAL_OPTIONS_TABLE_COL2", ImGuiTableColumnFlags.WidthStretch); + + // Checkbox AUTO_OPEN + ImGui.TableNextColumn(); + bool config_AUTO_OPEN = this.ConfigurationProfile.AUTO_OPEN; + ImGui.Text("Automatically open configuration panel."); + ImGui.TableNextColumn(); + if(ImGui.Checkbox("###GENERAL_OPTIONS_AUTO_OPEN", ref config_AUTO_OPEN)) { + this.ConfigurationProfile.AUTO_OPEN = config_AUTO_OPEN; + this.Configuration.Save(); + } + ImGui.TableNextRow(); + + + // Checkbox MAX_VIBE_THRESHOLD + ImGui.TableNextColumn(); + ImGui.Text("Global threshold: "); + ImGui.TableNextColumn(); + int config_MAX_VIBE_THRESHOLD = this.ConfigurationProfile.MAX_VIBE_THRESHOLD; + ImGui.SetNextItemWidth(201); + if(ImGui.SliderInt("###OPTION_MaximumThreshold", ref config_MAX_VIBE_THRESHOLD, 2, 100)) { + this.ConfigurationProfile.MAX_VIBE_THRESHOLD = config_MAX_VIBE_THRESHOLD; + this.Configuration.Save(); + } + ImGui.SameLine(); + ImGuiComponents.HelpMarker("Maximum threshold for vibes (will override every devices)."); + + // Checkbox OPTION_VERBOSE_SPELL + ImGui.TableNextColumn(); + ImGui.Text("Log casted spells:"); + ImGui.TableNextColumn(); + if(ImGui.Checkbox("###OPTION_VERBOSE_SPELL", ref this.ConfigurationProfile.VERBOSE_SPELL)) { + this.Configuration.Save(); + } + ImGui.SameLine(); + ImGuiComponents.HelpMarker("Use the /xllog to see all casted spells. Disable this to have better ingame performance."); + ImGui.TableNextRow(); + + // Checkbox OPTION_VERBOSE_CHAT + ImGui.TableNextColumn(); + ImGui.Text("Log chat triggered:"); + ImGui.TableNextColumn(); + if(ImGui.Checkbox("###OPTION_VERBOSE_CHAT", ref this.ConfigurationProfile.VERBOSE_CHAT)) { + this.Configuration.Save(); + } + ImGui.SameLine(); + ImGuiComponents.HelpMarker("Use the /xllog to see all chat message. Disable this to have better ingame performance."); + + ImGui.EndTable(); + } + ImGui.EndChild(); + + if(this.ConfigurationProfile.VERBOSE_CHAT || this.ConfigurationProfile.VERBOSE_SPELL) { + ImGui.TextColored(ImGuiColors.DalamudOrange, "Please, disabled chat and spell logs for better ingame performance."); + } + } + + public void DrawDevicesTab() { + ImGui.Spacing(); + + ImGui.TextColored(ImGuiColors.DalamudViolet, "Actions"); + ImGui.BeginChild("###DevicesTab_General", new Vector2(-1, 40f), true); + { + if(this.DevicesController.IsScanning()) { + if(ImGui.Button("Stop scanning", new Vector2(100, 24))) { + this.DevicesController.StopScanningDevice(); + } + } else { + if(ImGui.Button("Scan device", new Vector2(100, 24))) { + this.DevicesController.ScanDevice(); + } + } + + ImGui.SameLine(); + if(ImGui.Button("Update Battery", new Vector2(100, 24))) { + this.DevicesController.UpdateAllBatteryLevel(); + } + ImGui.SameLine(); + if(ImGui.Button("Stop All", new Vector2(100, 24))) { + this.DevicesController.StopAll(); + this.simulator_currentAllIntensity = 0; + } + } + ImGui.EndChild(); + + if(ImGui.CollapsingHeader($"All devices")) { + ImGui.Text("Send to all:"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(200); + if(ImGui.SliderInt("###SendVibeAll_Intensity", ref this.simulator_currentAllIntensity, 0, 100)) { + this.DevicesController.SendVibeToAll(this.simulator_currentAllIntensity); + } + } + + foreach(Device.Device device in this.DevicesController.GetDevices()) { + if(ImGui.CollapsingHeader($"[{device.Id}] {device.Name} - Battery: {device.GetBatteryPercentage()}")) { + ImGui.TextWrapped(device.ToString()); + if(device.CanVibrate) { + ImGui.TextColored(ImGuiColors.DalamudViolet, "VIBRATE"); + ImGui.Indent(10); + for(int i = 0; i < device.VibrateMotors; i++) { + ImGui.Text($"Motor {i + 1}: "); + ImGui.SameLine(); + ImGui.SetNextItemWidth(200); + if(ImGui.SliderInt($"###{device.Id} Intensity Vibrate Motor {i}", ref device.CurrentVibrateIntensity[i], 0, 100)) { + this.DevicesController.SendVibrate(device, device.CurrentVibrateIntensity[i], i); + } + } + ImGui.Unindent(10); + } + + if(device.CanRotate) { + ImGui.TextColored(ImGuiColors.DalamudViolet, "ROTATE"); + ImGui.Indent(10); + for(int i = 0; i < device.RotateMotors; i++) { + ImGui.Text($"Motor {i + 1}: "); + ImGui.SameLine(); + ImGui.SetNextItemWidth(200); + if(ImGui.SliderInt($"###{device.Id} Intensity Rotate Motor {i}", ref device.CurrentRotateIntensity[i], 0, 100)) { + this.DevicesController.SendRotate(device, device.CurrentRotateIntensity[i], i, true); + } + } + ImGui.Unindent(10); + } + + if(device.CanLinear) { + ImGui.TextColored(ImGuiColors.DalamudViolet, "LINEAR VIBES"); + ImGui.Indent(10); + for(int i = 0; i < device.LinearMotors; i++) { + ImGui.Text($"Motor {i + 1}: "); + ImGui.SameLine(); + ImGui.SetNextItemWidth(200); + if(ImGui.SliderInt($"###{device.Id} Intensity Linear Motor {i}", ref device.CurrentLinearIntensity[i], 0, 100)) { + this.DevicesController.SendLinear(device, device.CurrentLinearIntensity[i], 500, i); + } + } + ImGui.Unindent(10); + } + } + + } + } + + public unsafe void DrawTriggersTab() { + List triggers = this.TriggerController.GetTriggers(); + string selectedId = this.SelectedTrigger != null ? this.SelectedTrigger.Id : ""; + if(ImGui.BeginChild("###TriggersSelector", new Vector2(ImGui.GetWindowContentRegionMax().X/3, -ImGui.GetFrameHeightWithSpacing()), true)) { + ImGui.SetNextItemWidth(185); + ImGui.InputText("###TriggersSelector_SearchBar", ref this.CURRENT_TRIGGER_SELECTOR_SEARCHBAR, 200); + ImGui.Spacing(); + + for(int triggerIndex=0; triggerIndex -1 && ImGui.IsMouseReleased(ImGuiMouseButton.Left)) { + int srcIndex = this._tmp_currentDraggingTriggerIndex; + int targetIndex = triggerIndex; + (triggers[srcIndex], triggers[targetIndex]) = (triggers[targetIndex], triggers[srcIndex]); + this._tmp_currentDraggingTriggerIndex = -1; + this.Configuration.Save(); + } + ImGui.EndDragDropTarget(); + } + + } + } + ImGui.EndChild(); + } + + ImGui.SameLine(); + if(ImGui.BeginChild("###TriggerViewerPanel", new Vector2(0, -ImGui.GetFrameHeightWithSpacing()), true)) { + if(this.triggersViewMode == "default") { + ImGui.Text("Please select or add a trigger"); + } else if(this.triggersViewMode == "edit") { + if(this.SelectedTrigger != null) { + + // Init table + ImGui.BeginTable("###TRIGGER_FORM_TABLE_GENERAL", 2); + ImGui.TableSetupColumn("###TRIGGER_FORM_TABLE_COL1", ImGuiTableColumnFlags.WidthFixed, COLUMN0_WIDTH); + ImGui.TableSetupColumn("###TRIGGER_FORM_TABLE_COL2", ImGuiTableColumnFlags.WidthStretch); + + // Displaying the trigger ID + ImGui.TableNextColumn(); + ImGui.Text($"TriggerID:"); + ImGui.TableNextColumn(); + ImGui.Text($"{this.SelectedTrigger.GetShortID()}"); + ImGui.TableNextRow(); + + // TRIGGER ENABLED + ImGui.TableNextColumn(); + ImGui.Text("Enabled:"); + ImGui.TableNextColumn(); + if(ImGui.Checkbox("###TRIGGER_ENABLED", ref this.SelectedTrigger.Enabled)) { + this.Configuration.Save(); + }; + ImGui.TableNextRow(); + + // TRIGGER NAME + ImGui.TableNextColumn(); + ImGui.Text("Trigger Name:"); + ImGui.TableNextColumn(); + if(ImGui.InputText("###TRIGGER_NAME", ref this.SelectedTrigger.Name, 99)) { + if(this.SelectedTrigger.Name == "") { + this.SelectedTrigger.Name = "no_name"; + } + this.Configuration.Save(); + }; + ImGui.TableNextRow(); + + // TRIGGER NAME + ImGui.TableNextColumn(); + ImGui.Text("Trigger Description:"); + ImGui.TableNextColumn(); + if (ImGui.InputTextMultiline("###TRIGGER_DESCRIPTION", ref this.SelectedTrigger.Description, 500, new Vector2(190, 50))) { + if (this.SelectedTrigger.Description == "") { + this.SelectedTrigger.Description = "no_description"; + } + this.Configuration.Save(); + }; + ImGui.TableNextRow(); + + + // TRIGGER KIND + ImGui.TableNextColumn(); + ImGui.Text("Kind:"); + ImGui.TableNextColumn(); + string[] TRIGGER_KIND = System.Enum.GetNames(typeof(Triggers.KIND)); + int currentKind = (int)this.SelectedTrigger.Kind; + if(ImGui.Combo("###TRIGGER_FORM_KIND", ref currentKind, TRIGGER_KIND, TRIGGER_KIND.Length)) { + this.SelectedTrigger.Kind = currentKind; + if(currentKind == (int)Triggers.KIND.HPChange) { + this.SelectedTrigger.StartAfter = 0; + this.SelectedTrigger.StopAfter = 0; + } + this.Configuration.Save(); + } + ImGui.TableNextRow(); + + // TRIGGER FROM_PLAYER_NAME + ImGui.TableNextColumn(); + ImGui.Text("Player name:"); + ImGui.TableNextColumn(); + if(ImGui.InputText("###TRIGGER_CHAT_FROM_PLAYER_NAME", ref this.SelectedTrigger.FromPlayerName, 100)) { + this.SelectedTrigger.FromPlayerName = this.SelectedTrigger.FromPlayerName.Trim(); + this.Configuration.Save(); + }; + ImGui.SameLine(); + ImGuiComponents.HelpMarker("You can use RegExp. Leave empty for any. Ignored if chat listening to 'Echo' and chat message we through it."); + ImGui.TableNextRow(); + + + // TRIGGER START_AFTER + ImGui.TableNextColumn(); + ImGui.Text("Start after"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(185); + if(ImGui.SliderFloat("###TRIGGER_FORM_START_AFTER", ref this.SelectedTrigger.StartAfter, this.TRIGGER_MIN_AFTER, this.TRIGGER_MAX_AFTER)) { + this.SelectedTrigger.StartAfter = Helpers.ClampFloat(this.SelectedTrigger.StartAfter, this.TRIGGER_MIN_AFTER, this.TRIGGER_MAX_AFTER); + this.Configuration.Save(); + } + ImGui.SameLine(); + ImGui.SetNextItemWidth(45); + + if(ImGui.InputFloat("###TRIGGER_FORM_START_AFTER_INPUT", ref this.SelectedTrigger.StartAfter, this.TRIGGER_MIN_AFTER, this.TRIGGER_MAX_AFTER)) { + this.SelectedTrigger.StartAfter = Helpers.ClampFloat(this.SelectedTrigger.StartAfter, this.TRIGGER_MIN_AFTER, this.TRIGGER_MAX_AFTER); + this.Configuration.Save(); + } + ImGui.SameLine(); + ImGuiComponents.HelpMarker("In seconds"); + ImGui.TableNextRow(); + + // TRIGGER STOP_AFTER + ImGui.TableNextColumn(); + ImGui.Text("Stop after"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(185); + if(ImGui.SliderFloat("###TRIGGER_FORM_STOP_AFTER", ref this.SelectedTrigger.StopAfter, this.TRIGGER_MIN_AFTER, this.TRIGGER_MAX_AFTER)) { + this.SelectedTrigger.StopAfter = Helpers.ClampFloat(this.SelectedTrigger.StopAfter, this.TRIGGER_MIN_AFTER, this.TRIGGER_MAX_AFTER); + this.Configuration.Save(); + } + ImGui.SameLine(); + ImGui.SetNextItemWidth(45); + if(ImGui.InputFloat("###TRIGGER_FORM_STOP_AFTER_INPUT", ref this.SelectedTrigger.StopAfter, this.TRIGGER_MIN_AFTER, this.TRIGGER_MAX_AFTER)) { + this.SelectedTrigger.StopAfter = Helpers.ClampFloat(this.SelectedTrigger.StopAfter, this.TRIGGER_MIN_AFTER, this.TRIGGER_MAX_AFTER); + this.Configuration.Save(); + } + ImGui.SameLine(); + ImGuiComponents.HelpMarker("In seconds. Use zero to avoid stopping."); + ImGui.TableNextRow(); + + + // TRIGGER PRIORITY + ImGui.TableNextColumn(); + ImGui.Text("Priority"); + ImGui.TableNextColumn(); + if(ImGui.InputInt("###TRIGGER_FORM_PRIORITY", ref this.SelectedTrigger.Priority, 1)) { + this.Configuration.Save(); + } + ImGui.SameLine(); + ImGuiComponents.HelpMarker("If a trigger have a lower priority, it will be ignored."); + ImGui.TableNextRow(); + + ImGui.EndTable(); + + ImGui.Separator(); + + // TRIGGER KIND:CHAT OPTIONS + if(this.SelectedTrigger.Kind == (int)Triggers.KIND.Chat) { + + // TRIGGER FORM_TABLE_KIND_CHAT + ImGui.BeginTable("###TRIGGER_FORM_TABLE_KIND_CHAT", 2); + ImGui.TableSetupColumn("###TRIGGER_FORM_TABLE_KIND_CHAT_COL1", ImGuiTableColumnFlags.WidthFixed, COLUMN0_WIDTH); + ImGui.TableSetupColumn("###TRIGGER_FORM_TABLE_KIND_CHAT_COL2", ImGuiTableColumnFlags.WidthStretch); + + // TRIGGER CHAT_TEXT + ImGui.TableNextColumn(); + ImGui.Text("Chat text:"); + ImGui.TableNextColumn(); + string currentChatText = this.SelectedTrigger.ChatText; + if(ImGui.InputText("###TRIGGER_CHAT_TEXT", ref currentChatText, 250)) { + this.SelectedTrigger.ChatText = currentChatText.ToLower(); // ChatMsg is always lower + this.Configuration.Save(); + }; + ImGui.SameLine(); + ImGuiComponents.HelpMarker("You can use RegExp."); + ImGui.TableNextRow(); + + // TRIGGER CHAT_TEXT_TYPE_ALLOWED + ImGui.TableNextColumn(); + ImGui.Text("Add chat type:"); + ImGui.TableNextColumn(); + int currentTypeAllowed = 0; + string[] ChatTypesAllowedStrings = Enum.GetNames(typeof(XivChatType)); + if(ImGui.Combo("###TRIGGER_CHAT_TEXT_TYPE_ALLOWED", ref currentTypeAllowed, ChatTypesAllowedStrings, ChatTypesAllowedStrings.Length)) { + if(!this.SelectedTrigger.AllowedChatTypes.Contains(currentTypeAllowed)) { + int XivChatTypeValue = (int)(XivChatType)Enum.Parse(typeof(XivChatType), ChatTypesAllowedStrings[currentTypeAllowed]); + this.SelectedTrigger.AllowedChatTypes.Add(XivChatTypeValue); + } + this.Configuration.Save(); + } + ImGuiComponents.HelpMarker("Select some chats to observe or unselect all to watch every chats."); + ImGui.TableNextRow(); + + if(this.SelectedTrigger.AllowedChatTypes.Count > 0) { + + ImGui.TableNextColumn(); + ImGui.Text("Allowed Type:"); + ImGui.TableNextColumn(); + for(int indexAllowedChatType = 0; indexAllowedChatType < this.SelectedTrigger.AllowedChatTypes.Count; indexAllowedChatType++) { + int XivChatTypeValue = this.SelectedTrigger.AllowedChatTypes[indexAllowedChatType]; + if(ImGuiComponents.IconButton(indexAllowedChatType, Dalamud.Interface.FontAwesomeIcon.Minus)) { + this.SelectedTrigger.AllowedChatTypes.RemoveAt(indexAllowedChatType); + this.Configuration.Save(); + }; + ImGui.SameLine(); + string XivChatTypeName = ((XivChatType)XivChatTypeValue).ToString(); + ImGui.Text($"{XivChatTypeName}"); + + } + ImGui.TableNextRow(); + } + + + // END OF TABLE + ImGui.EndTable(); + } + + // TRIGGER FORM_TABLE_KIND_CHAT + ImGui.BeginTable("###TRIGGER_FORM_TABLE_KIND_SPELL", 2); + ImGui.TableSetupColumn("###TRIGGER_FORM_TABLE_KIND_SPELL_COL1", ImGuiTableColumnFlags.WidthFixed, COLUMN0_WIDTH); + ImGui.TableSetupColumn("###TRIGGER_FORM_TABLE_KIND_SPELL_COL2", ImGuiTableColumnFlags.WidthStretch); + + // TRIGGER KIND:SPELL OPTIONS + if(this.SelectedTrigger.Kind == (int)Triggers.KIND.Spell){ + // TRIGGER TYPE + ImGui.TableNextColumn(); + ImGui.Text("Type:"); + ImGui.TableNextColumn(); + string[] TRIGGER = System.Enum.GetNames(typeof(FFXIV_Vibe_Plugin.Commons.Structures.ActionEffectType)); + int currentEffectType = (int)this.SelectedTrigger.ActionEffectType; + if(ImGui.Combo("###TRIGGER_FORM_EVENT", ref currentEffectType, TRIGGER, TRIGGER.Length)) { + this.SelectedTrigger.ActionEffectType = currentEffectType; + this.SelectedTrigger.Reset(); + this.Configuration.Save(); + } + ImGui.TableNextRow(); + + //TRIGGER SPELL TEXT + ImGui.TableNextColumn(); + ImGui.Text("Spell Text:"); + ImGui.TableNextColumn(); + if(ImGui.InputText("###TRIGGER_FORM_SPELLNAME", ref this.SelectedTrigger.SpellText, 100)) { + this.Configuration.Save(); + } + ImGui.SameLine(); + ImGuiComponents.HelpMarker("You can use RegExp."); + ImGui.TableNextRow(); + + //TRIGGER DIRECTION + ImGui.TableNextColumn(); + ImGui.Text("Direction:"); + ImGui.TableNextColumn(); + string[] DIRECTIONS = System.Enum.GetNames(typeof(Triggers.DIRECTION)); + int currentDirection = (int)this.SelectedTrigger.Direction; + if(ImGui.Combo("###TRIGGER_FORM_DIRECTION", ref currentDirection, DIRECTIONS, DIRECTIONS.Length)) { + this.SelectedTrigger.Direction = currentDirection; + this.Configuration.Save(); + } + ImGui.SameLine(); + ImGuiComponents.HelpMarker("Warning: Hitting no target will result to self as if you cast on yourself"); + ImGui.TableNextRow(); + } + + if( + this.SelectedTrigger.ActionEffectType == (int)Structures.ActionEffectType.Damage || + this.SelectedTrigger.ActionEffectType == (int)Structures.ActionEffectType.Heal + || + this.SelectedTrigger.Kind == (int)Triggers.KIND.HPChange) + { + // Min/Max amount values + string type = ""; + if(this.SelectedTrigger.ActionEffectType == (int)Structures.ActionEffectType.Damage) { type = "damage"; } + if(this.SelectedTrigger.ActionEffectType == (int)Structures.ActionEffectType.Heal) { type = "heal"; } + if(this.SelectedTrigger.Kind == (int)Triggers.KIND.HPChange) { type = "health"; } + + // TRIGGER AMOUNT IN PERCENTAGE + ImGui.TableNextColumn(); + ImGui.Text("Amount in percentage?"); + ImGui.TableNextColumn(); + if(ImGui.Checkbox("###TRIGGER_AMOUNT_IN_PERCENTAGE", ref this.SelectedTrigger.AmountInPercentage)){ + this.SelectedTrigger.AmountMinValue = 0; + this.SelectedTrigger.AmountMaxValue = 100; + this.Configuration.Save(); + } + + + + // TRIGGER MIN_VALUE + ImGui.TableNextColumn(); + ImGui.Text($"Min {type} value:"); + ImGui.TableNextColumn(); + if (this.SelectedTrigger.AmountInPercentage) { + if(ImGui.SliderInt("###TRIGGER_FORM_MIN_AMOUNT", ref this.SelectedTrigger.AmountMinValue, 0, 100)) { + this.Configuration.Save(); + } + } else { + if (ImGui.InputInt("###TRIGGER_FORM_MIN_AMOUNT", ref this.SelectedTrigger.AmountMinValue, 100)) { + this.Configuration.Save(); + } + } + ImGui.TableNextRow(); + + // TRIGGER MAX_VALUE + ImGui.TableNextColumn(); + ImGui.Text($"Max {type} value:"); + ImGui.TableNextColumn(); + if (this.SelectedTrigger.AmountInPercentage) { + if (ImGui.SliderInt("###TRIGGER_FORM_MAX_AMOUNT", ref this.SelectedTrigger.AmountMaxValue, 0, 100)) { + this.Configuration.Save(); + } + } + else { + if (ImGui.InputInt("###TRIGGER_FORM_MAX_AMOUNT", ref this.SelectedTrigger.AmountMaxValue, 100)) { + this.Configuration.Save(); + } + } + ImGui.TableNextRow(); + } + ImGui.EndTable(); + + ImGui.TextColored(ImGuiColors.DalamudViolet, "Actions & Devices"); + ImGui.Separator(); + + // TRIGGER COMBO_DEVICES + Dictionary visitedDevice = DevicesController.GetVisitedDevices(); + if(visitedDevice.Count == 0) { + ImGui.TextColored(ImGuiColors.DalamudRed, "Please connect yourself to intiface and add device(s)..."); + } else { + string[] devicesStrings = visitedDevice.Keys.ToArray(); + ImGui.Combo("###TRIGGER_FORM_COMBO_DEVICES", ref this.TRIGGER_CURRENT_SELECTED_DEVICE, devicesStrings, devicesStrings.Length); + ImGui.SameLine(); + List triggerDevices = this.SelectedTrigger.Devices; + if(ImGuiComponents.IconButton(Dalamud.Interface.FontAwesomeIcon.Plus)) { + if(this.TRIGGER_CURRENT_SELECTED_DEVICE >= 0) { + Device.Device device = visitedDevice[devicesStrings[this.TRIGGER_CURRENT_SELECTED_DEVICE]]; + Triggers.TriggerDevice newTriggerDevice = new(device); + triggerDevices.Add(newTriggerDevice); + this.Configuration.Save(); + } + }; + + string[] patternNames = this.Patterns.GetAllPatterns().Select(p => p.Name).ToArray(); + + for(int indexDevice = 0; indexDevice < triggerDevices.Count; indexDevice++) { + string prefixLabel = $"###TRIGGER_FORM_COMBO_DEVICE_${indexDevice}"; + Triggers.TriggerDevice triggerDevice = triggerDevices[indexDevice]; + string deviceName = triggerDevice.Device != null ? triggerDevice.Device.Name : "UnknownDevice"; + if(ImGui.CollapsingHeader($"{deviceName}")) { + ImGui.Indent(10); + + if(triggerDevice != null && triggerDevice.Device != null) { + if(triggerDevice.Device.CanVibrate) { + if(ImGui.Checkbox($"{prefixLabel}_SHOULD_VIBRATE", ref triggerDevice.ShouldVibrate)) { + triggerDevice.ShouldStop = false; + this.Configuration.Save(); + } + ImGui.SameLine(); + ImGui.Text("Should Vibrate"); + if(triggerDevice.ShouldVibrate) { + ImGui.Indent(20); + for(int motorId = 0; motorId < triggerDevice.Device.VibrateMotors; motorId++) { + ImGui.Text($"Motor {motorId + 1}"); + ImGui.SameLine(); + // Display Vibrate Motor checkbox + if(ImGui.Checkbox($"{prefixLabel}_SHOULD_VIBRATE_MOTOR_{motorId}", ref triggerDevice.VibrateSelectedMotors[motorId])) { + this.Configuration.Save(); + } + + if(triggerDevice.VibrateSelectedMotors[motorId]) { + ImGui.SameLine(); + ImGui.SetNextItemWidth(90); + if(ImGui.Combo($"###{prefixLabel}_VIBRATE_PATTERNS_{motorId}", ref triggerDevice.VibrateMotorsPattern[motorId], patternNames, patternNames.Length)) { + this.Configuration.Save(); + } + + // Special intensity pattern asks for intensity param. + int currentPatternIndex = triggerDevice.VibrateMotorsPattern[motorId]; + ImGui.SameLine(); + ImGui.SetNextItemWidth(180); + if(ImGui.SliderInt($"{prefixLabel}_SHOULD_VIBRATE_MOTOR_{motorId}_THRESHOLD", ref triggerDevice.VibrateMotorsThreshold[motorId], 0, 100)) { + if(triggerDevice.VibrateMotorsThreshold[motorId] > 0) { + triggerDevice.VibrateSelectedMotors[motorId] = true; + } + this.Configuration.Save(); + } + } + } + ImGui.Indent(-20); + } + } + if(triggerDevice.Device.CanRotate) { + if(ImGui.Checkbox($"{prefixLabel}_SHOULD_ROTATE", ref triggerDevice.ShouldRotate)) { + triggerDevice.ShouldStop = false; + this.Configuration.Save(); + } + ImGui.SameLine(); + ImGui.Text("Should Rotate"); + if(triggerDevice.ShouldRotate) { + ImGui.Indent(20); + for(int motorId = 0; motorId < triggerDevice.Device.RotateMotors; motorId++) { + ImGui.Text($"Motor {motorId + 1}"); + ImGui.SameLine(); + if(ImGui.Checkbox($"{prefixLabel}_SHOULD_ROTATE_MOTOR_{motorId}", ref triggerDevice.RotateSelectedMotors[motorId])) { + this.Configuration.Save(); + } + if(triggerDevice.RotateSelectedMotors[motorId]) { + ImGui.SameLine(); + ImGui.SetNextItemWidth(90); + if(ImGui.Combo($"###{prefixLabel}_ROTATE_PATTERNS_{motorId}", ref triggerDevice.RotateMotorsPattern[motorId], patternNames, patternNames.Length)) { + this.Configuration.Save(); + } + // Special intensity pattern asks for intensity param. + int currentPatternIndex = triggerDevice.RotateMotorsPattern[motorId]; + ImGui.SameLine(); + ImGui.SetNextItemWidth(180); + if(ImGui.SliderInt($"{prefixLabel}_SHOULD_ROTATE_MOTOR_{motorId}_THRESHOLD", ref triggerDevice.RotateMotorsThreshold[motorId], 0, 100)) { + if(triggerDevice.RotateMotorsThreshold[motorId] > 0) { + triggerDevice.RotateSelectedMotors[motorId] = true; + } + this.Configuration.Save(); + } + + } + } + ImGui.Indent(-20); + } + } + if(triggerDevice.Device.CanLinear) { + if(ImGui.Checkbox($"{prefixLabel}_SHOULD_LINEAR", ref triggerDevice.ShouldLinear)) { + triggerDevice.ShouldStop = false; + this.Configuration.Save(); + } + ImGui.SameLine(); + ImGui.Text("Should Linear"); + if(triggerDevice.ShouldLinear) { + ImGui.Indent(20); + for(int motorId = 0; motorId < triggerDevice.Device.LinearMotors; motorId++) { + ImGui.Text($"Motor {motorId + 1}"); + ImGui.SameLine(); + if(ImGui.Checkbox($"{prefixLabel}_SHOULD_LINEAR_MOTOR_{motorId}", ref triggerDevice.LinearSelectedMotors[motorId])) { + this.Configuration.Save(); + } + if(triggerDevice.LinearSelectedMotors[motorId]) { + ImGui.SameLine(); + ImGui.SetNextItemWidth(90); + if(ImGui.Combo($"###{prefixLabel}_LINEAR_PATTERNS_{motorId}", ref triggerDevice.LinearMotorsPattern[motorId], patternNames, patternNames.Length)) { + this.Configuration.Save(); + } + // Special intensity pattern asks for intensity param. + int currentPatternIndex = triggerDevice.LinearMotorsPattern[motorId]; + ImGui.SameLine(); + ImGui.SetNextItemWidth(180); + if(ImGui.SliderInt($"{prefixLabel}_SHOULD_LINEAR_MOTOR_{motorId}_THRESHOLD", ref triggerDevice.LinearMotorsThreshold[motorId], 0, 100)) { + if(triggerDevice.LinearMotorsThreshold[motorId] > 0) { + triggerDevice.LinearSelectedMotors[motorId] = true; + } + this.Configuration.Save(); + } + } + } + ImGui.Indent(-20); + } + } + if(triggerDevice.Device.CanStop) { + if(ImGui.Checkbox($"{prefixLabel}_SHOULD_STOP", ref triggerDevice.ShouldStop)) { + triggerDevice.ShouldVibrate = false; + triggerDevice.ShouldRotate = false; + triggerDevice.ShouldLinear = false; + this.Configuration.Save(); + } + ImGui.SameLine(); + ImGui.Text("Should stop all motors"); + ImGui.SameLine(); + ImGuiComponents.HelpMarker("Instantly stop all motors for this device."); + } + if(ImGui.Button($"Remove###{prefixLabel}_REMOVE")) { + triggerDevices.RemoveAt(indexDevice); + this.Logger.Log($"DEBUG: removing {indexDevice}"); + this.Configuration.Save(); + } + } + ImGui.Indent(-10); + } + } + } + + + } else { + ImGui.TextColored(ImGuiColors.DalamudRed, "Current selected trigger is null"); + } + } else if(this.triggersViewMode == "delete") { + if(this.SelectedTrigger != null) { + ImGui.TextColored(ImGuiColors.DalamudRed, $"Are you sure you want to delete trigger ID: {this.SelectedTrigger.Id}"); + if(ImGui.Button("Yes")) { + if(this.SelectedTrigger != null) { + this.TriggerController.RemoveTrigger(this.SelectedTrigger); + this.SelectedTrigger = null; + this.Configuration.Save(); + } + this.triggersViewMode = "default"; + }; + ImGui.SameLine(); + if(ImGui.Button("No")) { + this.SelectedTrigger = null; + this.triggersViewMode = "default"; + }; + } + } + ImGui.EndChild(); + } + + if(ImGui.Button("Add")) { + Triggers.Trigger trigger = new("New Trigger"); + this.TriggerController.AddTrigger(trigger); + this.SelectedTrigger = trigger; + this.triggersViewMode = "edit"; + this.Configuration.Save(); + }; + ImGui.SameLine(); + if(ImGui.Button("Delete")) { + this.triggersViewMode = "delete"; + } + + } + + public void DrawPatternsTab() { + ImGui.TextColored(ImGuiColors.DalamudViolet, "Add or edit a new pattern:"); + ImGui.Indent(20); + List customPatterns = this.Patterns.GetCustomPatterns(); + ImGui.BeginTable("###PATTERN_ADD_FORM", 3); + ImGui.TableSetupColumn("###PATTERN_ADD_FORM_COL1", ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("###PATTERN_ADD_FORM_COL2", ImGuiTableColumnFlags.WidthFixed, 300); + ImGui.TableSetupColumn("###PATTERN_ADD_FORM_COL3", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableNextColumn(); + ImGui.Text("Pattern Name:"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(300); + if(ImGui.InputText("###PATTERNS_CURRENT_PATTERN_NAME_TO_ADD", ref this._tmp_currentPatternNameToAdd, 150)) { + this._tmp_currentPatternNameToAdd = this._tmp_currentPatternNameToAdd.Trim(); + } + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text("Pattern Value:"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(300); + if(ImGui.InputText("###PATTERNS_CURRENT_PATTERN_VALUE_TO_ADD", ref this._tmp_currentPatternValueToAdd, 500)) { + this._tmp_currentPatternValueToAdd = this._tmp_currentPatternValueToAdd.Trim(); + string value = this._tmp_currentPatternValueToAdd.Trim(); + if(value == "") { + this._tmp_currentPatternValueState = "unset"; + } else { + this._tmp_currentPatternValueState = Helpers.RegExpMatch(this.Logger, this._tmp_currentPatternValueToAdd, this.VALID_REGEXP_PATTERN) ? "valid" : "unvalid"; + } + } + + + + + if(this._tmp_currentPatternNameToAdd.Trim() != "" && this._tmp_currentPatternValueState == "valid") { + ImGui.TableNextColumn(); + if(ImGui.Button("Save")) { + Pattern newPattern = new(this._tmp_currentPatternNameToAdd, this._tmp_currentPatternValueToAdd); + this.Patterns.AddCustomPattern(newPattern); + this.ConfigurationProfile.PatternList = this.Patterns.GetCustomPatterns(); + this.Configuration.Save(); + this._tmp_currentPatternNameToAdd = ""; + this._tmp_currentPatternValueToAdd = ""; + this._tmp_currentPatternValueState = "unset"; + } + } + ImGui.TableNextRow(); + + if(this._tmp_currentPatternValueState == "unvalid") { + ImGui.TableNextColumn(); + ImGui.TextColored(ImGuiColors.DalamudRed, "WRONG FORMAT!"); + ImGui.TableNextColumn(); + ImGui.TextColored(ImGuiColors.DalamudRed, "Format: :|:..."); + ImGui.TableNextColumn(); + ImGui.TextColored(ImGuiColors.DalamudRed, "Eg: 10:500|100:1000|20:500|0:0"); + } + + ImGui.EndTable(); + ImGui.Indent(-20); + + + ImGui.Separator(); + + + if(customPatterns.Count == 0) { + ImGui.Text("No custom patterns, please add some"); + } else { + ImGui.TextColored(ImGuiColors.DalamudViolet, "Custom Patterns:"); + ImGui.Indent(20); + + ImGui.BeginTable("###PATTERN_CUSTOM_LIST", 3); + ImGui.TableSetupColumn("###PATTERN_CUSTOM_LIST_COL1", ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("###PATTERN_CUSTOM_LIST_COL2", ImGuiTableColumnFlags.WidthFixed, 430); + ImGui.TableSetupColumn("###PATTERN_CUSTOM_LIST_COL3", ImGuiTableColumnFlags.WidthStretch); + + // Searchbar + ImGui.TableNextColumn(); + ImGui.TextColored(ImGuiColors.DalamudGrey2, "Search name:"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(150); + ImGui.InputText("###PATTERN_SEARCH_BAR", ref CURRENT_PATTERN_SEARCHBAR, 200); + ImGui.TableNextRow(); + + + for(int patternIndex = 0; patternIndex < customPatterns.Count; patternIndex++) { + Pattern pattern = customPatterns[patternIndex]; + if(!Helpers.RegExpMatch(this.Logger, pattern.Name, this.CURRENT_PATTERN_SEARCHBAR)) { + continue; + } + ImGui.TableNextColumn(); + ImGui.Text($"{pattern.Name}"); + if(ImGui.IsItemHovered()) { + ImGui.SetTooltip($"{pattern.Name}"); + } + ImGui.TableNextColumn(); + string valueShort = pattern.Value; + if(valueShort.Length > 70) { + valueShort = $"{valueShort[..70]}..."; + } + ImGui.Text(valueShort); + if(ImGui.IsItemHovered()) { + ImGui.SetTooltip($"{pattern.Value}"); + } + + ImGui.TableNextColumn(); + + if(ImGuiComponents.IconButton(patternIndex, Dalamud.Interface.FontAwesomeIcon.Trash)) { + bool ok = this.Patterns.RemoveCustomPattern(pattern); + if(!ok) { + this.Logger.Error($"Could not remove pattern {pattern.Name}"); + } else { + List newPatternList = this.Patterns.GetCustomPatterns(); + this.ConfigurationProfile.PatternList = newPatternList; + this.Configuration.Save(); + } + } + ImGui.SameLine(); + if(ImGuiComponents.IconButton(patternIndex, Dalamud.Interface.FontAwesomeIcon.Pen)) { + this._tmp_currentPatternNameToAdd = pattern.Name; + this._tmp_currentPatternValueToAdd = pattern.Value; + this._tmp_currentPatternValueState = "valid"; + } + ImGui.TableNextRow(); + } + ImGui.EndTable(); + ImGui.Indent(-20); + } + + } + + public void DrawHelpTab() { + string help = App.GetHelp(this.app.CommandName); + ImGui.TextWrapped(help); + ImGui.TextColored(ImGuiColors.DalamudViolet, "Plugin information"); + ImGui.Text($"App version: {System.Reflection.Assembly.GetExecutingAssembly().GetName().Version}"); + ImGui.Text($"Config version: {this.Configuration.Version}"); + ImGui.TextColored(ImGuiColors.DalamudViolet, "Pattern information"); + ImGui.TextWrapped("You should use a string separated by the | (pipe) symbol with a pair of and ."); + ImGui.TextWrapped("Below is an example of a pattern that would vibe 1sec at 50pct intensity and 2sec at 100pct:"); + ImGui.TextWrapped("Pattern example:"); + this._tmp_void = "50:1000|100:2000"; + ImGui.InputText("###HELP_PATTERN_EXAMPLE", ref this._tmp_void, 50); + } + + } +} diff --git a/FFXIV_Vibe_Plugin/App/UI/UIBanner.cs b/FFXIV_Vibe_Plugin/App/UI/UIBanner.cs new file mode 100644 index 0000000..3a66ae4 --- /dev/null +++ b/FFXIV_Vibe_Plugin/App/UI/UIBanner.cs @@ -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); + + } + } +} diff --git a/FFXIV_Vibe_Plugin/App/UI/UIConnect.cs b/FFXIV_Vibe_Plugin/App/UI/UIConnect.cs new file mode 100644 index 0000000..1ab5d8a --- /dev/null +++ b/FFXIV_Vibe_Plugin/App/UI/UIConnect.cs @@ -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(); + } + } +} diff --git a/FFXIV_Vibe_Plugin/App/_Migrations/Migration_2.0.0_to_2.1.0_config_profile.cs b/FFXIV_Vibe_Plugin/App/_Migrations/Migration_2.0.0_to_2.1.0_config_profile.cs new file mode 100644 index 0000000..86f131d --- /dev/null +++ b/FFXIV_Vibe_Plugin/App/_Migrations/Migration_2.0.0_to_2.1.0_config_profile.cs @@ -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; + } + + + } + + +} diff --git a/FFXIV_Vibe_Plugin/Configuration.cs b/FFXIV_Vibe_Plugin/Configuration.cs deleted file mode 100644 index 432ed34..0000000 --- a/FFXIV_Vibe_Plugin/Configuration.cs +++ /dev/null @@ -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); - } - } -} diff --git a/FFXIV_Vibe_Plugin/FFXIV_Vibe_Plugin.csproj b/FFXIV_Vibe_Plugin/FFXIV_Vibe_Plugin.csproj index cea1413..64c578b 100644 --- a/FFXIV_Vibe_Plugin/FFXIV_Vibe_Plugin.csproj +++ b/FFXIV_Vibe_Plugin/FFXIV_Vibe_Plugin.csproj @@ -21,7 +21,7 @@ - + PreserveNewest false @@ -37,6 +37,7 @@ + $(DalamudLibPath)FFXIVClientStructs.dll diff --git a/FFXIV_Vibe_Plugin/FFXIV_Vibe_Plugin.json b/FFXIV_Vibe_Plugin/FFXIV_Vibe_Plugin.json index 89ec8e6..5d58dd0 100644 --- a/FFXIV_Vibe_Plugin/FFXIV_Vibe_Plugin.json +++ b/FFXIV_Vibe_Plugin/FFXIV_Vibe_Plugin.json @@ -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" + ] } diff --git a/FFXIV_Vibe_Plugin/Plugin.cs b/FFXIV_Vibe_Plugin/Plugin.cs index db8a82a..c0f96e8 100644 --- a/FFXIV_Vibe_Plugin/Plugin.cs +++ b/FFXIV_Vibe_Plugin/Plugin.cs @@ -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; + } + } } diff --git a/FFXIV_Vibe_Plugin/Windows/ConfigWindow.cs b/FFXIV_Vibe_Plugin/Windows/ConfigWindow.cs index 22b328c..d09287a 100644 --- a/FFXIV_Vibe_Plugin/Windows/ConfigWindow.cs +++ b/FFXIV_Vibe_Plugin/Windows/ConfigWindow.cs @@ -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(); + }*/ + } } diff --git a/FFXIV_Vibe_Plugin/Windows/MainWindow.cs b/FFXIV_Vibe_Plugin/Windows/MainWindow.cs index fe7fc40..c047068 100644 --- a/FFXIV_Vibe_Plugin/Windows/MainWindow.cs +++ b/FFXIV_Vibe_Plugin/Windows/MainWindow.cs @@ -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); + } } diff --git a/FFXIV_Vibe_Plugin/packages.lock.json b/FFXIV_Vibe_Plugin/packages.lock.json index 2426061..c317704 100644 --- a/FFXIV_Vibe_Plugin/packages.lock.json +++ b/FFXIV_Vibe_Plugin/packages.lock.json @@ -1,13 +1,39 @@ -{ - "version": 1, - "dependencies": { - "net7.0-windows7.0": { - "DalamudPackager": { - "type": "Direct", - "requested": "[2.1.10, )", - "resolved": "2.1.10", - "contentHash": "S6NrvvOnLgT4GDdgwuKVJjbFo+8ZEj+JsEYk9ojjOR/MMfv1dIFpT8aRJQfI24rtDcw1uF+GnSSMN4WW1yt7fw==" - } - } - } +{ + "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==" + } + } + } } \ No newline at end of file