Initial HSMappy release (fork of Mappy)
Made-with: Cursor
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user