Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/Aetherphone.Tests/PhotoZoomViewTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System.Numerics;
using Aetherphone.Core;
using Aetherphone.Windows.Components;
using Xunit;

namespace Aetherphone.Tests;

public sealed class PhotoZoomViewTests
{
private static readonly Rect Stage = new(Vector2.Zero, new Vector2(260f, 260f));
private static readonly Vector2 TextureSize = new(2048f, 2048f);

[Fact]
public void FocusOnATightClusterClampsToMaxZoom()
{
var view = new PhotoZoomView();
var bounds = new Rect(new Vector2(0.45f, 0.45f), new Vector2(0.55f, 0.55f));

view.FocusOn(Stage, TextureSize, bounds);

Assert.Equal(4f, view.Zoom, 3);
}

[Fact]
public void FocusOnTheFullCanvasStaysAtMinZoomWithNoPan()
{
var view = new PhotoZoomView();
var bounds = new Rect(Vector2.Zero, Vector2.One);

view.FocusOn(Stage, TextureSize, bounds, paddingFraction: 0f);

Assert.Equal(1f, view.Zoom, 3);
Assert.Equal(0f, view.Pan.X, 3);
Assert.Equal(0f, view.Pan.Y, 3);
}

[Fact]
public void FocusOnAnOffCenterClusterPansTowardItWithoutExceedingTheClamp()
{
var view = new PhotoZoomView();
var bounds = new Rect(new Vector2(0.05f, 0.4f), new Vector2(0.25f, 0.6f));

view.FocusOn(Stage, TextureSize, bounds);

Assert.True(view.Zoom > 1f);
var fit = PhotoZoomView.FitScale(Stage, TextureSize);
var drawn = TextureSize * fit * view.Zoom;
var maxPanX = MathF.Max(0f, (drawn.X - Stage.Width) * 0.5f);
Assert.InRange(view.Pan.X, -maxPanX, maxPanX);
}

[Fact]
public void SnapToClampsZoomToTheConfiguredRange()
{
var view = new PhotoZoomView();

view.SnapTo(Stage, TextureSize, 50f, Vector2.Zero);

Assert.Equal(4f, view.Zoom, 3);
}
}
3 changes: 0 additions & 3 deletions src/Aetherphone/Aetherphone.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@
<Content Include="Hunts\*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Hunts\Maps\*.jpg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Images\Icon.png" Condition="Exists('Images\Icon.png')">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
Expand Down
75 changes: 57 additions & 18 deletions src/Aetherphone/Apps/Hunts/HuntsApp.Detail.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ internal sealed partial class HuntsApp
private string detailMapZoneId = string.Empty;
private readonly List<HuntPoiEntry> detailMapPoints = new();
private readonly List<HuntPoiEntry> detailMapAetherytePoints = new();
private readonly Dictionary<(uint TerritoryId, string ZoneId, string Language), string> zoneLabelCache = new();
private readonly PhotoZoomView detailMapZoom = new();
private bool detailMapHovered;
private bool detailMapPendingFocus;

private (int WindowNum, int PhaseNum)? detailMapActivePhase;
private string? detailMapConfirmedZoneId;
Expand Down Expand Up @@ -84,7 +86,7 @@ private void OpenDetailFor(string mobId, string worldId, int zoneInstance)
detailMapActivePhase = hunts.PhaseFor(mobId, worldId, zoneInstance);
detailMapConfirmedZoneId = hunts.ZoneIdFor(mobId, worldId, zoneInstance);
ResolveDetailMap(mobCatalog.Find(mobId), detailMapActivePhase, detailMapConfirmedZoneId);
detailMapZoom.Reset();
detailMapPendingFocus = true;
detailMapHovered = false;
}

Expand Down Expand Up @@ -459,8 +461,9 @@ private bool DrawDetailZoneMap(float scale, int? confirmedPoiId, HuntsView view)
}

