diff --git a/FModel/MainWindow.xaml b/FModel/MainWindow.xaml
index 0369c96e..3f76aa26 100644
--- a/FModel/MainWindow.xaml
+++ b/FModel/MainWindow.xaml
@@ -14,11 +14,8 @@
diff --git a/FModel/ViewModels/ApplicationViewModel.cs b/FModel/ViewModels/ApplicationViewModel.cs
index f2f3c699..bfc7aca2 100644
--- a/FModel/ViewModels/ApplicationViewModel.cs
+++ b/FModel/ViewModels/ApplicationViewModel.cs
@@ -44,7 +44,6 @@ namespace FModel.ViewModels
set
{
SetProperty(ref _status, value);
- RaisePropertyChanged(nameof(TitleExtra));
IsReady = Status != EStatusKind.Loading && Status != EStatusKind.Stopping;
}
}
diff --git a/FModel/ViewModels/MapViewerViewModel.cs b/FModel/ViewModels/MapViewerViewModel.cs
index adee6a51..3dfaa8e7 100644
--- a/FModel/ViewModels/MapViewerViewModel.cs
+++ b/FModel/ViewModels/MapViewerViewModel.cs
@@ -1,16 +1,21 @@
-using System.Collections.Generic;
-using System.Linq;
+using System;
+using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows.Media.Imaging;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Assets.Objects;
+using CUE4Parse.UE4.Objects.Core.i18N;
using CUE4Parse.UE4.Objects.Core.Math;
+using CUE4Parse.UE4.Objects.GameplayTags;
+using CUE4Parse.UE4.Objects.UObject;
using FModel.Creator;
+using FModel.Extensions;
using FModel.Framework;
using FModel.Services;
using SkiaSharp;
+using SkiaSharp.HarfBuzz;
namespace FModel.ViewModels
{
@@ -22,32 +27,42 @@ namespace FModel.ViewModels
public class MapViewerViewModel : ViewModel
{
- private const string _FIRST_BITMAP = "mapBase";
private ThreadWorkerViewModel _threadWorkerView => ApplicationService.ThreadWorkerView;
- private int _mapIndex = -1;
- public int MapIndex // 0 is BR, 1 is PR
+ #region BINDINGS
+
+ private bool _poisCheck;
+ public bool PoisCheck
{
- get => _mapIndex;
- set
- {
- switch (value)
- {
- case 0:
- _mapRadius = 135000;
- _mapImage = _brMiniMapImage;
- break;
- case 1:
- _mapRadius = 51000;
- _mapImage = _prMiniMapImage;
- break;
- }
-
- SetProperty(ref _mapIndex, value);
- }
+ get => _poisCheck;
+ set => SetProperty(ref _poisCheck, value, "ApolloGameplay_MapPois");
+ }
+
+ private bool _brLandmarksCheck;
+ public bool BrLandmarksCheck
+ {
+ get => _brLandmarksCheck;
+ set => SetProperty(ref _brLandmarksCheck, value, "ApolloGameplay_MapLandmarks");
+ }
+
+ private bool _patrolsPathCheck;
+ public bool PatrolsPathCheck
+ {
+ get => _patrolsPathCheck;
+ set => SetProperty(ref _patrolsPathCheck, value, "ApolloGameplay_PatrolsPath");
+ }
+
+ private bool _prLandmarksCheck;
+ public bool PrLandmarksCheck
+ {
+ get => _prLandmarksCheck;
+ set => SetProperty(ref _prLandmarksCheck, value, "PapayaGameplay_MapLandmarks");
}
- private int _mapRadius;
+ #endregion
+
+ #region BITMAP IMAGES
+
private BitmapImage _brMiniMapImage;
private BitmapImage _prMiniMapImage;
private BitmapImage _mapImage;
@@ -56,6 +71,9 @@ namespace FModel.ViewModels
get => _mapImage;
set => SetProperty(ref _mapImage, value);
}
+
+ private BitmapImage _brLayerImage;
+ private BitmapImage _prLayerImage;
private BitmapImage _layerImage;
public BitmapImage LayerImage
{
@@ -63,6 +81,23 @@ namespace FModel.ViewModels
set => SetProperty(ref _layerImage, value);
}
+ private const int _widthHeight = 2048;
+ private const int _brRadius = 135000;
+ private const int _prRadius = 51000;
+ private int _mapIndex;
+ public int MapIndex // 0 is BR, 1 is PR
+ {
+ get => _mapIndex;
+ set
+ {
+ SetProperty(ref _mapIndex, value);
+ TriggerChange();
+ }
+ }
+
+ #endregion
+
+ private const string _FIRST_BITMAP = "MapCheck";
private readonly Dictionary[] _bitmaps; // first bitmap is the displayed map, others are overlays of the map
private readonly CUE4ParseViewModel _cue4Parse;
@@ -79,43 +114,66 @@ namespace FModel.ViewModels
public async void Initialize()
{
Utils.Typefaces ??= new Typefaces(_cue4Parse);
+ _imagePaint.Typeface = Utils.Typefaces.Bottom ?? Utils.Typefaces.DisplayName;
await LoadBrMiniMap();
await LoadPrMiniMap();
- MapIndex = 1; // don't forget br is selected by default
- MapIndex = 0; // this will trigger the br map to be shown
+ TriggerChange();
}
- public BitmapImage GetImageToSave()
+ public BitmapImage GetImageToSave() => GetImageSource(GetLayerBitmap(true));
+ private SKBitmap GetLayerBitmap(bool withMap)
{
- var ret = new SKBitmap(_bitmaps[MapIndex][_FIRST_BITMAP].Layer.Width, _bitmaps[MapIndex][_FIRST_BITMAP].Layer.Height, SKColorType.Rgba8888, SKAlphaType.Premul);
+ var ret = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(ret);
- foreach (var layer in _bitmaps[MapIndex].Values.Where(layer => layer.IsEnabled))
+ foreach (var (key, value) in _bitmaps[MapIndex])
{
- c.DrawBitmap(layer.Layer, 0, 0);
+ if (!value.IsEnabled || !withMap && key == _FIRST_BITMAP) continue;
+ c.DrawBitmap(value.Layer, new SKRect(0, 0, _widthHeight, _widthHeight));
}
- return GetImageSource(ret);
+ return ret;
}
- public async Task GenericToggle(string key, bool enabled)
+ protected override bool SetProperty(ref T storage, T value, string propertyName = null) // don't delete, else nothing will update for some reason
+ {
+ var ret = base.SetProperty(ref storage, value, propertyName);
+ if (bool.TryParse(value.ToString(), out var b)) GenericToggle(propertyName, b);
+ return ret;
+ }
+
+ private async void GenericToggle(string key, bool enabled)
{
if (_bitmaps[MapIndex].TryGetValue(key, out var layer) && layer.Layer != null)
{
layer.IsEnabled = enabled;
}
- else // load layer
+ else if (enabled) // load layer
{
switch (key)
{
-
+ case "ApolloGameplay_MapPois":
+ case "ApolloGameplay_MapLandmarks":
+ case "PapayaGameplay_MapLandmarks":
+ await LoadQuestIndicatorData();
+ break;
+ case "ApolloGameplay_PatrolsPath":
+ await LoadPatrolsPath();
+ break;
}
+ _bitmaps[MapIndex][key].IsEnabled = true;
}
- }
- protected override bool SetProperty(ref T storage, T value, string propertyName = null) // don't delete, else nothing will update for some reason
- {
- return base.SetProperty(ref storage, value, propertyName);
+ switch (MapIndex)
+ {
+ case 0:
+ _brLayerImage = GetImageSource(GetLayerBitmap(false));
+ break;
+ case 1:
+ _prLayerImage = GetImageSource(GetLayerBitmap(false));
+ break;
+ }
+ TriggerChange();
}
private BitmapImage GetImageSource(SKBitmap bitmap)
@@ -130,6 +188,24 @@ namespace FModel.ViewModels
image.Freeze();
return image;
}
+
+ private void TriggerChange()
+ {
+ switch (_mapIndex)
+ {
+ case 0:
+ _mapImage = _brMiniMapImage;
+ _layerImage = _brLayerImage;
+ break;
+ case 1:
+ _mapImage = _prMiniMapImage;
+ _layerImage = _prLayerImage;
+ break;
+ }
+
+ RaisePropertyChanged(nameof(MapImage));
+ RaisePropertyChanged(nameof(LayerImage));
+ }
private readonly SKPaint _imagePaint = new()
{
@@ -140,20 +216,22 @@ namespace FModel.ViewModels
private readonly SKPaint _pathPaint = new()
{
IsAntialias = true, FilterQuality = SKFilterQuality.High, IsStroke = true,
- Style = SKPaintStyle.Stroke, StrokeWidth = 10, Color = SKColors.Red,
+ Style = SKPaintStyle.Stroke, StrokeWidth = 5, Color = SKColors.Red,
ImageFilter = SKImageFilter.CreateDropShadow(4, 4, 8, 8, SKColors.Black)
};
- private FVector2D GetMapPosition(FVector vector)
+ private FVector2D GetMapPosition(FVector vector, int mapRadius)
{
- var nx = (vector.Y + _mapRadius) / (_mapRadius * 2) * _bitmaps[MapIndex][_FIRST_BITMAP].Layer.Width;
- var ny = (1 - (vector.X + _mapRadius) / (_mapRadius * 2)) * _bitmaps[MapIndex][_FIRST_BITMAP].Layer.Height;
+ var nx = (vector.Y + mapRadius) / (mapRadius * 2) * _widthHeight;
+ var ny = (1 - (vector.X + mapRadius) / (mapRadius * 2)) * _widthHeight;
return new FVector2D(nx, ny);
}
private async Task LoadBrMiniMap()
{
- if (_bitmaps[0].TryGetValue(_FIRST_BITMAP, out var layer) && layer.Layer != null) return;
+ if (_bitmaps[0].TryGetValue(_FIRST_BITMAP, out var brMap) && brMap.Layer != null)
+ return; // if map already loaded
+
await _threadWorkerView.Begin(_ =>
{
if (!Utils.TryLoadObject("FortniteGame/Content/UI/IngameMap/UIMapManagerBR.Default__UIMapManagerBR_C", out UObject mapManager) ||
@@ -169,7 +247,9 @@ namespace FModel.ViewModels
private async Task LoadPrMiniMap()
{
- if (_bitmaps[1].TryGetValue(_FIRST_BITMAP, out var layer) && layer.Layer != null) return;
+ if (_bitmaps[1].TryGetValue(_FIRST_BITMAP, out var prMap) && prMap.Layer != null)
+ return; // if map already loaded
+
await _threadWorkerView.Begin(_ =>
{
if (!Utils.TryLoadObject("FortniteGame/Content/UI/IngameMap/UIMapManagerPapaya.Default__UIMapManagerPapaya_C", out UObject mapManager) ||
@@ -180,5 +260,112 @@ namespace FModel.ViewModels
_prMiniMapImage = GetImageSource(_bitmaps[1][_FIRST_BITMAP].Layer);
});
}
+
+ private async Task LoadQuestIndicatorData()
+ {
+ await _threadWorkerView.Begin(_ =>
+ {
+ var poisBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
+ var brLandmarksBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
+ var prLandmarksBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
+ using var pois = new SKCanvas(poisBitmap);
+ using var brLandmarks = new SKCanvas(brLandmarksBitmap);
+ using var prLandmarks = new SKCanvas(prLandmarksBitmap);
+
+ if (Utils.TryLoadObject("FortniteGame/Content/Quests/QuestIndicatorData", out UObject indicatorData) &&
+ indicatorData.TryGetValue(out FStructFallback[] challengeMapPoiData, "ChallengeMapPoiData"))
+ {
+ foreach (var poiData in challengeMapPoiData)
+ {
+ if (!poiData.TryGetValue(out FSoftObjectPath discoveryQuest, "DiscoveryQuest") ||
+ !poiData.TryGetValue(out FText text, "Text") || string.IsNullOrEmpty(text.Text) ||
+ !poiData.TryGetValue(out FVector worldLocation, "WorldLocation") ||
+ !poiData.TryGetValue(out FName discoverBackend, "DiscoverObjectiveBackendName")) continue;
+
+ var shaper = new CustomSKShaper(_imagePaint.Typeface);
+ var shapedText = shaper.Shape(text.Text, _imagePaint);
+
+ if (discoverBackend.Text.Contains("papaya", StringComparison.OrdinalIgnoreCase))
+ {
+ var vector = GetMapPosition(worldLocation, _prRadius);
+ prLandmarks.DrawPoint(vector.X, vector.Y, _pathPaint);
+ prLandmarks.DrawShapedText(shaper, text.Text, vector.X - shapedText.Points[^1].X / 2, vector.Y - 12.5F, _imagePaint);
+ }
+ else if (discoveryQuest.AssetPathName.Text.Contains("landmarks", StringComparison.OrdinalIgnoreCase))
+ {
+ var vector = GetMapPosition(worldLocation, _brRadius);
+ brLandmarks.DrawPoint(vector.X, vector.Y, _pathPaint);
+ brLandmarks.DrawShapedText(shaper, text.Text, vector.X - shapedText.Points[^1].X / 2, vector.Y - 12.5F, _imagePaint);
+ }
+ else
+ {
+ var vector = GetMapPosition(worldLocation, _brRadius);
+ pois.DrawPoint(vector.X, vector.Y, _pathPaint);
+ pois.DrawShapedText(shaper, text.Text, vector.X - shapedText.Points[^1].X / 2, vector.Y - 12.5F, _imagePaint);
+ }
+ }
+ }
+
+ _bitmaps[0]["ApolloGameplay_MapPois"] = new MapLayer {Layer = poisBitmap, IsEnabled = false};
+ _bitmaps[0]["ApolloGameplay_MapLandmarks"] = new MapLayer {Layer = brLandmarksBitmap, IsEnabled = false};
+ _bitmaps[1]["PapayaGameplay_MapLandmarks"] = new MapLayer {Layer = prLandmarksBitmap, IsEnabled = false};
+ });
+ }
+
+ private async Task LoadPatrolsPath()
+ {
+ await _threadWorkerView.Begin(_ =>
+ {
+ var patrolsPathBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
+ using var c = new SKCanvas(patrolsPathBitmap);
+
+ if (!Utils.TryLoadObject("FortniteGame/Plugins/GameFeatures/NPCLibrary/Content/GameFeatureData.GameFeatureData", out UObject gameFeatureData) ||
+ !gameFeatureData.TryGetValue(out FPackageIndex levelOverlayConfig, "LevelOverlayConfig") ||
+ !Utils.TryGetPackageIndexExport(levelOverlayConfig, out UObject npcLibrary) ||
+ !npcLibrary.TryGetValue(out FStructFallback[] overlayList, "OverlayList"))
+ return;
+
+ foreach (var overlay in overlayList)
+ {
+ if (!overlay.TryGetValue(out FSoftObjectPath overlayWorld, "OverlayWorld"))
+ continue;
+
+ var exports = Utils.LoadExports(overlayWorld.AssetPathName.Text.SubstringBeforeLast("."));
+ foreach (var export in exports)
+ {
+ if (!(export is UObject uObject)) continue;
+ if (!uObject.ExportType.Equals("FortAthenaPatrolPath", StringComparison.OrdinalIgnoreCase) ||
+ !uObject.TryGetValue(out FGameplayTagContainer gameplayTags, "GameplayTags") ||
+ !uObject.TryGetValue(out FPackageIndex[] patrolPoints, "PatrolPoints")) continue;
+
+ if (!Utils.TryGetPackageIndexExport(patrolPoints[0], out uObject) ||
+ !uObject.TryGetValue(out FPackageIndex rootComponent, "RootComponent") ||
+ !Utils.TryGetPackageIndexExport(rootComponent, out uObject) ||
+ !uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue;
+
+ var path = new SKPath();
+ var vector = GetMapPosition(relativeLocation, _brRadius);
+ path.MoveTo(vector.X, vector.Y);
+
+ for (var i = 1; i < patrolPoints.Length; i++)
+ {
+ if (!Utils.TryGetPackageIndexExport(patrolPoints[i], out uObject) ||
+ !uObject.TryGetValue(out rootComponent, "RootComponent") ||
+ !Utils.TryGetPackageIndexExport(rootComponent, out uObject) ||
+ !uObject.TryGetValue(out relativeLocation, "RelativeLocation")) continue;
+
+ vector = GetMapPosition(relativeLocation, _brRadius);
+ path.LineTo(vector.X, vector.Y);
+ }
+
+ path.Close();
+ c.DrawPath(path, _pathPaint);
+ c.DrawText(gameplayTags.GameplayTags[0].Text.SubstringAfterLast("."), vector.X, vector.Y - 12.5F, _imagePaint);
+ }
+ }
+
+ _bitmaps[0]["ApolloGameplay_PatrolsPath"] = new MapLayer {Layer = patrolsPathBitmap, IsEnabled = false};
+ });
+ }
}
}
\ No newline at end of file
diff --git a/FModel/Views/MapViewer.xaml b/FModel/Views/MapViewer.xaml
index b3097de6..2f467041 100644
--- a/FModel/Views/MapViewer.xaml
+++ b/FModel/Views/MapViewer.xaml
@@ -1,6 +1,7 @@
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FModel/Views/MapViewer.xaml.cs b/FModel/Views/MapViewer.xaml.cs
index 74507a26..16da3b4b 100644
--- a/FModel/Views/MapViewer.xaml.cs
+++ b/FModel/Views/MapViewer.xaml.cs
@@ -74,11 +74,5 @@ namespace FModel.Views
break;
}
}
-
- private async void ToggleOnChange(object sender, RoutedEventArgs e)
- {
- if (sender is not CheckBox checkBox) return;
- await _applicationView.MapViewer.GenericToggle(checkBox.Content.ToString(), checkBox.IsChecked ?? true);
- }
}
}
\ No newline at end of file