Initial HSMappy release (fork of Mappy)
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
# Remove the line below if you want to inherit .editorconfig settings from higher directories
|
||||
root = true
|
||||
|
||||
# C# files
|
||||
[*.cs]
|
||||
|
||||
#### Core EditorConfig Options ####
|
||||
|
||||
# Indentation and spacing
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
tab_width = 4
|
||||
|
||||
# New line preferences
|
||||
end_of_line = crlf
|
||||
insert_final_newline = false
|
||||
|
||||
#### .NET Code Actions ####
|
||||
|
||||
# Type members
|
||||
dotnet_hide_advanced_members = false
|
||||
dotnet_member_insertion_location = with_other_members_of_the_same_kind
|
||||
dotnet_property_generation_behavior = prefer_throwing_properties
|
||||
|
||||
# Symbol search
|
||||
dotnet_search_reference_assemblies = true
|
||||
|
||||
#### .NET Coding Conventions ####
|
||||
|
||||
# Organize usings
|
||||
dotnet_separate_import_directive_groups = false
|
||||
dotnet_sort_system_directives_first = true
|
||||
file_header_template = unset
|
||||
|
||||
# this. and Me. preferences
|
||||
dotnet_style_qualification_for_event = false
|
||||
dotnet_style_qualification_for_field = false
|
||||
dotnet_style_qualification_for_method = false
|
||||
dotnet_style_qualification_for_property = false
|
||||
|
||||
# Language keywords vs BCL types preferences
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true
|
||||
dotnet_style_predefined_type_for_member_access = true
|
||||
|
||||
# Parentheses preferences
|
||||
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
|
||||
|
||||
# Modifier preferences
|
||||
dotnet_style_require_accessibility_modifiers = for_non_interface_members
|
||||
|
||||
# Expression-level preferences
|
||||
dotnet_prefer_system_hash_code = true
|
||||
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_collection_expression = when_types_loosely_match
|
||||
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
|
||||
|
||||
# Field preferences
|
||||
dotnet_style_readonly_field = true
|
||||
|
||||
# Parameter preferences
|
||||
dotnet_code_quality_unused_parameters = all
|
||||
|
||||
# Suppression preferences
|
||||
dotnet_remove_unnecessary_suppression_exclusions = none
|
||||
|
||||
# New line preferences
|
||||
dotnet_style_allow_multiple_blank_lines_experimental = true
|
||||
dotnet_style_allow_statement_immediately_after_block_experimental = true
|
||||
|
||||
#### C# Coding Conventions ####
|
||||
|
||||
# var preferences
|
||||
csharp_style_var_elsewhere = true:silent
|
||||
csharp_style_var_for_built_in_types = true:silent
|
||||
csharp_style_var_when_type_is_apparent = true:silent
|
||||
|
||||
# Expression-bodied members
|
||||
csharp_style_expression_bodied_accessors = true:silent
|
||||
csharp_style_expression_bodied_constructors = false:silent
|
||||
csharp_style_expression_bodied_indexers = true:silent
|
||||
csharp_style_expression_bodied_lambdas = true:silent
|
||||
csharp_style_expression_bodied_local_functions = false:silent
|
||||
csharp_style_expression_bodied_methods = false:silent
|
||||
csharp_style_expression_bodied_operators = false:silent
|
||||
csharp_style_expression_bodied_properties = true:silent
|
||||
|
||||
# Pattern matching preferences
|
||||
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
|
||||
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
|
||||
csharp_style_prefer_extended_property_pattern = true:suggestion
|
||||
csharp_style_prefer_not_pattern = true:suggestion
|
||||
csharp_style_prefer_pattern_matching = true:silent
|
||||
csharp_style_prefer_switch_expression = true:suggestion
|
||||
|
||||
# Null-checking preferences
|
||||
csharp_style_conditional_delegate_call = true:suggestion
|
||||
|
||||
# Modifier preferences
|
||||
csharp_prefer_static_anonymous_function = true:suggestion
|
||||
csharp_prefer_static_local_function = true:suggestion
|
||||
csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
|
||||
csharp_style_prefer_readonly_struct = true:suggestion
|
||||
csharp_style_prefer_readonly_struct_member = true:suggestion
|
||||
|
||||
# Code-block preferences
|
||||
csharp_prefer_braces = true:silent
|
||||
csharp_prefer_simple_using_statement = true:suggestion
|
||||
csharp_prefer_system_threading_lock = true:suggestion
|
||||
csharp_style_namespace_declarations = file_scoped:silent
|
||||
csharp_style_prefer_method_group_conversion = true:silent
|
||||
csharp_style_prefer_primary_constructors = true:suggestion
|
||||
csharp_style_prefer_top_level_statements = true:silent
|
||||
|
||||
# Expression-level preferences
|
||||
csharp_prefer_simple_default_expression = true:suggestion
|
||||
csharp_style_deconstructed_variable_declaration = true:suggestion
|
||||
csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
|
||||
csharp_style_inlined_variable_declaration = true:suggestion
|
||||
csharp_style_prefer_implicitly_typed_lambda_expression = true:suggestion
|
||||
csharp_style_prefer_index_operator = true:suggestion
|
||||
csharp_style_prefer_local_over_anonymous_function = true:suggestion
|
||||
csharp_style_prefer_null_check_over_type_check = true:suggestion
|
||||
csharp_style_prefer_range_operator = true:suggestion
|
||||
csharp_style_prefer_tuple_swap = true:suggestion
|
||||
csharp_style_prefer_unbound_generic_type_in_nameof = true:suggestion
|
||||
csharp_style_prefer_utf8_string_literals = true:suggestion
|
||||
csharp_style_throw_expression = true:suggestion
|
||||
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
|
||||
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
|
||||
|
||||
# 'using' directive preferences
|
||||
csharp_using_directive_placement = outside_namespace:silent
|
||||
|
||||
# New line preferences
|
||||
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
|
||||
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent
|
||||
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent
|
||||
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent
|
||||
csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent
|
||||
|
||||
#### C# Formatting Rules ####
|
||||
|
||||
# New line preferences
|
||||
csharp_new_line_before_catch = true
|
||||
csharp_new_line_before_else = true
|
||||
csharp_new_line_before_finally = true
|
||||
csharp_new_line_before_members_in_anonymous_types = true
|
||||
csharp_new_line_before_members_in_object_initializers = true
|
||||
csharp_new_line_before_open_brace = all
|
||||
csharp_new_line_between_query_expression_clauses = true
|
||||
|
||||
# Indentation preferences
|
||||
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
|
||||
|
||||
# Space preferences
|
||||
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
|
||||
|
||||
# Wrapping preferences
|
||||
csharp_preserve_single_line_blocks = true
|
||||
csharp_preserve_single_line_statements = true
|
||||
|
||||
#### Naming styles ####
|
||||
|
||||
# Naming rules
|
||||
|
||||
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
|
||||
|
||||
# Symbol specifications
|
||||
|
||||
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 =
|
||||
|
||||
# Naming styles
|
||||
|
||||
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
|
||||
|
||||
[*.{cs,vb}]
|
||||
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||
tab_width = 4
|
||||
indent_size = 4
|
||||
end_of_line = crlf
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
|
||||
dotnet_style_prefer_auto_properties = true:silent
|
||||
dotnet_style_object_initializer = true:suggestion
|
||||
dotnet_style_collection_initializer = true:suggestion
|
||||
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
|
||||
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
|
||||
dotnet_style_prefer_conditional_expression_over_return = true:silent
|
||||
dotnet_style_explicit_tuple_names = true:suggestion
|
||||
dotnet_style_prefer_inferred_tuple_names = true:suggestion
|
||||
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
|
||||
dotnet_style_prefer_compound_assignment = true:suggestion
|
||||
dotnet_style_prefer_simplified_interpolation = true:suggestion
|
||||
dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
|
||||
dotnet_style_namespace_match_folder = true:suggestion
|
||||
dotnet_style_readonly_field = true:suggestion
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
|
||||
dotnet_style_predefined_type_for_member_access = true:silent
|
||||
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
|
||||
dotnet_style_allow_multiple_blank_lines_experimental = true:silent
|
||||
dotnet_style_allow_statement_immediately_after_block_experimental = true:silent
|
||||
dotnet_code_quality_unused_parameters = all:suggestion
|
||||
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
|
||||
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
|
||||
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
|
||||
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
|
||||
dotnet_style_qualification_for_field = false:silent
|
||||
dotnet_style_qualification_for_property = false:silent
|
||||
dotnet_style_qualification_for_method = false:silent
|
||||
dotnet_style_qualification_for_event = false:silent
|
||||
@@ -0,0 +1,18 @@
|
||||
using KamiLib.Classes;
|
||||
using KamiLib.Extensions;
|
||||
using Lumina.Excel.Sheets;
|
||||
|
||||
namespace Mappy.Classes.Caches;
|
||||
|
||||
public class AetheryteAethernetCache : Cache<uint, Aetheryte?>
|
||||
{
|
||||
protected override Aetheryte? LoadValue(uint key)
|
||||
{
|
||||
if (Service.DataManager.GetExcelSheet<Aetheryte>().FirstOrNull(aetheryte => aetheryte.AethernetName.RowId == key) is not { AethernetGroup: var aethernetGroup })
|
||||
return null;
|
||||
if (Service.DataManager.GetExcelSheet<Aetheryte>().FirstOrNull(aetheryte => aetheryte.IsAetheryte && aetheryte.AethernetGroup == aethernetGroup) is not { } targetAetheryte)
|
||||
return null;
|
||||
|
||||
return targetAetheryte;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Linq;
|
||||
using KamiLib.Classes;
|
||||
using Lumina.Excel.Sheets;
|
||||
|
||||
namespace Mappy.Classes.Caches;
|
||||
|
||||
public class CardRewardCache : Cache<uint, string>
|
||||
{
|
||||
protected override string LoadValue(uint key)
|
||||
{
|
||||
if (Service.DataManager.GetExcelSheet<TripleTriad>().GetRow(key) is { RowId: not 0 } triadInfo) {
|
||||
var cardRewards = triadInfo.ItemPossibleReward
|
||||
.Where(reward => reward.RowId is not 0)
|
||||
.Select(reward => reward.Value)
|
||||
.Where(item => item.RowId is not 0)
|
||||
.Select(item => item.Name.ExtractText());
|
||||
|
||||
return string.Join("\n", cardRewards);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using KamiLib.Classes;
|
||||
using Lumina.Excel.Sheets;
|
||||
|
||||
namespace Mappy.Classes.Caches;
|
||||
|
||||
public class GatheringPointIconCache : Cache<uint, uint>
|
||||
{
|
||||
protected override uint LoadValue(uint key)
|
||||
{
|
||||
var gatheringPoint = Service.DataManager.GetExcelSheet<GatheringPoint>().GetRow(key);
|
||||
var gatheringPointBase = Service.DataManager.GetExcelSheet<GatheringPointBase>().GetRow(gatheringPoint.GatheringPointBase.RowId);
|
||||
|
||||
return gatheringPointBase.GatheringType.RowId switch
|
||||
{
|
||||
0 => 60438,
|
||||
1 => 60437,
|
||||
2 => 60433,
|
||||
3 => 60432,
|
||||
5 => 60445,
|
||||
_ => throw new Exception($"Unknown Gathering Type: {gatheringPointBase.GatheringType.RowId}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using KamiLib.Classes;
|
||||
using Lumina.Excel.Sheets;
|
||||
|
||||
namespace Mappy.Classes.Caches;
|
||||
|
||||
public class GatheringPointNameCache : Cache<(uint dataId, string name), string>
|
||||
{
|
||||
protected override string LoadValue((uint dataId, string name) key)
|
||||
{
|
||||
var gatheringPoint = Service.DataManager.GetExcelSheet<GatheringPoint>().GetRow(key.dataId);
|
||||
var gatheringPointBase = Service.DataManager.GetExcelSheet<GatheringPointBase>().GetRow(gatheringPoint.GatheringPointBase.RowId);
|
||||
|
||||
return $"Lv. {gatheringPointBase.GatheringLevel.ToString()} {key.name}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using KamiLib.Classes;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Lumina.Extensions;
|
||||
|
||||
namespace Mappy.Classes.Caches;
|
||||
|
||||
public class TooltipCache : Cache<uint, string>
|
||||
{
|
||||
protected override string LoadValue(uint key)
|
||||
{
|
||||
var mapMarker = Service.DataManager.GetExcelSheet<MapSymbol>().FirstOrNull(marker => marker.Icon == key);
|
||||
|
||||
if (mapMarker is null) return string.Empty;
|
||||
if (!mapMarker.Value.PlaceName.IsValid) return string.Empty;
|
||||
|
||||
return mapMarker.Value.PlaceName.Value.Name.ExtractText();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using KamiLib.Classes;
|
||||
using Lumina.Excel.Sheets;
|
||||
|
||||
namespace Mappy.Classes.Caches;
|
||||
|
||||
public class TripleTriadCache : Cache<uint, bool>
|
||||
{
|
||||
protected override bool LoadValue(uint key) => Service.DataManager.GetExcelSheet<TripleTriad>().HasRow(key);
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using KamiLib.Classes;
|
||||
using Mappy.Data;
|
||||
using SeString = Dalamud.Game.Text.SeStringHandling.SeString;
|
||||
|
||||
namespace Mappy.Classes;
|
||||
|
||||
public class MarkerInfo
|
||||
{
|
||||
public required Vector2 Position { get; set; }
|
||||
public required Vector2 Offset { get; set; }
|
||||
public required float Scale { get; set; }
|
||||
public uint? ObjectiveId { get; init; }
|
||||
public uint? DataId { get; set; }
|
||||
public MarkerType MarkerType { get; set; }
|
||||
public uint IconId { get; set; }
|
||||
public Func<string?>? PrimaryText { get; set; }
|
||||
public Func<string?>? SecondaryText { get; set; }
|
||||
public float? Radius { get; set; }
|
||||
public Vector4 RadiusColor { get; set; } = KnownColor.CornflowerBlue.Vector();
|
||||
public Vector4 RadiusOutlineColor { get; set; } = KnownColor.CornflowerBlue.Vector();
|
||||
public Action? OnRightClicked { get; set; }
|
||||
public Action? OnLeftClicked { get; set; }
|
||||
public bool IsDynamicMarker { get; init; }
|
||||
}
|
||||
|
||||
public static class DrawHelpers
|
||||
{
|
||||
private static bool DebugMode => System.SystemConfig.DebugMode;
|
||||
|
||||
public const uint QuestionMarkIcon = 60071;
|
||||
|
||||
/// <summary>
|
||||
/// Offset Vector of SelectedX, SelectedY, scaled with SelectedSizeFactor
|
||||
/// </summary>
|
||||
public static Vector2 GetMapOffsetVector() => GetRawMapOffsetVector() * GetMapScaleFactor();
|
||||
|
||||
/// <summary>
|
||||
/// Unscaled Vector of SelectedX, SelectedY
|
||||
/// </summary>
|
||||
public static unsafe Vector2 GetRawMapOffsetVector() => new(AgentMap.Instance()->SelectedOffsetX, AgentMap.Instance()->SelectedOffsetY);
|
||||
|
||||
/// <summary>
|
||||
/// Selected Scale Factor
|
||||
/// </summary>
|
||||
public static unsafe float GetMapScaleFactor() => AgentMap.Instance()->SelectedMapSizeFactorFloat;
|
||||
|
||||
/// <summary>
|
||||
/// 1024 vector, center offset vector
|
||||
/// </summary>
|
||||
public static Vector2 GetMapCenterOffsetVector() => new(1024.0f, 1024.0f);
|
||||
|
||||
/// <summary>
|
||||
/// Offset for the top left corner of the drawn map
|
||||
/// </summary>
|
||||
public static Vector2 GetCombinedOffsetVector() => -GetMapOffsetVector() + GetMapCenterOffsetVector();
|
||||
|
||||
public static void DrawMapMarker(MarkerInfo markerInfo)
|
||||
{
|
||||
if (markerInfo.IconId is 0) return;
|
||||
|
||||
// Don't draw markers that are positioned off the map texture
|
||||
if (markerInfo.Position.X < 0.0f || markerInfo.Position.X > 2048.0f * markerInfo.Scale || markerInfo.Position.Y < 0.0f ||
|
||||
markerInfo.Position.Y > 2048.0f * markerInfo.Scale)
|
||||
return;
|
||||
|
||||
markerInfo.IconId = markerInfo.IconId switch
|
||||
{
|
||||
// Translate circle markers that don't have icons, into [?] icon
|
||||
>= 60483 and <= 60494 => QuestionMarkIcon,
|
||||
|
||||
// Translate Gemstone Trader Icon into smaller version... why square, why.
|
||||
60091 => 61731,
|
||||
|
||||
// Leave all other icons as they were
|
||||
_ => markerInfo.IconId,
|
||||
};
|
||||
|
||||
if (DebugMode) {
|
||||
markerInfo.SecondaryText = markerInfo.PrimaryText;
|
||||
markerInfo.PrimaryText = () => $"[Debug] IconId: {markerInfo.IconId}";
|
||||
}
|
||||
|
||||
// If this is the first time we have seen this iconId, save it
|
||||
if (System.IconConfig.IconSettingMap.TryAdd(markerInfo.IconId, new IconSetting { IconId = markerInfo.IconId, })) {
|
||||
System.IconConfig.Save();
|
||||
}
|
||||
|
||||
// If this icon is disabled, don't even process it
|
||||
if (System.IconConfig.IconSettingMap[markerInfo.IconId] is { Hide: true }) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only process modules for Dynamic Markers
|
||||
if (markerInfo.IsDynamicMarker) {
|
||||
foreach (var module in System.Modules) {
|
||||
if (module.ProcessMarker(markerInfo)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DrawRadiusUnderlay(markerInfo);
|
||||
DrawIcon(markerInfo);
|
||||
ProcessInteractions(markerInfo);
|
||||
DrawTooltip(markerInfo);
|
||||
}
|
||||
|
||||
private static unsafe void DrawRadiusUnderlay(MarkerInfo markerInfo)
|
||||
{
|
||||
if (markerInfo is not { Radius: { } markerRadius and > 1.0f }) return;
|
||||
|
||||
var center = markerInfo.Position + markerInfo.Offset + ImGui.GetWindowPos();
|
||||
DrawRadiusCircle(center, markerRadius, markerInfo.Scale, AgentMap.Instance()->SelectedMapSizeFactorFloat,
|
||||
markerInfo.RadiusColor with { W = System.SystemConfig.AreaColor.W },
|
||||
markerInfo.RadiusOutlineColor with { W = System.SystemConfig.AreaOutlineColor.W });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw the quest/area radius circle using the same formula as the area map.
|
||||
/// Used by both the area map (DrawRadiusUnderlay) and the minimap so behavior is identical.
|
||||
/// </summary>
|
||||
public static unsafe void DrawRadiusCircle(Vector2 centerScreen, float markerRadius, float mapScale, float sizeFactor, Vector4? fillColor = null, Vector4? outlineColor = null)
|
||||
{
|
||||
if (markerRadius <= 1.0f) return;
|
||||
var radiusPixels = markerRadius * mapScale * sizeFactor;
|
||||
if (radiusPixels < 0.5f) return;
|
||||
|
||||
var fill = ImGui.GetColorU32(fillColor ?? System.SystemConfig.AreaColor);
|
||||
var outline = ImGui.GetColorU32(outlineColor ?? System.SystemConfig.AreaOutlineColor);
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
drawList.AddCircleFilled(centerScreen, radiusPixels, fill);
|
||||
drawList.AddCircle(centerScreen, radiusPixels, outline, 0, 3.0f);
|
||||
}
|
||||
|
||||
private static void DrawIcon(MarkerInfo markerInfo)
|
||||
{
|
||||
var texture = Service.TextureProvider.GetFromGameIcon(markerInfo.IconId).GetWrapOrEmpty();
|
||||
var scale = System.SystemConfig.ScaleWithZoom ? markerInfo.Scale : 1.0f;
|
||||
|
||||
var iconScale = System.SystemConfig.IconScale;
|
||||
|
||||
if (markerInfo.IconId is 60401 or 60402) {
|
||||
scale *= 2.0f;
|
||||
}
|
||||
|
||||
// Fixed scale not supported for map region markers
|
||||
if (IsRegionIcon(markerInfo.IconId)) {
|
||||
scale = markerInfo.Scale;
|
||||
iconScale = 0.42f;
|
||||
}
|
||||
|
||||
ImGui.SetCursorPos(markerInfo.Position + markerInfo.Offset - texture.Size * iconScale / 2.0f * scale * System.IconConfig.IconSettingMap[markerInfo.IconId].Scale);
|
||||
var cursorScreenPos = ImGui.GetCursorScreenPos();
|
||||
var iconSize = texture.Size * scale * iconScale * System.IconConfig.IconSettingMap[markerInfo.IconId].Scale;
|
||||
|
||||
ImGui.Image(texture.Handle, iconSize, Vector2.Zero, Vector2.One, System.IconConfig.IconSettingMap[markerInfo.IconId].Color);
|
||||
|
||||
if (DebugMode) {
|
||||
foreach (var x in Enumerable.Range(-1, 3)) {
|
||||
foreach (var y in Enumerable.Range(-1, 3)) {
|
||||
ImGui.GetWindowDrawList().AddRect(cursorScreenPos + new Vector2(x, y), cursorScreenPos + iconSize, ImGui.GetColorU32(KnownColor.White.Vector()), 3.0f);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.GetWindowDrawList().AddRect(cursorScreenPos, cursorScreenPos + iconSize, ImGui.GetColorU32(KnownColor.Red.Vector()), 3.0f);
|
||||
}
|
||||
}
|
||||
|
||||
public static void DrawText(MarkerInfo markerInfo, SeString text) => DrawText(markerInfo, text.ToString());
|
||||
|
||||
public static void DrawText(MarkerInfo markerInfo, string text)
|
||||
{
|
||||
using var largeFont = System.LargeAxisFontHandle.Push();
|
||||
ImGui.SetWindowFontScale(markerInfo.Scale);
|
||||
|
||||
var textSize = ImGui.CalcTextSize(text);
|
||||
var drawPosition = markerInfo.Position + markerInfo.Offset + ImGui.GetWindowPos() - textSize / 2.0f;
|
||||
|
||||
drawPosition = new Vector2(MathF.Round(drawPosition.X), MathF.Round(drawPosition.Y));
|
||||
|
||||
if (System.SystemConfig.DebugMode) {
|
||||
ImGui.GetWindowDrawList().AddCircleFilled(markerInfo.Position + markerInfo.Offset + ImGui.GetWindowPos(), 5.0f, ImGui.GetColorU32(KnownColor.Red.Vector()));
|
||||
ImGui.GetWindowDrawList().AddRect(drawPosition, drawPosition + textSize, ImGui.GetColorU32(KnownColor.Green.Vector()), 3.0f);
|
||||
}
|
||||
|
||||
foreach (var x in Enumerable.Range(-1, 3)) {
|
||||
foreach (var y in Enumerable.Range(-1, 3)) {
|
||||
if (x is 0 && y is 0) continue;
|
||||
|
||||
ImGui.SetCursorScreenPos(drawPosition + new Vector2(x, y));
|
||||
ImGui.TextColored(KnownColor.Black.Vector(), text);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SetCursorScreenPos(drawPosition);
|
||||
ImGui.TextColored(KnownColor.White.Vector(), text);
|
||||
|
||||
ImGui.SetWindowFontScale(1.0f);
|
||||
}
|
||||
|
||||
private static void ProcessInteractions(MarkerInfo markerInfo)
|
||||
{
|
||||
if (System.IconConfig.IconSettingMap[markerInfo.IconId] is not { AllowClick: true }) return;
|
||||
|
||||
if (markerInfo is { OnRightClicked: { } rightClickAction } && ImGui.IsItemClicked(ImGuiMouseButton.Right)) {
|
||||
rightClickAction.Invoke();
|
||||
}
|
||||
|
||||
if (markerInfo is { OnLeftClicked: { } leftClickAction } && ImGui.IsItemClicked(ImGuiMouseButton.Left)) {
|
||||
leftClickAction.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe void DrawTooltip(MarkerInfo markerInfo)
|
||||
{
|
||||
if (System.IconConfig.IconSettingMap[markerInfo.IconId] is { AllowTooltip: false } && !DebugMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
var isActivatedViaRadius = false;
|
||||
|
||||
if (markerInfo is { Radius: { } sameRadius and > 1.0f }) {
|
||||
var center = markerInfo.Position + markerInfo.Offset + ImGui.GetWindowPos();
|
||||
var radius = sameRadius * markerInfo.Scale * AgentMap.Instance()->SelectedMapSizeFactorFloat;
|
||||
|
||||
if (Vector2.Distance(ImGui.GetMousePos() - System.MapWindow.MapDrawOffset + ImGui.GetWindowPos(), center) <= radius && System.MapWindow.HoveredFlags.Any()) {
|
||||
isActivatedViaRadius = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isActivatedViaRadius || ImGui.IsItemHovered()) {
|
||||
if (markerInfo.PrimaryText?.Invoke() is { Length: > 0 } primaryText) {
|
||||
using var tooltip = ImRaii.Tooltip();
|
||||
|
||||
ImGui.Image(Service.TextureProvider.GetFromGameIcon(markerInfo.IconId).GetWrapOrEmpty().Handle, ImGuiHelpers.ScaledVector2(32.0f, 32.0f));
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 7.5f * ImGuiHelpers.GlobalScale);
|
||||
var cursorPosition = ImGui.GetCursorPos();
|
||||
ImGui.Text(primaryText);
|
||||
|
||||
if (markerInfo.SecondaryText?.Invoke() is { Length: > 0 } secondaryText) {
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPos(cursorPosition);
|
||||
ImGuiTweaks.TextColoredUnformatted(KnownColor.Gray.Vector(), $"\n{secondaryText}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsDisallowedIcon(uint iconId) =>
|
||||
iconId switch
|
||||
{
|
||||
60091 => true,
|
||||
_ when IsRegionIcon(iconId) => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
public static bool IsRegionIcon(uint iconId) =>
|
||||
iconId switch
|
||||
{
|
||||
>= 63200 and < 63900 => true,
|
||||
>= 62620 and < 62800 => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
|
||||
namespace Mappy.Classes;
|
||||
|
||||
[Flags]
|
||||
public enum HoverFlags
|
||||
{
|
||||
Nothing = 0,
|
||||
MapTexture = 1 << 0,
|
||||
Toolbar = 1 << 1,
|
||||
CoordinateBar = 1 << 2,
|
||||
Window = 1 << 3,
|
||||
WindowInnerFrame = 1 << 4,
|
||||
}
|
||||
|
||||
public static class HoverFlagsExtensions
|
||||
{
|
||||
public static bool Any(this HoverFlags flags) => flags != HoverFlags.Nothing;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using KamiLib.Window;
|
||||
using Mappy.Data;
|
||||
using Mappy.Windows;
|
||||
|
||||
namespace Mappy.Classes.MapWindowComponents;
|
||||
|
||||
public unsafe class MapContextMenu
|
||||
{
|
||||
public void Draw(Vector2 mapDrawOffset)
|
||||
{
|
||||
using var contextMenu = ImRaii.ContextPopup("Mappy_Context_Menu");
|
||||
|
||||
if (!contextMenu) return;
|
||||
|
||||
if (ImGui.MenuItem("Place Flag")) {
|
||||
var cursorPosition = ImGui.GetMousePosOnOpeningCurrentPopup(); // Get initial cursor position (screen relative)
|
||||
var mapChildOffset = mapDrawOffset; // Get the screen position we started drawing the map at
|
||||
var mapDrawPositionOffset = System.MapRenderer.DrawPosition; // Get the map texture top left offset vector
|
||||
var textureClickLocation = (cursorPosition - mapChildOffset - mapDrawPositionOffset) / MapRenderer.MapRenderer.Scale; // Math
|
||||
var result = textureClickLocation - new Vector2(1024.0f, 1024.0f); // One of our vectors made the map centered, undo it.
|
||||
var scaledResult = result / DrawHelpers.GetMapScaleFactor() + DrawHelpers.GetRawMapOffsetVector(); // Apply offset x/y and scalefactor
|
||||
|
||||
AgentMap.Instance()->FlagMarkerCount = 0;
|
||||
AgentMap.Instance()->SetFlagMapMarker(AgentMap.Instance()->SelectedTerritoryId, AgentMap.Instance()->SelectedMapId, scaledResult.X, scaledResult.Y);
|
||||
AgentChatLog.Instance()->InsertTextCommandParam(1048, false);
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem("Remove Flag", false, AgentMap.Instance()->FlagMarkerCount is not 0)) {
|
||||
AgentMap.Instance()->FlagMarkerCount = 0;
|
||||
}
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5.0f);
|
||||
|
||||
if (ImGui.MenuItem("Center on Player", false, Service.ObjectTable.LocalPlayer is not null) && Service.ObjectTable.LocalPlayer is not null) {
|
||||
System.IntegrationsController.OpenOccupiedMap();
|
||||
System.MapRenderer.CenterOnGameObject(Service.ObjectTable.LocalPlayer);
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem("Center on Map")) {
|
||||
System.SystemConfig.FollowPlayer = false;
|
||||
System.MapRenderer.DrawOffset = Vector2.Zero;
|
||||
}
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5.0f);
|
||||
|
||||
if (ImGui.MenuItem("Lock Zoom", "", ref System.SystemConfig.ZoomLocked)) {
|
||||
SystemConfig.Save();
|
||||
}
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5.0f);
|
||||
|
||||
if (ImGui.MenuItem("Open Quest List", false, System.WindowManager.GetWindow<QuestListWindow>() is null)) {
|
||||
System.WindowManager.AddWindow(new QuestListWindow(), WindowFlags.OpenImmediately | WindowFlags.RequireLoggedIn);
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem("Open Fate List", false, System.WindowManager.GetWindow<FateListWindow>() is null)) {
|
||||
System.WindowManager.AddWindow(new FateListWindow(), WindowFlags.OpenImmediately | WindowFlags.RequireLoggedIn);
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem("Open Flag List", false, System.WindowManager.GetWindow<FlagHistoryWindow>() is null)) {
|
||||
System.WindowManager.AddWindow(new FlagHistoryWindow(), WindowFlags.OpenImmediately | WindowFlags.RequireLoggedIn);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
|
||||
namespace Mappy.Classes.MapWindowComponents;
|
||||
|
||||
public unsafe class MapCoordinateBar
|
||||
{
|
||||
public void Draw(bool isMapHovered, Vector2 mapDrawOffset)
|
||||
{
|
||||
var coordinateBarSize = new Vector2(ImGui.GetContentRegionMax().X, 20.0f * ImGuiHelpers.GlobalScale);
|
||||
ImGui.SetCursorPos(ImGui.GetContentRegionMax() - coordinateBarSize);
|
||||
|
||||
using var childBackgroundStyle = ImRaii.PushColor(ImGuiCol.ChildBg, Vector4.Zero with { W = System.SystemConfig.CoordinateBarFade });
|
||||
using var coordinateChild = ImRaii.Child("coordinate_child", coordinateBarSize);
|
||||
if (!coordinateChild) return;
|
||||
|
||||
var offsetX = -AgentMap.Instance()->SelectedOffsetX;
|
||||
var offsetY = -AgentMap.Instance()->SelectedOffsetY;
|
||||
var scale = AgentMap.Instance()->SelectedMapSizeFactor;
|
||||
|
||||
var characterMapPosition = MapUtil.WorldToMap(Service.ObjectTable.LocalPlayer?.Position ?? Vector3.Zero, offsetX, offsetY, 0, (uint)scale);
|
||||
var characterPosition = $"Character {characterMapPosition.X:F1} {characterMapPosition.Y:F1}";
|
||||
|
||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 2.0f * ImGuiHelpers.GlobalScale);
|
||||
|
||||
var characterStringSize = ImGui.CalcTextSize(characterPosition);
|
||||
ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X / 3.0f - characterStringSize.X / 2.0f);
|
||||
|
||||
if (AgentMap.Instance()->SelectedMapId == AgentMap.Instance()->CurrentMapId) {
|
||||
ImGui.TextColored(System.SystemConfig.CoordinateTextColor, characterPosition);
|
||||
}
|
||||
|
||||
if (isMapHovered) {
|
||||
var cursorPosition = ImGui.GetMousePos() - mapDrawOffset;
|
||||
cursorPosition -= System.MapRenderer.DrawPosition;
|
||||
cursorPosition /= MapRenderer.MapRenderer.Scale;
|
||||
cursorPosition -= new Vector2(1024.0f, 1024.0f);
|
||||
cursorPosition -= new Vector2(offsetX, offsetY);
|
||||
cursorPosition /= AgentMap.Instance()->SelectedMapSizeFactorFloat;
|
||||
|
||||
var cursorMapPosition = MapUtil.WorldToMap(new Vector3(cursorPosition.X, 0.0f, cursorPosition.Y), offsetX, offsetY, 0, (uint)scale);
|
||||
var cursorPositionString = $"Cursor {cursorMapPosition.X:F1} {cursorMapPosition.Y:F1}";
|
||||
var cursorStringSize = ImGui.CalcTextSize(characterPosition);
|
||||
ImGui.SameLine(ImGui.GetContentRegionMax().X * 2.0f / 3.0f - cursorStringSize.X / 2.0f);
|
||||
ImGui.TextColored(System.SystemConfig.CoordinateTextColor, cursorPositionString);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiLib.Extensions;
|
||||
using KamiLib.Window;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Mappy.Windows;
|
||||
using MapType = Lumina.Excel.Sheets.MapType;
|
||||
|
||||
namespace Mappy.Classes.MapWindowComponents;
|
||||
|
||||
public unsafe class MapToolbar
|
||||
{
|
||||
public void Draw()
|
||||
{
|
||||
var toolbarSize = new Vector2(ImGui.GetContentRegionMax().X, 33.0f * ImGuiHelpers.GlobalScale);
|
||||
|
||||
using var childBackgroundStyle = ImRaii.PushColor(ImGuiCol.ChildBg, Vector4.Zero with { W = System.SystemConfig.ToolbarFade });
|
||||
using var toolbarChild = ImRaii.Child("toolbar_child", toolbarSize);
|
||||
if (!toolbarChild) return;
|
||||
|
||||
ImGui.SetCursorPos(new Vector2(5.0f, 5.0f));
|
||||
|
||||
if (MappyGuiTweaks.IconButton(FontAwesomeIcon.ArrowUp, "up", "Open Parent Map")) {
|
||||
var valueArgs = new AtkValue
|
||||
{
|
||||
Type = ValueType.Int, Int = 5,
|
||||
};
|
||||
|
||||
var returnValue = new AtkValue();
|
||||
AgentMap.Instance()->ReceiveEvent(&returnValue, &valueArgs, 1, 0);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (MappyGuiTweaks.IconButton(FontAwesomeIcon.LayerGroup, "layers", "Show Map Layers")) {
|
||||
ImGui.OpenPopup("Mappy_Show_Layers");
|
||||
}
|
||||
|
||||
DrawLayersContextMenu();
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
using (var _ = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetStyle().GetColor(ImGuiCol.ButtonActive), System.SystemConfig.FollowPlayer)) {
|
||||
if (MappyGuiTweaks.IconButton(FontAwesomeIcon.LocationArrow, "follow", "Toggle Follow Player")) {
|
||||
System.SystemConfig.FollowPlayer = !System.SystemConfig.FollowPlayer;
|
||||
|
||||
if (System.SystemConfig.FollowPlayer) {
|
||||
System.IntegrationsController.OpenOccupiedMap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (MappyGuiTweaks.IconButton(FontAwesomeIcon.ArrowsToCircle, "centerPlayer", "Center on Player") && Service.ObjectTable.LocalPlayer is not null) {
|
||||
// Don't center on player if we are already following the player.
|
||||
if (!System.SystemConfig.FollowPlayer) {
|
||||
System.IntegrationsController.OpenOccupiedMap();
|
||||
System.MapRenderer.CenterOnGameObject(Service.ObjectTable.LocalPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (MappyGuiTweaks.IconButton(FontAwesomeIcon.MapMarked, "centerMap", "Center on Map")) {
|
||||
System.SystemConfig.FollowPlayer = false;
|
||||
System.MapRenderer.DrawOffset = Vector2.Zero;
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (MappyGuiTweaks.IconButton(FontAwesomeIcon.Search, "search", "Search for Map")) {
|
||||
System.WindowManager.AddWindow(new MapSelectionWindow
|
||||
{
|
||||
SingleSelectionCallback = selection =>
|
||||
{
|
||||
if (selection?.Map != null) {
|
||||
if (AgentMap.Instance()->SelectedMapId != selection.Map.RowId) {
|
||||
System.IntegrationsController.OpenMap(selection.Map.RowId);
|
||||
}
|
||||
|
||||
if (selection.MarkerLocation is { } location) {
|
||||
System.SystemConfig.FollowPlayer = false;
|
||||
System.MapRenderer.DrawOffset = -location + DrawHelpers.GetMapCenterOffsetVector();
|
||||
}
|
||||
}
|
||||
},
|
||||
}, WindowFlags.OpenImmediately | WindowFlags.RequireLoggedIn);
|
||||
}
|
||||
|
||||
var offset = System.SystemConfig.HideWindowFrame ? 50.0f : 25.0f;
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - offset * ImGuiHelpers.GlobalScale - ImGui.GetStyle().ItemSpacing.X);
|
||||
|
||||
if (MappyGuiTweaks.IconButton(FontAwesomeIcon.Cog, "settings", "Open Settings")) {
|
||||
System.ConfigWindow.UnCollapseOrShow();
|
||||
ImGui.SetWindowFocus(System.ConfigWindow.WindowName);
|
||||
}
|
||||
|
||||
if (!System.SystemConfig.HideWindowFrame) return;
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (MappyGuiTweaks.IconButton(FontAwesomeIcon.Times, "closeMap", "Close Map"))
|
||||
{
|
||||
System.MapWindow.Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawLayersContextMenu()
|
||||
{
|
||||
using var contextMenu = ImRaii.Popup("Mappy_Show_Layers");
|
||||
if (!contextMenu) return;
|
||||
|
||||
var currentMap = Service.DataManager.GetExcelSheet<Map>().GetRow(AgentMap.Instance()->SelectedMapId);
|
||||
if (currentMap.RowId is 0) return;
|
||||
|
||||
// If this is a region map
|
||||
if (currentMap.MapType.RowId == 3) {
|
||||
foreach (var marker in AgentMap.Instance()->MapMarkers) {
|
||||
if (!DrawHelpers.IsRegionIcon(marker.MapMarker.IconId)) continue;
|
||||
|
||||
var label = marker.MapMarker.Subtext.AsDalamudSeString();
|
||||
|
||||
if (ImGui.MenuItem(label.ToString())) {
|
||||
System.IntegrationsController.OpenMap(marker.DataKey);
|
||||
System.SystemConfig.FollowPlayer = false;
|
||||
System.MapRenderer.DrawOffset = Vector2.Zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Any other map
|
||||
else {
|
||||
var layers = Service.DataManager.GetExcelSheet<Map>()
|
||||
.Where(eachMap => eachMap.PlaceName.RowId == currentMap.PlaceName.RowId)
|
||||
.Where(eachMap => eachMap.MapIndex != 0)
|
||||
.OrderBy(eachMap => eachMap.MapIndex)
|
||||
.ToList();
|
||||
|
||||
if (layers.Count is 0) {
|
||||
ImGui.Text("No layers for this map");
|
||||
}
|
||||
|
||||
foreach (var layer in layers) {
|
||||
if (ImGui.MenuItem(layer.PlaceNameSub.Value.Name.ExtractText(), "", AgentMap.Instance()->SelectedMapId == layer.RowId)) {
|
||||
System.IntegrationsController.OpenMap(layer.RowId);
|
||||
System.SystemConfig.FollowPlayer = false;
|
||||
System.MapRenderer.DrawOffset = Vector2.Zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
|
||||
namespace Mappy.Classes;
|
||||
|
||||
public static class MappyGuiTweaks
|
||||
{
|
||||
public static bool IconButton(FontAwesomeIcon icon, string id, string? tooltip)
|
||||
{
|
||||
using var imRaiiId = ImRaii.PushId(id);
|
||||
|
||||
bool result;
|
||||
|
||||
using (Service.PluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) {
|
||||
result = ImGui.Button($"{icon.ToIconString()}");
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled) && tooltip is not null) {
|
||||
ImGui.SetTooltip(tooltip);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Mappy.Classes;
|
||||
|
||||
public enum MarkerType
|
||||
{
|
||||
Unknown = 0,
|
||||
Fate = 1,
|
||||
Stellar = 6,
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using KamiLib.Extensions;
|
||||
using Lumina.Excel.Sheets;
|
||||
|
||||
namespace Mappy.Classes.SelectionWindowComponents;
|
||||
|
||||
public class AetheryteDrawableOption : DrawableOption
|
||||
{
|
||||
public required Aetheryte Aetheryte { get; set; }
|
||||
|
||||
public override string ExtraLineLong => GetName();
|
||||
|
||||
public override Map Map => GetAetheryteMap()!.Value; // Probably a bad idea
|
||||
|
||||
protected override string[] GetAdditionalFilterStrings() =>
|
||||
[
|
||||
Aetheryte.PlaceName.Value.Name.ExtractText(),
|
||||
Aetheryte.AethernetName.Value.Name.ExtractText(),
|
||||
];
|
||||
|
||||
protected override void DrawIcon()
|
||||
{
|
||||
using var imageFrame = ImRaii.Child($"image_frame{Aetheryte.RowId}#{MarkerLocation}#{ExtraLineLong}", new Vector2(Width, Height), false, ImGuiWindowFlags.NoInputs);
|
||||
if (!imageFrame) return;
|
||||
|
||||
var xOffset = (Width - Height) / 2.0f;
|
||||
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + xOffset);
|
||||
ImGui.Image(Service.TextureProvider.GetFromGameIcon(Aetheryte.IsAetheryte ? 60453 : 60430).GetWrapOrEmpty().Handle, new Vector2(Height, Height));
|
||||
}
|
||||
|
||||
private Map? GetAetheryteMap()
|
||||
{
|
||||
if (Aetheryte.Map.RowId is not 0) return Aetheryte.Map.Value;
|
||||
|
||||
if (Service.DataManager.GetExcelSheet<Aetheryte>().FirstOrNull(aetheryte => aetheryte.IsAetheryte && aetheryte.AethernetGroup == Aetheryte.AethernetGroup) is not
|
||||
{ } targetAetheryte)
|
||||
return null;
|
||||
|
||||
return targetAetheryte.Map.Value;
|
||||
}
|
||||
|
||||
private string GetName()
|
||||
{
|
||||
if (Aetheryte.AethernetName.RowId is not 0) return Aetheryte.AethernetName.Value.Name.ExtractText();
|
||||
if (Aetheryte.PlaceName.RowId is not 0) return Aetheryte.PlaceName.Value.Name.ExtractText();
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public override string GetElementKey() => base.GetElementKey() + $"{Aetheryte.RowId}";
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Utility;
|
||||
using Lumina.Excel.Sheets;
|
||||
|
||||
namespace Mappy.Classes.SelectionWindowComponents;
|
||||
|
||||
public abstract class DrawableOption
|
||||
{
|
||||
protected virtual string[] GetAdditionalFilterStrings() => [];
|
||||
|
||||
public virtual Map Map { get; set; }
|
||||
|
||||
protected static float Width => 133.5f * ImGuiHelpers.GlobalScale;
|
||||
|
||||
protected static float Height => 75.0f * ImGuiHelpers.GlobalScale;
|
||||
|
||||
protected abstract void DrawIcon();
|
||||
|
||||
public virtual string ExtraLineLong => string.Empty;
|
||||
|
||||
public virtual string ExtraLineShort => string.Empty;
|
||||
|
||||
public virtual Vector2? MarkerLocation => null;
|
||||
|
||||
public virtual string GetElementKey() => $"{Map.RowId}{MarkerLocation}{ExtraLineShort}{ExtraLineLong}";
|
||||
|
||||
public string[] GetFilterStrings()
|
||||
{
|
||||
if (Map.RowId is 0) return [];
|
||||
|
||||
var baseStrings = new[]
|
||||
{
|
||||
Map.PlaceNameRegion.ValueNullable?.Name.ExtractText() ?? string.Empty, Map.PlaceName.ValueNullable?.Name.ExtractText() ?? string.Empty,
|
||||
Map.PlaceNameSub.ValueNullable?.Name.ExtractText() ?? string.Empty, Map.TerritoryType.ValueNullable?.Name.ExtractText() ?? string.Empty, Map.Id.ExtractText(),
|
||||
};
|
||||
|
||||
return baseStrings.Concat(GetAdditionalFilterStrings()).ToArray();
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
using var id = ImRaii.PushId(Map.RowId.ToString());
|
||||
|
||||
DrawIcon();
|
||||
ImGui.SameLine();
|
||||
|
||||
using var contentsFrame = ImRaii.Child($"contents_frame#{GetElementKey()}", new Vector2(ImGui.GetContentRegionAvail().X, Height), false, ImGuiWindowFlags.NoInputs);
|
||||
if (!contentsFrame) return;
|
||||
|
||||
ImGuiHelpers.ScaledDummy(1.0f);
|
||||
|
||||
using var table = ImRaii.Table("data_table", 2, ImGuiTableFlags.SizingStretchProp);
|
||||
if (!table) return;
|
||||
|
||||
ImGui.TableSetupColumn("##column1", ImGuiTableColumnFlags.None, 2.0f);
|
||||
ImGui.TableSetupColumn("##column2", ImGuiTableColumnFlags.None, 1.0f);
|
||||
|
||||
var placeName = Map.PlaceName.ValueNullable?.Name.ExtractText() ?? string.Empty;
|
||||
var zoneName = Map.PlaceNameSub.ValueNullable?.Name.ExtractText() ?? string.Empty;
|
||||
var regionName = Map.PlaceNameRegion.ValueNullable?.Name.ExtractText() ?? string.Empty;
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(placeName);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(Map.RowId.ToString());
|
||||
|
||||
ImGui.TableNextRow();
|
||||
ImGui.TableNextColumn();
|
||||
|
||||
using var grayColor = ImRaii.PushColor(ImGuiCol.Text, KnownColor.DarkGray.Vector());
|
||||
if (!zoneName.IsNullOrEmpty() && !regionName.IsNullOrEmpty()) {
|
||||
ImGui.TextUnformatted($"{regionName}, {zoneName}");
|
||||
}
|
||||
else if (!zoneName.IsNullOrEmpty()) {
|
||||
ImGui.TextUnformatted($"{zoneName}");
|
||||
}
|
||||
else if (!regionName.IsNullOrEmpty()) {
|
||||
ImGui.TextUnformatted($"{regionName}");
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted($"{Map.Id}");
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(ExtraLineLong);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(ExtraLineShort);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Lumina.Excel.Sheets;
|
||||
|
||||
namespace Mappy.Classes.SelectionWindowComponents;
|
||||
|
||||
public class MapDrawableOption : DrawableOption
|
||||
{
|
||||
protected override void DrawIcon()
|
||||
{
|
||||
var option = Map.TerritoryType.Value;
|
||||
|
||||
using var imageFrame = ImRaii.Child($"image_frame{option}", new Vector2(Width, Height), false, ImGuiWindowFlags.NoInputs);
|
||||
if (!imageFrame) return;
|
||||
|
||||
var texture = GetMapTexture(Map.RowId);
|
||||
if (texture is not null) {
|
||||
ImGui.Image(texture.Handle, new Vector2(Width, Height), new Vector2(0.15f, 0.15f), new Vector2(0.85f, 0.85f));
|
||||
}
|
||||
else {
|
||||
ImGuiHelpers.ScaledDummy(Width, Height);
|
||||
}
|
||||
}
|
||||
|
||||
public static IDalamudTextureWrap? GetMapTexture(uint mapId)
|
||||
{
|
||||
if (mapId is 0) return null;
|
||||
|
||||
var map = Service.DataManager.GetExcelSheet<Map>().GetRow(mapId);
|
||||
var territory = map.TerritoryType;
|
||||
if (!territory.IsValid) return null;
|
||||
|
||||
var loadingImage = territory.Value.LoadingImage;
|
||||
if (!loadingImage.IsValid) return null;
|
||||
|
||||
var texturePath = $"ui/loadingimage/{loadingImage.Value.FileName}_hr1.tex";
|
||||
return Service.TextureProvider.GetFromGame(texturePath).GetWrapOrDefault();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Lumina.Excel.Sheets;
|
||||
|
||||
namespace Mappy.Classes.SelectionWindowComponents;
|
||||
|
||||
public class PoiDrawableOption : DrawableOption
|
||||
{
|
||||
public required MapMarker MapMarker { get; set; }
|
||||
|
||||
public override Vector2? MarkerLocation => new Vector2(MapMarker.X, MapMarker.Y);
|
||||
|
||||
public override Map Map => Service.DataManager.GetExcelSheet<Map>().FirstOrDefault(map => map.MapMarkerRange == MapMarker.RowId);
|
||||
|
||||
public override string ExtraLineLong => MapMarker.PlaceNameSubtext.Value.Name.ExtractText();
|
||||
|
||||
protected override void DrawIcon()
|
||||
{
|
||||
using var imageFrame = ImRaii.Child($"image_frame{MapMarker.RowId}#{MarkerLocation}", new Vector2(Width, Height), false, ImGuiWindowFlags.NoInputs);
|
||||
if (!imageFrame) return;
|
||||
|
||||
var xOffset = (Width - Height) / 2.0f;
|
||||
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + xOffset);
|
||||
ImGui.Image(Service.TextureProvider.GetFromGameIcon((uint)MapMarker.Icon).GetWrapOrEmpty().Handle, new Vector2(Height, Height));
|
||||
}
|
||||
|
||||
protected override string[] GetAdditionalFilterStrings() =>
|
||||
[
|
||||
MapMarker.PlaceNameSubtext.Value.Name.ExtractText(),
|
||||
];
|
||||
|
||||
public override string GetElementKey() => base.GetElementKey() + $"{MapMarker.RowId}";
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using Dalamud.Game.Addon.Lifecycle;
|
||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using KamiLib.CommandManager;
|
||||
using KamiLib.Extensions;
|
||||
using Mappy.Extensions;
|
||||
using Mappy.Windows;
|
||||
|
||||
namespace Mappy.Controllers;
|
||||
|
||||
public unsafe class AddonAreaMapController : IDisposable
|
||||
{
|
||||
public AddonAreaMapController()
|
||||
{
|
||||
Service.Log.Debug("Beginning Listening for AddonAreaMap");
|
||||
Service.Framework.Update += AddonAreaMapListener;
|
||||
|
||||
Service.AddonLifecycle.RegisterListener(AddonEvent.PreDraw, "AreaMap", OnAreaMapDraw);
|
||||
|
||||
Service.AddonLifecycle.RegisterListener(AddonEvent.PreShow, "AreaMap", OnAreaMapPreShow);
|
||||
Service.AddonLifecycle.RegisterListener(AddonEvent.PreHide, "AreaMap", OnAreaMapPreHide);
|
||||
|
||||
// Add a special error handler for the case that somehow the addon is stuck offscreen
|
||||
System.CommandManager.RegisterCommand(new CommandHandler
|
||||
{
|
||||
ActivationPath = "/areamap/reset",
|
||||
Delegate = _ =>
|
||||
{
|
||||
var addon = Service.GameGui.GetAddonByName<AddonAreaMap>("AreaMap");
|
||||
if (addon is not null && addon->RootNode is not null)
|
||||
{
|
||||
addon->RootNode->SetPositionFloat(addon->X, addon->Y);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private void AddonAreaMapListener(IFramework framework)
|
||||
{
|
||||
var addonAreaMap = Service.GameGui.GetAddonByName<AddonAreaMap>("AreaMap");
|
||||
|
||||
if (addonAreaMap is null) return;
|
||||
|
||||
if (System.SystemConfig.SuppressNativeMapOpenSound) {
|
||||
addonAreaMap->OpenSoundEffectId = 0;
|
||||
addonAreaMap->Flags1A2 |= (byte)(1 << BitOperations.Log2(0x20));
|
||||
}
|
||||
|
||||
Service.Framework.Update -= AddonAreaMapListener;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Service.AddonLifecycle.UnregisterListener(OnAreaMapDraw);
|
||||
Service.AddonLifecycle.UnregisterListener(AddonEvent.PreShow, "AreaMap");
|
||||
Service.AddonLifecycle.UnregisterListener(AddonEvent.PreHide, "AreaMap");
|
||||
Service.Framework.Update -= AddonAreaMapListener;
|
||||
|
||||
// Reset windows root node position on dispose
|
||||
var addonAreaMap = Service.GameGui.GetAddonByName<AddonAreaMap>("AreaMap");
|
||||
if (addonAreaMap is not null)
|
||||
{
|
||||
addonAreaMap->RootNode->SetPositionFloat(addonAreaMap->X, addonAreaMap->Y);
|
||||
|
||||
if (System.SystemConfig.SuppressNativeMapOpenSound) {
|
||||
addonAreaMap->OpenSoundEffectId = 23;
|
||||
addonAreaMap->Flags1A2 &= (byte)~(1 << BitOperations.Log2(0x20));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// public void EnableIntegrations()
|
||||
// {
|
||||
// Service.Log.Debug("Enabling Area Map Integrations");
|
||||
// }
|
||||
//
|
||||
// //
|
||||
// public void DisableIntegrations()
|
||||
// {
|
||||
// Service.Log.Debug("Disabling Area Map Integrations");
|
||||
// }
|
||||
//
|
||||
|
||||
private void OnAreaMapPreShow(AddonEvent type, AddonArgs args)
|
||||
{
|
||||
Service.Log.Verbose($"[AreaMap] AddonEventPreShow");
|
||||
if (IntegrationsController.SilentRefreshInProgress) return; // silent refresh: keep map "open" but don't show Mappy's window
|
||||
System.WindowManager.GetWindow<MapWindow>()?.Open();
|
||||
}
|
||||
|
||||
private void OnAreaMapPreHide(AddonEvent type, AddonArgs args)
|
||||
{
|
||||
Service.Log.Verbose($"[AreaMap] AreaMapPreHide");
|
||||
|
||||
if (System.SystemConfig.KeepOpen)
|
||||
{
|
||||
Service.Log.Verbose("[AreaMap] Keeping Open");
|
||||
return;
|
||||
}
|
||||
|
||||
// If the window actually considered closed by the agent.
|
||||
if (AgentMap.Instance()->AddonId is 0)
|
||||
{
|
||||
System.WindowManager.GetWindow<MapWindow>()?.Close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void OnAreaMapDraw(AddonEvent type, AddonArgs args)
|
||||
{
|
||||
var addon = args.GetAddon<AddonAreaMap>();
|
||||
|
||||
if (Service.ClientState is { IsPvP: true })
|
||||
{
|
||||
if (addon->IsOffscreen())
|
||||
addon->RestorePosition();
|
||||
return;
|
||||
}
|
||||
|
||||
// Have to check for color, because it likes to animate a fadeout,
|
||||
// and we want the map to stay completely hidden until it's done.
|
||||
if (addon->IsVisible || addon->RootNode->Color.A is not 0x00)
|
||||
{
|
||||
addon->ForceOffscreen();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// only if the window is actually closed
|
||||
if (AgentMap.Instance()->AddonId is 0)
|
||||
{
|
||||
addon->RestorePosition();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using Dalamud.Hooking;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using KamiLib.Classes;
|
||||
using Mappy.Data;
|
||||
|
||||
namespace Mappy.Controllers;
|
||||
|
||||
public unsafe class FlagController : IDisposable
|
||||
{
|
||||
private readonly Hook<AgentMap.Delegates.SetFlagMapMarker>? setFlagMapMarkerHook;
|
||||
|
||||
public FlagController()
|
||||
{
|
||||
setFlagMapMarkerHook ??= Service.Hooker.HookFromAddress<AgentMap.Delegates.SetFlagMapMarker>(AgentMap.MemberFunctionPointers.SetFlagMapMarker, OnSetFlagMapMarker);
|
||||
|
||||
if (Service.ClientState is { IsPvP: false }) {
|
||||
EnableIntegrations();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
setFlagMapMarkerHook?.Dispose();
|
||||
}
|
||||
|
||||
public void EnableIntegrations()
|
||||
{
|
||||
setFlagMapMarkerHook?.Enable();
|
||||
}
|
||||
|
||||
public void DisableIntegrations()
|
||||
{
|
||||
setFlagMapMarkerHook?.Disable();
|
||||
}
|
||||
|
||||
private void OnSetFlagMapMarker(AgentMap* thisPtr, uint territoryId, uint mapId, float x, float y, uint iconId) =>
|
||||
HookSafety.ExecuteSafe(() =>
|
||||
{
|
||||
var newFlagData = new Flag(territoryId, mapId, x, y, iconId);
|
||||
var dataFile = System.FlagConfig;
|
||||
|
||||
if (!dataFile.FlagHistory.Contains(newFlagData)) {
|
||||
dataFile.FlagHistory.AddFirst(new Flag(territoryId, mapId, x, y, iconId));
|
||||
|
||||
if (dataFile.FlagHistory.Count > dataFile.HistoryLimit) {
|
||||
dataFile.FlagHistory.RemoveLast();
|
||||
}
|
||||
|
||||
dataFile.Save();
|
||||
}
|
||||
|
||||
setFlagMapMarkerHook!.Original.Invoke(thisPtr, territoryId, mapId, x, y, iconId);
|
||||
}, Service.Log, "Exception during OnSetFlagMapMarker");
|
||||
}
|
||||
@@ -0,0 +1,527 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Control;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using KamiLib.Classes;
|
||||
using KamiLib.Extensions;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Mappy.Classes;
|
||||
using Mappy.Extensions;
|
||||
using MapType = FFXIVClientStructs.FFXIV.Client.UI.Agent.MapType;
|
||||
|
||||
namespace Mappy.Controllers;
|
||||
|
||||
public unsafe class IntegrationsController : IDisposable
|
||||
{
|
||||
private readonly Hook<AgentMap.Delegates.ShowMap>? showMapHook;
|
||||
private readonly Hook<AgentMap.Delegates.OpenMap>? openMapHook;
|
||||
|
||||
private bool _wasBetweenAreas;
|
||||
private int _lastQuestCount = -1;
|
||||
private int _lastTempMarkerCount = -1;
|
||||
/// <summary>Snapshot of (QuestId, Sequence) for each active quest; when this changes we refresh so markers update (e.g. multi-step objective).</summary>
|
||||
private string _lastQuestSequenceSnapshot = string.Empty;
|
||||
private bool _refreshedDuringLoad;
|
||||
/// <summary>When true, request a silent refresh on the next framework update (e.g. after plugin load).</summary>
|
||||
private bool _requestRefreshOnLoad = true;
|
||||
/// <summary>Frames to wait before moving the map off-screen after a silent refresh so the game has time to populate markers.</summary>
|
||||
private int _silentRefreshHideFramesRemaining;
|
||||
|
||||
/// <summary>True while we're doing a silent refresh; OnAreaMapPreShow should not open the MapWindow.</summary>
|
||||
public static bool SilentRefreshInProgress { get; private set; }
|
||||
|
||||
public IntegrationsController()
|
||||
{
|
||||
showMapHook ??=
|
||||
Service.Hooker.HookFromAddress<AgentMap.Delegates.ShowMap>(AgentMap.MemberFunctionPointers.ShowMap,
|
||||
OnShowHook);
|
||||
openMapHook ??=
|
||||
Service.Hooker.HookFromAddress<AgentMap.Delegates.OpenMap>(AgentMap.MemberFunctionPointers.OpenMap,
|
||||
OnOpenMapHook);
|
||||
|
||||
if (Service.ClientState is { IsPvP: false })
|
||||
{
|
||||
EnableIntegrations();
|
||||
}
|
||||
|
||||
Service.ClientState.EnterPvP += DisableIntegrations;
|
||||
Service.ClientState.LeavePvP += EnableIntegrations;
|
||||
Service.Framework.Update += OnFrameworkUpdate;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Service.Framework.Update -= OnFrameworkUpdate;
|
||||
DisableIntegrations();
|
||||
|
||||
showMapHook?.Dispose();
|
||||
openMapHook?.Dispose();
|
||||
|
||||
Service.ClientState.EnterPvP -= DisableIntegrations;
|
||||
Service.ClientState.LeavePvP -= EnableIntegrations;
|
||||
}
|
||||
|
||||
private void EnableIntegrations()
|
||||
{
|
||||
Service.Log.Debug("Enabling Integrations");
|
||||
|
||||
showMapHook?.Enable();
|
||||
openMapHook?.Enable();
|
||||
|
||||
// System.AreaMapController.EnableIntegrations();
|
||||
System.FlagController.EnableIntegrations();
|
||||
}
|
||||
|
||||
private void DisableIntegrations()
|
||||
{
|
||||
Service.Log.Debug("Disabling Integrations");
|
||||
|
||||
showMapHook?.Disable();
|
||||
openMapHook?.Disable();
|
||||
|
||||
// System.AreaMapController.DisableIntegrations();
|
||||
System.FlagController.DisableIntegrations();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// On load/quest accept/turn-in/objective update we open the map briefly so the game populates markers, then Hide().
|
||||
/// The map renderer caches static, temp, and event marker positions during that time so the minimap can draw them after the map is closed.
|
||||
/// </summary>
|
||||
private void OnFrameworkUpdate(IFramework framework)
|
||||
{
|
||||
if (Service.ClientState is not { IsLoggedIn: true } or { IsPvP: true }) return;
|
||||
|
||||
// If we're in the middle of a silent refresh, count down then Hide() so the map closes and we don't affect other plugins
|
||||
if (_silentRefreshHideFramesRemaining > 0) {
|
||||
_silentRefreshHideFramesRemaining--;
|
||||
if (_silentRefreshHideFramesRemaining == 0) {
|
||||
try { AgentMap.Instance()->Hide(); } catch { }
|
||||
SilentRefreshInProgress = false;
|
||||
}
|
||||
_wasBetweenAreas = Service.Condition.IsBetweenAreas();
|
||||
_lastQuestCount = GetActiveQuestCount();
|
||||
try { _lastTempMarkerCount = (int)AgentMap.Instance()->TempMapMarkerCount; } catch { }
|
||||
return;
|
||||
}
|
||||
|
||||
var betweenAreas = Service.Condition.IsBetweenAreas();
|
||||
|
||||
// First frame after leaving a load screen: refresh so minimap has static POI and markers without user opening map
|
||||
if (_wasBetweenAreas && !betweenAreas)
|
||||
RequestSilentRefresh();
|
||||
|
||||
// Once per load screen: refresh while the screen is black so the game populates markers
|
||||
if (betweenAreas) {
|
||||
if (!_refreshedDuringLoad)
|
||||
RequestSilentRefresh();
|
||||
_refreshedDuringLoad = true;
|
||||
} else {
|
||||
_refreshedDuringLoad = false;
|
||||
}
|
||||
|
||||
_wasBetweenAreas = betweenAreas;
|
||||
|
||||
// On plugin load (first frame we're in a zone), refresh so markers are populated immediately
|
||||
if (_requestRefreshOnLoad) {
|
||||
_requestRefreshOnLoad = false;
|
||||
RequestSilentRefresh();
|
||||
}
|
||||
|
||||
// Quest turned in, quest accepted, or objectives updated: refresh so markers stay in sync
|
||||
var questCount = GetActiveQuestCount();
|
||||
var tempCount = -1;
|
||||
try { tempCount = (int)AgentMap.Instance()->TempMapMarkerCount; } catch { }
|
||||
if (_lastQuestCount >= 0 && questCount < _lastQuestCount)
|
||||
RequestSilentRefresh(); // quest turned in
|
||||
if (_lastQuestCount >= 0 && questCount > _lastQuestCount)
|
||||
RequestSilentRefresh(); // quest accepted
|
||||
if (_lastTempMarkerCount >= 0 && tempCount >= 0 && tempCount < _lastTempMarkerCount)
|
||||
RequestSilentRefresh(); // objectives decreased
|
||||
if (_lastTempMarkerCount >= 0 && tempCount >= 0 && tempCount > _lastTempMarkerCount)
|
||||
RequestSilentRefresh(); // objectives added (e.g. new quest)
|
||||
var sequenceSnapshot = GetQuestSequenceSnapshot();
|
||||
if (_lastQuestSequenceSnapshot.Length > 0 && sequenceSnapshot != _lastQuestSequenceSnapshot)
|
||||
RequestSilentRefresh(); // quest step advanced (multi-step objective)
|
||||
_lastQuestCount = questCount;
|
||||
_lastTempMarkerCount = tempCount;
|
||||
_lastQuestSequenceSnapshot = sequenceSnapshot;
|
||||
}
|
||||
|
||||
/// <summary>Build a string of (QuestId, Sequence) for each active quest so we can detect step advances.</summary>
|
||||
private static unsafe string GetQuestSequenceSnapshot()
|
||||
{
|
||||
try
|
||||
{
|
||||
var parts = new List<string>();
|
||||
foreach (var q in QuestManager.Instance()->NormalQuests)
|
||||
{
|
||||
if (q.QuestId is 0) continue;
|
||||
parts.Add($"{q.QuestId}:{q.Sequence}");
|
||||
}
|
||||
return string.Join("|", parts);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Request a silent map refresh; opens the map, waits a few frames for the game to populate markers (and caches), then Hide().</summary>
|
||||
private void RequestSilentRefresh()
|
||||
{
|
||||
RequestSilentRefreshCore(framesBeforeHide: 5);
|
||||
}
|
||||
|
||||
private void RequestSilentRefreshCore(int framesBeforeHide)
|
||||
{
|
||||
if (_silentRefreshHideFramesRemaining > 0) return;
|
||||
try {
|
||||
var agent = AgentMap.Instance();
|
||||
var currentMapId = agent->CurrentMapId;
|
||||
if (currentMapId == 0) return;
|
||||
|
||||
SilentRefreshInProgress = true;
|
||||
agent->OpenMapByMapId(currentMapId, 0, true);
|
||||
agent->ResetMapMarkers();
|
||||
_silentRefreshHideFramesRemaining = framesBeforeHide;
|
||||
} catch {
|
||||
SilentRefreshInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static int GetActiveQuestCount()
|
||||
{
|
||||
try {
|
||||
var count = 0;
|
||||
foreach (var q in QuestManager.Instance()->NormalQuests) {
|
||||
if (q.QuestId is not 0) count++;
|
||||
}
|
||||
return count;
|
||||
} catch {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Open the current map and hide it after a few frames (see RequestSilentRefresh) so the game
|
||||
/// populates MapMarkers, EventMarkers, and TempMapMarkers. Used when a refresh is requested
|
||||
/// from code paths that don't use the frame-delayed flow (e.g. if needed elsewhere).
|
||||
/// </summary>
|
||||
private static void SilentRefreshMapMarkers()
|
||||
{
|
||||
try {
|
||||
var agent = AgentMap.Instance();
|
||||
var currentMapId = agent->CurrentMapId;
|
||||
if (currentMapId == 0) return;
|
||||
|
||||
agent->OpenMapByMapId(currentMapId, 0, true);
|
||||
agent->ResetMapMarkers();
|
||||
agent->Hide();
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
private void OnShowHook(AgentMap* agent, bool a1, bool a2) =>
|
||||
HookSafety.ExecuteSafe(() =>
|
||||
{
|
||||
Service.Log.Verbose("[OnShow] Beginning Show");
|
||||
|
||||
// If you managed to open the window while the agent says it should be closed
|
||||
if (System.MapWindow.IsOpen && AgentMap.Instance()->AddonId is 0)
|
||||
{
|
||||
Service.Log.Debug("[OnShow] MapWindow can not be open now.");
|
||||
System.MapWindow.Close();
|
||||
}
|
||||
|
||||
if (!ShouldShowMap())
|
||||
{
|
||||
Service.Log.Debug("[OnShow] Condition to open map is rejected, aborting.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (AgentMap.Instance()->AddonId is not 0 &&
|
||||
AgentMap.Instance()->CurrentMapId != AgentMap.Instance()->SelectedMapId)
|
||||
{
|
||||
if (!System.SystemConfig.KeepOpen)
|
||||
{
|
||||
AgentMap.Instance()->Hide();
|
||||
}
|
||||
|
||||
Service.Log.Verbose("[OnShow] Vanilla tried to return to current map, aborted.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (System.SystemConfig.KeepOpen)
|
||||
{
|
||||
Service.Log.Verbose("[OnShow] Keeping Open");
|
||||
return;
|
||||
}
|
||||
|
||||
showMapHook!.Original(agent, a1, a2);
|
||||
}, Service.Log, "Exception during OnShowHook");
|
||||
|
||||
private void OnOpenMapHook(AgentMap* agent, OpenMapInfo* mapInfo) =>
|
||||
HookSafety.ExecuteSafe(() =>
|
||||
{
|
||||
openMapHook!.Original(agent, mapInfo);
|
||||
|
||||
switch (mapInfo->Type)
|
||||
{
|
||||
case MapType.QuestLog:
|
||||
ProcessQuestLink(agent, mapInfo);
|
||||
break;
|
||||
|
||||
case MapType.GatheringLog:
|
||||
ProcessGatheringLink(agent);
|
||||
break;
|
||||
|
||||
case MapType.FlagMarker:
|
||||
ProcessFlagLink(agent);
|
||||
break;
|
||||
|
||||
case MapType.Bozja:
|
||||
ProcessForayLink(agent, mapInfo);
|
||||
break;
|
||||
|
||||
case MapType.MobHunt:
|
||||
case MapType.SharedFate:
|
||||
case MapType.Teleport:
|
||||
case MapType.Treasure:
|
||||
ProcessTeleportLink(agent, mapInfo);
|
||||
break;
|
||||
|
||||
// This appears to get triggered after a Teleport/Shared Fate teleport event.
|
||||
case MapType.Centered:
|
||||
|
||||
case MapType.AetherCurrent:
|
||||
default:
|
||||
Service.Log.Debug($"[OpenMap] Ignoring MapType: {mapInfo->Type}");
|
||||
break;
|
||||
}
|
||||
|
||||
if (System.SystemConfig.AutoZoom)
|
||||
{
|
||||
MapRenderer.MapRenderer.Scale =
|
||||
DrawHelpers.GetMapScaleFactor() * System.SystemConfig.AutoZoomScaleFactor;
|
||||
}
|
||||
}, Service.Log, "Exception during OpenMap");
|
||||
|
||||
private void ProcessQuestLink(AgentMap* agent, OpenMapInfo* mapInfo)
|
||||
{
|
||||
Service.Log.Debug("[OpenMap] Processing QuestLog Event");
|
||||
|
||||
var targetMapId = mapInfo->MapId;
|
||||
|
||||
if (GetMapIdForQuest(mapInfo) is { } foundMapId)
|
||||
{
|
||||
Service.Log.Debug($"[OpenMap] GetMapIdForQuest identified Quest Target Map as MapId: {foundMapId}");
|
||||
|
||||
if (targetMapId is 0)
|
||||
{
|
||||
Service.Log.Debug($"[OpenMap] targetMapId was {targetMapId} using foundMapId: {foundMapId}");
|
||||
targetMapId = foundMapId;
|
||||
}
|
||||
}
|
||||
|
||||
if (agent->SelectedMapId != targetMapId)
|
||||
{
|
||||
Service.Log.Debug($"[OpenMap] Opening MapId: {targetMapId}");
|
||||
OpenMap(targetMapId);
|
||||
}
|
||||
else
|
||||
{
|
||||
Service.Log.Debug($"[OpenMap] Already in MapId: {targetMapId}, aborting.");
|
||||
}
|
||||
|
||||
if (System.SystemConfig.CenterOnQuest)
|
||||
{
|
||||
ref var targetMarker = ref agent->TempMapMarkers[0].MapMarker;
|
||||
CenterOnMarker(targetMarker);
|
||||
Service.Log.Debug($"[OpenMap] Centering Map on X = {targetMarker.X}, Y = {targetMarker.Y}");
|
||||
}
|
||||
|
||||
System.MapWindow.ProcessingCommand = true;
|
||||
}
|
||||
|
||||
private static void ProcessGatheringLink(AgentMap* agent)
|
||||
{
|
||||
Service.Log.Debug("[OpenMap] Processing GatheringLog Event");
|
||||
|
||||
if (System.SystemConfig.CenterOnGathering)
|
||||
{
|
||||
ref var targetMarker = ref agent->TempMapMarkers[0].MapMarker;
|
||||
|
||||
CenterOnMarker(targetMarker);
|
||||
Service.Log.Debug($"[OpenMap] Centering Map on X = {targetMarker.X}, Y = {targetMarker.Y}");
|
||||
}
|
||||
|
||||
System.MapWindow.ProcessingCommand = true;
|
||||
}
|
||||
|
||||
private static void ProcessFlagLink(AgentMap* agent)
|
||||
{
|
||||
Service.Log.Debug("[OpenMap] Processing FlagMarker Event");
|
||||
|
||||
if (System.SystemConfig.CenterOnFlag)
|
||||
{
|
||||
ref var targetMarker = ref agent->FlagMapMarkers[0].MapMarker;
|
||||
|
||||
CenterOnMarker(targetMarker);
|
||||
Service.Log.Debug($"[OpenMap] Centering Map on X = {targetMarker.X}, Y = {targetMarker.Y}");
|
||||
}
|
||||
|
||||
System.MapWindow.ProcessingCommand = true;
|
||||
}
|
||||
|
||||
private static void ProcessForayLink(AgentMap* agent, OpenMapInfo* mapInfo)
|
||||
{
|
||||
Service.Log.Debug("[OpenMap] Processing Bozja Event");
|
||||
|
||||
var eventMarker =
|
||||
agent->EventMarkers.FirstOrNull(marker => marker.DataId == mapInfo->FateId && marker.Flags == 0x40);
|
||||
if (eventMarker is not null)
|
||||
{
|
||||
CenterOnMarker(eventMarker.Value);
|
||||
}
|
||||
|
||||
System.MapWindow.ProcessingCommand = true;
|
||||
}
|
||||
|
||||
private void ProcessTeleportLink(AgentMap* agent, OpenMapInfo* mapInfo)
|
||||
{
|
||||
Service.Log.Debug("[OpenMap] Processing Teleport Event");
|
||||
|
||||
var targetMapId = mapInfo->MapId;
|
||||
|
||||
if (agent->CurrentMapId != targetMapId)
|
||||
{
|
||||
Service.Log.Debug($"[OpenMap] Opening MapId: {targetMapId}");
|
||||
|
||||
OpenMap(mapInfo->MapId);
|
||||
|
||||
System.MapWindow.ProcessingCommand = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Service.Log.Debug($"[OpenMap] Already in MapId: {targetMapId}, aborting.");
|
||||
System.MapWindow.ProcessingCommand = true;
|
||||
}
|
||||
|
||||
public void OpenMap(uint mapId)
|
||||
{
|
||||
AgentMap.Instance()->OpenMapByMapId(mapId, 0, true);
|
||||
|
||||
// Since this is effecting state, we need to keep an eye on it for potential issues.
|
||||
AgentMap.Instance()->ResetMapMarkers();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ask the game to refresh its map markers (quests, FATEs, gathering, etc.) for the current map.
|
||||
/// Uses OpenMapByMapId with show=false so the game repopulates markers without opening the Area Map UI.
|
||||
/// Call periodically when the minimap is visible so it stays in sync.
|
||||
/// </summary>
|
||||
public static void RefreshMapMarkers()
|
||||
{
|
||||
try {
|
||||
var agent = AgentMap.Instance();
|
||||
var currentMapId = agent->CurrentMapId;
|
||||
if (currentMapId == 0) return;
|
||||
// OpenMapByMapId with show=false: refresh map/marker data for current map without showing the map addon.
|
||||
agent->OpenMapByMapId(currentMapId, 0, false);
|
||||
} catch {
|
||||
// Ignore if agent not ready
|
||||
}
|
||||
}
|
||||
|
||||
public void OpenOccupiedMap() => OpenMap(AgentMap.Instance()->CurrentMapId);
|
||||
|
||||
private static void CenterOnMarker(MapMarkerBase marker)
|
||||
{
|
||||
var coordinates = new Vector2(marker.X, marker.Y) / 16.0f * DrawHelpers.GetMapScaleFactor() -
|
||||
DrawHelpers.GetMapOffsetVector();
|
||||
|
||||
System.SystemConfig.FollowPlayer = false;
|
||||
System.MapRenderer.DrawOffset = -coordinates;
|
||||
}
|
||||
|
||||
private static void CenterOnMarker(MapMarkerData marker)
|
||||
{
|
||||
var coordinates = marker.Position.AsMapVector() * DrawHelpers.GetMapScaleFactor() -
|
||||
DrawHelpers.GetMapOffsetVector();
|
||||
|
||||
System.SystemConfig.FollowPlayer = false;
|
||||
System.MapRenderer.DrawOffset = -coordinates;
|
||||
}
|
||||
|
||||
/// <summary>When true, minimap uses same visibility as main map (hides during dialogue/interaction if Hide With Game GUI is on). When false, minimap stays visible during dialogue and object interaction.</summary>
|
||||
public static bool ShouldShowMinimap()
|
||||
{
|
||||
if (Service.ClientState is { IsLoggedIn: false } or { IsPvP: true }) return false;
|
||||
if (System.SystemConfig.HideInCombat && Service.Condition.IsInCombat()) return false;
|
||||
if (System.SystemConfig.HideBetweenAreas && Service.Condition.IsBetweenAreas()) return false;
|
||||
if (!System.SystemConfig.MinimapHideWithGameGui) return true;
|
||||
// Same as main map
|
||||
if (System.SystemConfig.HideWithGameGui && !IsNamePlateAddonVisible()) return false;
|
||||
if (System.SystemConfig.HideWithGameGui && Control.Instance()->TargetSystem.TargetModeIndex is 1) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool ShouldShowMap()
|
||||
{
|
||||
if (Service.ClientState is { IsLoggedIn: false } or { IsPvP: true }) return false;
|
||||
if (System.SystemConfig.HideInCombat && Service.Condition.IsInCombat()) return false;
|
||||
if (System.SystemConfig.HideBetweenAreas && Service.Condition.IsBetweenAreas()) return false;
|
||||
if (System.SystemConfig.HideWithGameGui && !IsNamePlateAddonVisible()) return false;
|
||||
if (System.SystemConfig.HideWithGameGui && Control.Instance()->TargetSystem.TargetModeIndex is 1) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsNamePlateAddonVisible() =>
|
||||
!RaptureAtkUnitManager.Instance()->UiFlags.HasFlag(UIModule.UiFlags.Nameplates);
|
||||
|
||||
private uint? GetMapIdForQuest(OpenMapInfo* mapInfo)
|
||||
{
|
||||
foreach (var leveQuest in QuestManager.Instance()->LeveQuests)
|
||||
{
|
||||
if (leveQuest.LeveId is 0) continue;
|
||||
|
||||
var leveData = Service.DataManager.GetExcelSheet<Leve>().GetRow(leveQuest.LeveId);
|
||||
if (!IsNameMatch(leveData.Name.ExtractText(), mapInfo)) continue;
|
||||
|
||||
return leveData.LevelStart.Value.Map.RowId;
|
||||
}
|
||||
|
||||
foreach (var quest in QuestManager.Instance()->NormalQuests)
|
||||
{
|
||||
if (quest.QuestId is 0) continue;
|
||||
|
||||
// Is this the quest we are looking for?
|
||||
var questData = Service.DataManager.GetExcelSheet<Quest>().GetRow(quest.QuestId + 65536u);
|
||||
if (!IsNameMatch(questData.Name.ExtractText(), mapInfo)) continue;
|
||||
|
||||
return questData
|
||||
.TodoParams.FirstOrDefault(param => param.ToDoCompleteSeq == quest.Sequence)
|
||||
.ToDoLocation.FirstOrDefault(location => location is not { RowId: 0, ValueNullable: null })
|
||||
.Value.Map.RowId;
|
||||
}
|
||||
|
||||
var possibleQuests = Service.DataManager.GetExcelSheet<Quest>()
|
||||
.Where(quest => quest is { IssuerLocation: { IsValid: true, RowId: not 0 } })
|
||||
.FirstOrNull(quest => IsNameMatch(quest.Name.ExtractText(), mapInfo));
|
||||
|
||||
return possibleQuests?.IssuerLocation.Value.Map.RowId ?? null;
|
||||
}
|
||||
|
||||
private static bool IsNameMatch(string name, OpenMapInfo* mapInfo) => string.Equals(name,
|
||||
mapInfo->TitleString.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<DalamudLibPath Condition="$([MSBuild]::IsOSPlatform('Windows'))">$(appdata)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
|
||||
<DalamudLibPath Condition="$([MSBuild]::IsOSPlatform('Linux'))">$(HOME)/.xlcore/dalamud/Hooks/dev/</DalamudLibPath>
|
||||
<DalamudLibPath Condition="$([MSBuild]::IsOSPlatform('OSX'))">$(HOME)/Library/Application Support/XIV on Mac/dalamud/Hooks/dev/</DalamudLibPath>
|
||||
<DalamudLibPath Condition="$(DALAMUD_HOME) != ''">$(DALAMUD_HOME)/</DalamudLibPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(DalamudLibPath)/targets/Dalamud.Plugin.targets"/>
|
||||
</Project>
|
||||
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using KamiLib.Configuration;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Mappy.Classes.SelectionWindowComponents;
|
||||
|
||||
namespace Mappy.Data;
|
||||
|
||||
public unsafe record Flag(uint Territory, uint Map, float X, float Y, uint IconId)
|
||||
{
|
||||
public IDalamudTextureWrap? GetMapTexture() => MapDrawableOption.GetMapTexture(Map);
|
||||
|
||||
public Map GetMap() => Service.DataManager.GetExcelSheet<Map>().GetRow(Map);
|
||||
|
||||
public TerritoryType GetTerritoryType() => Service.DataManager.GetExcelSheet<TerritoryType>().GetRow(Territory);
|
||||
|
||||
public void PlaceFlag()
|
||||
{
|
||||
AgentMap.Instance()->FlagMarkerCount = 0;
|
||||
AgentMap.Instance()->SetFlagMapMarker(Territory, Map, X, Y, IconId);
|
||||
|
||||
if (System.SystemConfig.CenterOnFlag) {
|
||||
Focus();
|
||||
}
|
||||
}
|
||||
|
||||
public string GetIdString() => $"{Territory}_{Map}_{X}_{Y}_{IconId}";
|
||||
|
||||
public Vector2 GetCoordinate() => new(X, Y);
|
||||
|
||||
public Vector2 GetMapCoordinate() => MapUtil.WorldToMap(GetCoordinate());
|
||||
|
||||
public void Focus()
|
||||
{
|
||||
System.SystemConfig.FollowPlayer = false;
|
||||
System.IntegrationsController.OpenMap(Map);
|
||||
System.MapRenderer.CenterOnCoordinate(GetCoordinate());
|
||||
}
|
||||
|
||||
public bool IsFlagSet()
|
||||
{
|
||||
if (AgentMap.Instance()->FlagMarkerCount is 0) return false;
|
||||
ref var setMarker = ref AgentMap.Instance()->FlagMapMarkers[0];
|
||||
|
||||
if (setMarker.TerritoryId != Territory) return false;
|
||||
if (setMarker.MapId != Map) return false;
|
||||
if (Math.Abs(setMarker.XFloat - X) > 0.01f) return false;
|
||||
if (Math.Abs(setMarker.YFloat - Y) > 0.01f) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class FlagConfig
|
||||
{
|
||||
public LinkedList<Flag> FlagHistory = [];
|
||||
|
||||
// Not exposed to users, might be in the future.
|
||||
public int HistoryLimit = 10;
|
||||
|
||||
public static FlagConfig Load() => Service.PluginInterface.LoadConfigFile<FlagConfig>("Flags.data.json");
|
||||
|
||||
public void Save() => Service.PluginInterface.SaveConfigFile("Flags.data.json", System.FlagConfig);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
using Dalamud.Interface;
|
||||
using KamiLib.Configuration;
|
||||
|
||||
namespace Mappy.Data;
|
||||
|
||||
public class IconSetting
|
||||
{
|
||||
public required uint IconId { get; set; }
|
||||
public bool Hide;
|
||||
public bool AllowTooltip = true;
|
||||
public float Scale = 1.0f;
|
||||
public bool AllowClick = true;
|
||||
public Vector4 Color = KnownColor.White.Vector();
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Hide = false;
|
||||
AllowTooltip = true;
|
||||
Scale = 1.0f;
|
||||
AllowClick = true;
|
||||
Color = KnownColor.White.Vector();
|
||||
}
|
||||
}
|
||||
|
||||
public class IconConfig
|
||||
{
|
||||
public Dictionary<uint, IconSetting> IconSettingMap = [];
|
||||
|
||||
public static IconConfig Load() => Service.PluginInterface.LoadConfigFile<IconConfig>("Icons.config.json");
|
||||
|
||||
public void Save() => Service.PluginInterface.SaveConfigFile("Icons.config.json", System.IconConfig);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
using System.Text.Json.Serialization;
|
||||
using Dalamud.Interface;
|
||||
using KamiLib.Configuration;
|
||||
|
||||
namespace Mappy.Data;
|
||||
|
||||
public enum CenterTarget
|
||||
{
|
||||
[Description("Disabled")]
|
||||
Disabled = 0,
|
||||
|
||||
[Description("Player")]
|
||||
Player = 1,
|
||||
|
||||
[Description("Map")]
|
||||
Map = 2,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum FadeMode
|
||||
{
|
||||
[Description("Always")]
|
||||
Always = 1 << 0,
|
||||
|
||||
[Description("When Moving")]
|
||||
WhenMoving = 1 << 2,
|
||||
|
||||
[Description("When Focused")]
|
||||
WhenFocused = 1 << 3,
|
||||
|
||||
[Description("When Unfocused")]
|
||||
WhenUnFocused = 1 << 4,
|
||||
}
|
||||
|
||||
public class SystemConfig : CharacterConfiguration
|
||||
{
|
||||
public bool UseLinearZoom = false;
|
||||
public float ZoomSpeed = 0.25f;
|
||||
public float IconScale = 0.50f;
|
||||
public bool ShowMiscTooltips = true;
|
||||
public bool HideWithGameGui = true;
|
||||
public bool HideBetweenAreas = false;
|
||||
public bool HideInCombat = false;
|
||||
public bool KeepOpen = false;
|
||||
public bool FollowOnOpen = false;
|
||||
public bool FollowPlayer = true;
|
||||
public CenterTarget CenterOnOpen = CenterTarget.Disabled;
|
||||
public bool ScalePlayerCone = false;
|
||||
public float ConeSize = 150.0f;
|
||||
public bool ShowRadar = true;
|
||||
public bool ShowRadarInDuties = false;
|
||||
public Vector4 RadarColor = KnownColor.Gray.Vector() with { W = 0.10f };
|
||||
public Vector4 RadarOutlineColor = KnownColor.Gray.Vector() with { W = 0.30f };
|
||||
public bool HideWindowFrame = false;
|
||||
public bool HideWindowBackground = false;
|
||||
public bool EnableShiftDragMove = false;
|
||||
public bool LockWindow = false;
|
||||
public float FadePercent = 0.60f;
|
||||
public FadeMode FadeMode = FadeMode.WhenUnFocused | FadeMode.WhenMoving;
|
||||
public Vector2 WindowPosition = new(1024.0f, 700.0f);
|
||||
public Vector2 WindowSize = new(500.0f, 500.0f);
|
||||
public bool AlwaysShowToolbar = false;
|
||||
public bool ShowToolbarOnHover = true;
|
||||
public bool ScaleWithZoom = true;
|
||||
public bool AcceptedSpoilerWarning = false;
|
||||
public Vector4 AreaColor = KnownColor.CornflowerBlue.Vector() with { W = 0.33f };
|
||||
public Vector4 AreaOutlineColor = KnownColor.CornflowerBlue.Vector() with { W = 0.30f };
|
||||
public Vector4 PlayerConeColor = KnownColor.CornflowerBlue.Vector() with { W = 0.33f };
|
||||
public Vector4 PlayerConeOutlineColor = KnownColor.CornflowerBlue.Vector() with { W = 1.0f };
|
||||
public bool CenterOnFlag = true;
|
||||
public bool CenterOnGathering = true;
|
||||
public bool CenterOnQuest = true;
|
||||
public bool LockCenterOnMap = false;
|
||||
public bool ShowCoordinateBar = true;
|
||||
public float ToolbarFade = 0.33f;
|
||||
public float CoordinateBarFade = 0.66f;
|
||||
public Vector4 CoordinateTextColor = KnownColor.White.Vector();
|
||||
public bool ZoomLocked = false;
|
||||
public bool ShowPlayers = true;
|
||||
public bool SetFlagOnFateClick = false;
|
||||
public bool ShowPlayerIcon = true;
|
||||
public float PlayerIconScale = 1.0f;
|
||||
public float MapScale = 1.0f;
|
||||
public bool AutoZoom = false;
|
||||
public bool ShowRegionLabel = true;
|
||||
public bool ShowMapLabel = true;
|
||||
public bool ShowAreaLabel = true;
|
||||
public bool ShowSubAreaLabel = true;
|
||||
public bool NoFocusOnAppear = false;
|
||||
public float LargeAreaTextScale = 1.5f;
|
||||
public float SmallAreaTextScale = 1.0f;
|
||||
public bool ShowTextLabels = true;
|
||||
public bool ShowFogOfWar = true;
|
||||
public bool ScaleTextWithZoom = true;
|
||||
public float AutoZoomScaleFactor = 0.33f;
|
||||
|
||||
public bool SuppressNativeMapOpenSound = true;
|
||||
|
||||
// Minimap
|
||||
public bool ShowMinimap = false;
|
||||
public float MinimapSize = 200.0f;
|
||||
public Vector2 MinimapPosition = new(50.0f, 50.0f);
|
||||
public float MinimapOpacity = 0.85f;
|
||||
public float MinimapZoom = 0.112f; // 0.112 = max zoom out (see more map), lower = zoomed in (down to 0.03)
|
||||
public bool MinimapShowPlayerCone = true;
|
||||
public bool MinimapLockPosition = false;
|
||||
/// <summary>Show an arrow at the edge of the minimap pointing toward the current quest objective or waymark (like the default minimap).</summary>
|
||||
public bool MinimapShowQuestDirectionArrow = true;
|
||||
/// <summary>Show direction arrows for nearby FATEs on the minimap (purple, matching FATE marker).</summary>
|
||||
public bool MinimapShowFateDirectionArrows = true;
|
||||
/// <summary>Show quest objective area radius circles on the minimap (same as Area Map).</summary>
|
||||
public bool MinimapShowQuestAreaRadius = true;
|
||||
/// <summary>When true, minimap hides with the game GUI (dialogue, interaction, nameplates off). When false, minimap stays visible during dialogue and object interaction.</summary>
|
||||
public bool MinimapHideWithGameGui = false;
|
||||
|
||||
// Do not persist this setting
|
||||
[JsonIgnore]
|
||||
public bool DebugMode = false;
|
||||
|
||||
public static SystemConfig Load() => Service.PluginInterface.LoadConfigFile<SystemConfig>("System.config.json");
|
||||
|
||||
public static void Save() => Service.PluginInterface.SaveConfigFile("System.config.json", System.SystemConfig);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
|
||||
namespace Mappy.Extensions;
|
||||
|
||||
public static unsafe class AddonAreaMapExtensions
|
||||
{
|
||||
public static void ForceOffscreen(this ref AddonAreaMap addon)
|
||||
{
|
||||
if (!addon.IsReady) return;
|
||||
if (addon.RootNode is null) return;
|
||||
|
||||
addon.RootNode->SetPositionFloat(-9001.0f, -9001.0f);
|
||||
}
|
||||
|
||||
public static void RestorePosition(this ref AddonAreaMap addon)
|
||||
{
|
||||
if (!addon.IsReady) return;
|
||||
if (addon.RootNode is null) return;
|
||||
|
||||
addon.RootNode->SetPositionFloat(addon.X, addon.Y);
|
||||
}
|
||||
|
||||
public static bool IsOffscreen(this ref AddonAreaMap addon)
|
||||
{
|
||||
if (!addon.IsReady) return false;
|
||||
if (addon.RootNode is null) return false;
|
||||
|
||||
var xAdjusted = addon.RootNode->X < -9000.0f;
|
||||
var yAdjusted = addon.RootNode->Y < -9000.0f;
|
||||
|
||||
return xAdjusted && yAdjusted;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
using Dalamud.Interface;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Fate;
|
||||
using FFXIVClientStructs.Interop;
|
||||
|
||||
namespace Mappy.Extensions;
|
||||
|
||||
public static unsafe class FateContextExtensions
|
||||
{
|
||||
public static Vector4 GetColor(this Pointer<FateContext> context, float alpha = 0.33f)
|
||||
{
|
||||
var timeRemaining = GetTimeRemaining(context);
|
||||
if (timeRemaining <= TimeSpan.FromSeconds(300) && timeRemaining.TotalSeconds > 0) {
|
||||
var hue = (float)(timeRemaining.TotalSeconds / 300.0f * 25.0f);
|
||||
|
||||
var hsvColor = new ColorHelpers.HsvaColor(hue / 100.0f, 1.0f, 1.0f, alpha);
|
||||
return ColorHelpers.HsvToRgb(hsvColor);
|
||||
}
|
||||
|
||||
return KnownColor.White.Vector();
|
||||
}
|
||||
|
||||
public static TimeSpan GetTimeRemaining(this Pointer<FateContext> context)
|
||||
{
|
||||
if (context.Value->Duration is 0) return TimeSpan.Zero;
|
||||
|
||||
return TimeSpan.FromSeconds(context.Value->StartTimeEpoch + context.Value->Duration - DateTimeOffset.Now.ToUnixTimeSeconds());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Numerics;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Mappy.Classes;
|
||||
|
||||
namespace Mappy.Extensions;
|
||||
|
||||
public static class GameObjectExtensions
|
||||
{
|
||||
public static Vector2 GetMapPosition(this IGameObject obj) => new Vector2(obj.Position.X, obj.Position.Z) * DrawHelpers.GetMapScaleFactor();
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using Mappy.Classes;
|
||||
|
||||
namespace Mappy.Extensions;
|
||||
|
||||
// Represents standard non-dynamic map markers, things that don't change, and may reference datasheet data with their key data
|
||||
public static class MapMarkerBaseExtensions
|
||||
{
|
||||
public static void Draw(this MapMarkerBase marker, Vector2 offset, float scale)
|
||||
{
|
||||
var tooltipText = marker.Subtext.AsDalamudSeString();
|
||||
|
||||
DrawHelpers.DrawMapMarker(new MarkerInfo
|
||||
{
|
||||
// Divide by 16, as it seems they use a fixed scalar
|
||||
// Add 1024 * scale, to offset from top-left, to center-based coordinate
|
||||
// Add offset for drawing relative to map when its moved around
|
||||
Position = new Vector2(marker.X, marker.Y) / 16.0f * scale + DrawHelpers.GetMapCenterOffsetVector() * scale,
|
||||
Offset = offset,
|
||||
Scale = scale,
|
||||
Radius = marker.Scale,
|
||||
RadiusColor = KnownColor.MediumPurple.Vector(),
|
||||
IconId = marker.IconId,
|
||||
PrimaryText =
|
||||
() => tooltipText.TextValue.IsNullOrEmpty() && System.SystemConfig.ShowMiscTooltips ? System.TooltipCache.GetValue(marker.IconId) : tooltipText.ToString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Numerics;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||
using Mappy.Classes;
|
||||
using MarkerInfo = Mappy.Classes.MarkerInfo;
|
||||
|
||||
namespace Mappy.Extensions;
|
||||
|
||||
// MapMarkerData struct represents dynamic markers that have information like radius, and other fields.
|
||||
public static class MapMarkerDataExtensions
|
||||
{
|
||||
public static void Draw(this MapMarkerData marker, Vector2 offset, float scale)
|
||||
{
|
||||
if ((marker.Flags & 1) == 1) return;
|
||||
|
||||
DrawHelpers.DrawMapMarker(new MarkerInfo
|
||||
{
|
||||
Position = (marker.Position.AsMapVector() * DrawHelpers.GetMapScaleFactor() - DrawHelpers.GetMapOffsetVector() + DrawHelpers.GetMapCenterOffsetVector()) * scale,
|
||||
Offset = offset,
|
||||
Scale = scale,
|
||||
IconId = marker.IconId,
|
||||
Radius = marker.Radius,
|
||||
RadiusColor = System.SystemConfig.AreaColor,
|
||||
RadiusOutlineColor = System.SystemConfig.AreaOutlineColor,
|
||||
PrimaryText = () => GetMarkerPrimaryText(marker),
|
||||
IsDynamicMarker = true,
|
||||
ObjectiveId = marker.ObjectiveId,
|
||||
MarkerType = (MarkerType)marker.MarkerType,
|
||||
DataId = marker.DataId,
|
||||
});
|
||||
}
|
||||
|
||||
private static unsafe string GetMarkerPrimaryText(MapMarkerData marker)
|
||||
{
|
||||
if (marker.TooltipString is null) return string.Empty;
|
||||
if (marker.TooltipString->StringPtr.Value is null) return string.Empty;
|
||||
if (marker.TooltipString->StringPtr.ExtractText().IsNullOrEmpty()) return string.Empty;
|
||||
|
||||
var text = marker.TooltipString->StringPtr.ExtractText();
|
||||
return marker.RecommendedLevel is 0 ? text : $"Lv. {marker.RecommendedLevel} {text}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Mappy.Classes;
|
||||
using Map = Lumina.Excel.Sheets.Map;
|
||||
using MarkerInfo = Mappy.Classes.MarkerInfo;
|
||||
|
||||
namespace Mappy.Extensions;
|
||||
|
||||
public static class MapMarkerInfoExtensions
|
||||
{
|
||||
public static void Draw(this MapMarkerInfo marker, Vector2 offset, float scale)
|
||||
{
|
||||
var tooltipText = marker.MapMarker.Subtext.AsDalamudSeString();
|
||||
|
||||
var markerInfo = new MarkerInfo
|
||||
{
|
||||
// Divide by 16, as it seems they use a fixed scalar
|
||||
// Add 1024 * scale, to offset from top-left, to center-based coordinate
|
||||
// Add offset for drawing relative to map when it's moved around
|
||||
Position = new Vector2(marker.MapMarker.X, marker.MapMarker.Y) / 16.0f * scale + DrawHelpers.GetMapCenterOffsetVector() * scale,
|
||||
Offset = offset,
|
||||
Scale = scale,
|
||||
Radius = marker.MapMarker.Scale,
|
||||
RadiusColor = KnownColor.MediumPurple.Vector(),
|
||||
IconId = marker.MapMarker.IconId,
|
||||
PrimaryText = GetMarkerPrimaryTooltip(marker, tooltipText),
|
||||
OnLeftClicked = () => OnMarkerClicked(ref marker),
|
||||
SecondaryText = () => GetTooltip(ref marker),
|
||||
};
|
||||
|
||||
if (marker.MapMarker.IconId is 0 && marker.MapMarker.Index is not 0) {
|
||||
TryDrawText(marker, markerInfo, tooltipText);
|
||||
}
|
||||
else {
|
||||
DrawHelpers.DrawMapMarker(markerInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryDrawText(MapMarkerInfo marker, MarkerInfo markerInfo, SeString tooltipText)
|
||||
{
|
||||
if (!System.SystemConfig.ShowTextLabels) return;
|
||||
|
||||
var textTypeScalar = marker.MapMarker.SubtextStyle switch
|
||||
{
|
||||
1 => System.SystemConfig.LargeAreaTextScale,
|
||||
_ => System.SystemConfig.SmallAreaTextScale,
|
||||
};
|
||||
|
||||
if (System.SystemConfig.ScaleTextWithZoom) {
|
||||
markerInfo.Scale *= textTypeScalar * 0.33f;
|
||||
}
|
||||
else {
|
||||
markerInfo.Scale = textTypeScalar * 0.33f;
|
||||
}
|
||||
|
||||
DrawHelpers.DrawText(markerInfo, tooltipText);
|
||||
}
|
||||
|
||||
private static void OnMarkerClicked(ref MapMarkerInfo marker)
|
||||
{
|
||||
switch (marker.DataType) {
|
||||
case 1: // MapLinkMarker
|
||||
OnMapLinkMarkerClicked(ref marker);
|
||||
break;
|
||||
|
||||
case 2: // InstanceLink
|
||||
OnInstanceLinkClicked(ref marker);
|
||||
break;
|
||||
|
||||
case 3: // Aetheryte
|
||||
OnAetheryteClicked(ref marker);
|
||||
break;
|
||||
|
||||
case 4: // Aethernet
|
||||
OnAethernetClicked(ref marker);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnMapLinkMarkerClicked(ref MapMarkerInfo marker)
|
||||
{
|
||||
if (marker.DataKey is 0) return;
|
||||
if (DrawHelpers.IsDisallowedIcon(marker.MapMarker.IconId)) return;
|
||||
|
||||
System.IntegrationsController.OpenMap(marker.DataKey);
|
||||
}
|
||||
|
||||
private static void OnInstanceLinkClicked(ref MapMarkerInfo _)
|
||||
{
|
||||
// Might consider opening contents finder to this duty, maybe
|
||||
}
|
||||
|
||||
private static void OnAetheryteClicked(ref MapMarkerInfo marker)
|
||||
{
|
||||
if (marker.DataKey is 0) return;
|
||||
|
||||
System.Teleporter.Teleport(marker.DataKey);
|
||||
}
|
||||
|
||||
private static void OnAethernetClicked(ref MapMarkerInfo marker)
|
||||
{
|
||||
var aetheryte = GetAetheryteForAethernet(marker.DataKey);
|
||||
if (aetheryte is null) return;
|
||||
if (aetheryte.Value.RowId is 0) return;
|
||||
|
||||
System.Teleporter.Teleport(aetheryte.Value.RowId);
|
||||
}
|
||||
|
||||
private static string GetTooltip(ref MapMarkerInfo marker)
|
||||
{
|
||||
switch (marker.DataType)
|
||||
{
|
||||
case 1: // MapLinkMarker
|
||||
return GetMapLinkTooltip(ref marker);
|
||||
|
||||
case 2: // InstanceLink
|
||||
return GetInstanceLinkTooltip(ref marker);
|
||||
|
||||
case 3: // Aetheryte
|
||||
return GetAetheryteTooltip(ref marker);
|
||||
|
||||
case 4: // Aethernet
|
||||
return GetAethernetTooltip(ref marker);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string GetMapLinkTooltip(ref MapMarkerInfo marker)
|
||||
{
|
||||
if (marker.DataKey is 0) return string.Empty;
|
||||
if (DrawHelpers.IsDisallowedIcon(marker.MapMarker.IconId)) return string.Empty;
|
||||
|
||||
var map = Service.DataManager.GetExcelSheet<Map>().GetRow(marker.DataKey);
|
||||
var mapPlaceName = map.PlaceName.ValueNullable?.Name.ExtractText() ?? string.Empty;
|
||||
|
||||
return $"Open Map {mapPlaceName}";
|
||||
}
|
||||
|
||||
private static string GetInstanceLinkTooltip(ref MapMarkerInfo marker)
|
||||
{
|
||||
return $"Instance Link {marker.DataKey}";
|
||||
}
|
||||
|
||||
private static string GetAetheryteTooltip(ref MapMarkerInfo marker)
|
||||
{
|
||||
if (marker.DataKey is 0) return string.Empty;
|
||||
|
||||
var aetheryteTeleportCost = GetAetheryteTeleportGilCost(marker.DataKey);
|
||||
if (aetheryteTeleportCost is null) return "Not attuned to aetheryte";
|
||||
|
||||
var aetheryte = Service.DataManager.GetExcelSheet<Aetheryte>().GetRow(marker.DataKey);
|
||||
var aetherytePlaceName = aetheryte.PlaceName.ValueNullable?.Name.ExtractText() ?? string.Empty;
|
||||
var aetheryteCost = GetAetheryteTeleportCost(marker.DataKey);
|
||||
|
||||
return $"Teleport to {aetherytePlaceName} {aetheryteCost}";
|
||||
}
|
||||
|
||||
private static string GetAethernetTooltip(ref MapMarkerInfo marker)
|
||||
{
|
||||
if (marker.DataKey is 0) return string.Empty;
|
||||
|
||||
var aetheryte = GetAetheryteForAethernet(marker.DataKey);
|
||||
if (aetheryte is null) return string.Empty;
|
||||
if (aetheryte.Value.RowId is 0) return string.Empty;
|
||||
|
||||
var aetherytePlaceName = aetheryte.Value.PlaceName.ValueNullable?.Name.ExtractText() ?? string.Empty;
|
||||
|
||||
return $"Teleport to {aetherytePlaceName} {GetAetheryteTeleportCost(aetheryte.Value.RowId)}";
|
||||
}
|
||||
|
||||
private static Aetheryte? GetAetheryteForAethernet(uint aethernetKey) => System.AetheryteAethernetCache.GetValue(aethernetKey);
|
||||
|
||||
private static uint? GetAetheryteTeleportGilCost(uint aethernetKey) => Service.AetheryteList.FirstOrDefault(entry => entry.AetheryteId == aethernetKey)?.GilCost;
|
||||
|
||||
private static string GetAetheryteTeleportCost(uint targetDataKey) => $"({GetAetheryteTeleportGilCost(targetDataKey) ?? 0:n0} {SeIconChar.Gil.ToIconChar()})";
|
||||
|
||||
private static Func<string> GetMarkerPrimaryTooltip(MapMarkerInfo marker, SeString tooltipText)
|
||||
{
|
||||
if (DrawHelpers.IsDisallowedIcon(marker.MapMarker.IconId)) return () => string.Empty;
|
||||
if (!System.SystemConfig.ShowMiscTooltips) return () => string.Empty;
|
||||
if (!tooltipText.TextValue.IsNullOrEmpty()) return tooltipText.ToString;
|
||||
|
||||
return marker.DataType switch
|
||||
{
|
||||
4 => () => Service.DataManager.GetExcelSheet<PlaceName>().GetRow(marker.DataKey).Name.ExtractText(),
|
||||
_ => () => System.TooltipCache.GetValue(marker.MapMarker.IconId) ?? string.Empty,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Numerics;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
|
||||
namespace Mappy.Extensions;
|
||||
|
||||
public static class MiniMapGatheringMarkerExtensions
|
||||
{
|
||||
public static void Draw(this MiniMapGatheringMarker marker, Vector2 offset, float scale)
|
||||
{
|
||||
if (marker.ShouldRender is 0) return;
|
||||
|
||||
marker.MapMarker.Scale = 50;
|
||||
marker.MapMarker.Draw(offset, scale);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Numerics;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using Mappy.Classes;
|
||||
|
||||
namespace Mappy.Extensions;
|
||||
|
||||
public static class TempMapMarkerExtensions
|
||||
{
|
||||
public static void Draw(this TempMapMarker marker, Vector2 offset, float scale)
|
||||
{
|
||||
DrawHelpers.DrawMapMarker(new MarkerInfo
|
||||
{
|
||||
// Divide by 16, as it seems they use a fixed scalar
|
||||
// Add 1024 * scale, to offset from top-left, to center-based coordinate
|
||||
// Add offset for drawing relative to map when its moved around
|
||||
Position = (new Vector2(marker.MapMarker.X, marker.MapMarker.Y) / 16.0f * DrawHelpers.GetMapScaleFactor() + DrawHelpers.GetCombinedOffsetVector()) * scale,
|
||||
Offset = offset,
|
||||
Scale = scale,
|
||||
IconId = marker.MapMarker.IconId,
|
||||
Radius = marker.MapMarker.Scale,
|
||||
PrimaryText = () => marker.TooltipText.ToString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace Mappy.Extensions;
|
||||
|
||||
public static class VectorExtensions
|
||||
{
|
||||
public static Vector2 AsMapVector(this Vector3 vector) => new(vector.X, vector.Z);
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Interface.Textures;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using Lumina.Data.Files;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Lumina.Extensions;
|
||||
using Mappy.Classes;
|
||||
|
||||
namespace Mappy.MapRenderer;
|
||||
|
||||
public unsafe partial class MapRenderer : IDisposable
|
||||
{
|
||||
private const int MinimapCacheMaxEntries = 8;
|
||||
|
||||
private sealed class MinimapCacheEntry
|
||||
{
|
||||
public IDalamudTextureWrap? Texture;
|
||||
public string PathKey = string.Empty;
|
||||
public float ScaleFactor;
|
||||
public float OffsetX;
|
||||
public float OffsetY;
|
||||
}
|
||||
|
||||
private readonly Dictionary<uint, MinimapCacheEntry> _minimapCache = new();
|
||||
public static float Scale
|
||||
{
|
||||
get => System.SystemConfig.MapScale;
|
||||
set => System.SystemConfig.MapScale = value;
|
||||
}
|
||||
|
||||
public Vector2 DrawOffset { get; set; }
|
||||
public Vector2 DrawPosition { get; private set; }
|
||||
|
||||
private IDalamudTextureWrap? blendedTexture;
|
||||
private string blendedPath = string.Empty;
|
||||
|
||||
public MapRenderer()
|
||||
{
|
||||
LoadFogHooks();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var entry in _minimapCache.Values)
|
||||
entry.Texture?.Dispose();
|
||||
_minimapCache.Clear();
|
||||
UnloadFogHooks();
|
||||
}
|
||||
|
||||
public void CenterOnGameObject(IGameObject obj) => CenterOnCoordinate(new Vector2(obj.Position.X, obj.Position.Z));
|
||||
|
||||
public void CenterOnCoordinate(Vector2 coord) => DrawOffset = -coord * DrawHelpers.GetMapScaleFactor() + DrawHelpers.GetMapOffsetVector();
|
||||
|
||||
public void DrawBaseTexture()
|
||||
{
|
||||
UpdateScaleLimits();
|
||||
UpdateDrawOffset();
|
||||
|
||||
DrawBackgroundTexture();
|
||||
}
|
||||
|
||||
public void DrawDynamicElements()
|
||||
{
|
||||
DrawFogOfWar();
|
||||
DrawMapMarkers();
|
||||
}
|
||||
|
||||
private void UpdateScaleLimits() => Scale = Math.Clamp(Scale, 0.05f, 20.0f);
|
||||
|
||||
private void UpdateDrawOffset()
|
||||
{
|
||||
var childCenterOffset = ImGui.GetContentRegionAvail() / 2.0f;
|
||||
var mapCenterOffset = new Vector2(1024.0f, 1024.0f) * Scale;
|
||||
|
||||
DrawPosition = childCenterOffset - mapCenterOffset + DrawOffset * Scale;
|
||||
}
|
||||
|
||||
private void DrawBackgroundTexture()
|
||||
{
|
||||
if (AgentMap.Instance()->SelectedMapBgPath.Length is 0) {
|
||||
var texture = Service.TextureProvider.GetFromGame($"{AgentMap.Instance()->SelectedMapPath.ToString()}.tex").GetWrapOrEmpty();
|
||||
|
||||
ImGui.SetCursorPos(DrawPosition);
|
||||
ImGui.Image(texture.Handle, texture.Size * Scale);
|
||||
}
|
||||
else {
|
||||
if (blendedPath != AgentMap.Instance()->SelectedMapBgPath.ToString()) {
|
||||
fogTexture = null;
|
||||
blendedTexture?.Dispose();
|
||||
blendedTexture = LoadTexture();
|
||||
blendedPath = AgentMap.Instance()->SelectedMapBgPath.ToString();
|
||||
}
|
||||
|
||||
if (blendedTexture is not null) {
|
||||
ImGui.SetCursorPos(DrawPosition);
|
||||
ImGui.Image(blendedTexture.Handle, blendedTexture.Size * Scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw map texture at a specific position and scale (for minimap).
|
||||
/// </summary>
|
||||
private void DrawBackgroundTextureAt(Vector2 drawPosition, float scale)
|
||||
{
|
||||
if (AgentMap.Instance()->SelectedMapBgPath.Length is 0) {
|
||||
var texture = Service.TextureProvider.GetFromGame($"{AgentMap.Instance()->SelectedMapPath.ToString()}.tex").GetWrapOrEmpty();
|
||||
ImGui.SetCursorPos(drawPosition);
|
||||
ImGui.Image(texture.Handle, texture.Size * scale);
|
||||
}
|
||||
else {
|
||||
if (blendedPath != AgentMap.Instance()->SelectedMapBgPath.ToString()) {
|
||||
fogTexture = null;
|
||||
blendedTexture?.Dispose();
|
||||
blendedTexture = LoadTexture();
|
||||
blendedPath = AgentMap.Instance()->SelectedMapBgPath.ToString();
|
||||
}
|
||||
|
||||
if (blendedTexture is not null) {
|
||||
ImGui.SetCursorPos(drawPosition);
|
||||
ImGui.Image(blendedTexture.Handle, blendedTexture.Size * scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw cached map texture for minimap (used when area map is closed).
|
||||
/// </summary>
|
||||
private void DrawMinimapCachedTextureAt(Vector2 drawPosition, float scale, IDalamudTextureWrap texture)
|
||||
{
|
||||
ImGui.SetCursorPos(drawPosition);
|
||||
ImGui.Image(texture.Handle, texture.Size * scale);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if we have cached texture/transform for this map (e.g. after opening the area map once).
|
||||
/// </summary>
|
||||
public bool HasMinimapCacheFor(uint mapId) => _minimapCache.ContainsKey(mapId);
|
||||
|
||||
/// <summary>
|
||||
/// Try to load map texture and transform from Lumina (Map sheet) so the minimap can draw without opening the area map.
|
||||
/// Uses game map path conventions (ui/map/...) and Map.SizeFactor, Map.OffsetX/Y. On success, fills the cache for this map.
|
||||
/// </summary>
|
||||
public bool TryEnsureLuminaCacheFor(uint mapId)
|
||||
{
|
||||
if (mapId == 0 || _minimapCache.ContainsKey(mapId)) return true;
|
||||
|
||||
var map = Service.DataManager.GetExcelSheet<Map>().GetRow(mapId);
|
||||
if (map.RowId == 0) return false;
|
||||
|
||||
var idStr = map.Id.ExtractText()?.Trim() ?? string.Empty;
|
||||
if (string.IsNullOrEmpty(idStr)) return false;
|
||||
|
||||
// Try several path conventions so the minimap can show without ever requiring the user to open the Area Map.
|
||||
var fileName = idStr.Replace("/", "");
|
||||
var pathsToTry = new[]
|
||||
{
|
||||
$"ui/map/{idStr}/{fileName}_m.tex",
|
||||
$"ui/map/{idStr}/{fileName}_l.tex",
|
||||
$"ui/map/{idStr}/{fileName}.tex",
|
||||
$"ui/uld/areamap/{mapId:D4}.tex",
|
||||
$"ui/uld/areamap/{fileName}.tex",
|
||||
};
|
||||
|
||||
IDalamudTextureWrap? texture = null;
|
||||
foreach (var path in pathsToTry) {
|
||||
texture = LoadSingleTexture(path);
|
||||
if (texture is not null) break;
|
||||
}
|
||||
if (texture is null) return false;
|
||||
|
||||
TrimMinimapCacheToLimit();
|
||||
var entry = _minimapCache[mapId] = new MinimapCacheEntry();
|
||||
entry.PathKey = $"lumina:{mapId}";
|
||||
entry.Texture = texture;
|
||||
entry.ScaleFactor = map.SizeFactor / 100f;
|
||||
entry.OffsetX = map.OffsetX;
|
||||
entry.OffsetY = map.OffsetY;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static IDalamudTextureWrap? LoadSingleTexture(string path)
|
||||
{
|
||||
var file = GetTexFile(path);
|
||||
if (file is null) return null;
|
||||
var bytes = file.GetRgbaImageData();
|
||||
var w = file.Header.Width;
|
||||
var h = file.Header.Height;
|
||||
return Service.TextureProvider.CreateFromRaw(RawImageSpecification.Rgba32(w, h), bytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw minimap view (current map only, centered on player) into the current ImGui cursor region.
|
||||
/// Prefers Lumina-loaded data so the minimap shows automatically without opening the Area Map.
|
||||
/// If the user opens the main map (M), we cache that for a higher-quality display; otherwise we use Lumina when available.
|
||||
/// </summary>
|
||||
public void DrawMinimapContents(Vector2 size)
|
||||
{
|
||||
var agent = AgentMap.Instance();
|
||||
if (Service.ObjectTable.LocalPlayer is not { } localPlayer) return;
|
||||
|
||||
var currentMapId = agent->CurrentMapId;
|
||||
if (currentMapId == 0) return;
|
||||
|
||||
// Do not call OpenMapByMapId/RefreshMapMarkers from here: it opens the area map repeatedly.
|
||||
// Markers are drawn from whatever the game has already populated (e.g. after opening the map once).
|
||||
|
||||
// When the game has the current map loaded (area map open or just closed), update our cache for this map.
|
||||
if (agent->SelectedMapId == currentMapId && agent->SelectedMapPath.Length > 0) {
|
||||
var bgPath = $"{agent->SelectedMapBgPath}.tex";
|
||||
var fgPath = $"{agent->SelectedMapPath}.tex";
|
||||
var pathKey = bgPath + "|" + fgPath;
|
||||
if (!_minimapCache.TryGetValue(currentMapId, out var entry) || entry.PathKey != pathKey) {
|
||||
TrimMinimapCacheToLimit();
|
||||
entry = _minimapCache[currentMapId] = new MinimapCacheEntry();
|
||||
entry.PathKey = pathKey;
|
||||
entry.Texture?.Dispose();
|
||||
entry.Texture = LoadTextureFromPaths(bgPath, fgPath);
|
||||
}
|
||||
if (entry.Texture is not null) {
|
||||
entry.ScaleFactor = agent->SelectedMapSizeFactorFloat;
|
||||
entry.OffsetX = agent->SelectedOffsetX;
|
||||
entry.OffsetY = agent->SelectedOffsetY;
|
||||
}
|
||||
}
|
||||
|
||||
// If no cache yet, try loading from Lumina (Map sheet) so minimap works without opening the area map.
|
||||
TryEnsureLuminaCacheFor(currentMapId);
|
||||
|
||||
// Draw from cache if we have it for the current map.
|
||||
if (!_minimapCache.TryGetValue(currentMapId, out var cached) || cached.Texture is null)
|
||||
return;
|
||||
|
||||
// Use the size passed by the minimap window (window size) so zoom/center is stable.
|
||||
if (size.X <= 0 || size.Y <= 0) return;
|
||||
|
||||
var zoom = Math.Clamp(System.SystemConfig.MinimapZoom, 0.03f, 0.112f);
|
||||
var mapSize = cached.Texture.Size.X;
|
||||
// Scale so the map COVERS the view at zoom=1 (use max so no black bands at max zoom out).
|
||||
var fitScale = Math.Max(size.X, size.Y) / mapSize;
|
||||
var scale = fitScale / zoom;
|
||||
// Ensure map always covers the view at any zoom (no black edges when zoomed in).
|
||||
var minScale = Math.Max(size.X, size.Y) / mapSize;
|
||||
scale = Math.Clamp(scale, minScale, 20.0f);
|
||||
|
||||
var playerCoord = new Vector2(localPlayer.Position.X, localPlayer.Position.Z);
|
||||
var drawOffset = (-playerCoord + new Vector2(cached.OffsetX, cached.OffsetY)) * cached.ScaleFactor;
|
||||
|
||||
var centerOffset = size / 2.0f;
|
||||
var mapCenterOffset = (cached.Texture.Size / 2f) * scale;
|
||||
var drawPosition = centerOffset - mapCenterOffset + drawOffset * scale;
|
||||
|
||||
// Clamp so the map always fills the view (no black), but when zoomed in allow full pan so the player tracks.
|
||||
var texSize = cached.Texture.Size * scale;
|
||||
if (texSize.X > size.X && texSize.Y > size.Y) {
|
||||
// Zoomed in: allow full pan range so the map can shift in any direction and the player stays at center.
|
||||
drawPosition.X = Math.Clamp(drawPosition.X, size.X - texSize.X, texSize.X - size.X);
|
||||
drawPosition.Y = Math.Clamp(drawPosition.Y, size.Y - texSize.Y, texSize.Y - size.Y);
|
||||
} else {
|
||||
// Zoomed out or exact fit: clamp so the map rect covers (0,0)-(size) to avoid black edges.
|
||||
drawPosition.X = Math.Clamp(drawPosition.X, size.X - texSize.X, 0f);
|
||||
drawPosition.Y = Math.Clamp(drawPosition.Y, size.Y - texSize.Y, 0f);
|
||||
}
|
||||
|
||||
// Content top-left in screen space so player is drawn at the true center of the minimap (not offset by title bar).
|
||||
var contentTopLeft = ImGui.GetCursorScreenPos();
|
||||
DrawMinimapCachedTextureAt(drawPosition, scale, cached.Texture);
|
||||
var centerScreen = contentTopLeft + centerOffset;
|
||||
// Draw cone under markers so quest/FATE/POI markers stay visible on top of the cone.
|
||||
DrawMinimapConeAtCenter(centerScreen, scale);
|
||||
DrawMinimapMarkers(contentTopLeft, drawPosition, scale, size, cached.OffsetX, cached.OffsetY, cached.ScaleFactor);
|
||||
DrawPlayerAtCenter(centerScreen, scale);
|
||||
if (System.SystemConfig.MinimapShowQuestDirectionArrow)
|
||||
DrawMinimapQuestDirectionArrow(contentTopLeft, drawPosition, scale, size, centerOffset, cached.OffsetX, cached.OffsetY, cached.ScaleFactor, currentMapId);
|
||||
if (System.SystemConfig.MinimapShowFateDirectionArrows)
|
||||
DrawMinimapFateDirectionArrows(contentTopLeft, drawPosition, scale, size, centerOffset, cached.OffsetX, cached.OffsetY, cached.ScaleFactor);
|
||||
}
|
||||
|
||||
private void TrimMinimapCacheToLimit()
|
||||
{
|
||||
while (_minimapCache.Count >= MinimapCacheMaxEntries) {
|
||||
var first = default(uint);
|
||||
foreach (var k in _minimapCache.Keys) { first = k; break; }
|
||||
if (_minimapCache.Remove(first, out var entry))
|
||||
entry.Texture?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static IDalamudTextureWrap? LoadTextureFromPaths(string vanillaBgPath, string vanillaFgPath)
|
||||
{
|
||||
var bgFile = GetTexFile(vanillaBgPath);
|
||||
var fgFile = GetTexFile(vanillaFgPath);
|
||||
if (bgFile is null || fgFile is null) return null;
|
||||
|
||||
var backgroundBytes = bgFile.GetRgbaImageData();
|
||||
var foregroundBytes = fgFile.GetRgbaImageData();
|
||||
Parallel.For(0, 2048 * 2048, i =>
|
||||
{
|
||||
var index = i * 4;
|
||||
backgroundBytes[index + 0] = (byte)(backgroundBytes[index + 0] * foregroundBytes[index + 0] / 255);
|
||||
backgroundBytes[index + 1] = (byte)(backgroundBytes[index + 1] * foregroundBytes[index + 1] / 255);
|
||||
backgroundBytes[index + 2] = (byte)(backgroundBytes[index + 2] * foregroundBytes[index + 2] / 255);
|
||||
});
|
||||
return Service.TextureProvider.CreateFromRaw(RawImageSpecification.Rgba32(2048, 2048), backgroundBytes);
|
||||
}
|
||||
|
||||
private static IDalamudTextureWrap? LoadTexture()
|
||||
{
|
||||
var vanillaBgPath = $"{AgentMap.Instance()->SelectedMapBgPath.ToString()}.tex";
|
||||
var vanillaFgPath = $"{AgentMap.Instance()->SelectedMapPath.ToString()}.tex";
|
||||
|
||||
var bgFile = GetTexFile(vanillaBgPath);
|
||||
var fgFile = GetTexFile(vanillaFgPath);
|
||||
|
||||
if (bgFile is null || fgFile is null) {
|
||||
Service.Log.Warning("Failed to load map textures");
|
||||
return null;
|
||||
}
|
||||
|
||||
var backgroundBytes = bgFile.GetRgbaImageData();
|
||||
var foregroundBytes = fgFile.GetRgbaImageData();
|
||||
|
||||
// Blend textures together
|
||||
Parallel.For(0, 2048 * 2048, i =>
|
||||
{
|
||||
var index = i * 4;
|
||||
|
||||
// Blend, R, G, B, skip A.
|
||||
backgroundBytes[index + 0] = (byte)(backgroundBytes[index + 0] * foregroundBytes[index + 0] / 255);
|
||||
backgroundBytes[index + 1] = (byte)(backgroundBytes[index + 1] * foregroundBytes[index + 1] / 255);
|
||||
backgroundBytes[index + 2] = (byte)(backgroundBytes[index + 2] * foregroundBytes[index + 2] / 255);
|
||||
});
|
||||
|
||||
return Service.TextureProvider.CreateFromRaw(RawImageSpecification.Rgba32(2048, 2048), backgroundBytes);
|
||||
}
|
||||
|
||||
private static TexFile? GetTexFile(string rawPath)
|
||||
{
|
||||
var path = Service.TextureSubstitutionProvider.GetSubstitutedPath(rawPath);
|
||||
|
||||
if (Path.IsPathRooted(path)) {
|
||||
return Service.DataManager.GameData.GetFileFromDisk<TexFile>(path);
|
||||
}
|
||||
|
||||
return Service.DataManager.GetFile<TexFile>(path);
|
||||
}
|
||||
|
||||
private void DrawMapMarkers()
|
||||
{
|
||||
DrawStaticMapMarkers();
|
||||
DrawDynamicMarkers();
|
||||
DrawGameObjects();
|
||||
DrawGroupMembers();
|
||||
DrawTemporaryMarkers();
|
||||
DrawGatheringMarkers();
|
||||
DrawFieldMarkers();
|
||||
DrawPlayer();
|
||||
DrawStaticTextMarkers();
|
||||
DrawFlag();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Linq;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using Lumina.Extensions;
|
||||
using Mappy.Extensions;
|
||||
|
||||
namespace Mappy.MapRenderer;
|
||||
|
||||
public partial class MapRenderer
|
||||
{
|
||||
private unsafe void DrawDynamicMarkers()
|
||||
{
|
||||
// Group together icons based on their dataId, this is because square enix shows circles then draws the actual icon overtop
|
||||
var iconGroups = AgentMap.Instance()->EventMarkers.GroupBy(markers => (markers.DataId, markers.Position));
|
||||
|
||||
foreach (var group in iconGroups) {
|
||||
// Make a copy of the first marker in the set, we will be mutating this copy.
|
||||
var markerCopy = group.First();
|
||||
|
||||
// Get the actual iconId we want, typically the icon for the fate, not the circle
|
||||
var correctIconId = group.FirstOrNull(marker => marker.IconId is not 60493);
|
||||
markerCopy.IconId = correctIconId?.IconId ?? markerCopy.IconId;
|
||||
|
||||
// Get the actual radius value for this marker, typically the circle icon will have this value.
|
||||
markerCopy.Radius = group.Max(marker => marker.Radius);
|
||||
|
||||
// Disable radius markings for housing areas
|
||||
if (HousingManager.Instance()->CurrentTerritory is not null) {
|
||||
markerCopy.Radius = 0.0f;
|
||||
}
|
||||
|
||||
markerCopy.Draw(DrawPosition, Scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Numerics;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using Mappy.Classes;
|
||||
|
||||
namespace Mappy.MapRenderer;
|
||||
|
||||
public partial class MapRenderer
|
||||
{
|
||||
private unsafe void DrawFlag()
|
||||
{
|
||||
if (AgentMap.Instance()->FlagMarkerCount is not 0 && AgentMap.Instance()->FlagMapMarkers[0].TerritoryId == AgentMap.Instance()->SelectedTerritoryId) {
|
||||
ref var flagMarker = ref AgentMap.Instance()->FlagMapMarkers[0];
|
||||
|
||||
DrawHelpers.DrawMapMarker(new MarkerInfo
|
||||
{
|
||||
Position = new Vector2(flagMarker.XFloat, flagMarker.YFloat) * Scale * DrawHelpers.GetMapScaleFactor() + DrawHelpers.GetCombinedOffsetVector() * Scale,
|
||||
IconId = flagMarker.MapMarker.IconId,
|
||||
Offset = DrawPosition,
|
||||
Scale = Scale,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Interface.Textures;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using Dalamud.Utility;
|
||||
using Dalamud.Utility.Signatures;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiLib.Classes;
|
||||
using TerraFX.Interop.DirectX;
|
||||
|
||||
namespace Mappy.MapRenderer;
|
||||
|
||||
public unsafe partial class MapRenderer
|
||||
{
|
||||
private delegate void ImmediateContextProcessCommands(ImmediateContext* commands, RenderCommandBufferGroup* bufferGroup, uint a3);
|
||||
|
||||
[Signature("E8 ?? ?? ?? ?? 48 8B 4B 30 FF 15 ?? ?? ?? ??", DetourName = nameof(OnImmediateContextProcessCommands))]
|
||||
private readonly Hook<ImmediateContextProcessCommands>? immediateContextProcessCommandsHook = null;
|
||||
|
||||
private bool requestUpdatedMaskingTexture;
|
||||
private byte[]? maskingTextureBytes;
|
||||
|
||||
private byte[]? blockyFogBytes;
|
||||
private IDalamudTextureWrap? fogTexture;
|
||||
private int lastKnownDiscoveryFlags;
|
||||
private readonly Stopwatch textureLoadStopwatch = new();
|
||||
|
||||
private static int CurrentDiscoveryFlags => AtkStage.Instance()->GetNumberArrayData(NumberArrayType.AreaMap2)->IntArray[2];
|
||||
|
||||
private void LoadFogHooks()
|
||||
{
|
||||
Service.Hooker.InitializeFromAttributes(this);
|
||||
immediateContextProcessCommandsHook?.Enable();
|
||||
}
|
||||
|
||||
private void UnloadFogHooks()
|
||||
{
|
||||
immediateContextProcessCommandsHook?.Dispose();
|
||||
}
|
||||
|
||||
private void OnImmediateContextProcessCommands(ImmediateContext* commands, RenderCommandBufferGroup* bufferGroup, uint a3) =>
|
||||
HookSafety.ExecuteSafe(() =>
|
||||
{
|
||||
// Delay by a certain number of frames because the game hasn't loaded the new texture yet.
|
||||
if (requestUpdatedMaskingTexture && textureLoadStopwatch is { IsRunning: true, ElapsedMilliseconds: > 200 }) {
|
||||
maskingTextureBytes = null;
|
||||
maskingTextureBytes = GetPrebakedTextureBytes();
|
||||
requestUpdatedMaskingTexture = false;
|
||||
textureLoadStopwatch.Stop();
|
||||
|
||||
Task.Run(LoadFogTexture);
|
||||
}
|
||||
|
||||
immediateContextProcessCommandsHook!.Original(commands, bufferGroup, a3);
|
||||
}, Service.Log, "Exception during OnImmediateContextProcessCommands");
|
||||
|
||||
private void DrawFogOfWar()
|
||||
{
|
||||
if (!System.SystemConfig.ShowFogOfWar) return;
|
||||
if (CurrentDiscoveryFlags == AgentMap.Instance()->SelectedMapDiscoveryFlag) return;
|
||||
if (CurrentDiscoveryFlags == -1) return;
|
||||
|
||||
var flagsChanged = lastKnownDiscoveryFlags != CurrentDiscoveryFlags;
|
||||
lastKnownDiscoveryFlags = CurrentDiscoveryFlags;
|
||||
|
||||
if (flagsChanged) {
|
||||
Service.Log.Debug("[Fog of War] Discovery Bits Changed, updating fog texture.");
|
||||
requestUpdatedMaskingTexture = true;
|
||||
textureLoadStopwatch.Restart();
|
||||
fogTexture = null;
|
||||
}
|
||||
|
||||
if (fogTexture is not null) {
|
||||
ImGui.SetCursorPos(DrawPosition);
|
||||
ImGui.Image(fogTexture.Handle, fogTexture.Size * Scale);
|
||||
}
|
||||
else {
|
||||
var defaultBackgroundTexture = Service.TextureProvider.GetFromGame($"{AgentMap.Instance()->SelectedMapBgPath.ToString()}.tex").GetWrapOrEmpty();
|
||||
|
||||
ImGui.SetCursorPos(DrawPosition);
|
||||
ImGui.Image(defaultBackgroundTexture.Handle, defaultBackgroundTexture.Size * Scale);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawFogOfWarAt(Vector2 drawPosition, float scale)
|
||||
{
|
||||
if (!System.SystemConfig.ShowFogOfWar) return;
|
||||
if (CurrentDiscoveryFlags == AgentMap.Instance()->SelectedMapDiscoveryFlag) return;
|
||||
if (CurrentDiscoveryFlags == -1) return;
|
||||
|
||||
var flagsChanged = lastKnownDiscoveryFlags != CurrentDiscoveryFlags;
|
||||
lastKnownDiscoveryFlags = CurrentDiscoveryFlags;
|
||||
|
||||
if (flagsChanged) {
|
||||
Service.Log.Debug("[Fog of War] Discovery Bits Changed, updating fog texture.");
|
||||
requestUpdatedMaskingTexture = true;
|
||||
textureLoadStopwatch.Restart();
|
||||
fogTexture = null;
|
||||
}
|
||||
|
||||
if (fogTexture is not null) {
|
||||
ImGui.SetCursorPos(drawPosition);
|
||||
ImGui.Image(fogTexture.Handle, fogTexture.Size * scale);
|
||||
}
|
||||
else {
|
||||
var defaultBackgroundTexture = Service.TextureProvider.GetFromGame($"{AgentMap.Instance()->SelectedMapBgPath.ToString()}.tex").GetWrapOrEmpty();
|
||||
ImGui.SetCursorPos(drawPosition);
|
||||
ImGui.Image(defaultBackgroundTexture.Handle, defaultBackgroundTexture.Size * scale);
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadFogTexture()
|
||||
{
|
||||
var vanillaBgPath = $"{AgentMap.Instance()->SelectedMapBgPath.ToString()}.tex";
|
||||
var bgFile = GetTexFile(vanillaBgPath);
|
||||
|
||||
if (bgFile is null) {
|
||||
Service.Log.Warning("Failed to load map textures");
|
||||
return;
|
||||
}
|
||||
|
||||
// Load non-transparent background texture
|
||||
var backgroundBytes = bgFile.GetRgbaImageData();
|
||||
|
||||
// Load alpha mapping
|
||||
if (maskingTextureBytes is null) return;
|
||||
|
||||
var timer = Stopwatch.StartNew();
|
||||
|
||||
// Make background texture fully invisible
|
||||
for (var index = 0; index < 2048 * 2048; index++) {
|
||||
backgroundBytes[index * 4 + 3] = 0;
|
||||
}
|
||||
|
||||
// Make non-transparent any section that the player has not-already explored
|
||||
for (var x = 0; x < 128; x++)
|
||||
for (var y = 0; y < 128; y++) {
|
||||
var pixelIndex = (x + y * 128) * 4;
|
||||
var targetPixel = (x + 2048 * y) * 4;
|
||||
|
||||
var redAmount = maskingTextureBytes[pixelIndex + 0] / 255.0f;
|
||||
var greenAmount = maskingTextureBytes[pixelIndex + 1] / 255.0f;
|
||||
var blueAmount = maskingTextureBytes[pixelIndex + 2] / 255.0f;
|
||||
|
||||
var maxAlpha = Math.Max(redAmount, Math.Max(greenAmount, blueAmount));
|
||||
var alphaSum = (byte)(maxAlpha * 255);
|
||||
|
||||
if (alphaSum is not 0) {
|
||||
const int scaleFactor = 16;
|
||||
foreach (var xScalar in Enumerable.Range(0, scaleFactor))
|
||||
foreach (var yScalar in Enumerable.Range(0, scaleFactor)) {
|
||||
var scalingPixelTarget = targetPixel * scaleFactor + xScalar * 4 + yScalar * 2048 * 4;
|
||||
backgroundBytes[scalingPixelTarget + 3] = alphaSum;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Service.Log.Debug($"Fog of War Calculated in {timer.ElapsedMilliseconds} ms");
|
||||
|
||||
blockyFogBytes = backgroundBytes;
|
||||
fogTexture = Service.TextureProvider.CreateFromRaw(RawImageSpecification.Rgba32(2048, 2048), backgroundBytes);
|
||||
|
||||
Task.Run(CleanupFogTexture);
|
||||
}
|
||||
|
||||
private static byte[]? GetPrebakedTextureBytes()
|
||||
{
|
||||
var addon = Service.GameGui.GetAddonByName<AddonAreaMap>("AreaMap");
|
||||
if (addon is null) return null;
|
||||
|
||||
var componentMap = (void*)Marshal.ReadIntPtr((nint)addon, 0x430);
|
||||
if (componentMap is null) return null;
|
||||
|
||||
var texturePointer = (Texture*)Marshal.ReadIntPtr((nint)componentMap, 0x270);
|
||||
if (texturePointer is null) return null;
|
||||
|
||||
var device = (ID3D11Device*)Service.PluginInterface.UiBuilder.DeviceHandle;
|
||||
var texture = (ID3D11Texture2D*)texturePointer->D3D11Texture2D;
|
||||
|
||||
D3D11_TEXTURE2D_DESC description;
|
||||
texture->GetDesc(&description);
|
||||
|
||||
description.ArraySize = 1;
|
||||
description.BindFlags = 0;
|
||||
description.MipLevels = 1;
|
||||
description.MiscFlags = 0;
|
||||
description.CPUAccessFlags = (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ;
|
||||
description.Usage = D3D11_USAGE.D3D11_USAGE_STAGING;
|
||||
|
||||
|
||||
ID3D11Texture2D* stagingTexture;
|
||||
if (device->CreateTexture2D(&description, null, &stagingTexture)< 0)
|
||||
return null;
|
||||
|
||||
ID3D11DeviceContext* context;
|
||||
device->GetImmediateContext(&context);
|
||||
|
||||
context->CopyResource((ID3D11Resource*)stagingTexture, (ID3D11Resource*)texture);
|
||||
|
||||
D3D11_MAPPED_SUBRESOURCE mapped;
|
||||
if (context->Map((ID3D11Resource*)stagingTexture, 0, D3D11_MAP.D3D11_MAP_READ, 0, &mapped) < 0)
|
||||
{
|
||||
context->Release();
|
||||
stagingTexture->Release();
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
int bufferSize = (int)(description.Height * mapped.RowPitch);
|
||||
byte[] pixelData = new byte[bufferSize];
|
||||
|
||||
Marshal.Copy((IntPtr)mapped.pData, pixelData, 0, bufferSize);
|
||||
|
||||
context->Unmap((ID3D11Resource*)stagingTexture, 0);
|
||||
context->Release();
|
||||
stagingTexture->Release();
|
||||
|
||||
return pixelData;
|
||||
}
|
||||
|
||||
private void CleanupFogTexture()
|
||||
{
|
||||
if (blockyFogBytes is null) return;
|
||||
|
||||
var timer = Stopwatch.StartNew();
|
||||
|
||||
// Because we had to scale a 128x128 texture mapping onto a 2048x2048, it'll look very blurry, lets blend the alpha channel
|
||||
const int blurRadius = 8;
|
||||
|
||||
for (var x = 0; x < 2048; x++)
|
||||
for (var y = 0; y < 2048; y++) {
|
||||
var pixelIndex = (x + y * 2048) * 4;
|
||||
|
||||
var alphaAverage = 0.0f;
|
||||
var numAveraged = 0;
|
||||
|
||||
if (blockyFogBytes[pixelIndex + 3] == 255) continue;
|
||||
|
||||
for (var xBlur = -blurRadius; xBlur < -blurRadius + blurRadius * 2; ++xBlur) {
|
||||
var currentX = x + xBlur;
|
||||
if (currentX is < 0 or >= 2048) continue;
|
||||
var currentPixelIndex = (currentX + y * 2048) * 4;
|
||||
|
||||
alphaAverage += blockyFogBytes[currentPixelIndex + 3];
|
||||
numAveraged++;
|
||||
}
|
||||
|
||||
for (var yBlur = -blurRadius; yBlur < -blurRadius + blurRadius * 2; ++yBlur) {
|
||||
var currentY = y + yBlur;
|
||||
|
||||
if (currentY is < 0 or >= 2048) continue;
|
||||
var currentPixelIndex = (x + currentY * 2048) * 4;
|
||||
|
||||
alphaAverage += blockyFogBytes[currentPixelIndex + 3];
|
||||
numAveraged++;
|
||||
}
|
||||
|
||||
var newAlpha = (byte)(alphaAverage / numAveraged);
|
||||
blockyFogBytes[pixelIndex + 3] = newAlpha;
|
||||
}
|
||||
|
||||
fogTexture = Service.TextureProvider.CreateFromRaw(RawImageSpecification.Rgba32(2048, 2048), blockyFogBytes);
|
||||
|
||||
Service.Log.Debug($"Texture Cleanup completed in {timer.ElapsedMilliseconds} ms");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Event;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Group;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using KamiLib.Extensions;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Mappy.Classes;
|
||||
using Mappy.Extensions;
|
||||
using BattleNpcSubKind = Dalamud.Game.ClientState.Objects.Enums.BattleNpcSubKind;
|
||||
using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||
|
||||
namespace Mappy.MapRenderer;
|
||||
|
||||
public partial class MapRenderer
|
||||
{
|
||||
private unsafe void DrawGameObjects()
|
||||
{
|
||||
if (AgentMap.Instance()->SelectedMapId != AgentMap.Instance()->CurrentMapId) return;
|
||||
|
||||
if (Service.ObjectTable is not { LocalPlayer: { } player }) return;
|
||||
|
||||
if (System.SystemConfig.ShowRadar) {
|
||||
if ((Service.Condition.IsBoundByDuty() && System.SystemConfig.ShowRadarInDuties) || !Service.Condition.IsBoundByDuty()) {
|
||||
DrawRadar(player);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var obj in Service.ObjectTable.Reverse()) {
|
||||
if (!obj.IsTargetable) continue;
|
||||
if (GroupManager.Instance()->MainGroup.IsEntityIdInParty(obj.EntityId)) continue;
|
||||
if (GroupManager.Instance()->MainGroup.IsEntityIdInAlliance(obj.EntityId)) continue;
|
||||
if (Vector3.Distance(obj.Position, player.Position) >= 150.0f) continue;
|
||||
|
||||
DrawHelpers.DrawMapMarker(new MarkerInfo
|
||||
{
|
||||
Position = (obj.GetMapPosition() -
|
||||
DrawHelpers.GetMapOffsetVector() +
|
||||
DrawHelpers.GetMapCenterOffsetVector()) * Scale,
|
||||
Offset = DrawPosition,
|
||||
Scale = Scale,
|
||||
IconId = obj.ObjectKind switch
|
||||
{
|
||||
ObjectKind.Player when GroupManager.Instance()->MainGroup.MemberCount is 0 && System.SystemConfig.ShowPlayers => 60421,
|
||||
ObjectKind.Player when System.SystemConfig.ShowPlayers => 60444,
|
||||
ObjectKind.BattleNpc when IsBoss(obj) && obj.TargetObject is null => 60402,
|
||||
ObjectKind.BattleNpc when IsBoss(obj) && obj.TargetObject is not null => 60401,
|
||||
ObjectKind.BattleNpc when obj is { SubKind: (int)BattleNpcSubKind.Enemy, TargetObject: not null } => 60422,
|
||||
ObjectKind.BattleNpc when obj is { SubKind: (int)BattleNpcSubKind.Enemy, TargetObject: null } => 60424,
|
||||
ObjectKind.BattleNpc when obj.SubKind == (int)BattleNpcSubKind.Pet => 60961,
|
||||
ObjectKind.Treasure => 60003,
|
||||
ObjectKind.GatheringPoint => System.GatheringPointIconCache.GetValue(obj.BaseId),
|
||||
ObjectKind.EventObj when IsAetherCurrent(obj) => 60653,
|
||||
_ => 0
|
||||
},
|
||||
PrimaryText = () => GetTooltipForGameObject(obj),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawRadar(IPlayerCharacter gameObjectCenter)
|
||||
{
|
||||
var position = ImGui.GetWindowPos() +
|
||||
DrawPosition +
|
||||
(gameObjectCenter.GetMapPosition() -
|
||||
DrawHelpers.GetMapOffsetVector() +
|
||||
DrawHelpers.GetMapCenterOffsetVector()) * Scale;
|
||||
|
||||
ImGui.GetWindowDrawList().AddCircleFilled(position, 150.0f * Scale, ImGui.GetColorU32(System.SystemConfig.RadarColor));
|
||||
ImGui.GetWindowDrawList().AddCircle(position, 150.0f * Scale, ImGui.GetColorU32(System.SystemConfig.RadarOutlineColor));
|
||||
}
|
||||
|
||||
private string GetTooltipForGameObject(IGameObject obj)
|
||||
{
|
||||
return obj switch
|
||||
{
|
||||
IBattleNpc { Level: > 0 } battleNpc => $"Lv. {battleNpc.Level} {battleNpc.Name}",
|
||||
IPlayerCharacter { Level: > 0 } playerCharacter => $"Lv. {playerCharacter.Level} {playerCharacter.Name}",
|
||||
_ => obj.ObjectKind switch
|
||||
{
|
||||
ObjectKind.GatheringPoint => System.GatheringPointNameCache.GetValue((obj.BaseId, obj.Name.ToString())) ?? string.Empty,
|
||||
ObjectKind.Treasure => obj.Name.ToString(),
|
||||
ObjectKind.EventObj when IsAetherCurrent(obj) => obj.Name.ToString(),
|
||||
_ => string.Empty
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private unsafe bool IsAetherCurrent(IGameObject gameObject)
|
||||
{
|
||||
if (gameObject.ObjectKind is not ObjectKind.EventObj) return false;
|
||||
|
||||
var csEventObject = (GameObject*)gameObject.Address;
|
||||
if (csEventObject is null) return false;
|
||||
|
||||
if (csEventObject->EventHandler is null) return false;
|
||||
return csEventObject->EventHandler->Info.EventId.ContentId == EventHandlerContent.AetherCurrent;
|
||||
}
|
||||
|
||||
private bool IsBoss(IGameObject chara) => Service.DataManager.GetExcelSheet<BNpcBase>().GetRow(chara.BaseId).Rank is 2 or 6;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using Mappy.Extensions;
|
||||
|
||||
namespace Mappy.MapRenderer;
|
||||
|
||||
public partial class MapRenderer
|
||||
{
|
||||
private unsafe void DrawGatheringMarkers()
|
||||
{
|
||||
foreach (var marker in AgentMap.Instance()->MiniMapGatheringMarkers) {
|
||||
marker.Draw(DrawPosition, Scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Numerics;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Group;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using Mappy.Classes;
|
||||
|
||||
namespace Mappy.MapRenderer;
|
||||
|
||||
public unsafe partial class MapRenderer
|
||||
{
|
||||
private void DrawGroupMembers()
|
||||
{
|
||||
foreach (var partyMember in GroupManager.Instance()->MainGroup.PartyMembers[..GroupManager.Instance()->MainGroup.MemberCount]) {
|
||||
if (partyMember.EntityId is 0xE0000000) continue;
|
||||
if (partyMember.TerritoryType != AgentMap.Instance()->SelectedTerritoryId) continue;
|
||||
|
||||
DrawHelpers.DrawMapMarker(new MarkerInfo
|
||||
{
|
||||
Position = (new Vector2(partyMember.Position.X, partyMember.Position.Z) * DrawHelpers.GetMapScaleFactor() -
|
||||
DrawHelpers.GetMapOffsetVector() +
|
||||
DrawHelpers.GetMapCenterOffsetVector()) * Scale,
|
||||
Offset = DrawPosition,
|
||||
Scale = Scale,
|
||||
IconId = 60421,
|
||||
PrimaryText = () => $"Lv. {partyMember.Level} {partyMember.NameString}",
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var allianceMember in GroupManager.Instance()->MainGroup.AllianceMembers) {
|
||||
if (allianceMember.EntityId is 0xE0000000) continue;
|
||||
if (AgentMap.Instance()->SelectedMapId != AgentMap.Instance()->CurrentMapId) continue;
|
||||
|
||||
DrawHelpers.DrawMapMarker(new MarkerInfo
|
||||
{
|
||||
Position = (new Vector2(allianceMember.Position.X, allianceMember.Position.Z) * DrawHelpers.GetMapScaleFactor() -
|
||||
DrawHelpers.GetMapOffsetVector() +
|
||||
DrawHelpers.GetMapCenterOffsetVector()) * Scale,
|
||||
Offset = DrawPosition,
|
||||
Scale = Scale,
|
||||
IconId = 60403,
|
||||
PrimaryText = () => $"Lv. {allianceMember.Level} {allianceMember.NameString}",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,806 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Interface.Textures;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Fate;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Group;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using FFXIVClientStructs.Interop;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Mappy.Classes;
|
||||
using Mappy.Extensions;
|
||||
using KamiLib.Extensions;
|
||||
using FieldMarkerSheet = Lumina.Excel.Sheets.FieldMarker;
|
||||
|
||||
namespace Mappy.MapRenderer;
|
||||
|
||||
/// <summary>
|
||||
/// Draws map markers (POI, FATEs, quests, gathering, flag, etc.) on the minimap using the same data sources as the main map.
|
||||
/// </summary>
|
||||
public partial class MapRenderer
|
||||
{
|
||||
private const float MinimapIconSize = 28f;
|
||||
private const float MinimapIconScaleFromConfig = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// Draw all marker layers on the minimap. Call after the map texture and player are drawn.
|
||||
/// </summary>
|
||||
private void DrawMinimapMarkers(
|
||||
Vector2 contentTopLeft,
|
||||
Vector2 drawPosition,
|
||||
float scale,
|
||||
Vector2 size,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scaleFactor)
|
||||
{
|
||||
// Convert texture-space position (0..2048) to minimap content position.
|
||||
Vector2 TexToContent(float tx, float ty) => drawPosition + new Vector2(tx, ty) * scale;
|
||||
|
||||
// Static POI / map markers (aetherytes, quest NPCs, etc.)
|
||||
DrawMinimapStaticMarkers(contentTopLeft, TexToContent, size);
|
||||
// FATEs from FateManager so they update without opening the Area Map
|
||||
DrawMinimapFatesFromFateManager(contentTopLeft, TexToContent, size, scaleFactor, offsetX, offsetY);
|
||||
// Event markers (non-FATE; FATEs drawn above from FateManager)
|
||||
DrawMinimapEventMarkers(contentTopLeft, TexToContent, size, scaleFactor, offsetX, offsetY, scale);
|
||||
// Gathering points
|
||||
DrawMinimapGatheringMarkers(contentTopLeft, TexToContent, size);
|
||||
// Party / alliance members (same marker as main map)
|
||||
DrawMinimapGroupMembers(contentTopLeft, TexToContent, size, scaleFactor, offsetX, offsetY);
|
||||
// User flag
|
||||
DrawMinimapFlag(contentTopLeft, TexToContent, scaleFactor, offsetX, offsetY);
|
||||
// Temporary (quest objectives, etc.)
|
||||
DrawMinimapTempMarkers(contentTopLeft, TexToContent, size, scaleFactor, offsetX, offsetY, scale);
|
||||
// Field markers (waymarks)
|
||||
DrawMinimapFieldMarkers(contentTopLeft, TexToContent, size, scaleFactor, offsetX, offsetY);
|
||||
}
|
||||
|
||||
/// <summary>Max number of quest/objective direction arrows to show on the minimap (to avoid clutter).</summary>
|
||||
private const int MaximapQuestArrowLimit = 8;
|
||||
|
||||
/// <summary>Same orange as the quest direction arrow, for the minimap quest radius circle (more transparent).</summary>
|
||||
private static readonly Vector4 MinimapQuestCircleFill = new(0.95f, 0.78f, 0.4f, 0.48f);
|
||||
private static readonly Vector4 MinimapQuestCircleOutline = new(0.45f, 0.28f, 0.08f, 0.5f);
|
||||
|
||||
/// <summary>
|
||||
/// Draws direction arrows at the edge of the minimap pointing toward quest objectives and waymark in the current zone.
|
||||
/// Only shown when the target is off the minimap. Includes the quest/objective marker icon and default-style arrow shape.
|
||||
/// </summary>
|
||||
private unsafe void DrawMinimapQuestDirectionArrow(
|
||||
Vector2 contentTopLeft,
|
||||
Vector2 drawPosition,
|
||||
float scale,
|
||||
Vector2 size,
|
||||
Vector2 centerOffset,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scaleFactor,
|
||||
uint currentMapId)
|
||||
{
|
||||
var targets = GetAllQuestDirectionTargets(currentMapId, offsetX, offsetY, scaleFactor);
|
||||
if (targets.Count == 0) return;
|
||||
|
||||
var radius = Math.Min(size.X, size.Y) * 0.5f;
|
||||
var arrowDist = radius - 4f;
|
||||
var centerScreen = contentTopLeft + centerOffset;
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
|
||||
// Default UI-style arrowhead: slightly larger, fill + crisp outline (no shaft)
|
||||
const float arrowSize = 20f;
|
||||
const float baseHalf = 8f;
|
||||
const float headDepth = 5f;
|
||||
var colorHead = ImGui.GetColorU32(new Vector4(0.95f, 0.78f, 0.4f, 0.95f));
|
||||
var colorOutline = ImGui.GetColorU32(new Vector4(0.45f, 0.28f, 0.08f, 1f));
|
||||
|
||||
var drawn = 0;
|
||||
foreach (var (tx, ty, arrowTooltip, questMarkerIconId) in targets) {
|
||||
if (drawn >= MaximapQuestArrowLimit) break;
|
||||
|
||||
var targetInContent = drawPosition + new Vector2(tx, ty) * scale;
|
||||
var toTarget = targetInContent - centerOffset;
|
||||
var distToTarget = toTarget.Length();
|
||||
if (distToTarget < 0.01f) continue;
|
||||
if (distToTarget <= radius) continue; // Target visible on minimap, no arrow
|
||||
|
||||
var direction = toTarget / distToTarget;
|
||||
var arrowPos = centerScreen + direction * arrowDist;
|
||||
var cos = MathF.Cos(MathF.Atan2(direction.Y, direction.X));
|
||||
var sin = MathF.Sin(MathF.Atan2(direction.Y, direction.X));
|
||||
var perpX = -sin;
|
||||
var perpY = cos;
|
||||
var tipScreen = arrowPos + new Vector2(cos * arrowSize, sin * arrowSize);
|
||||
var baseBack = arrowPos - new Vector2(cos * headDepth, sin * headDepth);
|
||||
var base1Screen = baseBack + new Vector2(perpX * baseHalf, perpY * baseHalf);
|
||||
var base2Screen = baseBack - new Vector2(perpX * baseHalf, perpY * baseHalf);
|
||||
|
||||
drawList.AddTriangleFilled(tipScreen, base1Screen, base2Screen, colorHead);
|
||||
drawList.AddTriangle(tipScreen, base1Screen, base2Screen, colorOutline, 2f);
|
||||
|
||||
if (!string.IsNullOrEmpty(arrowTooltip)) {
|
||||
var minX = Math.Min(tipScreen.X, Math.Min(base1Screen.X, base2Screen.X)) - 4f;
|
||||
var minY = Math.Min(tipScreen.Y, Math.Min(base1Screen.Y, base2Screen.Y)) - 4f;
|
||||
var maxX = Math.Max(tipScreen.X, Math.Max(base1Screen.X, base2Screen.X)) + 4f;
|
||||
var maxY = Math.Max(tipScreen.Y, Math.Max(base1Screen.Y, base2Screen.Y)) + 4f;
|
||||
if (ImGui.IsMouseHoveringRect(new Vector2(minX, minY), new Vector2(maxX, maxY)))
|
||||
ImGui.SetTooltip(arrowTooltip);
|
||||
}
|
||||
drawn++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Collects all quest/flag direction targets in the current zone (flag, temp markers, cached temps, journal objectives).</summary>
|
||||
private unsafe List<(float tx, float ty, string? tooltip, uint? iconId)> GetAllQuestDirectionTargets(uint currentMapId, float offsetX, float offsetY, float scaleFactor)
|
||||
{
|
||||
var list = new List<(float tx, float ty, string? tooltip, uint? iconId)>();
|
||||
var agent = AgentMap.Instance();
|
||||
const float samePosEpsilon = 2f; // texture-space distance to consider same target
|
||||
|
||||
void AddIfNew(float tx, float ty, string? tooltip, uint? iconId)
|
||||
{
|
||||
foreach (var (otx, oty, _, _) in list)
|
||||
if (Math.Abs(otx - tx) < samePosEpsilon && Math.Abs(oty - ty) < samePosEpsilon) return;
|
||||
list.Add((tx, ty, tooltip, iconId));
|
||||
}
|
||||
|
||||
if (agent->FlagMarkerCount > 0) {
|
||||
ref var flag = ref agent->FlagMapMarkers[0];
|
||||
if (flag.TerritoryId == agent->CurrentMapId || flag.TerritoryId == agent->CurrentTerritoryId) {
|
||||
var tooltip = System.TooltipCache.GetValue(flag.MapMarker.IconId);
|
||||
if (string.IsNullOrEmpty(tooltip)) tooltip = "Flag";
|
||||
AddIfNew(1024.0f + (flag.XFloat - offsetX) * scaleFactor, 1024.0f + (flag.YFloat - offsetY) * scaleFactor, tooltip, flag.MapMarker.IconId);
|
||||
}
|
||||
}
|
||||
|
||||
if (agent->TempMapMarkerCount > 0) {
|
||||
var span = new Span<TempMapMarker>(Unsafe.AsPointer(ref agent->TempMapMarkers[0]), agent->TempMapMarkerCount);
|
||||
var groups = span.ToArray().GroupBy(m => new Vector2(m.MapMarker.X, m.MapMarker.Y));
|
||||
foreach (var group in groups) {
|
||||
var first = group.First();
|
||||
var iconId = group.FirstOrNull(m => m.MapMarker.IconId is not (60493 or 0))?.MapMarker.IconId ?? first.MapMarker.IconId;
|
||||
if (iconId is 0 && group.Count() == 2 && first.Type == 4 && group.Last() is { Type: 6, MapMarker.IconId: 0 })
|
||||
iconId = DrawHelpers.QuestionMarkIcon;
|
||||
var tooltip = first.TooltipText.ToString();
|
||||
var tx = 1024.0f + (first.MapMarker.X / 16.0f - offsetX) * scaleFactor;
|
||||
var ty = 1024.0f + (first.MapMarker.Y / 16.0f - offsetY) * scaleFactor;
|
||||
AddIfNew(tx, ty, tooltip, iconId is 0 ? null : iconId);
|
||||
}
|
||||
}
|
||||
|
||||
if (TempMarkerCache.TryGetValue(currentMapId, out var cached)) {
|
||||
foreach (var m in cached) {
|
||||
var tx = 1024.0f + (m.X / 16.0f - offsetX) * scaleFactor;
|
||||
var ty = 1024.0f + (m.Y / 16.0f - offsetY) * scaleFactor;
|
||||
AddIfNew(tx, ty, m.Tooltip, m.IconId);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (tx, ty, tooltip) in GetAllJournalQuestObjectivesInZone(currentMapId, offsetX, offsetY, scaleFactor))
|
||||
AddIfNew(tx, ty, tooltip, 60470u);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>Get texture positions and tooltips for all quests in the journal that have an objective on the given map.</summary>
|
||||
private static unsafe List<(float tx, float ty, string? tooltip)> GetAllJournalQuestObjectivesInZone(uint currentMapId, float offsetX, float offsetY, float scaleFactor)
|
||||
{
|
||||
var result = new List<(float tx, float ty, string? tooltip)>();
|
||||
try
|
||||
{
|
||||
var questSheet = Service.DataManager.GetExcelSheet<Quest>();
|
||||
if (questSheet is null) return result;
|
||||
|
||||
foreach (var quest in QuestManager.Instance()->NormalQuests)
|
||||
{
|
||||
if (quest.QuestId is 0) continue;
|
||||
|
||||
if (!questSheet.HasRow(quest.QuestId + 65536u)) continue;
|
||||
var questData = questSheet.GetRow(quest.QuestId + 65536u);
|
||||
|
||||
var todoParam = questData.TodoParams.FirstOrDefault(p => p.ToDoCompleteSeq == quest.Sequence);
|
||||
var location = todoParam.ToDoLocation.FirstOrDefault(loc => loc is not { RowId: 0, ValueNullable: null });
|
||||
if (location.ValueNullable is null || location.Value.Map.RowId != currentMapId) continue;
|
||||
|
||||
var name = questData.Name.ExtractText();
|
||||
if (string.IsNullOrWhiteSpace(name)) name = "Quest objective";
|
||||
|
||||
var worldX = location.Value.X;
|
||||
var worldZ = location.Value.Z;
|
||||
var tx = 1024.0f + (worldX - offsetX) * scaleFactor;
|
||||
var ty = 1024.0f + (worldZ - offsetY) * scaleFactor;
|
||||
result.Add((tx, ty, name));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore missing sheet, invalid rows, etc.
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Draws direction arrows for nearby FATEs at the edge of the minimap (purple). Only shown when the FATE is off the minimap.</summary>
|
||||
private static unsafe void DrawMinimapFateDirectionArrows(
|
||||
Vector2 contentTopLeft,
|
||||
Vector2 drawPosition,
|
||||
float scale,
|
||||
Vector2 size,
|
||||
Vector2 centerOffset,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scaleFactor)
|
||||
{
|
||||
if (Service.FateTable.Length is 0) return;
|
||||
|
||||
var radius = Math.Min(size.X, size.Y) * 0.5f;
|
||||
var arrowDist = radius - 4f;
|
||||
var centerScreen = contentTopLeft + centerOffset;
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
var fateColor = new Vector4(0.55f, 0.28f, 0.62f, 0.95f);
|
||||
var fateOutline = new Vector4(0.28f, 0.14f, 0.32f, 1f);
|
||||
// Same UI-style arrowhead as quest: larger, fill + crisp outline
|
||||
const float arrowSize = 20f;
|
||||
const float baseHalf = 8f;
|
||||
const float headDepth = 5f;
|
||||
|
||||
for (var i = 0; i < Service.FateTable.Length; i++)
|
||||
{
|
||||
var fateData = FateManager.Instance()->Fates[i];
|
||||
var fate = fateData.Value;
|
||||
if (fate->IconId is 0) continue;
|
||||
|
||||
var pos = new Vector2(fate->Location.X, fate->Location.Z);
|
||||
var tx = 1024.0f + (pos.X - offsetX) * scaleFactor;
|
||||
var ty = 1024.0f + (pos.Y - offsetY) * scaleFactor;
|
||||
var targetInContent = drawPosition + new Vector2(tx, ty) * scale;
|
||||
var toTarget = targetInContent - centerOffset;
|
||||
var distToTarget = toTarget.Length();
|
||||
if (distToTarget < 0.01f) continue;
|
||||
if (distToTarget <= radius) continue; // FATE visible on minimap, no arrow
|
||||
|
||||
var direction = toTarget / distToTarget;
|
||||
var arrowPos = centerScreen + direction * arrowDist;
|
||||
var cos = MathF.Cos(MathF.Atan2(direction.Y, direction.X));
|
||||
var sin = MathF.Sin(MathF.Atan2(direction.Y, direction.X));
|
||||
var perpX = -sin;
|
||||
var perpY = cos;
|
||||
var tipScreen = arrowPos + new Vector2(cos * arrowSize, sin * arrowSize);
|
||||
var baseBack = arrowPos - new Vector2(cos * headDepth, sin * headDepth);
|
||||
var base1Screen = baseBack + new Vector2(perpX * baseHalf, perpY * baseHalf);
|
||||
var base2Screen = baseBack - new Vector2(perpX * baseHalf, perpY * baseHalf);
|
||||
|
||||
var fateColorU32 = ImGui.GetColorU32(fateColor);
|
||||
var fateOutlineU32 = ImGui.GetColorU32(fateOutline);
|
||||
drawList.AddTriangleFilled(tipScreen, base1Screen, base2Screen, fateColorU32);
|
||||
drawList.AddTriangle(tipScreen, base1Screen, base2Screen, fateOutlineU32, 2f);
|
||||
|
||||
var tooltip = GetFateTooltip(fateData);
|
||||
var minX = Math.Min(tipScreen.X, Math.Min(base1Screen.X, base2Screen.X)) - 4f;
|
||||
var minY = Math.Min(tipScreen.Y, Math.Min(base1Screen.Y, base2Screen.Y)) - 4f;
|
||||
var maxX = Math.Max(tipScreen.X, Math.Max(base1Screen.X, base2Screen.X)) + 4f;
|
||||
var maxY = Math.Max(tipScreen.Y, Math.Max(base1Screen.Y, base2Screen.Y)) + 4f;
|
||||
if (ImGui.IsMouseHoveringRect(new Vector2(minX, minY), new Vector2(maxX, maxY)))
|
||||
ImGui.SetTooltip(tooltip);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsInMinimapBounds(Vector2 contentPos, Vector2 size, float margin)
|
||||
{
|
||||
// Use a large margin so we don't cull markers that are panned slightly off (zoomed in).
|
||||
return contentPos.X >= -margin && contentPos.Y >= -margin &&
|
||||
contentPos.X <= size.X + margin && contentPos.Y <= size.Y + margin;
|
||||
}
|
||||
|
||||
/// <summary>If the mouse is over the quest radius circle, show the quest tooltip.</summary>
|
||||
private static void ShowQuestRadiusTooltipIfHovered(Vector2 centerScreen, float markerRadius, float mapScale, float sizeFactor, string? tooltip)
|
||||
{
|
||||
if (markerRadius <= 1.0f || string.IsNullOrEmpty(tooltip)) return;
|
||||
var radiusPixels = markerRadius * mapScale * sizeFactor;
|
||||
if (radiusPixels < 0.5f) return;
|
||||
var mouse = ImGui.GetMousePos();
|
||||
if ((mouse - centerScreen).Length() <= radiusPixels)
|
||||
ImGui.SetTooltip(tooltip);
|
||||
}
|
||||
|
||||
/// <summary>Large margin so markers aren't culled when panned.</summary>
|
||||
private const float MinimapBoundsMargin = 200f;
|
||||
|
||||
/// <summary>Cached static POI (aetheryte, repair, moogle, etc.) per map so we can draw them on the minimap when the Area Map is closed.</summary>
|
||||
private static readonly Dictionary<uint, List<CachedStaticMarker>> StaticMarkerCache = new();
|
||||
|
||||
/// <summary>Cached quest/objective (temp) markers per map; populated during silent refresh on quest accept/turn-in/objective update.</summary>
|
||||
private static readonly Dictionary<uint, List<CachedTempMarker>> TempMarkerCache = new();
|
||||
|
||||
/// <summary>Cached non-FATE event markers per map; populated during silent refresh.</summary>
|
||||
private static readonly Dictionary<uint, List<CachedEventMarker>> EventMarkerCache = new();
|
||||
|
||||
private readonly struct CachedStaticMarker
|
||||
{
|
||||
public readonly uint IconId;
|
||||
public readonly int X;
|
||||
public readonly int Y;
|
||||
public readonly string? Tooltip;
|
||||
|
||||
public CachedStaticMarker(uint iconId, int x, int y, string? tooltip)
|
||||
{
|
||||
IconId = iconId;
|
||||
X = x;
|
||||
Y = y;
|
||||
Tooltip = tooltip;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct CachedTempMarker
|
||||
{
|
||||
public readonly uint IconId;
|
||||
public readonly int X;
|
||||
public readonly int Y;
|
||||
public readonly float Radius;
|
||||
public readonly string? Tooltip;
|
||||
|
||||
public CachedTempMarker(uint iconId, int x, int y, float radius, string? tooltip)
|
||||
{
|
||||
IconId = iconId;
|
||||
X = x;
|
||||
Y = y;
|
||||
Radius = radius;
|
||||
Tooltip = tooltip;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct CachedEventMarker
|
||||
{
|
||||
public readonly uint IconId;
|
||||
public readonly float MapX;
|
||||
public readonly float MapY;
|
||||
public readonly float Radius;
|
||||
public readonly string? Tooltip;
|
||||
|
||||
public CachedEventMarker(uint iconId, float mapX, float mapY, float radius, string? tooltip)
|
||||
{
|
||||
IconId = iconId;
|
||||
MapX = mapX;
|
||||
MapY = mapY;
|
||||
Radius = radius;
|
||||
Tooltip = tooltip;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void DrawMinimapStaticMarkers(Vector2 contentTopLeft, Func<float, float, Vector2> texToContent, Vector2 size)
|
||||
{
|
||||
var agent = AgentMap.Instance();
|
||||
var mapId = agent->CurrentMapId;
|
||||
|
||||
// When we have live data, update the cache for this map so we can draw static POI later when the map is closed.
|
||||
if (agent->MapMarkerCount > 0) {
|
||||
var list = new List<CachedStaticMarker>();
|
||||
for (var i = 0; i < agent->MapMarkerCount; i++) {
|
||||
ref var marker = ref agent->MapMarkers[i];
|
||||
if (marker.MapMarker.IconId is 0) continue;
|
||||
var tooltipText = marker.MapMarker.Subtext.AsDalamudSeString();
|
||||
var tooltip = (string.IsNullOrEmpty(tooltipText.TextValue) && System.SystemConfig.ShowMiscTooltips)
|
||||
? System.TooltipCache.GetValue(marker.MapMarker.IconId)
|
||||
: tooltipText.ToString();
|
||||
list.Add(new CachedStaticMarker(marker.MapMarker.IconId, marker.MapMarker.X, marker.MapMarker.Y, tooltip));
|
||||
}
|
||||
StaticMarkerCache[mapId] = list;
|
||||
}
|
||||
|
||||
// Draw from live data if available, otherwise from cache for current map.
|
||||
if (agent->MapMarkerCount > 0) {
|
||||
for (var i = 0; i < agent->MapMarkerCount; i++) {
|
||||
ref var marker = ref agent->MapMarkers[i];
|
||||
if (marker.MapMarker.IconId is 0) continue;
|
||||
if (System.IconConfig.IconSettingMap.TryGetValue(marker.MapMarker.IconId, out var setting) && setting.Hide) continue;
|
||||
|
||||
var tx = 1024.0f + marker.MapMarker.X / 16.0f;
|
||||
var ty = 1024.0f + marker.MapMarker.Y / 16.0f;
|
||||
var contentPos = texToContent(tx, ty);
|
||||
if (!IsInMinimapBounds(contentPos, size, MinimapBoundsMargin)) continue;
|
||||
|
||||
var tooltipText = marker.MapMarker.Subtext.AsDalamudSeString();
|
||||
var tooltip = (string.IsNullOrEmpty(tooltipText.TextValue) && System.SystemConfig.ShowMiscTooltips)
|
||||
? System.TooltipCache.GetValue(marker.MapMarker.IconId)
|
||||
: tooltipText.ToString();
|
||||
DrawMinimapIcon(marker.MapMarker.IconId, contentPos + contentTopLeft, sizeScale: 1.5f, tooltip);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!StaticMarkerCache.TryGetValue(mapId, out var cached))
|
||||
return;
|
||||
|
||||
foreach (var m in cached) {
|
||||
if (m.IconId is 0) continue;
|
||||
if (System.IconConfig.IconSettingMap.TryGetValue(m.IconId, out var setting) && setting.Hide) continue;
|
||||
|
||||
var tx = 1024.0f + m.X / 16.0f;
|
||||
var ty = 1024.0f + m.Y / 16.0f;
|
||||
var contentPos = texToContent(tx, ty);
|
||||
if (!IsInMinimapBounds(contentPos, size, MinimapBoundsMargin)) continue;
|
||||
|
||||
DrawMinimapIcon(m.IconId, contentPos + contentTopLeft, sizeScale: 1.5f, m.Tooltip);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void DrawMinimapEventMarkers(Vector2 contentTopLeft, Func<float, float, Vector2> texToContent, Vector2 size, float scaleFactor, float offsetX, float offsetY, float scale)
|
||||
{
|
||||
var agent = AgentMap.Instance();
|
||||
var mapId = agent->CurrentMapId;
|
||||
var groups = agent->EventMarkers.GroupBy(m => (m.DataId, m.Position.X, m.Position.Z));
|
||||
var showRadius = System.SystemConfig.MinimapShowQuestAreaRadius;
|
||||
// Use minimap scale so the circle scales with minimap zoom (radiusPixels = markerRadius * scale * scaleFactor).
|
||||
|
||||
var hasNonFate = false;
|
||||
var cacheList = new List<CachedEventMarker>();
|
||||
|
||||
foreach (var group in groups) {
|
||||
var first = group.First();
|
||||
if ((MarkerType)first.MarkerType is MarkerType.Fate) continue;
|
||||
hasNonFate = true;
|
||||
var iconId = group.FirstOrNull(m => m.IconId is not 60493)?.IconId ?? first.IconId;
|
||||
if (iconId is 0) continue;
|
||||
|
||||
var markerRadius = group.Max(m => m.Radius);
|
||||
if (HousingManager.Instance()->CurrentTerritory is not null) markerRadius = 0f;
|
||||
|
||||
var pos = first.Position.AsMapVector();
|
||||
var tooltip = GetEventMarkerTooltip(first);
|
||||
cacheList.Add(new CachedEventMarker(iconId, pos.X, pos.Y, markerRadius, tooltip));
|
||||
|
||||
var tx = 1024.0f + (pos.X - offsetX) * scaleFactor;
|
||||
var ty = 1024.0f + (pos.Y - offsetY) * scaleFactor;
|
||||
var contentPos = texToContent(tx, ty);
|
||||
var centerScreen = contentPos + contentTopLeft;
|
||||
|
||||
if (showRadius)
|
||||
DrawHelpers.DrawRadiusCircle(centerScreen, markerRadius, scale, scaleFactor, MinimapQuestCircleFill, MinimapQuestCircleOutline);
|
||||
ShowQuestRadiusTooltipIfHovered(centerScreen, markerRadius, scale, scaleFactor, tooltip);
|
||||
|
||||
if (System.IconConfig.IconSettingMap.TryGetValue(iconId, out var setting) && setting.Hide) continue;
|
||||
if (!IsInMinimapBounds(contentPos, size, MinimapBoundsMargin)) continue;
|
||||
|
||||
DrawMinimapIcon(iconId, centerScreen, sizeScale: 1.5f, tooltip);
|
||||
}
|
||||
|
||||
if (hasNonFate && cacheList.Count > 0)
|
||||
EventMarkerCache[mapId] = cacheList;
|
||||
else if (EventMarkerCache.TryGetValue(mapId, out var cached))
|
||||
{
|
||||
foreach (var m in cached) {
|
||||
if (m.IconId is 0) continue;
|
||||
if (System.IconConfig.IconSettingMap.TryGetValue(m.IconId, out var setting) && setting.Hide) continue;
|
||||
var tx = 1024.0f + (m.MapX - offsetX) * scaleFactor;
|
||||
var ty = 1024.0f + (m.MapY - offsetY) * scaleFactor;
|
||||
var contentPos = texToContent(tx, ty);
|
||||
var centerScreen = contentPos + contentTopLeft;
|
||||
|
||||
if (showRadius)
|
||||
DrawHelpers.DrawRadiusCircle(centerScreen, m.Radius, scale, scaleFactor, MinimapQuestCircleFill, MinimapQuestCircleOutline);
|
||||
ShowQuestRadiusTooltipIfHovered(centerScreen, m.Radius, scale, scaleFactor, m.Tooltip);
|
||||
|
||||
if (!IsInMinimapBounds(contentPos, size, MinimapBoundsMargin)) continue;
|
||||
DrawMinimapIcon(m.IconId, centerScreen, sizeScale: 1.5f, m.Tooltip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Draw FATE markers from FateManager so they update without opening the Area Map.</summary>
|
||||
private unsafe void DrawMinimapFatesFromFateManager(Vector2 contentTopLeft, Func<float, float, Vector2> texToContent, Vector2 size, float scaleFactor, float offsetX, float offsetY)
|
||||
{
|
||||
if (Service.FateTable.Length is 0) return;
|
||||
for (var i = 0; i < Service.FateTable.Length; i++) {
|
||||
var fateData = FateManager.Instance()->Fates[i];
|
||||
var fate = fateData.Value;
|
||||
if (fate->IconId is 0) continue;
|
||||
if (System.IconConfig.IconSettingMap.TryGetValue(fate->IconId, out var setting) && setting.Hide) continue;
|
||||
|
||||
var pos = new Vector2(fate->Location.X, fate->Location.Z);
|
||||
var tx = 1024.0f + (pos.X - offsetX) * scaleFactor;
|
||||
var ty = 1024.0f + (pos.Y - offsetY) * scaleFactor;
|
||||
var contentPos = texToContent(tx, ty);
|
||||
if (!IsInMinimapBounds(contentPos, size, MinimapBoundsMargin)) continue;
|
||||
|
||||
var tooltip = GetFateTooltip(fateData);
|
||||
DrawMinimapIcon(fate->IconId, contentPos + contentTopLeft, sizeScale: 1.5f, tooltip);
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe string GetFateTooltip(Pointer<FateContext> fateData)
|
||||
{
|
||||
try {
|
||||
var fate = fateData.Value;
|
||||
var name = fate->Name.ToString();
|
||||
var title = !string.IsNullOrWhiteSpace(name) ? $"{name}\nLv. {fate->Level} FATE" : $"Lv. {fate->Level} FATE";
|
||||
var timeRemaining = fateData.GetTimeRemaining();
|
||||
if (fate->State is FateState.Running) {
|
||||
var progressLine = $"Progress: {fate->Progress}%";
|
||||
if (timeRemaining > TimeSpan.Zero)
|
||||
title += $"\n{(fate->IsBonus ? "Exp Bonus! " : "")}{SeIconChar.Clock.ToIconString()} {timeRemaining:mm\\:ss} {progressLine}";
|
||||
else
|
||||
title += $"\n{progressLine}";
|
||||
}
|
||||
else if (fate->State is not FateState.Preparing) {
|
||||
title += $"\n{fate->State}";
|
||||
}
|
||||
return title;
|
||||
}
|
||||
catch {
|
||||
return "FATE";
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe string GetEventMarkerTooltip(MapMarkerData marker)
|
||||
{
|
||||
try {
|
||||
// FATE path never touches marker.TooltipString (avoids AccessViolationException from stale pointers).
|
||||
if ((MarkerType)marker.MarkerType is MarkerType.Fate) {
|
||||
var fateData = FateManager.Instance()->Fates.FirstOrNull(fate => fate.Value->FateId == marker.DataId);
|
||||
if (fateData is not null) {
|
||||
var fatePtr = fateData.Value.Value;
|
||||
var name = fatePtr->Name.ToString();
|
||||
var title = !string.IsNullOrWhiteSpace(name) ? $"{name}\nLv. {fatePtr->Level} FATE" : $"Lv. {fatePtr->Level} FATE";
|
||||
var timeRemaining = fateData.Value.GetTimeRemaining();
|
||||
if (fatePtr->State is FateState.Running) {
|
||||
var progressLine = $"Progress: {fatePtr->Progress}%";
|
||||
if (timeRemaining > TimeSpan.Zero)
|
||||
title += $"\n{(fatePtr->IsBonus ? "Exp Bonus! " : "")}{SeIconChar.Clock.ToIconString()} {timeRemaining:mm\\:ss} {progressLine}";
|
||||
else
|
||||
title += $"\n{progressLine}";
|
||||
}
|
||||
else if (fatePtr->State is not FateState.Preparing) {
|
||||
title += $"\n{fatePtr->State}";
|
||||
}
|
||||
return title;
|
||||
}
|
||||
return "FATE";
|
||||
}
|
||||
|
||||
// Other event markers: try to get name from Lumina (e.g. Quest by DataId) instead of "Lv. X Event".
|
||||
if (marker.DataId != 0) {
|
||||
try {
|
||||
var questRow = Service.DataManager.GetExcelSheet<Quest>()?.GetRow(marker.DataId + 65536u);
|
||||
var name = questRow?.Name.ExtractText();
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
return name;
|
||||
} catch { }
|
||||
}
|
||||
return marker.RecommendedLevel is 0 ? "Event" : $"Lv. {marker.RecommendedLevel} Event";
|
||||
}
|
||||
catch (AccessViolationException) {
|
||||
return string.Empty;
|
||||
}
|
||||
catch (NullReferenceException) {
|
||||
return string.Empty;
|
||||
}
|
||||
catch (Exception) {
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void DrawMinimapGatheringMarkers(Vector2 contentTopLeft, Func<float, float, Vector2> texToContent, Vector2 size)
|
||||
{
|
||||
var agent = AgentMap.Instance();
|
||||
foreach (var marker in agent->MiniMapGatheringMarkers) {
|
||||
if (marker.ShouldRender is 0) continue;
|
||||
var iconId = marker.MapMarker.IconId;
|
||||
if (iconId is 0) continue;
|
||||
if (System.IconConfig.IconSettingMap.TryGetValue(iconId, out var setting) && setting.Hide) continue;
|
||||
|
||||
var tx = 1024.0f + marker.MapMarker.X / 16.0f;
|
||||
var ty = 1024.0f + marker.MapMarker.Y / 16.0f;
|
||||
var contentPos = texToContent(tx, ty);
|
||||
if (!IsInMinimapBounds(contentPos, size, MinimapBoundsMargin)) continue;
|
||||
|
||||
var tooltipText = marker.MapMarker.Subtext.AsDalamudSeString();
|
||||
var tooltip = (string.IsNullOrEmpty(tooltipText.TextValue) && System.SystemConfig.ShowMiscTooltips)
|
||||
? System.TooltipCache.GetValue(marker.MapMarker.IconId)
|
||||
: tooltipText.ToString();
|
||||
DrawMinimapIcon(iconId, contentPos + contentTopLeft, sizeScale: 1.5f, tooltip);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Draw party and alliance members on the minimap. When a member is off the minimap circle, draw a faded marker at the rim.</summary>
|
||||
private unsafe void DrawMinimapGroupMembers(Vector2 contentTopLeft, Func<float, float, Vector2> texToContent, Vector2 size, float scaleFactor, float offsetX, float offsetY)
|
||||
{
|
||||
var agent = AgentMap.Instance();
|
||||
var currentTerritoryId = agent->CurrentTerritoryId;
|
||||
var centerOffset = size * 0.5f;
|
||||
var radius = Math.Min(size.X, size.Y) * 0.5f;
|
||||
const float rimMargin = 2f; // place icon at the rim (minimal inset)
|
||||
const float offMapFadeAlpha = 0.5f;
|
||||
|
||||
foreach (var partyMember in GroupManager.Instance()->MainGroup.PartyMembers[..(int)GroupManager.Instance()->MainGroup.MemberCount])
|
||||
{
|
||||
if (partyMember.EntityId is 0xE0000000) continue;
|
||||
if (partyMember.TerritoryType != currentTerritoryId) continue;
|
||||
|
||||
if (System.IconConfig.IconSettingMap.TryGetValue(60421, out var setting) && setting.Hide) continue;
|
||||
|
||||
var pos = new Vector2(partyMember.Position.X, partyMember.Position.Z);
|
||||
var tx = 1024.0f + (pos.X - offsetX) * scaleFactor;
|
||||
var ty = 1024.0f + (pos.Y - offsetY) * scaleFactor;
|
||||
var contentPos = texToContent(tx, ty);
|
||||
var distFromCenter = (contentPos - centerOffset).Length();
|
||||
var tooltip = $"Lv. {partyMember.Level} {partyMember.NameString}";
|
||||
|
||||
if (distFromCenter <= radius && IsInMinimapBounds(contentPos, size, MinimapBoundsMargin))
|
||||
{
|
||||
DrawMinimapIcon(60421, contentPos + contentTopLeft, sizeScale: 1.5f, tooltip);
|
||||
}
|
||||
else if (distFromCenter > radius)
|
||||
{
|
||||
var direction = (contentPos - centerOffset) / distFromCenter;
|
||||
var rimPos = centerOffset + direction * (radius - rimMargin);
|
||||
DrawMinimapIcon(60421, rimPos + contentTopLeft, sizeScale: 1.5f, tooltip, fadeAlpha: offMapFadeAlpha);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var allianceMember in GroupManager.Instance()->MainGroup.AllianceMembers)
|
||||
{
|
||||
if (allianceMember.EntityId is 0xE0000000) continue;
|
||||
if (allianceMember.TerritoryType != currentTerritoryId) continue;
|
||||
|
||||
if (System.IconConfig.IconSettingMap.TryGetValue(60403, out var allianceSetting) && allianceSetting.Hide) continue;
|
||||
|
||||
var pos = new Vector2(allianceMember.Position.X, allianceMember.Position.Z);
|
||||
var tx = 1024.0f + (pos.X - offsetX) * scaleFactor;
|
||||
var ty = 1024.0f + (pos.Y - offsetY) * scaleFactor;
|
||||
var contentPos = texToContent(tx, ty);
|
||||
var distFromCenter = (contentPos - centerOffset).Length();
|
||||
var allianceTooltip = $"Lv. {allianceMember.Level} {allianceMember.NameString}";
|
||||
|
||||
if (distFromCenter <= radius && IsInMinimapBounds(contentPos, size, MinimapBoundsMargin))
|
||||
{
|
||||
DrawMinimapIcon(60403, contentPos + contentTopLeft, sizeScale: 1.5f, allianceTooltip);
|
||||
}
|
||||
else if (distFromCenter > radius)
|
||||
{
|
||||
var direction = (contentPos - centerOffset) / distFromCenter;
|
||||
var rimPos = centerOffset + direction * (radius - rimMargin);
|
||||
DrawMinimapIcon(60403, rimPos + contentTopLeft, sizeScale: 1.5f, allianceTooltip, fadeAlpha: offMapFadeAlpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void DrawMinimapFlag(Vector2 contentTopLeft, Func<float, float, Vector2> texToContent, float scaleFactor, float offsetX, float offsetY)
|
||||
{
|
||||
var agent = AgentMap.Instance();
|
||||
if (agent->FlagMarkerCount is 0) return;
|
||||
ref var flag = ref agent->FlagMapMarkers[0];
|
||||
if (flag.TerritoryId != agent->CurrentMapId) return;
|
||||
|
||||
if (System.IconConfig.IconSettingMap.TryGetValue(flag.MapMarker.IconId, out var setting) && setting.Hide) return;
|
||||
|
||||
var tx = 1024.0f + (flag.XFloat - offsetX) * scaleFactor;
|
||||
var ty = 1024.0f + (flag.YFloat - offsetY) * scaleFactor;
|
||||
var contentPos = texToContent(tx, ty);
|
||||
var flagTooltip = System.TooltipCache.GetValue(flag.MapMarker.IconId);
|
||||
if (string.IsNullOrEmpty(flagTooltip)) flagTooltip = "Flag";
|
||||
DrawMinimapIcon(flag.MapMarker.IconId, contentPos + contentTopLeft, sizeScale: 1.5f, flagTooltip);
|
||||
}
|
||||
|
||||
private unsafe void DrawMinimapTempMarkers(Vector2 contentTopLeft, Func<float, float, Vector2> texToContent, Vector2 size, float scaleFactor, float offsetX, float offsetY, float scale)
|
||||
{
|
||||
var agent = AgentMap.Instance();
|
||||
var mapId = agent->CurrentMapId;
|
||||
var showRadius = System.SystemConfig.MinimapShowQuestAreaRadius;
|
||||
// Use minimap scale so the circle scales with minimap zoom and is not tied to area map zoom.
|
||||
// radiusPixels = markerRadius * scale * scaleFactor (same as position transform on minimap).
|
||||
|
||||
if (agent->TempMapMarkerCount > 0) {
|
||||
var span = new Span<TempMapMarker>(Unsafe.AsPointer(ref agent->TempMapMarkers[0]), agent->TempMapMarkerCount);
|
||||
var groups = span.ToArray().GroupBy(m => new Vector2(m.MapMarker.X, m.MapMarker.Y));
|
||||
var cacheList = new List<CachedTempMarker>();
|
||||
|
||||
foreach (var group in groups) {
|
||||
var first = group.First();
|
||||
var markerRadius = group.Max(m => m.MapMarker.Scale);
|
||||
var iconId = group.FirstOrNull(m => m.MapMarker.IconId is not (60493 or 0))?.MapMarker.IconId ?? first.MapMarker.IconId;
|
||||
if (iconId is 0 && group.Count() == 2 && first.Type == 4 && group.Last() is { Type: 6, MapMarker.IconId: 0 })
|
||||
iconId = DrawHelpers.QuestionMarkIcon;
|
||||
if (iconId is 0) continue;
|
||||
|
||||
var tooltip = first.TooltipText.ToString();
|
||||
cacheList.Add(new CachedTempMarker(iconId, first.MapMarker.X, first.MapMarker.Y, markerRadius, tooltip));
|
||||
|
||||
var tx = 1024.0f + (first.MapMarker.X / 16.0f - offsetX) * scaleFactor;
|
||||
var ty = 1024.0f + (first.MapMarker.Y / 16.0f - offsetY) * scaleFactor;
|
||||
var contentPos = texToContent(tx, ty);
|
||||
var centerScreen = contentPos + contentTopLeft;
|
||||
|
||||
if (showRadius)
|
||||
DrawHelpers.DrawRadiusCircle(centerScreen, markerRadius, scale, scaleFactor, MinimapQuestCircleFill, MinimapQuestCircleOutline);
|
||||
ShowQuestRadiusTooltipIfHovered(centerScreen, markerRadius, scale, scaleFactor, tooltip);
|
||||
|
||||
if (!IsInMinimapBounds(contentPos, size, MinimapBoundsMargin)) continue;
|
||||
if (System.IconConfig.IconSettingMap.TryGetValue(iconId, out var setting) && setting.Hide) continue;
|
||||
|
||||
DrawMinimapIcon(iconId, centerScreen, sizeScale: 1.5f, tooltip);
|
||||
}
|
||||
if (cacheList.Count > 0)
|
||||
TempMarkerCache[mapId] = cacheList;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TempMarkerCache.TryGetValue(mapId, out var cached))
|
||||
return;
|
||||
foreach (var m in cached) {
|
||||
if (m.IconId is 0) continue;
|
||||
if (System.IconConfig.IconSettingMap.TryGetValue(m.IconId, out var setting) && setting.Hide) continue;
|
||||
var tx = 1024.0f + (m.X / 16.0f - offsetX) * scaleFactor;
|
||||
var ty = 1024.0f + (m.Y / 16.0f - offsetY) * scaleFactor;
|
||||
var contentPos = texToContent(tx, ty);
|
||||
var centerScreen = contentPos + contentTopLeft;
|
||||
|
||||
if (showRadius)
|
||||
DrawHelpers.DrawRadiusCircle(centerScreen, m.Radius, scale, scaleFactor, MinimapQuestCircleFill, MinimapQuestCircleOutline);
|
||||
ShowQuestRadiusTooltipIfHovered(centerScreen, m.Radius, scale, scaleFactor, m.Tooltip);
|
||||
|
||||
if (!IsInMinimapBounds(contentPos, size, MinimapBoundsMargin)) continue;
|
||||
DrawMinimapIcon(m.IconId, centerScreen, sizeScale: 1.5f, m.Tooltip);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void DrawMinimapFieldMarkers(Vector2 contentTopLeft, Func<float, float, Vector2> texToContent, Vector2 size, float scaleFactor, float offsetX, float offsetY)
|
||||
{
|
||||
var agent = AgentMap.Instance();
|
||||
if (agent->CurrentMapId != agent->SelectedMapId) return;
|
||||
|
||||
var fieldMarkersSheet = Service.DataManager.GetExcelSheet<FieldMarkerSheet>().Where(m => m.MapIcon is not 0).ToList();
|
||||
var markerSpan = MarkingController.Instance()->FieldMarkers;
|
||||
|
||||
for (var i = 0; i < 8; i++) {
|
||||
if (markerSpan[i] is not { Active: true } marker) continue;
|
||||
if (i >= fieldMarkersSheet.Count) continue;
|
||||
var iconId = fieldMarkersSheet[i].MapIcon;
|
||||
if (iconId is 0) continue;
|
||||
if (System.IconConfig.IconSettingMap.TryGetValue(iconId, out var setting) && setting.Hide) continue;
|
||||
|
||||
// Field marker position: world coords / 1000 * scaleFactor, then texture = 1024 + (world - offset) * scaleFactor
|
||||
var wx = marker.X / 1000.0f;
|
||||
var wz = marker.Z / 1000.0f;
|
||||
var tx = 1024.0f + (wx - offsetX) * scaleFactor;
|
||||
var ty = 1024.0f + (wz - offsetY) * scaleFactor;
|
||||
var contentPos = texToContent(tx, ty);
|
||||
if (!IsInMinimapBounds(contentPos, size, MinimapBoundsMargin)) continue;
|
||||
|
||||
DrawMinimapIcon(iconId, contentPos + contentTopLeft, sizeScale: 1.5f, $"Waymark {i + 1}");
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawMinimapIcon(uint iconId, Vector2 screenPos, float sizeScale = 1f, string? tooltip = null, float? fadeAlpha = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var texture = Service.TextureProvider.GetFromGameIcon(iconId).GetWrapOrEmpty();
|
||||
var texSize = texture.Size;
|
||||
if (texSize.X <= 0 || texSize.Y <= 0) return;
|
||||
var iconScale = MinimapIconSize / Math.Max(texSize.X, texSize.Y) * MinimapIconScaleFromConfig * sizeScale;
|
||||
System.IconConfig.IconSettingMap.TryGetValue(iconId, out var setting);
|
||||
if (setting != null)
|
||||
iconScale *= setting.Scale;
|
||||
var size = texSize * iconScale;
|
||||
var half = size / 2f;
|
||||
var col = setting?.Color ?? Vector4.One;
|
||||
if (fadeAlpha is { } alpha)
|
||||
col.W *= alpha;
|
||||
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
drawList.AddImage(texture.Handle, screenPos - half, screenPos + half, Vector2.Zero, Vector2.One, ImGui.GetColorU32(col));
|
||||
|
||||
if (!string.IsNullOrEmpty(tooltip) && ImGui.IsMouseHoveringRect(screenPos - half, screenPos + half))
|
||||
ImGui.SetTooltip(tooltip);
|
||||
}
|
||||
catch (Dalamud.Interface.Textures.Internal.IconNotFoundException)
|
||||
{
|
||||
// Icon not in game data (e.g. 60494 HiRes), skip drawing
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Arrays;
|
||||
using Mappy.Classes;
|
||||
using Mappy.Extensions;
|
||||
|
||||
namespace Mappy.MapRenderer;
|
||||
|
||||
public partial class MapRenderer
|
||||
{
|
||||
private unsafe void DrawPlayer()
|
||||
{
|
||||
if (AgentMap.Instance()->SelectedMapId != AgentMap.Instance()->CurrentMapId) return;
|
||||
|
||||
if (Service.ObjectTable.LocalPlayer is { } localPlayer) {
|
||||
var position = ImGui.GetWindowPos() +
|
||||
DrawPosition +
|
||||
(localPlayer.GetMapPosition() -
|
||||
DrawHelpers.GetMapOffsetVector() +
|
||||
DrawHelpers.GetMapCenterOffsetVector()) * Scale;
|
||||
|
||||
DrawLookLine(position);
|
||||
DrawPlayerIcon(position);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawLookLine(Vector2 position)
|
||||
{
|
||||
var angle = GetCameraRotation();
|
||||
|
||||
var lineLength = System.SystemConfig.ConeSize * (System.SystemConfig.ScalePlayerCone ? 1.0f : Scale);
|
||||
var halfConeAngle = DegreesToRadians(90.0f) / 2.0f;
|
||||
|
||||
DrawAngledLineFromCenter(position, lineLength, angle - halfConeAngle);
|
||||
DrawAngledLineFromCenter(position, lineLength, angle + halfConeAngle);
|
||||
DrawLineArcFromCenter(position, lineLength, angle);
|
||||
|
||||
DrawFilledSemiCircle(position, lineLength, angle);
|
||||
}
|
||||
|
||||
private static void DrawAngledLineFromCenter(Vector2 center, float lineLength, float angle, Vector4? outlineColor = null)
|
||||
{
|
||||
var lineSegment = new Vector2(lineLength * MathF.Cos(angle), lineLength * MathF.Sin(angle));
|
||||
var color = outlineColor ?? System.SystemConfig.PlayerConeOutlineColor;
|
||||
ImGui.GetWindowDrawList().AddLine(center, center + lineSegment, ImGui.GetColorU32(color), 3.0f);
|
||||
}
|
||||
|
||||
private static void DrawLineArcFromCenter(Vector2 center, float distance, float rotation, Vector4? outlineColor = null)
|
||||
{
|
||||
var halfConeAngle = DegreesToRadians(90.0f) / 2.0f;
|
||||
var color = outlineColor ?? System.SystemConfig.PlayerConeOutlineColor;
|
||||
|
||||
var start = rotation - halfConeAngle;
|
||||
var stop = rotation + halfConeAngle;
|
||||
|
||||
ImGui.GetWindowDrawList().PathArcTo(center, distance, start, stop);
|
||||
ImGui.GetWindowDrawList().PathStroke(ImGui.GetColorU32(color), ImDrawFlags.None, 3.0f);
|
||||
}
|
||||
|
||||
private static void DrawFilledSemiCircle(Vector2 center, float distance, float rotation)
|
||||
{
|
||||
var halfConeAngle = DegreesToRadians(90.0f) / 2.0f;
|
||||
|
||||
var coneColor = ImGui.GetColorU32(System.SystemConfig.PlayerConeColor);
|
||||
var startAngle = rotation - halfConeAngle;
|
||||
var stopAngle = rotation + halfConeAngle;
|
||||
|
||||
var startPosition = new Vector2(distance * MathF.Cos(rotation - halfConeAngle), distance * MathF.Sin(rotation - halfConeAngle));
|
||||
|
||||
ImGui.GetWindowDrawList().PathArcTo(center, distance, startAngle, stopAngle);
|
||||
ImGui.GetWindowDrawList().PathLineTo(center);
|
||||
ImGui.GetWindowDrawList().PathLineTo(center + startPosition);
|
||||
ImGui.GetWindowDrawList().PathFillConvex(coneColor);
|
||||
}
|
||||
|
||||
private static unsafe float GetCameraRotation() => -DegreesToRadians(AreaMapNumberArray.Instance()->ConeRotation) - 0.5f * MathF.PI;
|
||||
|
||||
private static float DegreesToRadians(float degrees) => MathF.PI / 180.0f * degrees;
|
||||
|
||||
private void DrawPlayerIcon(Vector2 position)
|
||||
{
|
||||
if (!System.SystemConfig.ShowPlayerIcon) return;
|
||||
if (Service.ObjectTable is not { LocalPlayer: { } player }) return;
|
||||
|
||||
var texture = Service.TextureProvider.GetFromGameIcon(60443).GetWrapOrEmpty();
|
||||
var angle = -player.Rotation + MathF.PI / 2.0f;
|
||||
|
||||
var scale = System.SystemConfig.ScaleWithZoom ? Scale : 1.0f;
|
||||
scale *= System.SystemConfig.PlayerIconScale;
|
||||
|
||||
var vectors = GetRotationVectors(angle, position, texture.Size / 2.0f * scale);
|
||||
|
||||
ImGui.GetWindowDrawList().AddImageQuad(texture.Handle, vectors[0], vectors[1], vectors[2], vectors[3]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw only the minimap player cone (direction indicator). Call before DrawMinimapMarkers so markers draw on top of the cone.
|
||||
/// </summary>
|
||||
private void DrawMinimapConeAtCenter(Vector2 centerPos, float mapScale)
|
||||
{
|
||||
if (!System.SystemConfig.MinimapShowPlayerCone) return;
|
||||
var angle = GetCameraRotation();
|
||||
var lineLength = System.SystemConfig.ConeSize * 0.5f;
|
||||
var halfConeAngle = DegreesToRadians(90.0f) / 2.0f;
|
||||
DrawMinimapConeGradient(centerPos, lineLength, angle - halfConeAngle, angle + halfConeAngle);
|
||||
var softWhite = new Vector4(1f, 1f, 1f, 0.2f);
|
||||
DrawAngledLineFromCenter(centerPos, lineLength, angle - halfConeAngle, softWhite);
|
||||
DrawAngledLineFromCenter(centerPos, lineLength, angle + halfConeAngle, softWhite);
|
||||
DrawLineArcFromCenter(centerPos, lineLength, angle, softWhite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw player icon at center (for minimap). Cone is drawn earlier so markers can be drawn on top of it.
|
||||
/// </summary>
|
||||
private void DrawPlayerAtCenter(Vector2 centerPos, float mapScale)
|
||||
{
|
||||
if (Service.ObjectTable.LocalPlayer is not { } localPlayer) return;
|
||||
if (!System.SystemConfig.ShowPlayerIcon) return;
|
||||
|
||||
var texture = Service.TextureProvider.GetFromGameIcon(60443).GetWrapOrEmpty();
|
||||
var angle = -localPlayer.Rotation + MathF.PI / 2.0f;
|
||||
var iconScale = System.SystemConfig.PlayerIconScale * 1.5f; // 1.5x for minimap visibility
|
||||
var vectors = GetRotationVectors(angle, centerPos, texture.Size / 2.0f * iconScale);
|
||||
ImGui.GetWindowDrawList().AddImageQuad(texture.Handle, vectors[0], vectors[1], vectors[2], vectors[3]);
|
||||
}
|
||||
|
||||
/// <summary>Draw minimap cone as white light with radial gradient (bright at center, fading at edge). Kept quite transparent so markers underneath remain visible.</summary>
|
||||
private static void DrawMinimapConeGradient(Vector2 center, float radius, float startAngle, float endAngle)
|
||||
{
|
||||
const int segments = 24;
|
||||
const float maxAlpha = 0.18f;
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
for (var j = segments - 1; j >= 0; j--) {
|
||||
var rInner = radius * j / segments;
|
||||
var rOuter = radius * (j + 1) / segments;
|
||||
var t = (j + 0.5f) / segments;
|
||||
var alpha = maxAlpha * (1f - t);
|
||||
if (alpha <= 0f) continue;
|
||||
var color = ImGui.GetColorU32(new Vector4(1f, 1f, 1f, alpha));
|
||||
var outerStart = center + new Vector2(rOuter * MathF.Cos(startAngle), rOuter * MathF.Sin(startAngle));
|
||||
var innerEnd = center + new Vector2(rInner * MathF.Cos(endAngle), rInner * MathF.Sin(endAngle));
|
||||
drawList.PathClear();
|
||||
drawList.PathLineTo(center);
|
||||
drawList.PathLineTo(outerStart);
|
||||
drawList.PathArcTo(center, rOuter, startAngle, endAngle);
|
||||
drawList.PathLineTo(innerEnd);
|
||||
drawList.PathArcTo(center, rInner, endAngle, startAngle);
|
||||
drawList.PathFillConvex(color);
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector2[] GetRotationVectors(float angle, Vector2 center, Vector2 size)
|
||||
{
|
||||
var cosA = MathF.Cos(angle + 0.5f * MathF.PI);
|
||||
var sinA = MathF.Sin(angle + 0.5f * MathF.PI);
|
||||
|
||||
Vector2[] vectors =
|
||||
[
|
||||
center + ImRotate(new Vector2(-size.X * 0.5f, -size.Y * 0.5f), cosA, sinA),
|
||||
center + ImRotate(new Vector2(+size.X * 0.5f, -size.Y * 0.5f), cosA, sinA),
|
||||
center + ImRotate(new Vector2(+size.X * 0.5f, +size.Y * 0.5f), cosA, sinA),
|
||||
center + ImRotate(new Vector2(-size.X * 0.5f, +size.Y * 0.5f), cosA, sinA),
|
||||
];
|
||||
return vectors;
|
||||
}
|
||||
|
||||
private static Vector2 ImRotate(Vector2 v, float cosA, float sinA) => new(v.X * cosA - v.Y * sinA, v.X * sinA + v.Y * cosA);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.Linq;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using Mappy.Extensions;
|
||||
|
||||
namespace Mappy.MapRenderer;
|
||||
|
||||
public partial class MapRenderer
|
||||
{
|
||||
private unsafe void DrawStaticMapMarkers()
|
||||
{
|
||||
foreach (var index in Enumerable.Range(0, AgentMap.Instance()->MapMarkerCount)) {
|
||||
ref var marker = ref AgentMap.Instance()->MapMarkers[index];
|
||||
|
||||
if (marker.MapMarker.IconId is 0) continue;
|
||||
|
||||
marker.Draw(DrawPosition, Scale);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void DrawStaticTextMarkers()
|
||||
{
|
||||
foreach (var index in Enumerable.Range(0, AgentMap.Instance()->MapMarkerCount)) {
|
||||
ref var marker = ref AgentMap.Instance()->MapMarkers[index];
|
||||
|
||||
if (marker.MapMarker.IconId is not 0) continue;
|
||||
if (marker.MapMarker.Index is 0) continue;
|
||||
if (marker.MapMarker.SubtextOrientation is 0) continue;
|
||||
|
||||
marker.Draw(DrawPosition, Scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using KamiLib.Extensions;
|
||||
using Mappy.Classes;
|
||||
using Mappy.Extensions;
|
||||
|
||||
namespace Mappy.MapRenderer;
|
||||
|
||||
public partial class MapRenderer
|
||||
{
|
||||
private unsafe void DrawTemporaryMarkers()
|
||||
{
|
||||
if (AgentMap.Instance()->SelectedMapSub != AgentMap.Instance()->SelectedMapId) return;
|
||||
|
||||
// Group together icons based on their dataId, this is because square enix shows circles then draws the actual icon overtop
|
||||
var validMarkers = new Span<TempMapMarker>(Unsafe.AsPointer(ref AgentMap.Instance()->TempMapMarkers[0]), AgentMap.Instance()->TempMapMarkerCount);
|
||||
var iconGroups = validMarkers.ToArray().GroupBy(markers => new Vector2(markers.MapMarker.X, markers.MapMarker.Y));
|
||||
|
||||
foreach (var group in iconGroups) {
|
||||
// Make a copy of the first marker in the set, we will be mutating this copy.
|
||||
var markerCopy = group.First();
|
||||
|
||||
// Get the actual iconId we want, typically the icon for the marker, not the circle
|
||||
var correctIconId = group.FirstOrNull(marker => marker.MapMarker.IconId is not (60493 or 0));
|
||||
markerCopy.MapMarker.IconId = correctIconId?.MapMarker.IconId ?? markerCopy.MapMarker.IconId;
|
||||
|
||||
// Special handling for WKS Markers (in which both icon ids are 0)
|
||||
if (markerCopy.MapMarker.IconId == 0 && group.Count() == 2)
|
||||
{
|
||||
if (markerCopy.Type == 4 && group.Last() is { Type: 6, MapMarker.IconId: 0 })
|
||||
{
|
||||
markerCopy.MapMarker.IconId = DrawHelpers.QuestionMarkIcon;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the actual radius value for this marker, typically the circle icon will have this value.
|
||||
markerCopy.MapMarker.Scale = group.Max(marker => marker.MapMarker.Scale);
|
||||
|
||||
markerCopy.Draw(DrawPosition, Scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using Mappy.Classes;
|
||||
using FieldMarker = Lumina.Excel.Sheets.FieldMarker;
|
||||
using MarkerInfo = Mappy.Classes.MarkerInfo;
|
||||
|
||||
namespace Mappy.MapRenderer;
|
||||
|
||||
public partial class MapRenderer
|
||||
{
|
||||
private readonly List<FieldMarker> fieldMarkers = Service.DataManager.GetExcelSheet<FieldMarker>().Where(marker => marker.MapIcon is not 0).ToList();
|
||||
|
||||
private unsafe void DrawFieldMarkers()
|
||||
{
|
||||
if (AgentMap.Instance()->CurrentMapId != AgentMap.Instance()->SelectedMapId) return;
|
||||
var markerSpan = MarkingController.Instance()->FieldMarkers;
|
||||
|
||||
foreach (var index in Enumerable.Range(0, 8)) {
|
||||
if (markerSpan[index] is { Active: true } marker) {
|
||||
var markerPosition =
|
||||
new Vector2(marker.X, marker.Z) / 1000.0f * DrawHelpers.GetMapScaleFactor()
|
||||
- DrawHelpers.GetMapOffsetVector()
|
||||
+ DrawHelpers.GetMapCenterOffsetVector();
|
||||
|
||||
DrawHelpers.DrawMapMarker(new MarkerInfo
|
||||
{
|
||||
Offset = DrawPosition,
|
||||
Scale = Scale,
|
||||
Position = markerPosition * Scale,
|
||||
IconId = fieldMarkers[index].MapIcon,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Dalamud.NET.Sdk/14.0.1">
|
||||
<PropertyGroup>
|
||||
<AssemblyName>HSMappy</AssemblyName>
|
||||
<Name>HSMappy</Name>
|
||||
<InternalName>HSMappy</InternalName>
|
||||
<Author>Knack117</Author>
|
||||
<Version>1.0.0.0</Version>
|
||||
<Punchline>A more versatile in-game map.</Punchline>
|
||||
<Description>Replaces the in-game map with an ImGui implementation with several additional features. Fork with minimap improvements, quest radius on minimap, and more.</Description>
|
||||
<RepoUrl>http://brassnet.ddns.net:33983/KnackAtNite/HSMappy</RepoUrl>
|
||||
<Tags>map;mapping;overlay;utility</Tags>
|
||||
<CategoryTags>jobs</CategoryTags>
|
||||
<AcceptsFeedback>true</AcceptsFeedback>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\KamiLib\KamiLib.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="TerraFX.Interop.Windows" Private="false"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,128 @@
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Plugin;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using KamiLib.Classes;
|
||||
using KamiLib.CommandManager;
|
||||
using KamiLib.Window;
|
||||
using Mappy.Controllers;
|
||||
using Mappy.Data;
|
||||
using Mappy.Windows;
|
||||
|
||||
namespace Mappy;
|
||||
|
||||
public sealed class MappyPlugin : IDalamudPlugin
|
||||
{
|
||||
private static bool _minimapPushedPadding;
|
||||
|
||||
public MappyPlugin(IDalamudPluginInterface pluginInterface)
|
||||
{
|
||||
pluginInterface.Create<Service>();
|
||||
|
||||
System.LargeAxisFontHandle = Service.PluginInterface.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle
|
||||
{
|
||||
SizePt = 72.0f,
|
||||
FamilyAndSize = GameFontFamilyAndSize.Axis36,
|
||||
Italic = true,
|
||||
BaseSkewStrength = 16f,
|
||||
});
|
||||
|
||||
System.SystemConfig = SystemConfig.Load();
|
||||
System.IconConfig = IconConfig.Load();
|
||||
System.FlagConfig = FlagConfig.Load();
|
||||
|
||||
System.Teleporter = new Teleporter(Service.PluginInterface);
|
||||
|
||||
System.CommandManager = new CommandManager(Service.PluginInterface, "hsmappy");
|
||||
|
||||
System.MapRenderer = new MapRenderer.MapRenderer();
|
||||
|
||||
System.ConfigWindow = new ConfigurationWindow();
|
||||
System.MapWindow = new MapWindow();
|
||||
System.MinimapWindow = new MinimapWindow();
|
||||
|
||||
// Push zero padding before window system draw so the first window (minimap) gets it.
|
||||
Service.PluginInterface.UiBuilder.Draw += SetMinimapZeroPadding;
|
||||
System.WindowManager = new WindowManager(Service.PluginInterface);
|
||||
Service.PluginInterface.UiBuilder.Draw += PopMinimapPadding;
|
||||
|
||||
// Minimap is added first so it's drawn first and receives the zero padding.
|
||||
System.WindowManager.AddWindow(System.MinimapWindow, WindowFlags.RequireLoggedIn);
|
||||
System.WindowManager.AddWindow(System.ConfigWindow, WindowFlags.IsConfigWindow | WindowFlags.RequireLoggedIn);
|
||||
System.WindowManager.AddWindow(System.MapWindow, WindowFlags.RequireLoggedIn);
|
||||
|
||||
if (System.SystemConfig.ShowMinimap)
|
||||
System.MinimapWindow.UnCollapseOrShow();
|
||||
|
||||
// PopMinimapPadding already registered above (after WindowManager)
|
||||
|
||||
System.FlagController = new FlagController();
|
||||
System.AreaMapController = new AddonAreaMapController();
|
||||
System.IntegrationsController = new IntegrationsController();
|
||||
|
||||
Service.PluginInterface.UiBuilder.OpenMainUi += OpenMapWindow;
|
||||
|
||||
System.CommandManager.RegisterCommand(new ToggleCommandHandler
|
||||
{
|
||||
BaseActivationPath = "/fatelist",
|
||||
EnableDelegate = _ => System.WindowManager.OpenOrCreateUnique<FateListWindow>(WindowFlags.OpenImmediately | WindowFlags.RequireLoggedIn),
|
||||
DisableDelegate = _ => System.WindowManager.GetWindow<FateListWindow>()?.Close(),
|
||||
ToggleDelegate = _ => System.WindowManager.GetWindow<FateListWindow>()?.UnCollapseOrToggle(),
|
||||
});
|
||||
|
||||
System.CommandManager.RegisterCommand(new ToggleCommandHandler
|
||||
{
|
||||
BaseActivationPath = "/questlist",
|
||||
EnableDelegate = _ => System.WindowManager.OpenOrCreateUnique<QuestListWindow>(WindowFlags.OpenImmediately | WindowFlags.RequireLoggedIn),
|
||||
DisableDelegate = _ => System.WindowManager.GetWindow<QuestListWindow>()?.Close(),
|
||||
ToggleDelegate = _ => System.WindowManager.GetWindow<QuestListWindow>()?.UnCollapseOrToggle(),
|
||||
});
|
||||
|
||||
System.CommandManager.RegisterCommand(new ToggleCommandHandler
|
||||
{
|
||||
BaseActivationPath = "/flaglist",
|
||||
EnableDelegate = _ => System.WindowManager.OpenOrCreateUnique<FlagHistoryWindow>(WindowFlags.OpenImmediately | WindowFlags.RequireLoggedIn),
|
||||
DisableDelegate = _ => System.WindowManager.GetWindow<FlagHistoryWindow>()?.Close(),
|
||||
ToggleDelegate = _ => System.WindowManager.GetWindow<FlagHistoryWindow>()?.UnCollapseOrToggle(),
|
||||
});
|
||||
}
|
||||
|
||||
private unsafe void OpenMapWindow() => AgentMap.Instance()->Show();
|
||||
|
||||
/// <summary>
|
||||
/// Called first each frame so the first window (minimap) gets zero padding when visible.
|
||||
/// </summary>
|
||||
private static void SetMinimapZeroPadding()
|
||||
{
|
||||
if (System.SystemConfig.ShowMinimap && IntegrationsController.ShouldShowMinimap()) {
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(0f, 0f));
|
||||
_minimapPushedPadding = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after window system draw to pop the padding we pushed (keeps style stack balanced).
|
||||
/// </summary>
|
||||
private static void PopMinimapPadding()
|
||||
{
|
||||
if (_minimapPushedPadding) {
|
||||
ImGui.PopStyleVar();
|
||||
_minimapPushedPadding = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Service.PluginInterface.UiBuilder.Draw -= SetMinimapZeroPadding;
|
||||
Service.PluginInterface.UiBuilder.Draw -= PopMinimapPadding;
|
||||
System.MapWindow.OnClose();
|
||||
System.WindowManager.Dispose();
|
||||
System.IntegrationsController.Dispose();
|
||||
System.AreaMapController.Dispose();
|
||||
System.FlagController.Dispose();
|
||||
System.MapRenderer.Dispose();
|
||||
|
||||
Service.PluginInterface.UiBuilder.OpenMainUi -= OpenMapWindow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Fate;
|
||||
using Lumina.Extensions;
|
||||
using Mappy.Classes;
|
||||
using Mappy.Extensions;
|
||||
|
||||
namespace Mappy.Modules;
|
||||
|
||||
public class FateModule : ModuleBase
|
||||
{
|
||||
public override unsafe bool ProcessMarker(MarkerInfo markerInfo)
|
||||
{
|
||||
if (markerInfo.MarkerType is not MarkerType.Fate) return false;
|
||||
|
||||
var fateData = FateManager.Instance()->Fates.FirstOrNull(fate => fate.Value->FateId == markerInfo.DataId);
|
||||
if (fateData is null) return false;
|
||||
|
||||
var fate = fateData.Value;
|
||||
var timeRemaining = fate.GetTimeRemaining();
|
||||
|
||||
markerInfo.PrimaryText = () => $"Lv. {fate.Value->Level} {fate.Value->Name}";
|
||||
|
||||
// Don't show additional information for any fate that is preparing
|
||||
if (fate.Value->State is FateState.Preparing) return true;
|
||||
|
||||
if (timeRemaining >= TimeSpan.Zero) {
|
||||
markerInfo.SecondaryText = () => $"Time Remaining {timeRemaining:mm\\:ss}\nProgress {fate.Value->Progress}%";
|
||||
|
||||
if (timeRemaining.TotalSeconds <= 300) {
|
||||
markerInfo.RadiusColor = fate.GetColor();
|
||||
markerInfo.RadiusOutlineColor = fate.GetColor();
|
||||
}
|
||||
}
|
||||
else {
|
||||
markerInfo.SecondaryText = () => $"Progress {fate.Value->Progress}%";
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Mappy.Classes;
|
||||
|
||||
namespace Mappy.Modules;
|
||||
|
||||
public abstract class ModuleBase
|
||||
{
|
||||
public abstract bool ProcessMarker(MarkerInfo markerInfo);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Mappy.Classes;
|
||||
|
||||
namespace Mappy.Modules;
|
||||
|
||||
public class StellarModule : ModuleBase
|
||||
{
|
||||
public override bool ProcessMarker(MarkerInfo markerInfo)
|
||||
{
|
||||
if (markerInfo.MarkerType is not MarkerType.Stellar) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Mappy.Classes;
|
||||
|
||||
namespace Mappy.Modules;
|
||||
|
||||
public class TripleTriadModule : ModuleBase
|
||||
{
|
||||
public override bool ProcessMarker(MarkerInfo markerInfo)
|
||||
{
|
||||
if (markerInfo is not { ObjectiveId: { } objectiveId }) return false;
|
||||
if (!System.TripleTriadCache.GetValue(objectiveId)) return false;
|
||||
|
||||
markerInfo.SecondaryText = () => System.CardRewardCache.GetValue(objectiveId) ?? string.Empty;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Mappy;
|
||||
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
public class Service
|
||||
{
|
||||
[PluginService]
|
||||
public static IDalamudPluginInterface PluginInterface { get; set; }
|
||||
|
||||
[PluginService]
|
||||
public static IClientState ClientState { get; set; }
|
||||
|
||||
[PluginService]
|
||||
public static IDataManager DataManager { get; set; }
|
||||
|
||||
[PluginService]
|
||||
public static ITextureProvider TextureProvider { get; set; }
|
||||
|
||||
[PluginService]
|
||||
public static IObjectTable ObjectTable { get; set; }
|
||||
|
||||
[PluginService]
|
||||
public static IGameGui GameGui { get; set; }
|
||||
|
||||
[PluginService]
|
||||
public static IAetheryteList AetheryteList { get; set; }
|
||||
|
||||
[PluginService]
|
||||
public static IPluginLog Log { get; set; }
|
||||
|
||||
[PluginService]
|
||||
public static IGameInteropProvider Hooker { get; set; }
|
||||
|
||||
[PluginService]
|
||||
public static IFateTable FateTable { get; set; }
|
||||
|
||||
[PluginService]
|
||||
public static ICondition Condition { get; set; }
|
||||
|
||||
[PluginService]
|
||||
public static IKeyState KeyState { get; set; }
|
||||
|
||||
[PluginService]
|
||||
public static ITextureSubstitutionProvider TextureSubstitutionProvider { get; set; }
|
||||
|
||||
[PluginService]
|
||||
public static IFramework Framework { get; set; }
|
||||
|
||||
[PluginService]
|
||||
public static IAddonLifecycle AddonLifecycle { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Collections.Generic;
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using KamiLib.Classes;
|
||||
using KamiLib.CommandManager;
|
||||
using KamiLib.Window;
|
||||
using Mappy.Classes.Caches;
|
||||
using Mappy.Controllers;
|
||||
using Mappy.Data;
|
||||
using Mappy.Modules;
|
||||
using Mappy.Windows;
|
||||
|
||||
namespace Mappy;
|
||||
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
public static class System
|
||||
{
|
||||
public static SystemConfig SystemConfig { get; set; }
|
||||
public static IconConfig IconConfig { get; set; }
|
||||
public static FlagConfig FlagConfig { get; set; }
|
||||
public static WindowManager WindowManager { get; set; }
|
||||
public static MapWindow MapWindow { get; set; }
|
||||
public static MinimapWindow MinimapWindow { get; set; }
|
||||
public static ConfigurationWindow ConfigWindow { get; set; }
|
||||
public static MapRenderer.MapRenderer MapRenderer { get; set; }
|
||||
public static IntegrationsController IntegrationsController { get; set; }
|
||||
public static AddonAreaMapController AreaMapController { get; set; }
|
||||
public static FlagController FlagController { get; set; }
|
||||
public static CommandManager CommandManager { get; set; }
|
||||
public static Teleporter Teleporter { get; set; }
|
||||
|
||||
public static List<ModuleBase> Modules { get; set; } =
|
||||
[
|
||||
new TripleTriadModule(),
|
||||
new FateModule(),
|
||||
new StellarModule(),
|
||||
];
|
||||
|
||||
public static TooltipCache TooltipCache { get; set; } = new();
|
||||
public static CardRewardCache CardRewardCache { get; set; } = new();
|
||||
public static GatheringPointNameCache GatheringPointNameCache { get; set; } = new();
|
||||
public static GatheringPointIconCache GatheringPointIconCache { get; set; } = new();
|
||||
public static TripleTriadCache TripleTriadCache { get; set; } = new();
|
||||
public static AetheryteAethernetCache AetheryteAethernetCache { get; set; } = new();
|
||||
|
||||
public static IFontHandle LargeAxisFontHandle { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Components;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using KamiLib.Classes;
|
||||
using KamiLib.CommandManager;
|
||||
using KamiLib.Extensions;
|
||||
using KamiLib.Window;
|
||||
using Mappy.Classes;
|
||||
using Mappy.Data;
|
||||
|
||||
namespace Mappy.Windows;
|
||||
|
||||
public class ConfigurationWindow : Window
|
||||
{
|
||||
private readonly TabBar tabBar = new("mappy_tab_bar", [
|
||||
new IconConfigurationTab(),
|
||||
new MapFunctionsTab(),
|
||||
new StyleOptionsTab(),
|
||||
new MinimapOptionsTab(),
|
||||
new PlayerOptionsTab(),
|
||||
]);
|
||||
|
||||
public ConfigurationWindow() : base("HSMappy Configuration Window", new Vector2(500.0f, 580.0f))
|
||||
{
|
||||
System.CommandManager.RegisterCommand(new CommandHandler
|
||||
{
|
||||
Delegate = _ => System.ConfigWindow.Toggle(), ActivationPath = "/",
|
||||
});
|
||||
}
|
||||
|
||||
protected override void DrawContents() => tabBar.Draw();
|
||||
}
|
||||
|
||||
public class MapFunctionsTab : ITabItem
|
||||
{
|
||||
public string Name => "Map Functions";
|
||||
|
||||
public bool Disabled => false;
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
var configChanged = false;
|
||||
|
||||
ImGuiTweaks.Header("Zoom Options");
|
||||
using (ImRaii.PushIndent()) {
|
||||
configChanged |= ImGui.Checkbox("Use Linear Zoom", ref System.SystemConfig.UseLinearZoom);
|
||||
configChanged |= ImGui.Checkbox("Scale icons with zoom", ref System.SystemConfig.ScaleWithZoom);
|
||||
configChanged |= ImGui.Checkbox("Scale text labels with zoom", ref System.SystemConfig.ScaleTextWithZoom);
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5.0f);
|
||||
|
||||
configChanged |= ImGuiTweaks.Checkbox("Auto Zoom", ref System.SystemConfig.AutoZoom, "Automatically sets zoom to a reasonable value relative to the map size.");
|
||||
configChanged |= ImGui.SliderFloat("Auto Zoom Scale Factor", ref System.SystemConfig.AutoZoomScaleFactor, 0.20f, 1.00f);
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5.0f);
|
||||
|
||||
configChanged |= ImGui.SliderFloat("Zoom Speed", ref System.SystemConfig.ZoomSpeed, 0.001f, 0.500f);
|
||||
configChanged |= ImGui.SliderFloat("Icon Scale", ref System.SystemConfig.IconScale, 0.10f, 3.0f);
|
||||
}
|
||||
|
||||
ImGuiTweaks.Header("When Opening Map");
|
||||
using (ImRaii.PushIndent()) {
|
||||
configChanged |= ImGui.Checkbox("Follow On Open", ref System.SystemConfig.FollowOnOpen);
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5.0f);
|
||||
|
||||
DrawCenterModeRadio();
|
||||
}
|
||||
|
||||
ImGuiTweaks.Header("Link Behaviors");
|
||||
using (ImRaii.PushIndent()) {
|
||||
configChanged |= ImGui.Checkbox("Center on Flags", ref System.SystemConfig.CenterOnFlag);
|
||||
configChanged |= ImGui.Checkbox("Center on Gathering Areas", ref System.SystemConfig.CenterOnGathering);
|
||||
configChanged |= ImGui.Checkbox("Center on Quest", ref System.SystemConfig.CenterOnQuest);
|
||||
}
|
||||
|
||||
ImGuiTweaks.Header("Misc Options");
|
||||
using (ImRaii.PushIndent()) {
|
||||
configChanged |= ImGui.Checkbox("Show Misc Tooltips", ref System.SystemConfig.ShowMiscTooltips);
|
||||
configChanged |= ImGui.Checkbox("Lock Map on Center", ref System.SystemConfig.LockCenterOnMap);
|
||||
configChanged |= ImGui.Checkbox("Show Other Players", ref System.SystemConfig.ShowPlayers);
|
||||
configChanged |= ImGui.Checkbox("Disable Map Focus on Appear", ref System.SystemConfig.NoFocusOnAppear);
|
||||
configChanged |= ImGui.Checkbox("Suppress the native map's open/close sound effect.",
|
||||
ref System.SystemConfig.SuppressNativeMapOpenSound);
|
||||
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5.0f);
|
||||
|
||||
configChanged |= ImGui.Checkbox("Show Text Labels", ref System.SystemConfig.ShowTextLabels);
|
||||
configChanged |= ImGui.DragFloat("Large Label Scale", ref System.SystemConfig.LargeAreaTextScale, 0.01f, 1.0f, 4.0f);
|
||||
configChanged |= ImGui.DragFloat("Small Label Scale", ref System.SystemConfig.SmallAreaTextScale, 0.01f, 0.5f, 3.0f);
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5.0f);
|
||||
|
||||
configChanged |= ImGui.Checkbox("Show Fog of War", ref System.SystemConfig.ShowFogOfWar);
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5.0f);
|
||||
|
||||
configChanged |= ImGui.Checkbox("Debug Mode", ref System.SystemConfig.DebugMode);
|
||||
}
|
||||
|
||||
ImGuiTweaks.Header("Toolbar");
|
||||
using (ImRaii.PushIndent()) {
|
||||
configChanged |= ImGui.Checkbox("Always Show", ref System.SystemConfig.AlwaysShowToolbar);
|
||||
configChanged |= ImGui.Checkbox("Show On Hover", ref System.SystemConfig.ShowToolbarOnHover);
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5.0f);
|
||||
configChanged |= ImGui.DragFloat("Opacity##toolbar", ref System.SystemConfig.ToolbarFade, 0.01f, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
ImGuiTweaks.Header("Coordinates");
|
||||
using (ImRaii.PushIndent()) {
|
||||
configChanged |= ImGui.Checkbox("Show Coordinate Bar", ref System.SystemConfig.ShowCoordinateBar);
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5.0f);
|
||||
configChanged |= ImGuiTweaks.ColorEditWithDefault("Text Color", ref System.SystemConfig.CoordinateTextColor, KnownColor.White.Vector());
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5.0f);
|
||||
configChanged |= ImGui.DragFloat("Opacity##coordinatebar", ref System.SystemConfig.CoordinateBarFade, 0.01f, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
if (configChanged) {
|
||||
SystemConfig.Save();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawCenterModeRadio()
|
||||
{
|
||||
var enumObject = System.SystemConfig.CenterOnOpen;
|
||||
var firstLine = true;
|
||||
|
||||
foreach (Enum enumValue in Enum.GetValues(enumObject.GetType())) {
|
||||
if (!firstLine) ImGui.SameLine();
|
||||
|
||||
if (ImGui.RadioButton(enumValue.GetDescription(), enumValue.Equals(enumObject))) {
|
||||
System.SystemConfig.CenterOnOpen = (CenterTarget)enumValue;
|
||||
SystemConfig.Save();
|
||||
}
|
||||
|
||||
firstLine = false;
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.Text("\t\tCenter on Open");
|
||||
}
|
||||
}
|
||||
|
||||
public class StyleOptionsTab : ITabItem
|
||||
{
|
||||
public string Name => "Style";
|
||||
|
||||
public bool Disabled => false;
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
var configChanged = false;
|
||||
|
||||
ImGuiTweaks.Header("Window Options");
|
||||
using (ImRaii.PushIndent()) {
|
||||
configChanged |= ImGui.Checkbox("Keep Open", ref System.SystemConfig.KeepOpen);
|
||||
configChanged |= ImGui.Checkbox("Lock Window Position", ref System.SystemConfig.LockWindow);
|
||||
configChanged |= ImGui.Checkbox("Hide Window Frame", ref System.SystemConfig.HideWindowFrame);
|
||||
configChanged |= ImGui.Checkbox("Hide Window Background", ref System.SystemConfig.HideWindowBackground);
|
||||
configChanged |= ImGui.Checkbox("Enable Shift + Drag to Move Window Frame", ref System.SystemConfig.EnableShiftDragMove);
|
||||
}
|
||||
|
||||
ImGuiTweaks.Header("Window Hiding");
|
||||
using (ImRaii.PushIndent()) {
|
||||
configChanged |= ImGui.Checkbox("Hide With Game GUI", ref System.SystemConfig.HideWithGameGui);
|
||||
configChanged |= ImGui.Checkbox("Hide Between Areas", ref System.SystemConfig.HideBetweenAreas);
|
||||
configChanged |= ImGui.Checkbox("Hide in Combat", ref System.SystemConfig.HideInCombat);
|
||||
}
|
||||
|
||||
ImGuiTweaks.Header("Window Title");
|
||||
using (ImRaii.PushIndent()) {
|
||||
configChanged |= ImGui.Checkbox("Show Region Text", ref System.SystemConfig.ShowRegionLabel);
|
||||
configChanged |= ImGui.Checkbox("Show Map Text", ref System.SystemConfig.ShowMapLabel);
|
||||
configChanged |= ImGui.Checkbox("Show Area Text", ref System.SystemConfig.ShowAreaLabel);
|
||||
configChanged |= ImGui.Checkbox("Show Sub-Area Text", ref System.SystemConfig.ShowSubAreaLabel);
|
||||
}
|
||||
|
||||
ImGuiTweaks.Header("Window Location");
|
||||
using (ImRaii.PushIndent()) {
|
||||
configChanged |= ImGui.DragFloat2("Window Position", ref System.SystemConfig.WindowPosition);
|
||||
configChanged |= ImGui.DragFloat2("Window Size", ref System.SystemConfig.WindowSize);
|
||||
}
|
||||
|
||||
ImGuiTweaks.Header("Fade Options");
|
||||
using (ImRaii.PushIndent()) {
|
||||
using (var columns = ImRaii.Table("fade_options_toggles", 2)) {
|
||||
if (!columns) return;
|
||||
|
||||
var value = System.SystemConfig.FadeMode;
|
||||
ImGui.TableNextColumn();
|
||||
|
||||
foreach (Enum enumValue in Enum.GetValues(value.GetType())) {
|
||||
var isFlagSet = value.HasFlag(enumValue);
|
||||
if (ImGuiComponents.ToggleButton(enumValue.ToString(), ref isFlagSet)) {
|
||||
var sourceValue = Convert.ToInt32(value);
|
||||
var targetValue = Convert.ToInt32(enumValue);
|
||||
|
||||
if (value.HasFlag(enumValue)) {
|
||||
System.SystemConfig.FadeMode = (FadeMode)Enum.ToObject(value.GetType(), sourceValue & ~targetValue);
|
||||
}
|
||||
else {
|
||||
System.SystemConfig.FadeMode = (FadeMode)Enum.ToObject(value.GetType(), sourceValue | targetValue);
|
||||
}
|
||||
|
||||
configChanged = true;
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(enumValue.GetDescription());
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
}
|
||||
}
|
||||
|
||||
configChanged |= ImGui.DragFloat("Fade Opacity", ref System.SystemConfig.FadePercent, 0.01f, 0.05f, 1.0f);
|
||||
}
|
||||
|
||||
ImGuiTweaks.Header("Area Style");
|
||||
using (ImRaii.PushIndent()) {
|
||||
configChanged |= ImGuiTweaks.ColorEditWithDefault("Area Color", ref System.SystemConfig.AreaColor, KnownColor.CornflowerBlue.Vector() with { W = 0.33f });
|
||||
configChanged |= ImGuiTweaks.ColorEditWithDefault("Area Outline Color", ref System.SystemConfig.AreaOutlineColor,
|
||||
KnownColor.CornflowerBlue.Vector() with { W = 0.30f });
|
||||
}
|
||||
|
||||
if (configChanged) {
|
||||
if (System.MapWindow.SizeConstraints is { } constraints) {
|
||||
System.SystemConfig.WindowSize.X = MathF.Max(System.SystemConfig.WindowSize.X, constraints.MinimumSize.X);
|
||||
System.SystemConfig.WindowSize.Y = MathF.Max(System.SystemConfig.WindowSize.Y, constraints.MinimumSize.Y);
|
||||
}
|
||||
|
||||
System.MapWindow.RefreshTitle();
|
||||
SystemConfig.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class MinimapOptionsTab : ITabItem
|
||||
{
|
||||
public string Name => "Minimap";
|
||||
|
||||
public bool Disabled => false;
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
var configChanged = false;
|
||||
|
||||
ImGuiTweaks.Header("Minimap");
|
||||
using (ImRaii.PushIndent()) {
|
||||
var showMinimap = System.SystemConfig.ShowMinimap;
|
||||
if (ImGui.Checkbox("Show Minimap", ref showMinimap)) {
|
||||
System.SystemConfig.ShowMinimap = showMinimap;
|
||||
configChanged = true;
|
||||
if (showMinimap)
|
||||
System.MinimapWindow?.UnCollapseOrShow();
|
||||
}
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5.0f);
|
||||
|
||||
configChanged |= ImGui.DragFloat("Size", ref System.SystemConfig.MinimapSize, 5.0f, 80.0f, 400.0f, "%.0f");
|
||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||
ImGui.SetTooltip("Minimap size. Resize only via this setting (no corner grip).");
|
||||
configChanged |= ImGui.DragFloat2("Position", ref System.SystemConfig.MinimapPosition);
|
||||
configChanged |= ImGui.DragFloat("Opacity", ref System.SystemConfig.MinimapOpacity, 0.01f, 0.2f, 1.0f);
|
||||
configChanged |= ImGui.SliderFloat("Zoom Level", ref System.SystemConfig.MinimapZoom, 0.03f, 0.112f,
|
||||
"%.2f (0.1 = zoomed out, lower = zoomed in)");
|
||||
configChanged |= ImGui.Checkbox("Show Player Cone", ref System.SystemConfig.MinimapShowPlayerCone);
|
||||
configChanged |= ImGui.Checkbox("Show Quest Direction Arrow", ref System.SystemConfig.MinimapShowQuestDirectionArrow);
|
||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||
ImGui.SetTooltip("Show an arrow at the edge of the minimap pointing toward your quest objective or waymark (like the default game minimap).");
|
||||
configChanged |= ImGui.Checkbox("Show FATE Direction Arrows", ref System.SystemConfig.MinimapShowFateDirectionArrows);
|
||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||
ImGui.SetTooltip("Show purple arrows at the edge of the minimap pointing toward nearby FATEs.");
|
||||
configChanged |= ImGui.Checkbox("Show Quest Area Radius", ref System.SystemConfig.MinimapShowQuestAreaRadius);
|
||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||
ImGui.SetTooltip("Show quest objective area circles on the minimap (same as on the Area Map).");
|
||||
configChanged |= ImGui.Checkbox("Hide Minimap With Game GUI", ref System.SystemConfig.MinimapHideWithGameGui);
|
||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||
ImGui.SetTooltip("When enabled, the minimap hides during NPC dialogue, object interaction, and when the game hides nameplates (same as the main map). When disabled, the minimap stays visible in those situations.");
|
||||
configChanged |= ImGui.Checkbox("Lock Position", ref System.SystemConfig.MinimapLockPosition);
|
||||
}
|
||||
|
||||
if (configChanged) {
|
||||
SystemConfig.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class PlayerOptionsTab : ITabItem
|
||||
{
|
||||
public string Name => "Player";
|
||||
|
||||
public bool Disabled => false;
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
var configChanged = false;
|
||||
|
||||
ImGuiTweaks.Header("Cone Options");
|
||||
using (ImRaii.PushIndent()) {
|
||||
configChanged |= ImGui.Checkbox("Scale Player Cone", ref System.SystemConfig.ScalePlayerCone);
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5.0f);
|
||||
configChanged |= ImGui.DragFloat("Cone Size", ref System.SystemConfig.ConeSize, 0.25f);
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5.0f);
|
||||
configChanged |= ImGuiTweaks.ColorEditWithDefault("Cone Color", ref System.SystemConfig.PlayerConeColor, KnownColor.CornflowerBlue.Vector() with { W = 0.33f });
|
||||
configChanged |= ImGuiTweaks.ColorEditWithDefault("Cone Outline Color", ref System.SystemConfig.PlayerConeOutlineColor,
|
||||
KnownColor.CornflowerBlue.Vector() with { W = 1.00f });
|
||||
}
|
||||
|
||||
ImGuiTweaks.Header("Radar Options");
|
||||
using (ImRaii.PushIndent()) {
|
||||
configChanged |= ImGui.Checkbox("Show Radar Radius", ref System.SystemConfig.ShowRadar);
|
||||
configChanged |= ImGui.Checkbox("Show in Duties", ref System.SystemConfig.ShowRadarInDuties);
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5.0f);
|
||||
|
||||
configChanged |= ImGuiTweaks.ColorEditWithDefault("Radar Area Color", ref System.SystemConfig.RadarColor, KnownColor.Gray.Vector() with { W = 0.10f });
|
||||
configChanged |= ImGuiTweaks.ColorEditWithDefault("Radar Outline Color", ref System.SystemConfig.RadarOutlineColor, KnownColor.Gray.Vector() with { W = 0.30f });
|
||||
}
|
||||
|
||||
ImGuiTweaks.Header("Player Icon Options");
|
||||
using (ImRaii.PushIndent()) {
|
||||
configChanged |= ImGui.Checkbox("Show Player Icon", ref System.SystemConfig.ShowPlayerIcon);
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5.0f);
|
||||
configChanged |= ImGui.DragFloat("Player Icon Size", ref System.SystemConfig.PlayerIconScale, 0.05f);
|
||||
}
|
||||
|
||||
if (configChanged) {
|
||||
SystemConfig.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class IconConfigurationTab : ITabItem
|
||||
{
|
||||
public string Name => "Icon Settings";
|
||||
|
||||
public bool Disabled => false;
|
||||
|
||||
private IconSetting? currentSetting;
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
using (var leftChild = ImRaii.Child("left_child", new Vector2(48.0f * ImGuiHelpers.GlobalScale + ImGui.GetStyle().ItemSpacing.X, ImGui.GetContentRegionAvail().Y))) {
|
||||
if (leftChild) {
|
||||
using var selectionList = ImRaii.ListBox("iconSelection", ImGui.GetContentRegionAvail());
|
||||
|
||||
foreach (var (iconId, settings) in System.IconConfig.IconSettingMap.OrderBy(pairData => pairData.Key)) {
|
||||
if (iconId is 0) continue;
|
||||
if (DrawHelpers.IsDisallowedIcon(iconId)) continue;
|
||||
|
||||
var texture = Service.TextureProvider.GetFromGameIcon(iconId).GetWrapOrEmpty();
|
||||
var cursorStart = ImGui.GetCursorScreenPos();
|
||||
if (ImGui.Selectable($"##iconSelect{iconId}", currentSetting == settings, ImGuiSelectableFlags.None, ImGuiHelpers.ScaledVector2(32.0f, 32.0f))) {
|
||||
currentSetting = currentSetting == settings ? null : settings;
|
||||
}
|
||||
|
||||
ImGui.SetCursorScreenPos(cursorStart);
|
||||
ImGui.Image(texture.Handle, texture.Size / 2.0f * ImGuiHelpers.GlobalScale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
using (var rightChild = ImRaii.Child("right_child", ImGui.GetContentRegionAvail(), false, ImGuiWindowFlags.NoScrollbar)) {
|
||||
if (rightChild) {
|
||||
if (currentSetting is null) {
|
||||
using var textColor = ImRaii.PushColor(ImGuiCol.Text, KnownColor.Orange.Vector());
|
||||
|
||||
ImGui.SetCursorPosY(ImGui.GetContentRegionAvail().Y / 2.0f);
|
||||
ImGuiHelpers.CenteredText("Select an Icon to Edit Settings");
|
||||
}
|
||||
else {
|
||||
// Draw background texture
|
||||
var settingsChanged = false;
|
||||
var texture = Service.TextureProvider.GetFromGameIcon(currentSetting.IconId).GetWrapOrEmpty();
|
||||
var smallestAxis = MathF.Min(ImGui.GetContentRegionAvail().X, ImGui.GetContentRegionAvail().Y);
|
||||
|
||||
if (ImGui.GetContentRegionAvail().X > ImGui.GetContentRegionAvail().Y) {
|
||||
var remainingSpace = ImGui.GetContentRegionAvail().X - smallestAxis;
|
||||
ImGui.SetCursorPosX(remainingSpace / 2.0f);
|
||||
}
|
||||
|
||||
ImGui.Image(texture.Handle, new Vector2(smallestAxis, smallestAxis), Vector2.Zero, Vector2.One, new Vector4(1.0f, 1.0f, 1.0f, 0.20f));
|
||||
ImGui.SetCursorPos(Vector2.Zero);
|
||||
|
||||
// Draw settings
|
||||
ImGuiTweaks.Header($"Configure Marker #{currentSetting.IconId}");
|
||||
using (ImRaii.PushIndent()) {
|
||||
settingsChanged |= ImGui.Checkbox("Hide Icon", ref currentSetting.Hide);
|
||||
settingsChanged |= ImGui.Checkbox("Allow Tooltip", ref currentSetting.AllowTooltip);
|
||||
settingsChanged |= ImGui.Checkbox("Allow Click Interaction", ref currentSetting.AllowClick);
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5.0f);
|
||||
settingsChanged |= ImGuiTweaks.ColorEditWithDefault("Color", ref currentSetting.Color, KnownColor.White.Vector());
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5.0f);
|
||||
settingsChanged |= ImGui.DragFloat("Icon Scale", ref currentSetting.Scale, 0.01f, 0.05f, 20.0f);
|
||||
}
|
||||
|
||||
ImGui.SetCursorPosY(ImGui.GetContentRegionMax().Y - 25.0f * ImGuiHelpers.GlobalScale);
|
||||
if (ImGui.Button("Reset to Default", new Vector2(ImGui.GetContentRegionAvail().X, 25.0f * ImGuiHelpers.GlobalScale))) {
|
||||
currentSetting.Reset();
|
||||
System.IconConfig.Save();
|
||||
}
|
||||
|
||||
if (settingsChanged) {
|
||||
System.IconConfig.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Textures;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Fate;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using KamiLib.Extensions;
|
||||
using KamiLib.Window;
|
||||
using Mappy.Data;
|
||||
using Mappy.Extensions;
|
||||
|
||||
namespace Mappy.Windows;
|
||||
|
||||
public class FateListWindow : Window
|
||||
{
|
||||
private const float ElementHeight = 48.0f;
|
||||
|
||||
public FateListWindow() : base("HSMappy Fate List Window", new Vector2(300.0f, 400.0f))
|
||||
{
|
||||
AdditionalInfoTooltip = "Shows Fates for the zone you are currently in";
|
||||
}
|
||||
|
||||
protected override unsafe void DrawContents()
|
||||
{
|
||||
DrawBackgroundImage();
|
||||
|
||||
if (Service.FateTable.Length > 0) {
|
||||
DrawOptions();
|
||||
|
||||
foreach (var index in Enumerable.Range(0, Service.FateTable.Length)) {
|
||||
var fate = FateManager.Instance()->Fates[index].Value;
|
||||
|
||||
var cursorStart = ImGui.GetCursorScreenPos();
|
||||
if (ImGui.Selectable($"##{fate->FateId}_Selectable", false, ImGuiSelectableFlags.None,
|
||||
new Vector2(ImGui.GetContentRegionAvail().X, ElementHeight * ImGuiHelpers.GlobalScale))) {
|
||||
OnFateClick(fate);
|
||||
}
|
||||
|
||||
ImGui.SetCursorScreenPos(cursorStart);
|
||||
DrawFateInfo(fate);
|
||||
}
|
||||
}
|
||||
else {
|
||||
const string text = "No FATE's available";
|
||||
var textSize = ImGui.CalcTextSize(text);
|
||||
ImGui.SetCursorPosX(ImGui.GetContentRegionAvail().X / 2.0f - textSize.X / 2.0f);
|
||||
ImGui.SetCursorPosY(ImGui.GetContentRegionAvail().Y / 2.0f - textSize.Y / 2.0f);
|
||||
ImGui.TextColored(KnownColor.Orange.Vector(), text);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawBackgroundImage()
|
||||
{
|
||||
var windowCursorStart = ImGui.GetCursorPos();
|
||||
var windowWidth = ImGui.GetContentRegionMax().X;
|
||||
var windowHeight = ImGui.GetContentRegionMax().Y;
|
||||
|
||||
var startPos = windowHeight / 2 - windowWidth / 2;
|
||||
ImGui.SetCursorPosY(startPos);
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGui.Image(
|
||||
Service.TextureProvider.GetFromGameIcon(new GameIconLookup { IconId = 60502 }).GetWrapOrEmpty().Handle,
|
||||
new Vector2(ImGui.GetContentRegionMax().X, ImGui.GetContentRegionMax().X),
|
||||
Vector2.Zero,
|
||||
Vector2.One,
|
||||
new Vector4(1.0f, 1.0f, 1.0f, 0.15f));
|
||||
|
||||
ImGui.SetCursorPos(windowCursorStart);
|
||||
}
|
||||
|
||||
private static void DrawOptions()
|
||||
{
|
||||
using var toolbarChild = ImRaii.Child("fatelist_toolbar", new Vector2(ImGui.GetContentRegionAvail().X, 32.0f));
|
||||
if (toolbarChild) {
|
||||
using var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetStyle().GetColor(ImGuiCol.ButtonActive), System.SystemConfig.SetFlagOnFateClick);
|
||||
ImGui.Spacing();
|
||||
if (ImGui.Checkbox("Place Map Flag on Click", ref System.SystemConfig.SetFlagOnFateClick)) {
|
||||
SystemConfig.Save();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.Separator();
|
||||
}
|
||||
|
||||
private static unsafe void OnFateClick(FateContext* fate)
|
||||
{
|
||||
System.IntegrationsController.OpenOccupiedMap();
|
||||
System.SystemConfig.FollowPlayer = false;
|
||||
System.MapRenderer.DrawOffset = -new Vector2(fate->Location.X, fate->Location.Z);
|
||||
|
||||
if (System.SystemConfig.SetFlagOnFateClick) {
|
||||
AgentMap.Instance()->FlagMarkerCount = 0;
|
||||
AgentMap.Instance()->SetFlagMapMarker(AgentMap.Instance()->CurrentTerritoryId, AgentMap.Instance()->CurrentMapId, fate->Location.X, fate->Location.Z);
|
||||
AgentChatLog.Instance()->InsertTextCommandParam(1048, false);
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe void DrawFateInfo(FateContext* fate)
|
||||
{
|
||||
using (ImRaii.Child($"image_child_{fate->FateId}", new Vector2(ElementHeight, ElementHeight), false, ImGuiWindowFlags.NoInputs)) {
|
||||
ImGui.Image(Service.TextureProvider.GetFromGameIcon(fate->IconId).GetWrapOrEmpty().Handle, ImGuiHelpers.ScaledVector2(ElementHeight, ElementHeight));
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
using (ImRaii.Child($"text_child_{fate->FateId}", new Vector2(ImGui.GetContentRegionAvail().X, ElementHeight), false, ImGuiWindowFlags.NoInputs)) {
|
||||
ImGui.TextColored(FateContextExtensions.GetColor(fate, 1.0f), $"Lv. {fate->Level} {fate->Name}");
|
||||
|
||||
if (fate->State is FateState.Running) {
|
||||
ImGui.TextUnformatted($"Progress: {fate->Progress}%");
|
||||
|
||||
var timeRemaining = FateContextExtensions.GetTimeRemaining(fate);
|
||||
if (timeRemaining != TimeSpan.Zero) {
|
||||
var timeString = $"{(fate->IsBonus ? "Exp Bonus!\t" : string.Empty)}{SeIconChar.Clock.ToIconString()} {FateContextExtensions.GetTimeRemaining(fate):mm\\:ss}";
|
||||
ImGui.SameLine(ImGui.GetContentRegionMax().X - ImGui.CalcTextSize(timeString).X);
|
||||
ImGui.Text(timeString);
|
||||
}
|
||||
}
|
||||
else {
|
||||
ImGui.TextUnformatted(fate->State.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnClose()
|
||||
{
|
||||
System.WindowManager.RemoveWindow(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using KamiLib.Window;
|
||||
using Mappy.Data;
|
||||
|
||||
namespace Mappy.Windows;
|
||||
|
||||
public class FlagHistoryWindow : Window
|
||||
{
|
||||
private static float FlagElementHeight => 95.0f * ImGuiHelpers.GlobalScale;
|
||||
|
||||
public FlagHistoryWindow() : base("HSMappy Flag History Window", new Vector2(400.0f, 400.0f))
|
||||
{
|
||||
AdditionalInfoTooltip = "Shows a list of all recently used flags";
|
||||
}
|
||||
|
||||
protected override void DrawContents()
|
||||
{
|
||||
ImGuiClip.ClippedDraw(System.FlagConfig.FlagHistory.ToImmutableList(), DrawFlag, FlagElementHeight);
|
||||
}
|
||||
|
||||
private void DrawFlag(Flag flag)
|
||||
{
|
||||
using var id = ImRaii.PushId(flag.GetIdString());
|
||||
|
||||
using (ImRaii.Child("flag_container", new Vector2(ImGui.GetContentRegionAvail().X, FlagElementHeight - ImGui.GetStyle().FramePadding.Y * 2.0f))) {
|
||||
using (ImRaii.Child("flag_image_container", new Vector2(155.0f * ImGuiHelpers.GlobalScale, ImGui.GetContentRegionAvail().Y))) {
|
||||
DrawFlagImage(flag);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
using (ImRaii.Child("flag_contents_container", ImGui.GetContentRegionAvail())) {
|
||||
DrawFlagData(flag);
|
||||
DrawButtons(flag);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.Spacing();
|
||||
}
|
||||
|
||||
private void DrawFlagImage(Flag flag)
|
||||
{
|
||||
var texture = flag.GetMapTexture();
|
||||
if (texture is not null) {
|
||||
ImGui.Image(texture.Handle, ImGui.GetContentRegionAvail(), new Vector2(0.15f, 0.15f), new Vector2(0.85f, 0.85f));
|
||||
}
|
||||
else {
|
||||
ImGuiHelpers.ScaledDummy(ImGui.GetContentRegionAvail());
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawFlagData(Flag flag)
|
||||
{
|
||||
ImGui.Text(flag.GetMap().PlaceName.Value.Name.ExtractText());
|
||||
|
||||
ImGui.SameLine();
|
||||
var flagCoordinate = flag.GetMapCoordinate();
|
||||
var coordinateString = $"{flagCoordinate.X:F1}, {flagCoordinate.Y:F1}";
|
||||
var coordinateStringSize = ImGui.CalcTextSize(coordinateString);
|
||||
ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - coordinateStringSize.X);
|
||||
ImGui.Text(coordinateString);
|
||||
|
||||
ImGui.TextColored(KnownColor.Gray.Vector().Lighten(0.20f), flag.GetTerritoryType().PlaceNameZone.Value.Name.ExtractText());
|
||||
|
||||
if (flag.IsFlagSet()) {
|
||||
ImGui.Spacing();
|
||||
ImGui.TextColored(KnownColor.ForestGreen.Vector().Lighten(0.40f), "Flag is currently active");
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawButtons(Flag flag)
|
||||
{
|
||||
var buttonSize = ImGuiHelpers.ScaledVector2(100.0f, 24.0f);
|
||||
|
||||
ImGui.SetCursorPos(new Vector2(0.0f, ImGui.GetContentRegionMax().Y - buttonSize.Y));
|
||||
if (ImGui.Button("Focus", buttonSize)) {
|
||||
flag.Focus();
|
||||
}
|
||||
|
||||
ImGui.SetCursorPos(ImGui.GetContentRegionMax() - buttonSize);
|
||||
using (ImRaii.Disabled(flag.IsFlagSet())) {
|
||||
if (ImGui.Button("Place", buttonSize)) {
|
||||
flag.PlaceFlag();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnClose()
|
||||
{
|
||||
System.WindowManager.RemoveWindow(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Interface.Utility;
|
||||
using KamiLib.Window;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Mappy.Classes.SelectionWindowComponents;
|
||||
using Aetheryte = Lumina.Excel.Sheets.Aetheryte;
|
||||
|
||||
namespace Mappy.Windows;
|
||||
|
||||
public class MapSelectionWindow : SelectionWindowBase<DrawableOption>
|
||||
{
|
||||
protected override bool AllowMultiSelect => false;
|
||||
|
||||
protected override float SelectionHeight => 75.0f * ImGuiHelpers.GlobalScale;
|
||||
|
||||
public MapSelectionWindow() : base(new Vector2(500.0f, 800.0f), alternativeName: "Map Selection Window")
|
||||
{
|
||||
var maps = Service.DataManager.GetExcelSheet<Map>()
|
||||
.Where(map => map is { PlaceName.RowId: not 0, TerritoryType.ValueNullable.LoadingImage.RowId: not 0, })
|
||||
.Where(map => map is not { PriorityUI: 0, PriorityCategoryUI: 0 })
|
||||
.Select(map => new MapDrawableOption { Map = map, })
|
||||
.OfType<DrawableOption>()
|
||||
.ToList();
|
||||
|
||||
var poi = Service.DataManager.GetSubrowExcelSheet<MapMarker>()
|
||||
.SelectMany(subRowCollection => subRowCollection)
|
||||
.Where(marker => marker is { PlaceNameSubtext.RowId: not 0, Icon: 60442, })
|
||||
.Select(marker => new PoiDrawableOption { MapMarker = marker, })
|
||||
.OfType<DrawableOption>()
|
||||
.ToList();
|
||||
|
||||
var aetherytes = Service.DataManager.GetExcelSheet<Aetheryte>()
|
||||
.Where(aetheryte => aetheryte is not { PlaceName.RowId: 0, AethernetName.RowId: 0, AethernetGroup: 0, Map.RowId: 0, })
|
||||
.Select(aetheryte => new AetheryteDrawableOption { Aetheryte = aetheryte, })
|
||||
.OfType<DrawableOption>()
|
||||
.ToList();
|
||||
|
||||
SelectionOptions = maps
|
||||
.Concat(poi)
|
||||
.Concat(aetherytes)
|
||||
.ToList();
|
||||
|
||||
SelectionOptions.RemoveAll(option => option.Map.RowId is 0);
|
||||
}
|
||||
|
||||
protected override void DrawSelection(DrawableOption option)
|
||||
{
|
||||
option.Draw();
|
||||
}
|
||||
|
||||
protected override IEnumerable<string> GetFilterStrings(DrawableOption option) => option.GetFilterStrings();
|
||||
|
||||
protected override string GetElementKey(DrawableOption element) => $"{element.Map.RowId}{element.MarkerLocation}{element.ExtraLineShort}{element.ExtraLineLong}";
|
||||
}
|
||||
@@ -0,0 +1,505 @@
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game.ClientState.Keys;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using KamiLib.Classes;
|
||||
using KamiLib.CommandManager;
|
||||
using KamiLib.Window;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Mappy.Classes;
|
||||
using Mappy.Classes.MapWindowComponents;
|
||||
using Mappy.Controllers;
|
||||
using Mappy.Data;
|
||||
using Map = Lumina.Excel.Sheets.Map;
|
||||
|
||||
namespace Mappy.Windows;
|
||||
|
||||
public class MapWindow : Window
|
||||
{
|
||||
public Vector2 MapDrawOffset { get; private set; }
|
||||
public HoverFlags HoveredFlags { get; private set; }
|
||||
public bool ProcessingCommand { get; set; }
|
||||
|
||||
private bool isDragStarted;
|
||||
private Vector2 lastWindowSize;
|
||||
private uint lastMapId;
|
||||
private uint lastAreaPlaceNameId;
|
||||
private uint lastSubAreaPlaceNameId;
|
||||
|
||||
private readonly MapToolbar mapToolbar = new();
|
||||
private readonly MapCoordinateBar mapCoordinateBar = new();
|
||||
private readonly MapContextMenu mapContextMenu = new();
|
||||
|
||||
public MapWindow() : base("###HSMappyMapWindow", new Vector2(400.0f, 250.0f))
|
||||
{
|
||||
UpdateTitle();
|
||||
|
||||
DisableWindowSounds = true;
|
||||
RegisterCommands();
|
||||
}
|
||||
|
||||
public override bool DrawConditions() => IntegrationsController.ShouldShowMap();
|
||||
|
||||
public override unsafe void PreOpenCheck()
|
||||
{
|
||||
// If you managed to open the window while the agent says it should be closed
|
||||
if (System.MapWindow.IsOpen && AgentMap.Instance()->AddonId is 0)
|
||||
{
|
||||
Service.Log.Debug("[OnShow] MapWindow can not be open now.");
|
||||
IsOpen = false;
|
||||
}
|
||||
|
||||
if (System.SystemConfig.KeepOpen) {
|
||||
IsOpen = true;
|
||||
}
|
||||
|
||||
if (Service.ClientState is { IsLoggedIn: false } or { IsPvP: true }) IsOpen = false;
|
||||
}
|
||||
|
||||
public override void OnOpen()
|
||||
{
|
||||
if (ProcessingCommand) {
|
||||
ProcessingCommand = false;
|
||||
System.SystemConfig.FollowPlayer = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (System.SystemConfig.FollowOnOpen) {
|
||||
System.IntegrationsController.OpenOccupiedMap();
|
||||
System.SystemConfig.FollowPlayer = true;
|
||||
}
|
||||
|
||||
switch (System.SystemConfig.CenterOnOpen) {
|
||||
case CenterTarget.Player when Service.ObjectTable.LocalPlayer is { } localPlayer:
|
||||
System.MapRenderer.CenterOnGameObject(localPlayer);
|
||||
break;
|
||||
|
||||
case CenterTarget.Map:
|
||||
System.SystemConfig.FollowPlayer = false;
|
||||
System.MapRenderer.DrawOffset = Vector2.Zero;
|
||||
break;
|
||||
|
||||
case CenterTarget.Disabled:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void DrawContents()
|
||||
{
|
||||
UpdateTitle();
|
||||
UpdateStyle();
|
||||
UpdateSizePosition();
|
||||
|
||||
HoveredFlags = HoverFlags.Nothing;
|
||||
|
||||
if (WindowBounds.IsBoundedBy(ImGui.GetMousePos(), ImGui.GetCursorScreenPos(), ImGui.GetCursorScreenPos() + ImGui.GetContentRegionMax())) {
|
||||
HoveredFlags |= HoverFlags.Window;
|
||||
}
|
||||
|
||||
MapDrawOffset = ImGui.GetCursorScreenPos();
|
||||
using var fade = ImRaii.PushStyle(ImGuiStyleVar.Alpha, System.SystemConfig.FadePercent, ShouldFade());
|
||||
using (var renderChild = ImRaii.Child("render_child", ImGui.GetContentRegionAvail(), false, ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoScrollbar)) {
|
||||
if (!renderChild) return;
|
||||
if (!System.SystemConfig.AcceptedSpoilerWarning) {
|
||||
DrawSpoilerWarning();
|
||||
return;
|
||||
}
|
||||
|
||||
DrawMapElements();
|
||||
|
||||
// Reset Draw Position for Overlay Extras
|
||||
ImGui.SetCursorPos(Vector2.Zero);
|
||||
DrawToolbar();
|
||||
DrawCoordinateBar();
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered()) {
|
||||
HoveredFlags |= HoverFlags.WindowInnerFrame;
|
||||
}
|
||||
|
||||
// Process Inputs
|
||||
ProcessInputs();
|
||||
}
|
||||
|
||||
private void DrawMapElements()
|
||||
{
|
||||
System.MapRenderer.DrawBaseTexture();
|
||||
if (ImGui.IsItemHovered()) {
|
||||
HoveredFlags |= HoverFlags.MapTexture;
|
||||
}
|
||||
|
||||
System.MapRenderer.DrawDynamicElements();
|
||||
}
|
||||
|
||||
private void DrawToolbar()
|
||||
{
|
||||
if (!ShouldShowToolbar()) return;
|
||||
|
||||
using (ImRaii.Group()) {
|
||||
mapToolbar.Draw();
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered()) {
|
||||
HoveredFlags |= HoverFlags.Toolbar;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawCoordinateBar()
|
||||
{
|
||||
if (!System.SystemConfig.ShowCoordinateBar) return;
|
||||
|
||||
using (ImRaii.Group()) {
|
||||
mapCoordinateBar.Draw(HoveredFlags.HasFlag(HoverFlags.MapTexture), MapDrawOffset);
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered()) {
|
||||
HoveredFlags |= HoverFlags.CoordinateBar;
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldShowToolbar()
|
||||
{
|
||||
if (isDragStarted) return false;
|
||||
if (System.SystemConfig.ShowToolbarOnHover && HoveredFlags.Any()) return true;
|
||||
if (System.SystemConfig.AlwaysShowToolbar) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private unsafe void UpdateTitle()
|
||||
{
|
||||
var mapChanged = lastMapId != AgentMap.Instance()->SelectedMapId;
|
||||
var areaChanged = lastAreaPlaceNameId != TerritoryInfo.Instance()->AreaPlaceNameId;
|
||||
var subAreaChanged = lastSubAreaPlaceNameId != TerritoryInfo.Instance()->SubAreaPlaceNameId;
|
||||
var locationChanged = mapChanged || areaChanged || subAreaChanged;
|
||||
|
||||
if (!locationChanged) return;
|
||||
var subLocationString = string.Empty;
|
||||
var mapData = Service.DataManager.GetExcelSheet<Map>().GetRow(AgentMap.Instance()->SelectedMapId);
|
||||
|
||||
if (System.SystemConfig.ShowRegionLabel) {
|
||||
var mapRegionName = mapData.PlaceNameRegion.Value.Name.ExtractText();
|
||||
subLocationString += $" - {mapRegionName}";
|
||||
}
|
||||
|
||||
if (System.SystemConfig.ShowMapLabel) {
|
||||
var mapName = mapData.PlaceName.Value.Name.ExtractText();
|
||||
subLocationString += $" - {mapName}";
|
||||
}
|
||||
|
||||
// Don't show specific locations if we aren't there.
|
||||
if (AgentMap.Instance()->SelectedMapId == AgentMap.Instance()->CurrentMapId) {
|
||||
if (TerritoryInfo.Instance()->AreaPlaceNameId is not 0 && System.SystemConfig.ShowAreaLabel) {
|
||||
var areaLabel = Service.DataManager.GetExcelSheet<PlaceName>().GetRow(TerritoryInfo.Instance()->AreaPlaceNameId);
|
||||
subLocationString += $" - {areaLabel.Name}";
|
||||
}
|
||||
|
||||
if (TerritoryInfo.Instance()->SubAreaPlaceNameId is not 0 && System.SystemConfig.ShowSubAreaLabel) {
|
||||
var subAreaLabel = Service.DataManager.GetExcelSheet<PlaceName>().GetRow(TerritoryInfo.Instance()->SubAreaPlaceNameId);
|
||||
subLocationString += $" - {subAreaLabel.Name}";
|
||||
}
|
||||
}
|
||||
|
||||
WindowName = $"{subLocationString}###HSMappyMapWindow".TrimStart(['-',' ']);
|
||||
|
||||
lastMapId = AgentMap.Instance()->SelectedMapId;
|
||||
lastAreaPlaceNameId = TerritoryInfo.Instance()->AreaPlaceNameId;
|
||||
lastSubAreaPlaceNameId = TerritoryInfo.Instance()->SubAreaPlaceNameId;
|
||||
}
|
||||
|
||||
public void RefreshTitle()
|
||||
{
|
||||
lastMapId = 0;
|
||||
lastAreaPlaceNameId = 0;
|
||||
lastSubAreaPlaceNameId = 0;
|
||||
}
|
||||
|
||||
private void ProcessInputs()
|
||||
{
|
||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) {
|
||||
ImGui.OpenPopup("Mappy_Context_Menu");
|
||||
}
|
||||
else {
|
||||
if (HoveredFlags.Any()) {
|
||||
if (System.SystemConfig.EnableShiftDragMove && ImGui.GetIO().KeyShift) {
|
||||
Flags &= ~ImGuiWindowFlags.NoMove;
|
||||
}
|
||||
else {
|
||||
ProcessMouseScroll();
|
||||
ProcessMapDragStart();
|
||||
Flags |= ImGuiWindowFlags.NoMove;
|
||||
}
|
||||
}
|
||||
|
||||
ProcessMapDragDragging();
|
||||
ProcessMapDragEnd();
|
||||
}
|
||||
|
||||
// Draw Context Menu
|
||||
mapContextMenu.Draw(MapDrawOffset);
|
||||
}
|
||||
|
||||
private unsafe void UpdateStyle()
|
||||
{
|
||||
if (System.SystemConfig.HideWindowFrame) {
|
||||
Flags |= ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoBackground;
|
||||
}
|
||||
else {
|
||||
Flags &= ~(ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoBackground);
|
||||
}
|
||||
|
||||
if (System.SystemConfig.LockWindow) {
|
||||
Flags |= ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove;
|
||||
}
|
||||
else {
|
||||
Flags &= ~(ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove);
|
||||
}
|
||||
|
||||
if (System.SystemConfig.NoFocusOnAppear) {
|
||||
Flags |= ImGuiWindowFlags.NoFocusOnAppearing;
|
||||
}
|
||||
else {
|
||||
Flags &= ~ImGuiWindowFlags.NoFocusOnAppearing;
|
||||
}
|
||||
|
||||
if (System.SystemConfig.HideWindowBackground) {
|
||||
Flags |= ImGuiWindowFlags.NoBackground;
|
||||
}
|
||||
else {
|
||||
Flags &= ~ImGuiWindowFlags.NoBackground;
|
||||
}
|
||||
|
||||
if (Service.KeyState[VirtualKey.ESCAPE] && IsFocused && !IsMapLocked()) {
|
||||
AgentMap.Instance()->Hide();
|
||||
}
|
||||
|
||||
if (System.SystemConfig.FollowPlayer && Service.ObjectTable is { LocalPlayer: { } localPlayer }) {
|
||||
System.MapRenderer.CenterOnGameObject(localPlayer);
|
||||
}
|
||||
|
||||
if (System.SystemConfig.LockCenterOnMap) {
|
||||
System.SystemConfig.FollowPlayer = false;
|
||||
System.MapRenderer.DrawOffset = Vector2.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSizePosition()
|
||||
{
|
||||
var systemConfig = System.SystemConfig;
|
||||
var windowPosition = ImGui.GetWindowPos();
|
||||
var windowSize = ImGui.GetWindowSize();
|
||||
|
||||
if (!IsFocused) {
|
||||
if (windowPosition != systemConfig.WindowPosition) {
|
||||
ImGui.SetWindowPos(systemConfig.WindowPosition);
|
||||
}
|
||||
|
||||
if (windowSize != systemConfig.WindowSize) {
|
||||
ImGui.SetWindowSize(systemConfig.WindowSize);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// If focused
|
||||
if (systemConfig.WindowPosition != windowPosition) {
|
||||
systemConfig.WindowPosition = windowPosition;
|
||||
SystemConfig.Save();
|
||||
}
|
||||
|
||||
if (systemConfig.WindowSize != windowSize) {
|
||||
systemConfig.WindowSize = windowSize;
|
||||
SystemConfig.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawSpoilerWarning()
|
||||
{
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, KnownColor.Orange.Vector())) {
|
||||
const string warningLine1 = "Warning, HSMappy does not protect you from spoilers and will show everything.";
|
||||
const string warningLine2 = "Do not use HSMappy if you are not comfortable with this.";
|
||||
|
||||
ImGui.SetCursorPos(ImGui.GetContentRegionAvail() / 2.0f - (ImGui.CalcTextSize(warningLine1) * 2.0f) with { X = 0.0f });
|
||||
ImGuiHelpers.CenteredText(warningLine1);
|
||||
ImGuiHelpers.CenteredText(warningLine2);
|
||||
}
|
||||
|
||||
ImGuiHelpers.ScaledDummy(30.0f);
|
||||
ImGui.SetCursorPosX(ImGui.GetContentRegionAvail().X / 3.0f);
|
||||
using (ImRaii.Disabled(!(ImGui.GetIO().KeyShift && ImGui.GetIO().KeyCtrl))) {
|
||||
if (ImGui.Button("I understand", new Vector2(ImGui.GetContentRegionAvail().X / 2.0f, 23.0f * ImGuiHelpers.GlobalScale))) {
|
||||
System.SystemConfig.AcceptedSpoilerWarning = true;
|
||||
SystemConfig.Save();
|
||||
}
|
||||
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, 1.0f)) {
|
||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) {
|
||||
ImGui.SetTooltip("Hold Shift + Control while clicking activate button");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override unsafe void OnClose()
|
||||
{
|
||||
AgentMap.Instance()->Hide();
|
||||
SystemConfig.Save();
|
||||
}
|
||||
|
||||
private void ProcessMouseScroll()
|
||||
{
|
||||
if (System.SystemConfig.ZoomLocked) return;
|
||||
if (ImGui.GetIO().MouseWheel is 0) return;
|
||||
if (!HoveredFlags.HasFlag(HoverFlags.WindowInnerFrame)) return;
|
||||
|
||||
if (System.SystemConfig.UseLinearZoom) {
|
||||
MapRenderer.MapRenderer.Scale += System.SystemConfig.ZoomSpeed * ImGui.GetIO().MouseWheel;
|
||||
}
|
||||
else {
|
||||
MapRenderer.MapRenderer.Scale *= 1.0f + System.SystemConfig.ZoomSpeed * ImGui.GetIO().MouseWheel;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessMapDragDragging()
|
||||
{
|
||||
if (ImGui.IsMouseDragging(ImGuiMouseButton.Left) && isDragStarted) {
|
||||
System.MapRenderer.DrawOffset += ImGui.GetMouseDragDelta() / MapRenderer.MapRenderer.Scale;
|
||||
ImGui.ResetMouseDragDelta();
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessMapDragEnd()
|
||||
{
|
||||
if (ImGui.IsMouseReleased(ImGuiMouseButton.Left)) {
|
||||
isDragStarted = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessMapDragStart()
|
||||
{
|
||||
// Don't allow a drag to start if the window size is changing
|
||||
if (ImGui.GetWindowSize() == lastWindowSize) {
|
||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left) && !isDragStarted) {
|
||||
isDragStarted = true;
|
||||
System.SystemConfig.FollowPlayer = false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
lastWindowSize = ImGui.GetWindowSize();
|
||||
isDragStarted = false;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe bool ShouldFade() =>
|
||||
System.SystemConfig.FadeMode.HasFlag(FadeMode.Always) ||
|
||||
System.SystemConfig.FadeMode.HasFlag(FadeMode.WhenFocused) && IsFocused ||
|
||||
System.SystemConfig.FadeMode.HasFlag(FadeMode.WhenMoving) && AgentMap.Instance()->IsPlayerMoving ||
|
||||
System.SystemConfig.FadeMode.HasFlag(FadeMode.WhenUnFocused) && !IsFocused;
|
||||
|
||||
private void RegisterCommands()
|
||||
{
|
||||
System.CommandManager.RegisterCommand(new ToggleCommandHandler
|
||||
{
|
||||
UseShowHideText = true,
|
||||
BaseActivationPath = "/map",
|
||||
EnableDelegate = _ => System.MapWindow.UnCollapseOrShow(),
|
||||
DisableDelegate = _ => System.MapWindow.Close(),
|
||||
ToggleDelegate = _ => System.MapWindow.UnCollapseOrToggle(),
|
||||
});
|
||||
|
||||
System.CommandManager.RegisterCommand(new CommandHandler
|
||||
{
|
||||
ActivationPath = "/map/follow",
|
||||
Delegate = _ =>
|
||||
{
|
||||
System.SystemConfig.FollowPlayer = true;
|
||||
SystemConfig.Save();
|
||||
},
|
||||
});
|
||||
|
||||
System.CommandManager.RegisterCommand(new CommandHandler
|
||||
{
|
||||
ActivationPath = "/map/unfollow",
|
||||
Delegate = _ =>
|
||||
{
|
||||
System.SystemConfig.FollowPlayer = false;
|
||||
SystemConfig.Save();
|
||||
},
|
||||
});
|
||||
|
||||
System.CommandManager.RegisterCommand(new ToggleCommandHandler
|
||||
{
|
||||
BaseActivationPath = "/autofollow",
|
||||
EnableDelegate = _ =>
|
||||
{
|
||||
System.SystemConfig.FollowOnOpen = true;
|
||||
SystemConfig.Save();
|
||||
},
|
||||
DisableDelegate = _ =>
|
||||
{
|
||||
System.SystemConfig.FollowOnOpen = false;
|
||||
SystemConfig.Save();
|
||||
},
|
||||
ToggleDelegate = _ =>
|
||||
{
|
||||
System.SystemConfig.FollowOnOpen = !System.SystemConfig.FollowOnOpen;
|
||||
SystemConfig.Save();
|
||||
},
|
||||
});
|
||||
|
||||
System.CommandManager.RegisterCommand(new ToggleCommandHandler
|
||||
{
|
||||
BaseActivationPath = "/keepopen",
|
||||
EnableDelegate = _ =>
|
||||
{
|
||||
System.SystemConfig.KeepOpen = true;
|
||||
SystemConfig.Save();
|
||||
},
|
||||
DisableDelegate = _ =>
|
||||
{
|
||||
System.SystemConfig.KeepOpen = false;
|
||||
SystemConfig.Save();
|
||||
},
|
||||
ToggleDelegate = _ =>
|
||||
{
|
||||
System.SystemConfig.KeepOpen = !System.SystemConfig.KeepOpen;
|
||||
SystemConfig.Save();
|
||||
},
|
||||
});
|
||||
|
||||
System.CommandManager.RegisterCommand(new CommandHandler
|
||||
{
|
||||
ActivationPath = "/center/player",
|
||||
Delegate = _ =>
|
||||
{
|
||||
if (Service.ObjectTable.LocalPlayer is { } localPlayer) {
|
||||
System.MapRenderer.CenterOnGameObject(localPlayer);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
System.CommandManager.RegisterCommand(new CommandHandler
|
||||
{
|
||||
ActivationPath = "/center/map",
|
||||
Delegate = _ =>
|
||||
{
|
||||
System.SystemConfig.FollowPlayer = false;
|
||||
System.MapRenderer.DrawOffset = Vector2.Zero;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private static unsafe bool IsMapLocked()
|
||||
{
|
||||
var addon = Service.GameGui.GetAddonByName<AddonAreaMap>("AreaMap");
|
||||
if (addon is null || addon->RootNode is null) return false;
|
||||
|
||||
return (addon->Param & 0x8_0000) > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using KamiLib.Window;
|
||||
using Mappy.Controllers;
|
||||
using Mappy.Data;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
|
||||
namespace Mappy.Windows;
|
||||
|
||||
public class MinimapWindow : Window
|
||||
{
|
||||
public MinimapWindow() : base("HSMappy Minimap###HSMappyMinimap", new Vector2(200.0f, 200.0f))
|
||||
{
|
||||
DisableWindowSounds = true;
|
||||
Flags |= ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoScrollWithMouse;
|
||||
}
|
||||
|
||||
public override bool DrawConditions() =>
|
||||
IntegrationsController.ShouldShowMinimap() && System.SystemConfig.ShowMinimap;
|
||||
|
||||
public override void PreOpenCheck()
|
||||
{
|
||||
if (Service.ClientState is { IsLoggedIn: false } or { IsPvP: true })
|
||||
IsOpen = false;
|
||||
}
|
||||
|
||||
protected override unsafe void DrawContents()
|
||||
{
|
||||
var agent = AgentMap.Instance();
|
||||
// Try loading from Lumina first so minimap can show without ever opening the area map
|
||||
if (!System.MapRenderer.HasMinimapCacheFor(agent->CurrentMapId) && agent->SelectedMapId != agent->CurrentMapId)
|
||||
System.MapRenderer.TryEnsureLuminaCacheFor(agent->CurrentMapId);
|
||||
|
||||
var mapLoaded = agent->SelectedMapId == agent->CurrentMapId || System.MapRenderer.HasMinimapCacheFor(agent->CurrentMapId);
|
||||
|
||||
if (!mapLoaded)
|
||||
{
|
||||
// Map data could not be loaded for this area (no Lumina path matched). Minimap shows automatically when data is available.
|
||||
const string hint = "Map unavailable for this area.";
|
||||
var textSize = ImGui.CalcTextSize(hint);
|
||||
var pos = (ImGui.GetWindowSize() - textSize) * 0.5f;
|
||||
ImGui.SetCursorPos(new Vector2(Math.Max(0, pos.X), Math.Max(20, pos.Y)));
|
||||
ImGui.TextColored(new Vector4(0.7f, 0.7f, 0.7f, 0.9f), hint);
|
||||
UpdateStyle();
|
||||
UpdateSizePosition();
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateStyle();
|
||||
UpdateSizePosition();
|
||||
|
||||
// Compensate for window padding: draw the minimap child so it fills the full window (no black bands).
|
||||
var padding = ImGui.GetStyle().WindowPadding;
|
||||
var winSize = ImGui.GetWindowSize();
|
||||
ImGui.SetCursorPos(new Vector2(-padding.X, -padding.Y));
|
||||
var contentSize = winSize;
|
||||
|
||||
if (contentSize.X <= 0 || contentSize.Y <= 0) return;
|
||||
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, System.SystemConfig.MinimapOpacity))
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, 0f))
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, Vector2.Zero))
|
||||
using (var child = ImRaii.Child("minimap_render", contentSize, false, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse))
|
||||
{
|
||||
if (child) {
|
||||
// Use window size so map fills the full window; renderer clamps draw position so map always covers the view.
|
||||
System.MapRenderer.DrawMinimapContents(contentSize);
|
||||
// Mouse wheel over minimap: zoom in/out, and consume wheel so the window doesn't scroll
|
||||
if (ImGui.IsItemHovered()) {
|
||||
var io = ImGui.GetIO();
|
||||
var wheel = io.MouseWheel;
|
||||
if (wheel != 0) {
|
||||
var zoom = System.SystemConfig.MinimapZoom;
|
||||
zoom -= wheel * 0.012f; // Small step so zoom is incremental between max out (0.1) and max in (0.03)
|
||||
System.SystemConfig.MinimapZoom = Math.Clamp(zoom, 0.03f, 0.112f);
|
||||
SystemConfig.Save();
|
||||
}
|
||||
// Consume wheel so the window doesn't scroll when at min/max zoom or when we handled it
|
||||
io.MouseWheel = 0f;
|
||||
io.MouseWheelH = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore default padding for the next window is done in plugin Draw callback (PopStyleVar after all windows).
|
||||
}
|
||||
|
||||
public override void OnOpen()
|
||||
{
|
||||
ImGui.SetWindowPos(System.SystemConfig.MinimapPosition);
|
||||
ImGui.SetWindowSize(new Vector2(System.SystemConfig.MinimapSize, System.SystemConfig.MinimapSize));
|
||||
}
|
||||
|
||||
private void UpdateStyle()
|
||||
{
|
||||
if (System.SystemConfig.MinimapLockPosition)
|
||||
Flags |= ImGuiWindowFlags.NoMove;
|
||||
else
|
||||
Flags &= ~ImGuiWindowFlags.NoMove;
|
||||
}
|
||||
|
||||
private void UpdateSizePosition()
|
||||
{
|
||||
var config = System.SystemConfig;
|
||||
var windowPosition = ImGui.GetWindowPos();
|
||||
var windowSize = ImGui.GetWindowSize();
|
||||
var configSize = config.MinimapSize;
|
||||
|
||||
// Size is config-only (set in Mappy settings); always apply config size to window.
|
||||
if (Math.Abs(windowSize.X - configSize) > 0.1f || Math.Abs(windowSize.Y - configSize) > 0.1f)
|
||||
ImGui.SetWindowSize(new Vector2(configSize, configSize));
|
||||
|
||||
if (!ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows)) {
|
||||
// Not focused: apply config position to window
|
||||
if (windowPosition != config.MinimapPosition)
|
||||
ImGui.SetWindowPos(config.MinimapPosition);
|
||||
} else {
|
||||
// Focused: save window position to config (size is changed only via settings)
|
||||
if (config.MinimapPosition != windowPosition) {
|
||||
config.MinimapPosition = windowPosition;
|
||||
SystemConfig.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using KamiLib.Classes;
|
||||
using KamiLib.Window;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Mappy.Classes;
|
||||
using Mappy.Extensions;
|
||||
using Map = FFXIVClientStructs.FFXIV.Client.Game.UI.Map;
|
||||
|
||||
namespace Mappy.Windows;
|
||||
|
||||
public class QuestListWindow : Window
|
||||
{
|
||||
private readonly TabBar tabBar = new("questListTabBar", [
|
||||
new AcceptedQuestsTabItem(),
|
||||
new UnacceptedQuestsTabItem(),
|
||||
]);
|
||||
|
||||
public QuestListWindow() : base("HSMappy Quest List Window", new Vector2(300.0f, 500.0f))
|
||||
{
|
||||
AdditionalInfoTooltip = "Shows Quests for the zone you are currently in";
|
||||
}
|
||||
|
||||
protected override void DrawContents()
|
||||
{
|
||||
using var child = ImRaii.Child("quest_list_scrollable", ImGui.GetContentRegionAvail());
|
||||
if (!child) return;
|
||||
|
||||
tabBar.Draw();
|
||||
}
|
||||
|
||||
public override void OnClose()
|
||||
{
|
||||
System.WindowManager.RemoveWindow(this);
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe class UnacceptedQuestsTabItem : ITabItem
|
||||
{
|
||||
private const float ElementHeight = 48.0f;
|
||||
|
||||
public string Name => "Unaccepted Quests";
|
||||
|
||||
public bool Disabled => false;
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
if (Map.Instance()->UnacceptedQuestMarkers.Count > 0) {
|
||||
foreach (var quest in Map.Instance()->UnacceptedQuestMarkers) {
|
||||
var questData = Service.DataManager.GetExcelSheet<Quest>().GetRow(quest.ObjectiveId + 65536u);
|
||||
|
||||
foreach (var marker in quest.MarkerData) {
|
||||
var cursorStart = ImGui.GetCursorScreenPos();
|
||||
if (ImGui.Selectable($"##{quest.ObjectiveId}_Selectable_{marker.LevelId}", false, ImGuiSelectableFlags.None,
|
||||
new Vector2(ImGui.GetContentRegionAvail().X, ElementHeight * ImGuiHelpers.GlobalScale))) {
|
||||
System.IntegrationsController.OpenMap(marker.MapId);
|
||||
System.SystemConfig.FollowPlayer = false;
|
||||
|
||||
var mapOffsetVector = DrawHelpers.GetMapOffsetVector();
|
||||
System.MapRenderer.DrawOffset = -marker.Position.AsMapVector() * AgentMap.Instance()->SelectedMapSizeFactorFloat + mapOffsetVector;
|
||||
}
|
||||
|
||||
ImGui.SetCursorScreenPos(cursorStart);
|
||||
ImGui.Image(Service.TextureProvider.GetFromGameIcon(marker.IconId).GetWrapOrEmpty().Handle, ImGuiHelpers.ScaledVector2(ElementHeight, ElementHeight));
|
||||
|
||||
ImGui.SameLine();
|
||||
var text = $"Lv. {questData.ClassJobLevel.First()} {quest.Label}";
|
||||
|
||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ElementHeight * ImGuiHelpers.GlobalScale / 2.0f - ImGui.CalcTextSize(text).Y / 2.0f);
|
||||
ImGui.Text(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
const string text = "No quests available";
|
||||
var textSize = ImGui.CalcTextSize(text);
|
||||
ImGui.SetCursorPosX(ImGui.GetContentRegionAvail().X / 2.0f - textSize.X / 2.0f);
|
||||
ImGui.SetCursorPosY(ImGui.GetContentRegionAvail().Y / 2.0f - textSize.Y / 2.0f);
|
||||
ImGui.TextColored(KnownColor.Orange.Vector(), text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe class AcceptedQuestsTabItem : ITabItem
|
||||
{
|
||||
private const float ElementHeight = 48.0f;
|
||||
|
||||
public string Name => "Accepted Quests";
|
||||
|
||||
public bool Disabled => false;
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
if (AnyActiveQuests()) {
|
||||
foreach (var quest in Map.Instance()->QuestMarkers) {
|
||||
if (quest.ObjectiveId is 0) continue;
|
||||
|
||||
var questData = Service.DataManager.GetExcelSheet<Quest>().GetRow(quest.ObjectiveId + 65536u);
|
||||
|
||||
var index = 0;
|
||||
foreach (var marker in quest.MarkerData) {
|
||||
var cursorStart = ImGui.GetCursorScreenPos();
|
||||
if (ImGui.Selectable($"##{quest.ObjectiveId}_Selectable_{marker.LevelId}_{index++}", false, ImGuiSelectableFlags.None,
|
||||
new Vector2(ImGui.GetContentRegionAvail().X, ElementHeight * ImGuiHelpers.GlobalScale))) {
|
||||
System.IntegrationsController.OpenMap(marker.MapId);
|
||||
System.SystemConfig.FollowPlayer = false;
|
||||
System.MapRenderer.DrawOffset = -marker.Position.AsMapVector();
|
||||
}
|
||||
|
||||
var iconId = marker.IconId switch
|
||||
{
|
||||
>= 60483 and <= 60494 => DrawHelpers.QuestionMarkIcon,
|
||||
_ => marker.IconId,
|
||||
};
|
||||
|
||||
ImGui.SetCursorScreenPos(cursorStart);
|
||||
ImGui.Image(Service.TextureProvider.GetFromGameIcon(iconId).GetWrapOrEmpty().Handle, ImGuiHelpers.ScaledVector2(ElementHeight, ElementHeight));
|
||||
|
||||
ImGui.SameLine();
|
||||
var text = $"Lv. {questData.ClassJobLevel.First()} {quest.Label}";
|
||||
|
||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ElementHeight * ImGuiHelpers.GlobalScale / 2.0f - ImGui.CalcTextSize(text).Y / 2.0f);
|
||||
ImGui.Text(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
const string text = "No quests available";
|
||||
var textSize = ImGui.CalcTextSize(text);
|
||||
ImGui.SetCursorPosX(ImGui.GetContentRegionAvail().X / 2.0f - textSize.X / 2.0f);
|
||||
ImGui.SetCursorPosY(ImGui.GetContentRegionAvail().Y / 2.0f - textSize.Y / 2.0f);
|
||||
ImGui.TextColored(KnownColor.Orange.Vector(), text);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool AnyActiveQuests()
|
||||
{
|
||||
foreach (var questMarker in Map.Instance()->QuestMarkers) {
|
||||
if (questMarker.ObjectiveId is not 0) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
# Creates a complete HSMappy release zip with all required files.
|
||||
# Run after: dotnet build -c Release
|
||||
# Output: bin/Release/HSMappy/latest.zip
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ReleaseDir = "$PSScriptRoot\bin\Release"
|
||||
$ZipPath = "$ReleaseDir\HSMappy\latest.zip"
|
||||
$OutDir = "$ReleaseDir\HSMappy"
|
||||
|
||||
if (-not (Test-Path "$ReleaseDir\HSMappy.dll")) { throw "Missing: $ReleaseDir\HSMappy.dll (run dotnet build -c Release first)" }
|
||||
if (-not (Test-Path "$ReleaseDir\HSMappy.json")) { throw "Missing: $ReleaseDir\HSMappy.json" }
|
||||
|
||||
$Files = @(
|
||||
"$ReleaseDir\HSMappy.dll",
|
||||
"$ReleaseDir\HSMappy.json",
|
||||
"$ReleaseDir\KamiLib.dll",
|
||||
"$ReleaseDir\HtmlAgilityPack.dll",
|
||||
"$ReleaseDir\Karashiiro.HtmlAgilityPack.CssSelectors.NetCoreFork.dll",
|
||||
"$ReleaseDir\Microsoft.Extensions.ObjectPool.dll",
|
||||
"$ReleaseDir\NetStone.dll",
|
||||
"$ReleaseDir\Newtonsoft.Json.dll"
|
||||
)
|
||||
|
||||
foreach ($f in $Files) {
|
||||
if (-not (Test-Path $f)) { throw "Missing: $f" }
|
||||
}
|
||||
|
||||
if (-not (Test-Path $OutDir)) { New-Item -ItemType Directory -Path $OutDir -Force | Out-Null }
|
||||
if (Test-Path $ZipPath) { Remove-Item $ZipPath -Force }
|
||||
|
||||
# Add files to zip from Release root so zip contains HSMappy.dll etc. at root
|
||||
$FilesToZip = @()
|
||||
foreach ($f in $Files) {
|
||||
$FilesToZip += (Get-Item $f).FullName
|
||||
}
|
||||
Compress-Archive -Path $FilesToZip -DestinationPath $ZipPath -CompressionLevel Optimal
|
||||
Write-Host "Created: $ZipPath"
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"version": 1,
|
||||
"dependencies": {
|
||||
"net10.0-windows7.0": {
|
||||
"DalamudPackager": {
|
||||
"type": "Direct",
|
||||
"requested": "[14.0.1, )",
|
||||
"resolved": "14.0.1",
|
||||
"contentHash": "y0WWyUE6dhpGdolK3iKgwys05/nZaVf4ZPtIjpLhJBZvHxkkiE23zYRo7K7uqAgoK/QvK5cqF6l3VG5AbgC6KA=="
|
||||
},
|
||||
"DotNet.ReproducibleBuilds": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.2.39, )",
|
||||
"resolved": "1.2.39",
|
||||
"contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg=="
|
||||
},
|
||||
"HtmlAgilityPack": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.11.46",
|
||||
"contentHash": "dLMn4EVfJBHWmWK4Uh0XGD76FPLHI0qr2Tm0s1m/xmgiHb1JUb9zB8AzO8HtrkBBlMN6JfCUBYddhqC0hZNR+g=="
|
||||
},
|
||||
"Karashiiro.HtmlAgilityPack.CssSelectors.NetCoreFork": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.0.2",
|
||||
"contentHash": "+7cqe/+tN7gDW36pgrefSeyAQvhqznM34D0uTwnETgU25iAC6boPtblxbrMluW5rA9o6MzjEg4qZK7WrnT+tSw==",
|
||||
"dependencies": {
|
||||
"HtmlAgilityPack": "1.11.46"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.ObjectPool": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.0-preview.6.25358.103",
|
||||
"contentHash": "r49n6FoNDeCC0G/KPLHHxg/VneYlpRNVmpzWyAiKAJ3wfpSOsbXB84Owyt3loATmO5aP20th0xGmKavdgDItDA=="
|
||||
},
|
||||
"NetStone": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.1.1",
|
||||
"contentHash": "7AWc3j6082Ut3xTbJTWBhsZC2Zd7hYVprQiNiei+FGxSSvGacv+xQMBpBIfAnQnnmsKnEQ47NExNgZpt6UcI4A==",
|
||||
"dependencies": {
|
||||
"HtmlAgilityPack": "1.11.46",
|
||||
"Karashiiro.HtmlAgilityPack.CssSelectors.NetCoreFork": "0.0.2",
|
||||
"Newtonsoft.Json": "13.0.3"
|
||||
}
|
||||
},
|
||||
"Newtonsoft.Json": {
|
||||
"type": "Transitive",
|
||||
"resolved": "13.0.3",
|
||||
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
|
||||
},
|
||||
"kamilib": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.ObjectPool": "[10.0.0-preview.6.25358.103, )",
|
||||
"NetStone": "[1.1.1, )"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
[{"Author":"Knack117","Name":"HSMappy","Punchline":"A more versatile in-game map.","Description":"Replaces the in-game map with an ImGui implementation with several additional features. Fork with minimap improvements, quest radius on minimap, white gradient player cone, and more.","Changelog":"1.0.0.0: Initial HSMappy release. Minimap: quest radius circle (orange, transparent), tooltip; cone drawn under markers; white gradient cone; /hsmappy commands.","InternalName":"HSMappy","AssemblyVersion":"1.0.0.0","RepoUrl":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy","ApplicableVersion":"any","Tags":["map","mapping","overlay","utility"],"CategoryTags":["jobs"],"DalamudApiLevel":14,"DownloadLinkInstall":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy/releases/download/v1.0.0.0/latest.zip","IsHide":false,"IsTestingExclusive":false,"DownloadLinkTesting":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy/releases/download/v1.0.0.0/latest.zip","DownloadLinkUpdate":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy/releases/download/v1.0.0.0/latest.zip","LastUpdate":"1761700000"}]
|
||||
Reference in New Issue
Block a user