var zone = zoneCatalog.FindZone(detailMapZoneId);
var texture = HuntZoneMapTextures.Resolve(detailMapZoneId);
if (zone is null || texture is null || zone.Map.PixelSize <= 0)
var territoryId = zoneCatalog.ResolveTerritoryId(detailMapZoneId);
var texture = zoneMapTextures.Resolve(territoryId);
if (zone is null || texture is null)
{
detailMapHovered = false;
return false;
Expand All @@ -472,7 +475,7 @@ private bool DrawDetailZoneMap(float scale, int? confirmedPoiId, HuntsView view)
var mapLeft = origin.X + (width - size) * 0.5f;

var drawList = ImGui.GetWindowDrawList();
var zoneLabel = ResolveZoneLabel(zone);
var zoneLabel = ResolveZoneLabel(zone.Id, territoryId);
var labelSize = Typography.Measure(zoneLabel, TextStyles.Footnote);
var labelGap = 6f * scale;
Typography.Draw(drawList, new Vector2(mapLeft + (size - labelSize.X) * 0.5f, origin.Y), zoneLabel,
Expand All @@ -489,7 +492,7 @@ private bool DrawDetailZoneMap(float scale, int? confirmedPoiId, HuntsView view)
{
if (mapChild)
{
DrawDetailZoneMapContent(stage, zone, texture, scale, confirmedPoiId, view);
DrawDetailZoneMapContent(stage, texture, scale, confirmedPoiId, view, territoryId);
}
}

Expand All @@ -498,13 +501,34 @@ private bool DrawDetailZoneMap(float scale, int? confirmedPoiId, HuntsView view)
return true;
}

private string ResolveZoneLabel(HuntZoneDefinition zone) =>
zone.Name.GetValueOrDefault(configuration.Language) ?? zone.Name.GetValueOrDefault("en") ??
Prettify(zone.Id);
private string ResolveZoneLabel(string zoneId, uint territoryId)
{
var key = (territoryId, zoneId, HuntUiLanguage.Key());
if (zoneLabelCache.TryGetValue(key, out var cached))
{
return cached;
}

var label = ResolveLiveZoneName(territoryId) is { Length: > 0 } name ? name : Prettify(zoneId);
zoneLabelCache[key] = label;
return label;
}

private void DrawDetailZoneMapContent(Rect stage, HuntZoneDefinition zone, IDalamudTextureWrap texture,
float scale, int? confirmedPoiId, HuntsView view)
private static string? ResolveLiveZoneName(uint territoryId) =>
territoryId != 0 && Plugin.DataManager.GetExcelSheet<TerritoryType>(HuntUiLanguage.SheetLanguage())
.TryGetRow(territoryId, out var territory) && territory.PlaceName.RowId != 0
? territory.PlaceName.Value.Name.ExtractText()
: null;

private void DrawDetailZoneMapContent(Rect stage, IDalamudTextureWrap texture, float scale, int? confirmedPoiId,
HuntsView view, uint territoryId)
{
if (detailMapPendingFocus)
{
detailMapPendingFocus = false;
FocusDetailMap(stage, texture.Size);
}

var drawList = ImGui.GetWindowDrawList();
detailMapZoom.Draw(stage, texture, frameTheme, Metrics.Radius.Card * scale, showButtons: false);

Expand All @@ -529,9 +553,6 @@ private void DrawDetailZoneMapContent(Rect stage, HuntZoneDefinition zone, IDala

var finalLocationResolved = detailMapFinalPhase && detailMapZoneConfirmed && detailMapPoints.Count == 1;

var pixelSize = (float)zone.Map.PixelSize;
var offsetX = (float)zone.Map.Offset.X;
var offsetY = (float)zone.Map.Offset.Y;
drawList.PushClipRect(stage.Min, stage.Max, true);
for (var index = 0; index < detailMapPoints.Count; index++)
{
Expand All @@ -542,22 +563,19 @@ private void DrawDetailZoneMapContent(Rect stage, HuntZoneDefinition zone, IDala
}

var (rawX, rawY) = poi.ParsedLocation();
var normalizedX = (rawX - offsetX) / pixelSize;
var normalizedY = (rawY - offsetY) / pixelSize;
var (normalizedX, normalizedY) = MapPixelMath.NormalizeToFullCanvas(rawX, rawY);
var dotPosition = new Vector2(min.X + normalizedX * (max.X - min.X),
min.Y + normalizedY * (max.Y - min.Y));
DrawSpawnDot(drawList, dotPosition, scale, poi.Id, confirmedKnown, finalLocationResolved);
}

var territoryId = zoneCatalog.ResolveTerritoryId(detailMapZoneId);
var worldId = HuntDataCenterWorlds.WorldRowId(view.WorldId);
var mapId = ResolveMapId(territoryId);
for (var index = 0; index < detailMapAetherytePoints.Count; index++)
{
var poi = detailMapAetherytePoints[index];
var (rawX, rawY) = poi.ParsedLocation();
var normalizedX = (rawX - offsetX) / pixelSize;
var normalizedY = (rawY - offsetY) / pixelSize;
var (normalizedX, normalizedY) = MapPixelMath.NormalizeToFullCanvas(rawX, rawY);
var dotPosition = new Vector2(min.X + normalizedX * (max.X - min.X),
min.Y + normalizedY * (max.Y - min.Y));
DrawAetheryteDot(drawList, dotPosition, scale, poi, territoryId, worldId, mapId, view.ZoneInstance);
Expand All @@ -566,6 +584,27 @@ private void DrawDetailZoneMapContent(Rect stage, HuntZoneDefinition zone, IDala
drawList.PopClipRect();
}

private void FocusDetailMap(Rect stage, Vector2 textureSize)
{
if (detailMapPoints.Count == 0)
{
detailMapZoom.Reset();
return;
}

var min = new Vector2(float.MaxValue, float.MaxValue);
var max = new Vector2(float.MinValue, float.MinValue);
for (var index = 0; index < detailMapPoints.Count; index++)
{
var (rawX, rawY) = detailMapPoints[index].ParsedLocation();
var (normalizedX, normalizedY) = MapPixelMath.NormalizeToFullCanvas(rawX, rawY);
min = new Vector2(MathF.Min(min.X, normalizedX), MathF.Min(min.Y, normalizedY));
max = new Vector2(MathF.Max(max.X, normalizedX), MathF.Max(max.Y, normalizedY));
}

detailMapZoom.FocusOn(stage, textureSize, new Rect(min, max));
}

private void DrawSpawnDot(ImDrawListPtr drawList, Vector2 center, float scale, int poiId, bool confirmed,
bool finalLocation)
{
Expand Down
6 changes: 4 additions & 2 deletions src/Aetherphone/Apps/Hunts/HuntsApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ private readonly record struct HuntsView(HuntsRoute Route, string MobId = "", st
private readonly HuntsService hunts;
private readonly HuntMobCatalog mobCatalog;
private readonly HuntZoneCatalog zoneCatalog;
private readonly HuntZoneMapTextures zoneMapTextures;
private readonly HuntMobRewardCatalog rewardCatalog;
private readonly Configuration configuration;
private readonly ConfirmService confirm;
Expand All @@ -64,12 +65,13 @@ private readonly record struct HuntsView(HuntsRoute Route, string MobId = "", st
private readonly Comparison<HuntWindowDto> compareByPercentageDescending;

public HuntsApp(HuntsService hunts, HuntMobCatalog mobCatalog, HuntZoneCatalog zoneCatalog,
HuntMobRewardCatalog rewardCatalog, Configuration configuration, ConfirmService confirm,
HuntsLauncher launcher)
HuntZoneMapTextures zoneMapTextures, HuntMobRewardCatalog rewardCatalog, Configuration configuration,
ConfirmService confirm, HuntsLauncher launcher)
{
this.hunts = hunts;
this.mobCatalog = mobCatalog;
this.zoneCatalog = zoneCatalog;
this.zoneMapTextures = zoneMapTextures;
this.rewardCatalog = rewardCatalog;
this.configuration = configuration;
this.confirm = confirm;
Expand Down
3 changes: 2 additions & 1 deletion src/Aetherphone/Core/Apps/AppRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ public static AppBundle BuildDefault(PhoneServices services, VideoPlayer video,
apps.Add(new AppStoreApp(services.Installer, apps));
apps.Add(new HousingApp(services.Housing, services.Configuration, services.Confirm));
apps.Add(new HuntsApp(services.Hunts, services.HuntMobCatalog, services.HuntZoneCatalog,
services.HuntMobRewardCatalog, services.Configuration, services.Confirm, services.HuntsLauncher));
services.HuntZoneMapTextures, services.HuntMobRewardCatalog, services.Configuration, services.Confirm,
services.HuntsLauncher));

return new AppBundle
{
Expand Down
53 changes: 6 additions & 47 deletions src/Aetherphone/Core/Housing/HousingGameMaps.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text;
using Aetherphone.Core.Maps;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Plugin.Services;
using Lumina.Excel.Sheets;
Expand Down Expand Up @@ -306,51 +307,8 @@ private Dictionary<uint, List<Vector3>> CollectMarkerGroups(uint districtId)
private static float Normalize(float world, short offset, float scaleFactor) =>
((world + offset) * scaleFactor + MapPageSize * 0.5f) / MapPageSize;

private string? ResolveTexturePath(string mapId)
{
if (string.IsNullOrEmpty(mapId))
{
return null;
}

var candidates = TextureCandidates(mapId);
for (var index = 0; index < candidates.Length; index++)
{
if (FileExists(candidates[index]))
{
return candidates[index];
}
}

return null;
}

private static string[] TextureCandidates(string mapId)
{
var flat = mapId.Replace("/", string.Empty);
var underscored = mapId.Replace('/', '_');
return
[
$"ui/map/{mapId}/{flat}_m.tex",
$"ui/map/{mapId}/{flat}m_m.tex",
$"ui/map/{mapId}/{flat}_s.tex",
$"ui/map/{mapId}/{underscored}_m.tex",
$"ui/map/{mapId}/{underscored}m_m.tex",
];
}

private bool FileExists(string path)
{
try
{
return data.FileExists(path);
}
catch (Exception exception)
{
AepLog.Debug(exception, $"Housing could not test '{path}'");
return false;
}
}
private string? ResolveTexturePath(string mapId) =>
string.IsNullOrEmpty(mapId) ? null : MapTextures.ResolveTexturePath(data, mapId, "Housing");

public string Describe(uint districtId)
{
Expand Down Expand Up @@ -406,10 +364,11 @@ private void DescribeGroup(StringBuilder report, uint mapRowId, List<Vector3> ma
}

report.Append($" sizeFactor {map.SizeFactor} offset {map.OffsetX},{map.OffsetY}\n");
var candidates = TextureCandidates(mapId);
var candidates = MapTextures.Candidates(mapId);
for (var index = 0; index < candidates.Length; index++)
{
report.Append($" texture: {candidates[index]} -> {(FileExists(candidates[index]) ? "OK" : "missing")}\n");
report.Append(
$" texture: {candidates[index]} -> {(MapTextures.FileExists(data, candidates[index], "Housing") ? "OK" : "missing")}\n");
}

var sampleCount = Math.Min(3, markers.Count);
Expand Down
2 changes: 1 addition & 1 deletion src/Aetherphone/Core/Hunts/HuntMobLore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public static void Initialize(HuntMobTextCatalog descriptionCatalog, HuntMobText
public static bool DescriptionIsFallback(string mobId) =>
descriptions is not null && !descriptions.HasNativeText(mobId, Loc.Current.Code);

public static string? TipFor(string mobId) => tips?.TextFor(mobId, HuntUiLanguage.Key());
public static string? TipFor(string mobId) => tips?.TextFor(mobId, Loc.Current.Code);

public static bool TipIsFallback(string mobId) =>
tips is not null && !tips.HasNativeText(mobId, Loc.Current.Code);
Expand Down
9 changes: 9 additions & 0 deletions src/Aetherphone/Core/Hunts/HuntUiLanguage.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Aetherphone.Core.Localization;
using Dalamud.Game;

namespace Aetherphone.Core.Hunts;

Expand All @@ -11,4 +12,12 @@ internal static class HuntUiLanguage
"ja" => "ja",
_ => "en",
};

public static ClientLanguage SheetLanguage() => Loc.Current.Code switch
{
"de" => ClientLanguage.German,
"fr" => ClientLanguage.French,
"ja" => ClientLanguage.Japanese,
_ => ClientLanguage.English,
};
}
17 changes: 15 additions & 2 deletions src/Aetherphone/Core/Hunts/HuntZoneCatalogue.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Aetherphone.Core.Maps;
using Dalamud.Game;
using Lumina.Excel.Exceptions;
using Lumina.Excel.Sheets;

namespace Aetherphone.Core.Hunts;
Expand Down Expand Up @@ -48,7 +50,7 @@ public HuntZoneCatalog(FileInfo source)
public uint ResolveTerritoryId(string zoneId)
{
var zone = FindZone(zoneId);
var name = zone?.Name.GetValueOrDefault(HuntClientLanguage.Key()) ?? zone?.Name.GetValueOrDefault("en");
var name = zone?.Name;
if (string.IsNullOrEmpty(name))
{
return 0u;
Expand Down Expand Up @@ -76,7 +78,18 @@ private void OnLoaded(Dictionary<string, HuntZoneDefinition> parsed)
private static Dictionary<string, uint> BuildTerritoryIdLookup()
{
var lookup = new Dictionary<string, uint>(StringComparer.OrdinalIgnoreCase);
foreach (var territory in Plugin.DataManager.GetExcelSheet<TerritoryType>())
Lumina.Excel.ExcelSheet<TerritoryType> sheet;
try
{
sheet = Plugin.DataManager.GetExcelSheet<TerritoryType>(ClientLanguage.English);
}
catch (UnsupportedLanguageException exception)
{
AepLog.Warning(exception, "Hunts zone-to-territory lookup unavailable: client has no English TerritoryType sheet");
return lookup;
}

foreach (var territory in sheet)
{
if (territory.PlaceName.RowId == 0)
{
Expand Down
Loading