From 1a0790ffd4e403f4a9f281b831fe5eff91598a8b Mon Sep 17 00:00:00 2001 From: Kacie <90719658+kaciexx@users.noreply.github.com> Date: Tue, 24 Jan 2023 22:50:56 +0100 Subject: [PATCH] fix: moving to App wrapper --- .editorconfig | 363 ++++-- FFXIV_Vibe_Plugin/App/App.cs | 339 +++++ FFXIV_Vibe_Plugin/App/Commons/Helpers.cs | 50 + FFXIV_Vibe_Plugin/App/Commons/Logger.cs | 132 ++ FFXIV_Vibe_Plugin/App/Commons/OpCodes.cs | 251 ++++ FFXIV_Vibe_Plugin/App/Commons/Patterns.cs | 100 ++ FFXIV_Vibe_Plugin/App/Commons/Structures.cs | 130 ++ FFXIV_Vibe_Plugin/App/Configuration.cs | 131 ++ FFXIV_Vibe_Plugin/App/Data/logo.png | Bin 0 -> 134027 bytes FFXIV_Vibe_Plugin/App/Devices/Device.cs | 193 +++ .../App/Devices/DevicesController.cs | 435 +++++++ .../App/Experimental/NetworkCapture.cs | 86 ++ FFXIV_Vibe_Plugin/App/Hooks/ActionEffect.cs | 229 ++++ FFXIV_Vibe_Plugin/App/PlayerStats.cs | 84 ++ FFXIV_Vibe_Plugin/App/Triggers/ChatTrigger.cs | 27 + FFXIV_Vibe_Plugin/App/Triggers/Trigger.cs | 131 ++ .../App/Triggers/TriggersController.cs | 158 +++ .../App/UI/Components/ButtonLink.cs | 26 + FFXIV_Vibe_Plugin/App/UI/PluginUI.cs | 1128 +++++++++++++++++ FFXIV_Vibe_Plugin/App/UI/UIBanner.cs | 40 + FFXIV_Vibe_Plugin/App/UI/UIConnect.cs | 61 + ...Migration_2.0.0_to_2.1.0_config_profile.cs | 56 + FFXIV_Vibe_Plugin/Configuration.cs | 28 - FFXIV_Vibe_Plugin/FFXIV_Vibe_Plugin.csproj | 3 +- FFXIV_Vibe_Plugin/FFXIV_Vibe_Plugin.json | 24 +- FFXIV_Vibe_Plugin/Plugin.cs | 122 +- FFXIV_Vibe_Plugin/Windows/ConfigWindow.cs | 46 +- FFXIV_Vibe_Plugin/Windows/MainWindow.cs | 64 +- FFXIV_Vibe_Plugin/packages.lock.json | 50 +- 29 files changed, 4184 insertions(+), 303 deletions(-) create mode 100644 FFXIV_Vibe_Plugin/App/App.cs create mode 100644 FFXIV_Vibe_Plugin/App/Commons/Helpers.cs create mode 100644 FFXIV_Vibe_Plugin/App/Commons/Logger.cs create mode 100644 FFXIV_Vibe_Plugin/App/Commons/OpCodes.cs create mode 100644 FFXIV_Vibe_Plugin/App/Commons/Patterns.cs create mode 100644 FFXIV_Vibe_Plugin/App/Commons/Structures.cs create mode 100644 FFXIV_Vibe_Plugin/App/Configuration.cs create mode 100644 FFXIV_Vibe_Plugin/App/Data/logo.png create mode 100644 FFXIV_Vibe_Plugin/App/Devices/Device.cs create mode 100644 FFXIV_Vibe_Plugin/App/Devices/DevicesController.cs create mode 100644 FFXIV_Vibe_Plugin/App/Experimental/NetworkCapture.cs create mode 100644 FFXIV_Vibe_Plugin/App/Hooks/ActionEffect.cs create mode 100644 FFXIV_Vibe_Plugin/App/PlayerStats.cs create mode 100644 FFXIV_Vibe_Plugin/App/Triggers/ChatTrigger.cs create mode 100644 FFXIV_Vibe_Plugin/App/Triggers/Trigger.cs create mode 100644 FFXIV_Vibe_Plugin/App/Triggers/TriggersController.cs create mode 100644 FFXIV_Vibe_Plugin/App/UI/Components/ButtonLink.cs create mode 100644 FFXIV_Vibe_Plugin/App/UI/PluginUI.cs create mode 100644 FFXIV_Vibe_Plugin/App/UI/UIBanner.cs create mode 100644 FFXIV_Vibe_Plugin/App/UI/UIConnect.cs create mode 100644 FFXIV_Vibe_Plugin/App/_Migrations/Migration_2.0.0_to_2.1.0_config_profile.cs delete mode 100644 FFXIV_Vibe_Plugin/Configuration.cs 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 0000000000000000000000000000000000000000..27abd8007dd18520b8a86911e114f583594828a7 GIT binary patch literal 134027 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 literal 0 HcmV?d00001 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