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
+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"}]