Initial HSMappy release (fork of Mappy)

Made-with: Cursor
This commit is contained in:
2026-02-26 03:54:51 -05:00
commit 9659f7a7d1
72 changed files with 6625 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: MidoriKami
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+16
View File
@@ -0,0 +1,16 @@
# Build
[Bb]in/
[Oo]bj/
[Dd]ebug/
[Rr]elease/
*.user
*.suo
.vs/
.idea/
packages/
*.nupkg
*.snupkg
.DS_Store
Thumbs.db
# Nested repo - track source only
KamiLib/.git/
+4
View File
@@ -0,0 +1,4 @@
[submodule "KamiLib"]
path = KamiLib
url = https://github.com/MidoriKami/KamiLib.git
branch = master
Submodule
+1
Submodule KamiLib added at ec76c84280
+14
View File
@@ -0,0 +1,14 @@
Copyright (C) 2022 MidoriKami
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
+35
View File
@@ -0,0 +1,35 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mappy", "Mappy\Mappy.csproj", "{ED012F22-F994-4178-809F-7CDC44BFF840}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KamiLib", "KamiLib\KamiLib.csproj", "{B1015978-67C0-45C6-955F-018E4B270498}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{ED012F22-F994-4178-809F-7CDC44BFF840}.Debug|x64.ActiveCfg = Debug|x64
{ED012F22-F994-4178-809F-7CDC44BFF840}.Debug|x64.Build.0 = Debug|x64
{ED012F22-F994-4178-809F-7CDC44BFF840}.Release|x64.ActiveCfg = Release|x64
{ED012F22-F994-4178-809F-7CDC44BFF840}.Release|x64.Build.0 = Release|x64
{BDF198BE-0703-408A-B179-B7E00C7B8663}.Debug|x64.ActiveCfg = Debug|x64
{BDF198BE-0703-408A-B179-B7E00C7B8663}.Debug|x64.Build.0 = Debug|x64
{BDF198BE-0703-408A-B179-B7E00C7B8663}.Release|x64.ActiveCfg = Release|x64
{BDF198BE-0703-408A-B179-B7E00C7B8663}.Release|x64.Build.0 = Release|x64
{B1015978-67C0-45C6-955F-018E4B270498}.Debug|x64.ActiveCfg = Debug|x64
{B1015978-67C0-45C6-955F-018E4B270498}.Debug|x64.Build.0 = Debug|x64
{B1015978-67C0-45C6-955F-018E4B270498}.Release|x64.ActiveCfg = Release|x64
{B1015978-67C0-45C6-955F-018E4B270498}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B17E85B1-5F60-4440-9F9A-3DDE877E8CDF}
EndGlobalSection
EndGlobal
+2
View File
@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Bozja/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
+283
View File
@@ -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;
}
}
+23
View File
@@ -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}";
}
}
+18
View File
@@ -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();
}
}
+9
View File
@@ -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);
}
+275
View File
@@ -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,
};
}
+19
View File
@@ -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;
}
}
}
}
}
+25
View File
@@ -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;
}
}
+8
View File
@@ -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}";
}
+139
View File
@@ -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();
}
}
}
+55
View File
@@ -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");
}
+527
View File
@@ -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);
}
+11
View File
@@ -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>
+68
View File
@@ -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);
}
+35
View File
@@ -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);
}
+127
View File
@@ -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;
}
}
+31
View File
@@ -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());
}
}
+10
View File
@@ -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}";
}
}
+198
View File
@@ -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(),
});
}
}
+8
View File
@@ -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);
}
+369
View File
@@ -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();
}
}
+35
View File
@@ -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);
}
}
}
+23
View File
@@ -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,
});
}
}
}
+275
View File
@@ -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");
}
}
+105
View File
@@ -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
}
}
}
+170
View File
@@ -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);
}
+32
View File
@@ -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);
}
}
}
+38
View File
@@ -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,
});
}
}
}
}
+23
View File
@@ -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>
+128
View File
@@ -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;
}
}
+40
View File
@@ -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;
}
}
+8
View File
@@ -0,0 +1,8 @@
using Mappy.Classes;
namespace Mappy.Modules;
public abstract class ModuleBase
{
public abstract bool ProcessMarker(MarkerInfo markerInfo);
}
+13
View File
@@ -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;
}
}
+15
View File
@@ -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;
}
}
+54
View File
@@ -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; }
}
+46
View File
@@ -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; }
}
+427
View File
@@ -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();
}
}
}
}
}
}
+136
View File
@@ -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);
}
}
+98
View File
@@ -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);
}
}
+56
View File
@@ -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}";
}
+505
View File
@@ -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;
}
}
+127
View File
@@ -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();
}
}
}
}
+150
View File
@@ -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;
}
}
+37
View File
@@ -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"
+59
View File
@@ -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, )"
}
}
}
}
}
+1
View File
@@ -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"}]
+79
View File
@@ -0,0 +1,79 @@
# Mappy
[![Download count](https://img.shields.io/endpoint?url=https://qzysathwfhebdai6xgauhz4q7m0mzmrf.lambda-url.us-east-1.on.aws/Mappy)](https://github.com/MidoriKami/Mappy)
Mappy is a XivLauncher/Dalamud plugin.
Mappy is a total replacement plugin for the ingame main map.
Mappy offers a lot of customization options such as enabling and disabling specific icons, configuring colors of tooltips, or displayed areas.
Integrates seamlessly into the games built in functions, for example pressing `m` will open Mappy instead of the built in game map.
> [!IMPORTANT]
> Mappy is not intended to replace the Minimap, there are many functions of the Minimap that Mappy does not replicate.
## Map Window
The main feature of this plugin is the map window itself. It is rendered via ImGui as an overlay ontop of the game UI.
One major benefit of this is when you have Dalamud's Multi-Monitor Mode enabled, you can move Mappy onto another display.
Among many customization options you can configure Mappy to suit your needs and preferences.
![image](https://github.com/user-attachments/assets/23cc5cd9-f20b-4298-8aea-1c1942b4f29b)
### Simple Context Menu
Upon right clicking anywhere on the map, you will see a context menu that shows some additional actions you can perform.
No more will you have to remember "wait was it alt + click to place a flag???"
![image](https://github.com/user-attachments/assets/fba25c3c-0858-4f77-8ff1-0c169d401558)
## Search Window
Pressing the search button in the main ui will open a map search window that allows you to search for any
Map, Aetheryte, Point of Interest, or Aethernet to easily navigate to any map you wish.
Points of Interest will focus the map on the exact location of the point of interest.
Unfortunately the other search types don't include sufficient information to be able to center the map on them.
![image](https://github.com/user-attachments/assets/c34824be-0e11-4bff-a423-f18dd247433f)
## Configuration Window
Mappys configuration window allows you to customize the displayed map features however you like.
> [!TIP]
> I have done my best to set reasonable defaults for settings, but it is up to you to tailor your experience to be how you would like.
### Icon Settings
The main way that you can tailor your map to suit your needs is via the Icon Settings, here you can configure what icons show, how big they are, what color they are, and more.
You can configure any icon that the map has ever shown you, as you use Mappy more icons will become available as Mappy discovers them.
![image](https://github.com/user-attachments/assets/3f9bff5f-5ca3-4728-a1d7-6c19dcea262e)
## Quest List Window
Accessible from the context menu, the Quest List Window will show you all of the quests that you have accepted, and all the quests that are available to be claimed in the current area.
Clicking on any of these entries will focus the map on that specific quest.
| Accepted | Unaccepted |
|---|---|
| ![image](https://github.com/user-attachments/assets/4f40aa42-f7ab-4cb6-b1b2-7b838227142a) | ![image](https://github.com/user-attachments/assets/127d6052-533a-47b2-8c3d-ba32033c4317) |
## Fate List Window
Accessible from the context menu, the Fate List Window will show you all of the Fates that are currently active in the current area.
Fates that are expiring soon will have their named colored, and slowly fade from green to red the closer they get to expiring.
Fates with exp bonuses will have "Exp Bonus!" displayed.
Clicking on any entry in the Fate List Window will center the map on that fate.
![image](https://github.com/user-attachments/assets/eb41dc11-87e5-4198-afb6-f394c18b75ab)