Merge pull request #187 from iAmAsval/dev

new map viewer, mesh support, other stuff
This commit is contained in:
Valentin
2021-07-11 13:54:05 +02:00
committed by GitHub
55 changed files with 1706 additions and 525 deletions

View File

@@ -1,7 +1,12 @@
name: Artifact Generator
name: FModel Builder
on:
workflow_dispatch:
inputs:
appVersion:
description: 'FModel Version And Release Tag'
required: true
default: '4.0.X.X'
jobs:
build:
@@ -12,7 +17,6 @@ jobs:
uses: actions/checkout@v2
with:
submodules: 'true'
token: ${{ secrets.PAT_TOKEN }}
- name: .NET 5 Setup
uses: actions/setup-dotnet@v1
@@ -23,10 +27,19 @@ jobs:
run: dotnet restore FModel
- name: .NET Publish
run: dotnet publish FModel -c Release -f net5.0-windows -o "./FModel/bin/Publish/" -p:PublishReadyToRun=true -p:PublishSingleFile=true -p:DebugType=None -p:GenerateDocumentationFile=false -p:DebugSymbols=false --no-self-contained -r win-x64
run: dotnet publish FModel -c Release -f net5.0-windows -o "./FModel/bin/Publish/" -p:PublishReadyToRun=true -p:PublishSingleFile=true -p:DebugType=None -p:GenerateDocumentationFile=false -p:DebugSymbols=false -p:AssemblyVersion=${{ github.event.inputs.appVersion }} -p:FileVersion=${{ github.event.inputs.appVersion }} --no-self-contained -r win-x64
- name: EXE Upload
uses: actions/upload-artifact@v2
- name: ZIP File
uses: papeloto/action-zip@v1
with:
name: FModel
path: D:\a\FModel\FModel\FModel\bin\Publish\
files: ./FModel/bin/Publish/FModel.exe
dest: FModel.zip # will end up in working directory not the Publish folder
- name: GIT Release
uses: marvinpinto/action-automatic-releases@latest
with:
title: "FModel v${{ github.event.inputs.appVersion }}"
automatic_release_tag: ${{ github.event.inputs.appVersion }}
repo_token: ${{ secrets.GITHUB_TOKEN }}
prerelease: false
files: FModel.zip

View File

@@ -23,8 +23,14 @@ namespace FModel
/// </summary>
public partial class App
{
[DllImport("kernel32.dll")]
private static extern bool AttachConsole(int dwProcessId);
protected override void OnStartup(StartupEventArgs e)
{
#if DEBUG
AttachConsole(-1);
#endif
base.OnStartup(e);
try

View File

@@ -1,5 +1,4 @@
using System;
using System.Reflection;
using System.Reflection;
using CUE4Parse.UE4.Objects.Core.Misc;
namespace FModel
@@ -23,41 +22,5 @@ namespace FModel
public const string _FN_LIVE_TRIGGER = "fortnite-live.manifest";
public const string _VAL_LIVE_TRIGGER = "valorant-live.manifest";
public static string GetRandomColor()
{
return _randomColors[_random.Next(0, 255)];
}
private static readonly Random _random = new(Environment.TickCount);
private static readonly string[] _randomColors =
{
"F44336", "FFEBEE", "FFCDD2", "EF9A9A", "E57373", "EF5350", "E53935", "D32F2F", "C62828", "B71C1C",
"FF8A80", "FF5252", "FF1744", "D50000", "FCE4EC", "F8BBD0", "F48FB1", "F06292", "EC407A", "E91E63",
"D81B60", "C2185B", "AD1457", "880E4F", "FF80AB", "FF4081", "F50057", "C51162", "F3E5F5", "E1BEE7",
"CE93D8", "BA68C8", "AB47BC", "9C27B0", "8E24AA", "7B1FA2", "6A1B9A", "4A148C", "EA80FC", "E040FB",
"D500F9", "AA00FF", "EDE7F6", "D1C4E9", "B39DDB", "9575CD", "7E57C2", "673AB7", "5E35B1", "512DA8",
"4527A0", "311B92", "B388FF", "7C4DFF", "651FFF", "6200EA", "E8EAF6", "C5CAE9", "9FA8DA", "7986CB",
"5C6BC0", "3F51B5", "3949AB", "303F9F", "283593", "1A237E", "8C9EFF", "536DFE", "3D5AFE", "304FFE",
"E3F2FD", "BBDEFB", "90CAF9", "64B5F6", "42A5F5", "2196F3", "1E88E5", "1976D2", "1565C0", "0D47A1",
"82B1FF", "448AFF", "2979FF", "2962FF", "E1F5FE", "B3E5FC", "81D4FA", "4FC3F7", "29B6F6", "03A9F4",
"039BE5", "0288D1", "0277BD", "01579B", "80D8FF", "40C4FF", "00B0FF", "0091EA", "E0F7FA", "B2EBF2",
"80DEEA", "4DD0E1", "26C6DA", "00BCD4", "00ACC1", "0097A7", "00838F", "006064", "84FFFF", "18FFFF",
"00E5FF", "00B8D4", "E0F2F1", "B2DFDB", "80CBC4", "4DB6AC", "26A69A", "009688", "00897B", "00796B",
"00695C", "004D40", "A7FFEB", "64FFDA", "1DE9B6", "00BFA5", "E8F5E9", "C8E6C9", "A5D6A7", "81C784",
"66BB6A", "4CAF50", "43A047", "388E3C", "2E7D32", "1B5E20", "B9F6CA", "69F0AE", "00E676", "00C853",
"F1F8E9", "DCEDC8", "C5E1A5", "AED581", "9CCC65", "8BC34A", "7CB342", "689F38", "558B2F", "33691E",
"CCFF90", "B2FF59", "76FF03", "64DD17", "F9FBE7", "F0F4C3", "E6EE9C", "DCE775", "D4E157", "CDDC39",
"C0CA33", "AFB42B", "9E9D24", "827717", "F4FF81", "EEFF41", "C6FF00", "AEEA00", "FFFDE7", "FFF9C4",
"FFF59D", "FFF176", "FFEE58", "FFEB3B", "FDD835", "FBC02D", "F9A825", "F57F17", "FFFF8D", "FFFF00",
"FFEA00", "FFD600", "FFF8E1", "FFECB3", "FFE082", "FFD54F", "FFCA28", "FFC107", "FFB300", "FFA000",
"FF8F00", "FF6F00", "FFE57F", "FFD740", "FFC400", "FFAB00", "FFF3E0", "FFE0B2", "FFCC80", "FFB74D",
"FFA726", "FF9800", "FB8C00", "F57C00", "EF6C00", "E65100", "FFD180", "FFAB40", "FF9100", "FF6D00",
"FBE9E7", "FFCCBC", "FFAB91", "FF8A65", "FF7043", "FF5722", "F4511E", "E64A19", "D84315", "BF360C",
"FF9E80", "FF6E40", "FF3D00", "DD2C00", "EFEBE9", "D7CCC8", "BCAAA4", "A1887F", "8D6E63", "795548",
"6D4C41", "5D4037", "4E342E", "3E2723", "FAFAFA", "F5F5F5", "EEEEEE", "E0E0E0", "BDBDBD", "9E9E9E",
"757575", "616161", "424242", "212121", "ECEFF1", "CFD8DC", "B0BEC5", "90A4AE", "78909C", "607D8B",
"546E7A", "455A64", "37474F", "263238", "000000",
};
}
}

View File

@@ -88,8 +88,8 @@ namespace FModel.Creator.Bases.FN
if (_design.DrawSeason && gameplayTags.TryGetGameplayTag("Cosmetics.Filter.Season.", out var season))
_season = GetCosmeticSeason(season.Text, _design.DrawSeasonShort);
GetUserFacingFlags(gameplayTags.GetAllGameplayTags(
"Cosmetics.UserFacingFlags.", "Homebase.Class.", "NPC.CharacterType.Survivor.Defender."));
var triggers = _design.GameplayTags.DrawCustomOnly ? new[] {"Cosmetics.UserFacingFlags."} : new[] {"Cosmetics.UserFacingFlags.", "Homebase.Class.", "NPC.CharacterType.Survivor.Defender."};
GetUserFacingFlags(gameplayTags.GetAllGameplayTags(triggers));
}
private string GetCosmeticSet(string setName, bool bShort)
@@ -290,7 +290,7 @@ namespace FModel.Creator.Bases.FN
private void DrawUserFacingFlags(SKCanvas c, bool customOnly)
{
if (UserFacingFlags == null || UserFacingFlags.Length < 1) return;
if (UserFacingFlags == null || UserFacingFlags.Count < 1) return;
if (customOnly)
{
c.DrawBitmap(_design.GameplayTags.Custom, 0, 0, ImagePaint);

View File

@@ -23,7 +23,7 @@ namespace FModel.Creator.Bases.FN
public SKBitmap SeriesBackground { get; protected set; }
protected string ShortDescription { get; set; }
protected string CosmeticSource { get; set; }
protected SKBitmap[] UserFacingFlags { get; set; }
protected Dictionary<string, SKBitmap> UserFacingFlags { get; set; }
public BaseIcon(UObject uObject, EIconStyle style) : base(uObject, style)
{
@@ -269,24 +269,24 @@ namespace FModel.Creator.Bases.FN
if (!itemCategories.TryGetValue(out FStructFallback[] tertiaryCategories, "TertiaryCategories"))
return;
UserFacingFlags = new SKBitmap[userFacingFlags.Count];
for (var i = 0; i < UserFacingFlags.Length; i++)
UserFacingFlags = new Dictionary<string, SKBitmap>(userFacingFlags.Count);
foreach (var flag in userFacingFlags)
{
if (userFacingFlags[i].Equals("Cosmetics.UserFacingFlags.HasUpgradeQuests", StringComparison.OrdinalIgnoreCase))
if (flag.Equals("Cosmetics.UserFacingFlags.HasUpgradeQuests", StringComparison.OrdinalIgnoreCase))
{
if (Object.ExportType.Equals("AthenaPetCarrierItemDefinition", StringComparison.OrdinalIgnoreCase))
UserFacingFlags[i] = SKBitmap.Decode(Application.GetResourceStream(new Uri("pack://application:,,,/Resources/T-Icon-Pets-64.png"))?.Stream);
else UserFacingFlags[i] = SKBitmap.Decode(Application.GetResourceStream(new Uri("pack://application:,,,/Resources/T-Icon-Quests-64.png"))?.Stream);
UserFacingFlags[flag] = SKBitmap.Decode(Application.GetResourceStream(new Uri("pack://application:,,,/Resources/T-Icon-Pets-64.png"))?.Stream);
else UserFacingFlags[flag] = SKBitmap.Decode(Application.GetResourceStream(new Uri("pack://application:,,,/Resources/T-Icon-Quests-64.png"))?.Stream);
}
else
{
foreach (var category in tertiaryCategories)
{
if (category.TryGetValue(out FGameplayTagContainer tagContainer, "TagContainer") && tagContainer.TryGetGameplayTag(userFacingFlags[i], out _) &&
if (category.TryGetValue(out FGameplayTagContainer tagContainer, "TagContainer") && tagContainer.TryGetGameplayTag(flag, out _) &&
category.TryGetValue(out FStructFallback categoryBrush, "CategoryBrush") && categoryBrush.TryGetValue(out FStructFallback brushXxs, "Brush_XXS") &&
brushXxs.TryGetValue(out FPackageIndex resourceObject, "ResourceObject") && Utils.TryGetPackageIndexExport(resourceObject, out UTexture2D texture))
{
UserFacingFlags[i] = Utils.GetBitmap(texture);
UserFacingFlags[flag] = Utils.GetBitmap(texture);
}
}
}
@@ -299,7 +299,7 @@ namespace FModel.Creator.Bases.FN
const int size = 25;
var x = Margin * (int) 2.5;
foreach (var flag in UserFacingFlags)
foreach (var flag in UserFacingFlags.Values)
{
if (flag == null) continue;

View File

@@ -19,6 +19,7 @@ namespace FModel.Creator.Bases.FN
{
if (Object is not UMaterialInstanceConstant material) return;
texture_finding:
foreach (var textureParameter in material.TextureParameterValues) // get texture from base material
{
if (textureParameter.ParameterValue is not UTexture2D texture || Preview != null) continue;
@@ -45,6 +46,9 @@ namespace FModel.Creator.Bases.FN
if (material == null) return;
}
if (Preview == null)
goto texture_finding;
foreach (var vectorParameter in material.VectorParameterValues)
{
if (vectorParameter.ParameterValue == null) continue;

View File

@@ -114,7 +114,7 @@ namespace FModel.Creator.Bases.FN
}
else if (!_unauthorizedReward.Contains(name.Text))
{
_reward = new Reward(quantity, primaryAssetName);
_reward = new Reward(quantity, $"{name}:{primaryAssetName}");
}
}
}
@@ -256,4 +256,4 @@ namespace FModel.Creator.Bases.FN
_reward.DrawQuest(c, new SKRect(Height, outY + 25, Width - 20, y + Height - 25));
}
}
}
}

View File

@@ -1,5 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.UE4.Objects.Core.i18N;
@@ -10,10 +10,17 @@ using SkiaSharp.HarfBuzz;
namespace FModel.Creator.Bases.FN
{
public class Page
{
public int LevelsNeededForUnlock;
public int RewardsNeededForUnlock;
public Reward[] RewardEntryList;
}
public class BaseSeason : UCreator
{
private Reward _firstWinReward;
private Dictionary<int, List<Reward>> _bookXpSchedule;
private Page[] _bookXpSchedule;
private const int _headerHeight = 150;
// keep the list because rewards are ordered by least to most important
// we only care about the most but we also have filters so we can't just take the last reward
@@ -27,8 +34,8 @@ namespace FModel.Creator.Bases.FN
public override void ParseForInfo()
{
_bookXpSchedule = new Dictionary<int, List<Reward>>();
_bookXpSchedule = Array.Empty<Page>();
if (Object.TryGetValue(out FText displayName, "DisplayName"))
DisplayName = displayName.Text.ToUpperInvariant();
@@ -45,56 +52,48 @@ namespace FModel.Creator.Bases.FN
}
}
var freeLevels = Array.Empty<FStructFallback>();
var paidLevels = Array.Empty<FStructFallback>();
if (Object.TryGetValue(out FPackageIndex[] additionalSeasonData, "AdditionalSeasonData") &&
additionalSeasonData.Length > 0 && Utils.TryGetPackageIndexExport(additionalSeasonData[0], out UObject data) &&
data.TryGetValue(out FStructFallback battlePassXpScheduleFree, "BattlePassXpScheduleFree") &&
battlePassXpScheduleFree.TryGetValue(out freeLevels, "Levels") &&
data.TryGetValue(out FStructFallback battlePassXpSchedulePaid, "BattlePassXpSchedulePaid") &&
battlePassXpSchedulePaid.TryGetValue(out paidLevels, "Levels"))
if (Object.TryGetValue(out FPackageIndex[] additionalSeasonData, "AdditionalSeasonData"))
{
// we got them boys
}
else if (Object.TryGetValue(out FStructFallback bookXpScheduleFree, "BookXpScheduleFree") &&
bookXpScheduleFree.TryGetValue(out freeLevels, "Levels") &&
Object.TryGetValue(out FStructFallback bookXpSchedulePaid, "BookXpSchedulePaid") &&
bookXpSchedulePaid.TryGetValue(out paidLevels, "Levels"))
{
// we got them boys
}
for (var i = 0; i < freeLevels.Length; i++)
{
_bookXpSchedule[i] = new List<Reward>();
if (!freeLevels[i].TryGetValue(out rewards, "Rewards")) continue;
foreach (var reward in rewards)
foreach (var data in additionalSeasonData)
{
if (!reward.TryGetValue(out FSoftObjectPath itemDefinition, "ItemDefinition") ||
itemDefinition.AssetPathName.Text.Contains("/Items/Tokens/") ||
!Utils.TryLoadObject(itemDefinition.AssetPathName.Text, out UObject uObject)) continue;
_bookXpSchedule[i].Add(new Reward(uObject));
if (!Utils.TryGetPackageIndexExport(data, out UObject packageIndex) ||
!packageIndex.TryGetValue(out FStructFallback[] pageList, "PageList")) continue;
var i = 0;
_bookXpSchedule = new Page[pageList.Length];
foreach (var page in pageList)
{
if (!page.TryGetValue(out int levelsNeededForUnlock, "LevelsNeededForUnlock") ||
!page.TryGetValue(out int rewardsNeededForUnlock, "RewardsNeededForUnlock") ||
!page.TryGetValue(out FPackageIndex[] rewardEntryList, "RewardEntryList"))
continue;
var p = new Page
{
LevelsNeededForUnlock = levelsNeededForUnlock,
RewardsNeededForUnlock = rewardsNeededForUnlock,
RewardEntryList = new Reward[rewardEntryList.Length]
};
for (var j = 0; j < p.RewardEntryList.Length; j++)
{
if (!Utils.TryGetPackageIndexExport(rewardEntryList[j], out packageIndex) ||
!packageIndex.TryGetValue(out FStructFallback battlePassOffer, "BattlePassOffer") ||
!battlePassOffer.TryGetValue(out FStructFallback rewardItem, "RewardItem") ||
!rewardItem.TryGetValue(out FSoftObjectPath itemDefinition, "ItemDefinition") ||
!Utils.TryLoadObject(itemDefinition.AssetPathName.Text, out UObject uObject)) continue;
p.RewardEntryList[j] = new Reward(uObject);
}
_bookXpSchedule[i++] = p;
}
break;
}
}
for (var i = 0; i < paidLevels.Length; i++)
{
if (!paidLevels[i].TryGetValue(out rewards, "Rewards")) continue;
foreach (var reward in rewards)
{
if (!reward.TryGetValue(out FSoftObjectPath itemDefinition, "ItemDefinition") ||
!Utils.TryLoadObject(itemDefinition.AssetPathName.Text, out UObject uObject)) continue;
_bookXpSchedule[i].Add(new Reward(uObject));
break;
}
}
Height += 100 * _bookXpSchedule.Count / 10;
Height += 100 * _bookXpSchedule.Sum(x => x.RewardEntryList.Length) / _bookXpSchedule.Length;
}
public override SKImage Draw()
@@ -151,23 +150,15 @@ namespace FModel.Creator.Bases.FN
{
var x = 20;
var y = _headerHeight + 50;
foreach (var (index, reward) in _bookXpSchedule)
foreach (var page in _bookXpSchedule)
{
if (index == 0 || reward.Count == 0 || !reward[0].HasReward())
continue;
c.DrawText(index.ToString(), new SKPoint(x + _DEFAULT_AREA_SIZE / 2, y - 5), _bookPaint);
reward[0].DrawSeason(c, x, y, _DEFAULT_AREA_SIZE);
if (index != 1 && index % 10 == 0)
{
y += _DEFAULT_AREA_SIZE + 20;
x = 20;
}
else
foreach (var reward in page.RewardEntryList)
{
reward.DrawSeason(c, x, y, _DEFAULT_AREA_SIZE);
x += _DEFAULT_AREA_SIZE + 20;
}
y += _DEFAULT_AREA_SIZE + 20;
x = 20;
}
}
}

View File

@@ -120,13 +120,13 @@ namespace FModel.Creator.Bases.FN
_theReward = new BaseIcon(null, EIconStyle.Default);
_theReward.Border[0] = SKColor.Parse("FFDB67");
_theReward.Background[0] = SKColor.Parse("8F4A20");
_theReward.Preview = Utils.GetBitmap("FortniteGame/Content/UI/Foundation/Textures/Icons/Items/T-FNBR-BattlePoints.T-FNBR-BattlePoints");
_theReward.Preview = Utils.GetBitmap("FortniteGame/Content/Athena/UI/Frontend/Art/T_UI_BP_BattleStar_L.T_UI_BP_BattleStar_L");
break;
case "athenaseasonalxp":
_theReward = new BaseIcon(null, EIconStyle.Default);
_theReward.Border[0] = SKColor.Parse("E6FDB1");
_theReward.Background[0] = SKColor.Parse("51830F");
_theReward.Preview = Utils.GetBitmap("FortniteGame/Content/UI/Foundation/Textures/Icons/Items/T-FNBR-XPMedium.T-FNBR-XPMedium");
_theReward.Preview = Utils.GetBitmap("FortniteGame/Content/UI/Foundation/Textures/Icons/Items/T-FNBR-XPUncommon-L.T-FNBR-XPUncommon-L");
break;
case "mtxgiveaway":
_theReward = new BaseIcon(null, EIconStyle.Default);

View File

@@ -0,0 +1,98 @@
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.UE4.Objects.Core.i18N;
using FModel.Creator.Bases.FN;
using SkiaSharp;
namespace FModel.Creator.Bases.SOD2
{
public class BaseDecayIcon : BaseIcon
{
private int _maxStackCount;
private readonly SKBitmap _backgroundOverlay;
public BaseDecayIcon(UObject uObject, EIconStyle style) : base(uObject, style)
{
Margin = 0;
Background = new[] {SKColor.Parse("000000"), SKColor.Parse("B24E18")};
_backgroundOverlay = Utils.GetBitmap("StateOfDecay2/Content/Art/UI/settings/icon_sod_eagle_09_square.icon_sod_eagle_09_square").Resize(512);
}
public override void ParseForInfo()
{
if (Object.TryGetValue(out FStructFallback stackingInfo, "StackingInfo") &&
stackingInfo.TryGetValue(out int maxStackCount, "MaxStackCount"))
_maxStackCount = maxStackCount;
if (Object.Class.SuperStruct != null && Utils.TryGetPackageIndexExport(Object.Class.SuperStruct, out UObject t))
{
// TODO
}
if (Object.TryGetValue(out FStructFallback displayInfo, "DisplayInfo"))
{
if (displayInfo.TryGetValue(out FText displayName, "DisplayName"))
DisplayName = displayName.Text;
if (displayInfo.TryGetValue(out FText displayDescription, "DisplayDescription"))
Description = displayDescription.Text;
if (displayInfo.TryGetValue(out string iconFills, "IconFills"))
Preview = Utils.GetBitmap(iconFills);
}
}
// TODO
//
// ExtraLargeBackpackBase - for extra large
// SmallBackpackBase - for small
// ExtraSmallBackpackBase - for extra small
// MediumBackpackBase - for medium
// LargeBackpackBase - for large
public override SKImage Draw()
{
using var ret = new SKBitmap(Width, Height, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(ret);
DrawBackground(c);
DrawPreview(c);
return SKImage.FromBitmap(ret);
}
private new void DrawPreview(SKCanvas c)
{
c.DrawBitmap(Preview ?? DefaultPreview, new SKRect(Margin, Margin, Width - Margin, Height - Margin), new SKPaint
{
// BlendMode = SKBlendMode.SrcATop -- Need Asval's assistance.
});
}
private new void DrawBackground(SKCanvas c)
{
c.DrawRect(new SKRect(Margin, Margin, Width - Margin, Height - Margin),
new SKPaint
{
IsAntialias = true, FilterQuality = SKFilterQuality.High,
Shader = SKShader.CreateRadialGradient(new SKPoint(Width / 2, Height / 2), Width / 5 * 2,
new[] {Background[0], Background[1]},
SKShaderTileMode.Clamp)
});
for (var i = 0; i < _backgroundOverlay.Width; i++)
for (var j = 0; j < _backgroundOverlay.Height; j++)
if (_backgroundOverlay.GetPixel(i, j) == SKColors.Black)
{
_backgroundOverlay.SetPixel(i, j, SKColors.Transparent);
}
c.DrawBitmap(_backgroundOverlay, new SKRect(Margin, Margin, Width - Margin, Height - Margin), new SKPaint
{
IsAntialias = true,
ColorFilter = SKColorFilter.CreateBlendMode(SKColors.Black.WithAlpha(150), SKBlendMode.DstIn),
ImageFilter = SKImageFilter.CreateDropShadow(2, 2, 4, 4, new SKColor(0, 0, 0))
});
c.DrawColor(SKColors.Black.WithAlpha(125), SKBlendMode.DstIn);
}
}
}

View File

@@ -5,6 +5,7 @@ using FModel.Creator.Bases;
using FModel.Creator.Bases.BB;
using FModel.Creator.Bases.FN;
using FModel.Creator.Bases.SB;
using FModel.Creator.Bases.SOD2;
namespace FModel.Creator
{
@@ -125,7 +126,8 @@ namespace FModel.Creator
case "MaterialInstanceConstant"
when _object.Owner != null &&
(_object.Owner.Name.EndsWith($"/MI_OfferImages/{_object.Name}") ||
_object.Owner.Name.EndsWith($"/RenderSwitch_Materials/{_object.Name}")):
_object.Owner.Name.EndsWith($"/RenderSwitch_Materials/{_object.Name}") ||
_object.Owner.Name.EndsWith($"/MI_BPTile/{_object.Name}")):
creator = new BaseMaterialInstance(_object, _style);
return true;
case "FortMtxOfferData":
@@ -225,10 +227,38 @@ namespace FModel.Creator
case "GLeagueDivision":
creator = new BaseDivision(_object, EIconStyle.Default);
return true;
// TODO: Draw this properly
// case "GGameModeInfo":
// creator = new BaseGameModeInfo(_object, EIconStyle.Default);
// return true;
// State of Decay 2
case "CureItem":
case "AmmoItem":
case "Pro_Brake_C":
case "BackpackItem":
case "MagicAmmoItem":
case "ConsumableItem":
case "MeleeWeaponItem":
case "CloseCombatItem":
case "FacilityModItem":
case "RangedWeaponItem":
case "MiscellaneousItem":
case "RepairVehicleItem":
case "ResourceItemBase_C":
case "FuelResourceBase_C":
case "MedsResourceBase_C":
case "PartsResourceBase_C":
case "RangedWeaponModItem":
case "VehicleDeliveryItem":
case "ConsumableBase_BP_C":
case "SmallBackpackBase_C":
case "LargeBackpackBase_C":
case "MediumBackpackBase_C":
case "ConsumableMedsBase_BP_C":
case "MaterialsResourceBase_C":
case "ExtraLargeBackpackBase_C":
case "ExtraSmallBackpackBase_C":
case "ConsumableStimsBase_BP_C":
case "Consumable_TimedStatBuff_Base_BP_C":
case "Consumable_SuspendFatigue_Base_BP_C":
creator = new BaseDecayIcon(_object, EIconStyle.Default);
return true;
default:
creator = null;
return false;

View File

@@ -6,7 +6,6 @@ using System.Text.RegularExpressions;
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.UObject;
using CUE4Parse.Utils;
using CUE4Parse_Conversion.Textures;
@@ -38,12 +37,6 @@ namespace FModel.Creator
public static bool TryGetDisplayAsset(UObject uObject, out SKBitmap preview)
{
if (uObject.TryGetValue(out FSoftObjectPath displayAsset, "DisplayAssetPath"))
{
preview = GetDisplayAsset(displayAsset);
return preview != null;
}
if (uObject.TryGetValue(out FSoftObjectPath sidePanelIcon, "SidePanelIcon"))
{
preview = GetBitmap(sidePanelIcon);
@@ -61,35 +54,22 @@ namespace FModel.Creator
return preview != null;
}
public static SKBitmap GetDisplayAsset(FSoftObjectPath path)
{
if (!TryLoadObject(path.AssetPathName.Text, out UObject obj)) return null;
if (obj.TryGetValue(out FStructFallback type, "DetailsImage") &&
type.TryGetValue(out FPackageIndex resource, "ResourceObject") && resource.ResolvedObject?.Outer != null &&
!resource.ResolvedObject.Outer.Name.Text.Contains("FortniteGame/Content/Athena/Prototype/Textures/"))
{
return GetBitmap(resource);
}
return null;
}
public static SKBitmap GetBitmap(FPackageIndex packageIndex)
{
while (true)
{
if (!TryGetPackageIndexExport(packageIndex, out UExport export)) return null;
if (!TryGetPackageIndexExport(packageIndex, out UObject export)) return null;
switch (export)
{
case UTexture2D texture:
return GetBitmap(texture);
case UMaterialInstanceConstant material:
return GetBitmap(material);
case UObject uObject:
default:
{
if (uObject.TryGetValue(out FSoftObjectPath previewImage, "LargePreviewImage", "SmallPreviewImage")) return GetBitmap(previewImage);
if (uObject.TryGetValue(out string largePreview, "LargePreviewImage")) return GetBitmap(largePreview);
if (uObject.TryGetValue(out FPackageIndex smallPreview, "SmallPreviewImage"))
if (export.TryGetValue(out FSoftObjectPath previewImage, "LargePreviewImage", "SmallPreviewImage")) return GetBitmap(previewImage);
if (export.TryGetValue(out string largePreview, "LargePreviewImage")) return GetBitmap(largePreview);
if (export.TryGetValue(out FPackageIndex smallPreview, "SmallPreviewImage"))
{
packageIndex = smallPreview;
continue;
@@ -97,8 +77,6 @@ namespace FModel.Creator
return null;
}
default:
return null;
}
}
}
@@ -139,7 +117,7 @@ namespace FModel.Creator
return bmp;
}
public static bool TryGetPackageIndexExport<T>(FPackageIndex packageIndex, out T export) where T : UExport
public static bool TryGetPackageIndexExport<T>(FPackageIndex packageIndex, out T export) where T : UObject
{
if (packageIndex.ResolvedObject == null)
{
@@ -172,19 +150,19 @@ namespace FModel.Creator
}
// fullpath must be either without any extension or with the export objectname
public static bool TryLoadObject<T>(string fullPath, out T export) where T : UExport
public static bool TryLoadObject<T>(string fullPath, out T export) where T : UObject
{
return _applicationView.CUE4Parse.Provider.TryLoadObject(fullPath, out export);
}
public static IEnumerable<UExport> LoadExports(string fullPath)
public static IEnumerable<UObject> LoadExports(string fullPath)
{
return _applicationView.CUE4Parse.Provider.LoadObjectExports(fullPath);
}
public static string GetLocalizedResource(string namespacee, string key, string defaultValue)
public static string GetLocalizedResource(string @namespace, string key, string defaultValue)
{
return _applicationView.CUE4Parse.Provider.GetLocalizedString(namespacee, key, defaultValue);
return _applicationView.CUE4Parse.Provider.GetLocalizedString(@namespace, key, defaultValue);
}
public static string GetFullPath(string partialPath)

View File

@@ -88,7 +88,9 @@ namespace FModel
[Description("Star Wars: Jedi Fallen Order")]
SwGame,
[Description("Core")]
Platform
Platform,
[Description("Days Gone")]
BendGame
}
public enum ELoadingMode

View File

@@ -6,13 +6,14 @@
<UseWPF>true</UseWPF>
<ApplicationIcon>FModel.ico</ApplicationIcon>
<Version>4.0.0</Version>
<AssemblyVersion>4.0.0.1</AssemblyVersion>
<FileVersion>4.0.0.1</FileVersion>
<AssemblyVersion>4.0.1.0</AssemblyVersion>
<FileVersion>4.0.1.0</FileVersion>
<IsPackable>false</IsPackable>
<IsPublishable>true</IsPublishable>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishSingleFile>true</PublishSingleFile>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<StartupObject>FModel.App</StartupObject>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@@ -71,14 +72,13 @@
<None Remove="Resources\Cpp.xshd" />
<None Remove="Resources\unix.png" />
<None Remove="Resources\linux.png" />
<None Remove="Resources\stateofdecay2.png" />
<None Remove="Resources\T_Placeholder_Item_Image.png" />
<None Remove="Resources\T_ClipSize_Weapon_Stats.png" />
<None Remove="Resources\T_DamagePerBullet_Weapon_Stats.png" />
<None Remove="Resources\T_ReloadTime_Weapon_Stats.png" />
<None Remove="Resources\T-Icon-Pets-64.png" />
<None Remove="Resources\T-Icon-Quests-64.png" />
<None Remove="Resources\city_pin.png" />
<None Remove="Resources\pin.png" />
<None Remove="Resources\Default.png" />
<None Remove="Resources\NoBackground.png" />
<None Remove="Resources\NoText.png" />
@@ -162,14 +162,13 @@
<Resource Include="Resources\athena.png" />
<Resource Include="Resources\unix.png" />
<Resource Include="Resources\linux.png" />
<Resource Include="Resources\stateofdecay2.png" />
<Resource Include="Resources\T_Placeholder_Item_Image.png" />
<Resource Include="Resources\T_ClipSize_Weapon_Stats.png" />
<Resource Include="Resources\T_DamagePerBullet_Weapon_Stats.png" />
<Resource Include="Resources\T_ReloadTime_Weapon_Stats.png" />
<Resource Include="Resources\T-Icon-Pets-64.png" />
<Resource Include="Resources\T-Icon-Quests-64.png" />
<Resource Include="Resources\city_pin.png" />
<Resource Include="Resources\pin.png" />
<Resource Include="Resources\Default.png" />
<Resource Include="Resources\NoBackground.png" />
<Resource Include="Resources\NoText.png" />

View File

@@ -27,7 +27,7 @@ namespace FModel.Framework
protected ViewModelCommand(TContextViewModel contextViewModel)
{
ContextViewModel = contextViewModel ?? throw new ArgumentNullException(nameof(contextViewModel));
ContextViewModel = contextViewModel /*?? throw new ArgumentNullException(nameof(contextViewModel))*/;
}
public sealed override void Execute(object parameter)

View File

@@ -14,11 +14,8 @@
<Style TargetType="adonisControls:AdonisWindow" BasedOn="{StaticResource {x:Type adonisControls:AdonisWindow}}" >
<Setter Property="Title" Value="FModel" />
<Style.Triggers>
<DataTrigger
Binding="{Binding DataContext.TitleExtra, RelativeSource={RelativeSource Self}, Converter={x:Static converters:IsNullToBoolReversedConverter.Instance}}"
Value="True">
<Setter Property="Title"
Value="{Binding DataContext.TitleExtra, RelativeSource={RelativeSource Self}, StringFormat={}FModel - {0}}" />
<DataTrigger Binding="{Binding DataContext.TitleExtra, RelativeSource={RelativeSource Self}, Converter={x:Static converters:IsNullToBoolReversedConverter.Instance}}" Value="True">
<Setter Property="Title" Value="{Binding DataContext.TitleExtra, RelativeSource={RelativeSource Self}, StringFormat={}FModel - {0}}" />
</DataTrigger>
</Style.Triggers>
</Style>
@@ -132,12 +129,12 @@
<MenuItem Header="Save Textures" IsCheckable="True" StaysOpenOnClick="True"
InputGestureText="{Binding AutoSaveTextures, Source={x:Static local:Settings.UserSettings.Default}}"
IsChecked="{Binding IsAutoSaveTextures, Source={x:Static local:Settings.UserSettings.Default}}" />
<!--<MenuItem Header="Save Meshes" IsCheckable="True" StaysOpenOnClick="True"
InputGestureText="{Binding AutoSaveMeshes, Source={x:Static local:Settings.UserSettings.Default}}"
IsChecked="{Binding IsAutoSaveMeshes, Source={x:Static local:Settings.UserSettings.Default}}" />-->
<MenuItem Header="Save Materials" IsCheckable="True" StaysOpenOnClick="True"
InputGestureText="{Binding AutoSaveMaterials, Source={x:Static local:Settings.UserSettings.Default}}"
IsChecked="{Binding IsAutoSaveMaterials, Source={x:Static local:Settings.UserSettings.Default}}" />
<MenuItem Header="Save Meshes" IsCheckable="True" StaysOpenOnClick="True"
InputGestureText="{Binding AutoSaveMeshes, Source={x:Static local:Settings.UserSettings.Default}}"
IsChecked="{Binding IsAutoSaveMeshes, Source={x:Static local:Settings.UserSettings.Default}}" />
<MenuItem Header="Open Sounds" IsCheckable="True" StaysOpenOnClick="True"
InputGestureText="{Binding AutoOpenSounds, Source={x:Static local:Settings.UserSettings.Default}}"
IsChecked="{Binding IsAutoOpenSounds, Source={x:Static local:Settings.UserSettings.Default}}" />
@@ -438,9 +435,10 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Separator Grid.Row="0" Style="{StaticResource CustomSeparator}"
Tag="{Binding SelectedItem.Header, ElementName=AssetsFolderName, FallbackValue='ASSETS LIST', Converter={x:Static converters:FolderToSeparatorTagConverter.Instance}}" />
<controls:Breadcrumb Grid.Row="0" MaxWidth="{Binding ActualWidth, ElementName=AssetsSearchName}" HorizontalAlignment="Left" Margin="0 5 0 5"
DataContext="{Binding SelectedItem.PathAtThisPoint, ElementName=AssetsFolderName, FallbackValue='No/Directory/Detected/In/Folder'}"/>
<ListBox Grid.Row="1" x:Name="AssetsListName" Style="{StaticResource AssetsListBox}" PreviewMouseDoubleClick="OnAssetsListMouseDoubleClick">
<ListBox.ContextMenu>
<ContextMenu DataContext="{Binding PlacementTarget, RelativeSource={RelativeSource Self}}">

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.ComponentModel;
using System.Linq;
using System.Windows;
@@ -57,8 +57,10 @@ namespace FModel
private async void OnLoaded(object sender, RoutedEventArgs e)
{
#if !DEBUG
ApplicationService.ApiEndpointView.FModelApi.CheckForUpdates(UserSettings.Default.UpdateMode);
#endif
switch (UserSettings.Default.AesReload)
{
case EAesReload.Always:
@@ -236,7 +238,7 @@ namespace FModel
private void OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (sender is not ListBox listBox) return;
if (!_applicationView.IsReady || sender is not ListBox listBox) return;
UserSettings.Default.LoadingMode = ELoadingMode.Multiple;
_applicationView.LoadingModes.LoadCommand.Execute(listBox.SelectedItems);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -22,7 +22,7 @@ namespace FModel.Services
private readonly Assets _staticAssets = new()
{
LargeImageKey = "official_logo", SmallImageKey = "verified", SmallImageText = $"v{Constants.APP_VERSION[..5]}"
LargeImageKey = "official_logo", SmallImageKey = "verified", SmallImageText = $"v{Constants.APP_VERSION}"
};
private readonly Button[] _buttons =
@@ -38,7 +38,7 @@ namespace FModel.Services
Assets = _staticAssets,
Timestamps = _timestamps,
Buttons = _buttons,
State = $"{game.GetDescription()} - Idling"
Details = $"{game.GetDescription()} - Idling"
};
_client.OnReady += (_, args) => Log.Information("{Username}#{Discriminator} ({UserId}) is now ready", args.User.Username, args.User.Discriminator, args.User.ID);
@@ -60,7 +60,7 @@ namespace FModel.Services
_client.Invoke();
}
public void UpdateButDontSavePresence(string details, string state)
public void UpdateButDontSavePresence(string details = null, string state = null)
{
if (!_client.IsInitialized) return;
_client.SetPresence(new RichPresence
@@ -68,8 +68,8 @@ namespace FModel.Services
Assets = _staticAssets,
Timestamps = _timestamps,
Buttons = _buttons,
Details = details,
State = state
Details = details ?? _currentPresence.Details,
State = state ?? _currentPresence.State
});
_client.Invoke();
}

View File

@@ -1,8 +1,10 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Windows.Input;
using CUE4Parse.UE4.Versions;
using CUE4Parse_Conversion.Meshes;
using CUE4Parse_Conversion.Textures;
using FModel.Framework;
using FModel.ViewModels;
using FModel.ViewModels.ApiEndpoints.Models;
@@ -132,7 +134,7 @@ namespace FModel.Settings
set => SetProperty(ref _loadingMode, value);
}
private EUpdateMode _updateMode = EUpdateMode.Stable;
private EUpdateMode _updateMode = EUpdateMode.Beta;
public EUpdateMode UpdateMode
{
get => _updateMode;
@@ -205,12 +207,13 @@ namespace FModel.Settings
{FGame.Dungeons, EGame.GAME_UE4_LATEST},
{FGame.WorldExplorers, EGame.GAME_UE4_LATEST},
{FGame.g3, EGame.GAME_UE4_22},
{FGame.StateOfDecay2, EGame.GAME_UE4_LATEST},
{FGame.StateOfDecay2, EGame.GAME_SOD2},
{FGame.Prospect, EGame.GAME_UE4_LATEST},
{FGame.Indiana, EGame.GAME_UE4_LATEST},
{FGame.RogueCompany, EGame.GAME_UE4_LATEST},
{FGame.SwGame, EGame.GAME_UE4_LATEST},
{FGame.Platform, EGame.GAME_UE4_24}
{FGame.Platform, EGame.GAME_UE4_25},
{FGame.BendGame, EGame.GAME_UE4_11}
};
public IDictionary<FGame, EGame> OverridedGame
{
@@ -233,7 +236,8 @@ namespace FModel.Settings
{FGame.Indiana, UE4Version.VER_UE4_DETERMINE_BY_GAME},
{FGame.RogueCompany, UE4Version.VER_UE4_DETERMINE_BY_GAME},
{FGame.SwGame, UE4Version.VER_UE4_DETERMINE_BY_GAME},
{FGame.Platform, UE4Version.VER_UE4_DETERMINE_BY_GAME}
{FGame.Platform, UE4Version.VER_UE4_DETERMINE_BY_GAME},
{FGame.BendGame, UE4Version.VER_UE4_DETERMINE_BY_GAME}
};
public IDictionary<FGame, UE4Version> OverridedUEVersion
{
@@ -292,7 +296,8 @@ namespace FModel.Settings
{FGame.Indiana, new List<CustomDirectory>()},
{FGame.RogueCompany, new List<CustomDirectory>()},
{FGame.SwGame, new List<CustomDirectory>()},
{FGame.Platform, new List<CustomDirectory>()}
{FGame.Platform, new List<CustomDirectory>()},
{FGame.BendGame, new List<CustomDirectory>()}
};
public IDictionary<FGame, IList<CustomDirectory>> CustomDirectories
{
@@ -300,7 +305,7 @@ namespace FModel.Settings
set => SetProperty(ref _customDirectories, value);
}
private DateTime _lastAesReload = DateTime.Today;
private DateTime _lastAesReload = DateTime.Today.AddDays(-1);
public DateTime LastAesReload
{
get => _lastAesReload;
@@ -425,5 +430,26 @@ namespace FModel.Settings
get => _nextAudio;
set => SetProperty(ref _nextAudio, value);
}
private EMeshFormat _meshExportFormat = EMeshFormat.ActorX;
public EMeshFormat MeshExportFormat
{
get => _meshExportFormat;
set => SetProperty(ref _meshExportFormat, value);
}
private ELodFormat _lodExportFormat = ELodFormat.FirstLod;
public ELodFormat LodExportFormat
{
get => _lodExportFormat;
set => SetProperty(ref _lodExportFormat, value);
}
private ETextureFormat _textureExportFormat = ETextureFormat.Png;
public ETextureFormat TextureExportFormat
{
get => _textureExportFormat;
set => SetProperty(ref _textureExportFormat, value);
}
}
}

View File

@@ -64,28 +64,35 @@ namespace FModel.ViewModels
_keysFromSettings.MainKey = key;
}
else if (!_keysFromSettings.HasDynamicKeys)
{
HasChange = true;
_keysFromSettings.DynamicKeys = new List<DynamicKey>
{
new()
{
Key = key,
FileName = collection[e.CollectionIndex].Name,
Guid = collection[e.CollectionIndex].Guid.ToString()
}
};
}
else if (_keysFromSettings.DynamicKeys.FirstOrDefault(x => x.Guid == collection[e.CollectionIndex].Guid.ToString()) is { } d)
{
if (!HasChange)
HasChange = FixKey(d.Key) != key;
d.Key = key;
}
else
{
if (!_keysFromSettings.HasDynamicKeys)
HasChange = true;
_keysFromSettings.DynamicKeys.Add(new DynamicKey
{
HasChange = true;
_keysFromSettings.DynamicKeys = new[]
{
new DynamicKey
{
Key = key,
FileName = collection[e.CollectionIndex].Name,
Guid = collection[e.CollectionIndex].Guid.ToString()
}
};
}
else if (_keysFromSettings.DynamicKeys.FirstOrDefault(x => x.Guid == collection[e.CollectionIndex].Guid.ToString()) is { } d)
{
if (!HasChange)
HasChange = FixKey(d.Key) != key;
d.Key = key;
}
Key = key,
FileName = collection[e.CollectionIndex].Name,
Guid = collection[e.CollectionIndex].Guid.ToString()
});
}
}

View File

@@ -5,6 +5,7 @@ using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using AutoUpdaterDotNET;
using FModel.Settings;
using FModel.ViewModels.ApiEndpoints.Models;
using Newtonsoft.Json;
using RestSharp;
@@ -107,13 +108,16 @@ namespace FModel.ViewModels.ApiEndpoints
private void CheckForUpdateEvent(UpdateInfoEventArgs args)
{
if (args != null)
if (args is {CurrentVersion: { }})
{
if (!args.IsUpdateAvailable) return;
var currentVersion = new Version(args.CurrentVersion);
if (currentVersion == args.InstalledVersion) return;
var downgrade = currentVersion < args.InstalledVersion;
var messageBox = new MessageBoxModel
{
Text = $"FModel {args.CurrentVersion} is available. You are using version {args.InstalledVersion}. Do you want to update the application now?",
Caption = "Update Available",
Text = $"The latest version of FModel {UserSettings.Default.UpdateMode} is {args.CurrentVersion}. You are using version {args.InstalledVersion}. Do you want to {(downgrade ? "downgrade" : "update")} the application now?",
Caption = $"{(downgrade ? "Downgrade" : "Update")} Available",
Icon = MessageBoxImage.Question,
Buttons = MessageBoxButtons.YesNo(),
IsSoundEnabled = false

View File

@@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Collections.Generic;
using System.Diagnostics;
using J = Newtonsoft.Json.JsonPropertyAttribute;
namespace FModel.ViewModels.ApiEndpoints.Models
@@ -8,9 +9,9 @@ namespace FModel.ViewModels.ApiEndpoints.Models
{
[J("version")] public string Version { get; private set; }
[J("mainKey")] public string MainKey { get; set; }
[J("dynamicKeys")] public DynamicKey[] DynamicKeys { get; set; }
[J("dynamicKeys")] public List<DynamicKey> DynamicKeys { get; set; }
public bool HasDynamicKeys => DynamicKeys is {Length: > 0};
public bool HasDynamicKeys => DynamicKeys is {Count: > 0};
}
[DebuggerDisplay("{" + nameof(Key) + "}")]

View File

@@ -44,7 +44,6 @@ namespace FModel.ViewModels
set
{
SetProperty(ref _status, value);
RaisePropertyChanged(nameof(TitleExtra));
IsReady = Status != EStatusKind.Loading && Status != EStatusKind.Stopping;
}
}
@@ -105,6 +104,11 @@ namespace FModel.ViewModels
UserSettings.Default.GameDirectory = gameLauncherViewModel.SelectedDetectedGame.GameDirectory;
if (!bAlreadyLaunched || gameDirectory.Equals(gameLauncherViewModel.SelectedDetectedGame.GameDirectory)) return;
RestartWithWarning();
}
public void RestartWithWarning()
{
MessageBox.Show("It looks like you just changed something.\nFModel will restart to apply your changes.", "Uh oh, a restart is needed", MessageBoxButton.OK, MessageBoxImage.Warning);
Restart();
}

View File

@@ -15,6 +15,7 @@ using System.Windows.Data;
using CSCore.CoreAudioAPI;
using FModel.Extensions;
using FModel.Framework;
using FModel.Services;
using FModel.Settings;
using FModel.ViewModels.Commands;
using FModel.Views.Resources.Controls;
@@ -154,6 +155,7 @@ namespace FModel.ViewModels
public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
{
private DiscordHandler _discordHandler => DiscordService.DiscordHandler;
private static IWaveSource _waveSource;
private static ISoundOut _soundOut;
private Timer _sourceTimer;
@@ -385,6 +387,7 @@ namespace FModel.ViewModels
public void Play()
{
if (_soundOut == null || IsPlaying) return;
_discordHandler.UpdateButDontSavePresence(null, $"Audio Player: {PlayedFile.FileName} ({PlayedFile.Duration:g})");
_soundOut.Play();
}

View File

@@ -6,6 +6,7 @@ using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using AdonisUI.Controls;
using CUE4Parse.Encryption.Aes;
@@ -15,13 +16,19 @@ using CUE4Parse.MappingsProvider;
using CUE4Parse.UE4.AssetRegistry;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Assets.Exports.SkeletalMesh;
using CUE4Parse.UE4.Assets.Exports.Sound;
using CUE4Parse.UE4.Assets.Exports.StaticMesh;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Assets.Exports.Wwise;
using CUE4Parse.UE4.Localization;
using CUE4Parse.UE4.Objects.UObject;
using CUE4Parse.UE4.Oodle.Objects;
using CUE4Parse.UE4.Shaders;
using CUE4Parse.UE4.Wwise;
using CUE4Parse_Conversion;
using CUE4Parse_Conversion.Materials;
using CUE4Parse_Conversion.Meshes;
using CUE4Parse_Conversion.Sounds;
using CUE4Parse_Conversion.Textures;
using EpicManifestParser.Objects;
@@ -81,9 +88,18 @@ namespace FModel.ViewModels
}
default:
{
Game = gameDirectory.SubstringBeforeLast("\\Content\\Paks").SubstringAfterLast("\\").ToEnum(FGame.Unknown);
Provider = new DefaultFileProvider(gameDirectory, SearchOption.TopDirectoryOnly, true,
UserSettings.Default.OverridedGame[Game], UserSettings.Default.OverridedUEVersion[Game]);
Game = gameDirectory.SubstringBeforeLast("\\Content").SubstringAfterLast("\\").ToEnum(FGame.Unknown);
if (Game == FGame.StateOfDecay2)
Provider = new DefaultFileProvider(new DirectoryInfo(gameDirectory), new List<DirectoryInfo>
{
new(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\StateOfDecay2\\Saved\\Paks"),
new(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\StateOfDecay2\\Saved\\DisabledPaks")
},
SearchOption.AllDirectories, true, UserSettings.Default.OverridedGame[Game], UserSettings.Default.OverridedUEVersion[Game]);
else
Provider = new DefaultFileProvider(gameDirectory, SearchOption.AllDirectories, true, UserSettings.Default.OverridedGame[Game], UserSettings.Default.OverridedUEVersion[Game]);
break;
}
}
@@ -202,7 +218,7 @@ namespace FModel.ViewModels
foreach (var file in GameDirectory.DirectoryFiles)
{
cancellationToken.ThrowIfCancellationRequested();
if (!(Provider.MountedVfs.FirstOrDefault(x => x.Name == file.Name) is { } vfs))
if (Provider.MountedVfs.FirstOrDefault(x => x.Name == file.Name) is not { } vfs)
continue;
file.IsEnabled = true;
@@ -232,7 +248,7 @@ namespace FModel.ViewModels
await _threadWorkerView.Begin(cancellationToken =>
{
var aes = _apiEndpointView.BenbotApi.GetAesKeys(cancellationToken);
if (aes == null) return;
if (aes?.MainKey == null && aes?.DynamicKeys == null && aes?.Version == null) return;
UserSettings.Default.AesKeys[Game] = aes;
});
@@ -381,6 +397,7 @@ namespace FModel.ViewModels
{
case "ini":
case "txt":
case "log":
case "po":
case "bat":
case "xml":
@@ -466,14 +483,15 @@ namespace FModel.ViewModels
TabControl.SelectedTab.Image = null;
if (Provider.TryCreateReader(fullPath, out var archive))
{
var header = new FDictionaryHeader(archive);
var header = new FOodleDictionaryArchive(archive).Header;
TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(header, Formatting.Indented), bulkSave);
}
break;
}
case "png":
case "jpg":
case "bmp":
{
if (Provider.TrySaveAsset(fullPath, out var data))
{
@@ -488,8 +506,18 @@ namespace FModel.ViewModels
FLogger.AppendText($"Export '{fullPath.SubstringAfterLast('/')}' and change its extension if you want it to be an installable font file", Constants.WHITE, true);
break;
case "ushaderbytecode":
case "ushadercode":
{
TabControl.SelectedTab.Image = null;
if (Provider.TryCreateReader(fullPath, out var archive))
{
var ar = new FShaderCodeArchive(archive);
TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(ar, Formatting.Indented), bulkSave);
}
break;
}
default:
{
var exports = Provider.LoadObjectExports(fullPath);
@@ -522,13 +550,19 @@ namespace FModel.ViewModels
ExportData(fullPath);
}
private bool CheckExport(UExport export) // return if this loads an image
private bool CheckExport(UObject export) // return if this loads an image
{
switch (export)
{
case UTexture2D texture:
{
SetImage(texture.Decode());
var bNearest = false;
if (texture.TryGetValue(out FName trigger, "LODGroup", "Filter") && !trigger.IsNone)
bNearest = trigger.Text.EndsWith("TEXTUREGROUP_Pixels2D", StringComparison.OrdinalIgnoreCase) ||
trigger.Text.EndsWith("TF_Nearest", StringComparison.OrdinalIgnoreCase);
TabControl.SelectedTab.ImageRender = bNearest ? BitmapScalingMode.NearestNeighbor : BitmapScalingMode.Linear;
SetImage(texture.Decode(bNearest));
return true;
}
case UAkMediaAssetData:
@@ -542,30 +576,30 @@ namespace FModel.ViewModels
SaveAndPlaySound(Path.Combine(TabControl.SelectedTab.Directory, TabControl.SelectedTab.Header.SubstringBeforeLast('.')).Replace('\\', '/'), audioFormat, data);
return false;
}
case UMaterialInterface material when UserSettings.Default.IsAutoSaveMaterials:
case UMaterialInterface when UserSettings.Default.IsAutoSaveMaterials:
case UStaticMesh when UserSettings.Default.IsAutoSaveMeshes:
case USkeletalMesh when UserSettings.Default.IsAutoSaveMeshes:
{
var mat = new MaterialExporter(material);
if (mat.TryWriteTo(
Path.Combine(UserSettings.Default.OutputDirectory, "Saves"),
Path.Combine(UserSettings.Default.OutputDirectory, "Textures"),
out var savedFileName))
var toSave = new Exporter(export, UserSettings.Default.TextureExportFormat, UserSettings.Default.LodExportFormat);
var toSaveDirectory = new DirectoryInfo(Path.Combine(UserSettings.Default.OutputDirectory, "Saves"));
if (toSave.TryWriteToDir(toSaveDirectory, out var savedFileName))
{
Log.Information("{FileName} successfully saved", savedFileName);
Log.Information("Successfully saved {FileName}", savedFileName);
FLogger.AppendInformation();
FLogger.AppendText($"Successfully saved '{savedFileName}'", Constants.WHITE, true);
FLogger.AppendText($"Successfully saved {savedFileName}", Constants.WHITE, true);
}
else
{
Log.Error("{FileName} could not be saved", savedFileName);
FLogger.AppendError();
FLogger.AppendText($"Could not saved '{savedFileName}'", Constants.WHITE, true);
FLogger.AppendText($"Could not save '{savedFileName}'", Constants.WHITE, true);
}
return false;
}
case UObject uObject:
default:
{
using var package = new CreatorPackage(uObject, UserSettings.Default.CosmeticStyle);
using var package = new CreatorPackage(export, UserSettings.Default.CosmeticStyle);
if (!package.TryConstructCreator(out var creator)) return false;
creator.ParseForInfo();
@@ -573,8 +607,6 @@ namespace FModel.ViewModels
return true;
}
}
return false;
}
private void SetImage(SKImage img)
@@ -635,7 +667,7 @@ namespace FModel.ViewModels
Directory.CreateDirectory(path.SubstringBeforeLast('/'));
File.WriteAllBytes(path, kvp.Value);
}
Log.Information("{FileName} successfully exported", fileName);
FLogger.AppendInformation();
FLogger.AppendText($"Successfully exported '{fileName}'", Constants.WHITE, true);

View File

@@ -32,7 +32,8 @@ namespace FModel.ViewModels.Commands
{
foreach (var folder in root)
{
if (!folder.Header.Equals(folders[i])) continue;
if (!folder.Header.Equals(folders[i], i == 0 ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
continue;
folder.IsExpanded = true; // folder found = expand

View File

@@ -1,4 +1,5 @@
using System.Windows;
using System.Windows.Media;
using AdonisUI.Controls;
using FModel.Framework;
using FModel.Views.Resources.Controls;
@@ -21,14 +22,16 @@ namespace FModel.ViewModels.Commands
{
Helper.OpenWindow<AdonisWindow>(contextViewModel.Header + " (Image)", () =>
{
new ImagePopout
var popout = new ImagePopout
{
Title = contextViewModel.Header + " (Image)",
Width = contextViewModel.Image.Width,
Height = contextViewModel.Image.Height,
WindowState = contextViewModel.Image.Height > 1000 ? WindowState.Maximized : WindowState.Normal,
ImageCtrl = {Source = contextViewModel.Image}
}.Show();
};
RenderOptions.SetBitmapScalingMode(popout.ImageCtrl, contextViewModel.ImageRender);
popout.Show();
});
break;
}

View File

@@ -38,6 +38,13 @@ namespace FModel.ViewModels.Commands
public override async void Execute(LoadingModesViewModel contextViewModel, object parameter)
{
if (_applicationView.CUE4Parse.GameDirectory.HasNoFile) return;
if (_applicationView.CUE4Parse.Game == FGame.FortniteGame &&
_applicationView.CUE4Parse.Provider.MappingsContainer == null)
{
FLogger.AppendError();
FLogger.AppendText("Mappings could not get pulled, extracting assets might not work properly. If so, press F12 or please restart.", Constants.WHITE, true);
}
#if DEBUG
var loadingTime = Stopwatch.StartNew();
#endif

View File

@@ -80,6 +80,7 @@ namespace FModel.ViewModels
public class GameDirectoryViewModel : ViewModel
{
public bool HasNoFile => DirectoryFiles.Count < 1;
public readonly ObservableCollection<FileItem> DirectoryFiles;
public ICollectionView DirectoryFilesView { get; }

View File

@@ -55,11 +55,12 @@ namespace FModel.ViewModels
yield return GetUnrealEngineGame("Rosemallow", "\\Indiana\\Content\\Paks");
yield return GetUnrealEngineGame("Catnip", "\\OakGame\\Content\\Paks");
yield return GetUnrealEngineGame("AzaleaAlpha", "\\Prospect\\Content\\Paks");
yield return GetUnrealEngineGame("WorldExplorersLive", "\\WorldExplorers\\Content\\Paks");
yield return GetUnrealEngineGame("WorldExplorersLive", "\\WorldExplorers\\Content");
yield return GetUnrealEngineGame("Newt", "\\g3\\Content\\Paks");
yield return GetUnrealEngineGame("shoebill", "\\SwGame\\Content\\Paks");
yield return GetUnrealEngineGame("Snoek", "\\StateOfDecay2\\Content\\Paks");
yield return GetUnrealEngineGame("a99769d95d8f400baad1f67ab5dfe508", "\\Core\\Platform\\Content\\Paks");
yield return GetUnrealEngineGame("Nebula", "\\BendGame\\Content");
yield return GetRiotGame("VALORANT", "ShooterGame\\Content\\Paks");
yield return new DetectedGame {GameName = "Valorant [LIVE]", GameDirectory = Constants._VAL_LIVE_TRIGGER};
yield return GetMojangGame("MinecraftDungeons", "\\dungeons\\dungeons\\Dungeons\\Content\\Paks");

View File

@@ -1,8 +1,11 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Imaging;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Exports.Engine;
using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.UE4.Objects.Core.i18N;
@@ -18,118 +21,278 @@ using SkiaSharp.HarfBuzz;
namespace FModel.ViewModels
{
public class MapLayer
{
public SKBitmap Layer;
public bool IsEnabled;
}
public enum EWaypointType
{
Parkour,
TimeTrials
}
public class MapViewerViewModel : ViewModel
{
private ThreadWorkerViewModel _threadWorkerView => ApplicationService.ThreadWorkerView;
private DiscordHandler _discordHandler => DiscordService.DiscordHandler;
private bool _showCities;
public bool ShowCities
#region BINDINGS
private bool _brPois;
public bool BrPois
{
get => _showCities;
set => SetProperty(ref _showCities, value, nameof(ShowCities));
get => _brPois;
set => SetProperty(ref _brPois, value, "ApolloGameplay_MapPois");
}
private bool _showLandmarks;
public bool ShowLandmarks
private bool _brLandmarks;
public bool BrLandmarks
{
get => _showLandmarks;
set => SetProperty(ref _showLandmarks, value, nameof(ShowLandmarks));
get => _brLandmarks;
set => SetProperty(ref _brLandmarks, value, "ApolloGameplay_MapLandmarks");
}
private bool _brPatrolsPath;
public bool BrPatrolsPath
{
get => _brPatrolsPath;
set => SetProperty(ref _brPatrolsPath, value, "ApolloGameplay_PatrolsPath");
}
private bool _brUpgradeBenches;
public bool BrUpgradeBenches
{
get => _brUpgradeBenches;
set => SetProperty(ref _brUpgradeBenches, value, "ApolloGameplay_UpgradeBenches");
}
private bool _brPhonebooths;
public bool BrPhonebooths
{
get => _brPhonebooths;
set => SetProperty(ref _brPhonebooths, value, "ApolloGameplay_Phonebooths");
}
private bool _showPatrolPaths;
public bool ShowPatrolPaths
private bool _brTagsLocation;
public bool BrTagsLocation
{
get => _showPatrolPaths;
set => SetProperty(ref _showPatrolPaths, value, nameof(ShowPatrolPaths));
get => _brTagsLocation;
set => SetProperty(ref _brTagsLocation, value, "ApolloGameplay_TagsLocation");
}
private bool _brAlienArtifacts;
public bool BrAlienArtifacts
{
get => _brAlienArtifacts;
set => SetProperty(ref _brAlienArtifacts, value, "ApolloGameplay_AlienArtifacts");
}
private bool _prLandmarks;
public bool PrLandmarks
{
get => _prLandmarks;
set => SetProperty(ref _prLandmarks, value, "PapayaGameplay_MapLandmarks");
}
private bool _prCannonball;
public bool PrCannonball
{
get => _prCannonball;
set => SetProperty(ref _prCannonball, value, "PapayaGameplay_CannonballGame");
}
private bool _prSkydive;
public bool PrSkydive
{
get => _prSkydive;
set => SetProperty(ref _prSkydive, value, "PapayaGameplay_SkydiveGame");
}
private bool _prShootingTargets;
public bool PrShootingTargets
{
get => _prShootingTargets;
set => SetProperty(ref _prShootingTargets, value, "PapayaGameplay_ShootingTargets");
}
private bool _prParkour;
public bool PrParkour
{
get => _prParkour;
set => SetProperty(ref _prParkour, value, "PapayaGameplay_ParkourGame");
}
private bool _prTimeTrials;
public bool PrTimeTrials
{
get => _prTimeTrials;
set => SetProperty(ref _prTimeTrials, value, "PapayaGameplay_TimeTrials");
}
private bool _prVendingMachines;
public bool PrVendingMachines
{
get => _prVendingMachines;
set => SetProperty(ref _prVendingMachines, value, "PapayaGameplay_VendingMachines");
}
private bool _prMusicBlocks;
public bool PrMusicBlocks
{
get => _prMusicBlocks;
set => SetProperty(ref _prMusicBlocks, value, "PapayaGameplay_MusicBlocks");
}
#endregion
#region BITMAP IMAGES
private BitmapImage _brMiniMapImage;
private BitmapImage _prMiniMapImage;
private BitmapImage _mapImage;
public BitmapImage MapImage
{
get => _mapImage;
set => SetProperty(ref _mapImage, value);
}
private BitmapImage _citiesImage;
public BitmapImage CitiesImage
{
get => _citiesImage;
set => SetProperty(ref _citiesImage, value);
}
private BitmapImage _landmarksImage;
public BitmapImage LandmarksImage
private BitmapImage _brLayerImage;
private BitmapImage _prLayerImage;
private BitmapImage _layerImage;
public BitmapImage LayerImage
{
get => _landmarksImage;
set => SetProperty(ref _landmarksImage, value);
get => _layerImage;
set => SetProperty(ref _layerImage, value);
}
private BitmapImage _patrolPathImage;
public BitmapImage PatrolPathImage
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 => _patrolPathImage;
set => SetProperty(ref _patrolPathImage, value);
get => _mapIndex;
set
{
SetProperty(ref _mapIndex, value);
TriggerChange();
}
}
#endregion
private const string _FIRST_BITMAP = "MapCheck";
private readonly Dictionary<string, MapLayer>[] _bitmaps; // first bitmap is the displayed map, others are overlays of the map
private readonly CUE4ParseViewModel _cue4Parse;
public MapViewerViewModel(CUE4ParseViewModel cue4Parse)
{
_bitmaps = new[]
{
new Dictionary<string, MapLayer>(),
new Dictionary<string, MapLayer>()
};
_cue4Parse = cue4Parse;
}
public async void Initialize()
{
Utils.Typefaces ??= new Typefaces(_cue4Parse);
if (MapImage == null && _mapBitmap == null)
{
await LoadMiniMap();
}
_textPaint.Typeface = _fillPaint.Typeface = Utils.Typefaces.Bottom ?? Utils.Typefaces.DisplayName;
await LoadBrMiniMap();
await LoadPrMiniMap();
TriggerChange();
}
public BitmapImage GetImageToSave()
public BitmapImage GetImageToSave() => GetImageSource(GetLayerBitmap(true));
private SKBitmap GetLayerBitmap(bool withMap)
{
var ret = new SKBitmap(_mapBitmap.Width, _mapBitmap.Height, SKColorType.Rgba8888, SKAlphaType.Premul);
var ret = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(ret);
c.DrawBitmap(_mapBitmap, 0, 0);
if (ShowCities)
c.DrawBitmap(_citiesBitmap, 0, 0);
if (ShowLandmarks)
c.DrawBitmap(_landmarksBitmap, 0, 0);
if (ShowPatrolPaths)
c.DrawBitmap(_patrolPathBitmap, 0, 0);
foreach (var (key, value) in _bitmaps[MapIndex])
{
if (!value.IsEnabled || !withMap && key == _FIRST_BITMAP) continue;
c.DrawBitmap(value.Layer, new SKRect(0, 0, _widthHeight, _widthHeight));
}
return GetImageSource(ret);
}
protected override bool SetProperty<T>(ref T storage, T value, string propertyName = null)
{
var ret = base.SetProperty(ref storage, value, propertyName);
CheckForStuffToDraw(propertyName);
return ret;
}
private async void CheckForStuffToDraw(string propertyName = null)
protected override bool SetProperty<T>(ref T storage, T value, string propertyName = null) // don't delete, else nothing will update for some reason
{
switch (propertyName)
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)
{
case nameof(ShowCities) when _citiesBitmap == null && _landmarksBitmap == null && _mapBitmap != null:
{
await LoadCities();
break;
}
case nameof(ShowLandmarks) when _landmarksBitmap == null && _citiesBitmap == null && _mapBitmap != null:
{
await LoadCities();
break;
}
case nameof(ShowPatrolPaths) when _patrolPathBitmap == null && _mapBitmap != null:
{
await LoadPatrolPaths();
break;
}
layer.IsEnabled = enabled;
}
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;
case "ApolloGameplay_UpgradeBenches":
await LoadUpgradeBenches();
break;
case "ApolloGameplay_Phonebooths":
await LoadPhonebooths();
break;
case "ApolloGameplay_AlienArtifacts":
await LoadAlienArtifacts();
break;
case "ApolloGameplay_TagsLocation":
await LoadTagsLocation();
break;
case "PapayaGameplay_CannonballGame":
await LoadCannonballGame();
break;
case "PapayaGameplay_SkydiveGame":
await LoadSkydiveGame();
break;
case "PapayaGameplay_ShootingTargets":
await LoadShootingTargets();
break;
case "PapayaGameplay_ParkourGame":
await LoadWaypoint(EWaypointType.Parkour);
break;
case "PapayaGameplay_TimeTrials":
await LoadWaypoint(EWaypointType.TimeTrials);
break;
case "PapayaGameplay_VendingMachines":
await LoadVendingMachines();
break;
case "PapayaGameplay_MusicBlocks":
await LoadMusicBlocks();
break;
}
_bitmaps[MapIndex][key].IsEnabled = true;
}
switch (MapIndex)
{
case 0:
_brLayerImage = GetImageSource(GetLayerBitmap(false));
break;
case 1:
_prLayerImage = GetImageSource(GetLayerBitmap(false));
break;
}
TriggerChange();
}
private BitmapImage GetImageSource(SKBitmap bitmap)
@@ -145,37 +308,58 @@ namespace FModel.ViewModels
return image;
}
private const int WorldRadius = 135000;
private SKBitmap _mapBitmap;
private SKBitmap _citiesBitmap;
private SKBitmap _landmarksBitmap;
private SKBitmap _patrolPathBitmap;
private readonly SKBitmap _pinBitmap =
SKBitmap.Decode(Application.GetResourceStream(new Uri("pack://application:,,,/Resources/pin.png"))?.Stream);
private readonly SKBitmap _cityPinBitmap =
SKBitmap.Decode(Application.GetResourceStream(new Uri("pack://application:,,,/Resources/city_pin.png"))?.Stream);
private readonly SKPaint _imagePaint = new()
private void TriggerChange()
{
var layerCount = _bitmaps[_mapIndex].Count(x => x.Value.IsEnabled);
var layerString = $"{layerCount} Layer{(layerCount > 1 ? "s" : "")}";
switch (_mapIndex)
{
case 0:
_discordHandler.UpdateButDontSavePresence(null, $"Map Viewer: Battle Royale ({layerString})");
_mapImage = _brMiniMapImage;
_layerImage = _brLayerImage;
break;
case 1:
_discordHandler.UpdateButDontSavePresence(null, $"Map Viewer: Party Royale ({layerString})");
_mapImage = _prMiniMapImage;
_layerImage = _prLayerImage;
break;
}
RaisePropertyChanged(nameof(MapImage));
RaisePropertyChanged(nameof(LayerImage));
}
private readonly SKPaint _textPaint = new()
{
IsAntialias = true, FilterQuality = SKFilterQuality.High,
Color = SKColors.White, TextAlign = SKTextAlign.Center, TextSize = 25,
ImageFilter = SKImageFilter.CreateDropShadow(4, 4, 8, 8, SKColors.Black)
Color = SKColors.White, TextAlign = SKTextAlign.Center, TextSize = 26
};
private readonly SKPaint _fillPaint = new()
{
IsAntialias = true, FilterQuality = SKFilterQuality.High,
IsStroke = true, Color = SKColors.Black, TextSize = 26,
TextAlign = SKTextAlign.Center
};
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 + WorldRadius) / (WorldRadius * 2) * _mapBitmap.Width;
var ny = (1 - (vector.X + WorldRadius) / (WorldRadius * 2)) * _mapBitmap.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 LoadMiniMap()
private async Task LoadBrMiniMap()
{
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) ||
@@ -183,21 +367,39 @@ namespace FModel.ViewModels
!mapMaterial.TryGetValue(out FStructFallback cachedExpressionData, "CachedExpressionData") ||
!cachedExpressionData.TryGetValue(out FStructFallback parameters, "Parameters") ||
!parameters.TryGetValue(out UTexture2D[] textureValues, "TextureValues")) return;
_imagePaint.Typeface = Utils.Typefaces.Bottom ?? Utils.Typefaces.DisplayName;
_mapBitmap = Utils.GetBitmap(textureValues[0]);
MapImage = GetImageSource(_mapBitmap);
_bitmaps[0][_FIRST_BITMAP] = new MapLayer{Layer = Utils.GetBitmap(textureValues[0]), IsEnabled = true};
_brMiniMapImage = GetImageSource(_bitmaps[0][_FIRST_BITMAP].Layer);
});
}
private async Task LoadCities()
private async Task LoadPrMiniMap()
{
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) ||
!mapManager.TryGetValue(out UMaterial mapMaterial, "MapMaterial") ||
mapMaterial.ReferencedTextures.Count < 1) return;
_bitmaps[1][_FIRST_BITMAP] = new MapLayer{Layer = Utils.GetBitmap(mapMaterial.ReferencedTextures[0] as UTexture2D), IsEnabled = true};
_prMiniMapImage = GetImageSource(_bitmaps[1][_FIRST_BITMAP].Layer);
});
}
private async Task LoadQuestIndicatorData()
{
await _threadWorkerView.Begin(_ =>
{
_citiesBitmap = new SKBitmap(_mapBitmap.Width, _mapBitmap.Height, SKColorType.Rgba8888, SKAlphaType.Premul);
_landmarksBitmap = new SKBitmap(_mapBitmap.Width, _mapBitmap.Height, SKColorType.Rgba8888, SKAlphaType.Premul);
using var cities = new SKCanvas(_citiesBitmap);
using var landmarks = new SKCanvas(_landmarksBitmap);
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"))
{
@@ -205,86 +407,435 @@ namespace FModel.ViewModels
{
if (!poiData.TryGetValue(out FSoftObjectPath discoveryQuest, "DiscoveryQuest") ||
!poiData.TryGetValue(out FText text, "Text") || string.IsNullOrEmpty(text.Text) ||
!poiData.TryGetValue(out FVector worldLocation, "WorldLocation")) continue;
!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);
var vector = GetMapPosition(worldLocation);
var shaper = new CustomSKShaper(_textPaint.Typeface);
var shapedText = shaper.Shape(text.Text, _textPaint);
if (discoveryQuest.AssetPathName.Text.Contains("landmarks", StringComparison.OrdinalIgnoreCase))
if (discoverBackend.Text.Contains("papaya", StringComparison.OrdinalIgnoreCase))
{
landmarks.DrawPoint(vector.X, vector.Y, _pathPaint);
landmarks.DrawShapedText(shaper, text.Text, vector.X - shapedText.Points[^1].X / 2, vector.Y - 12.5F, _imagePaint);
_fillPaint.StrokeWidth = 5;
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, _fillPaint);
prLandmarks.DrawShapedText(shaper, text.Text, vector.X - shapedText.Points[^1].X / 2, vector.Y - 12.5F, _textPaint);
}
else if (discoveryQuest.AssetPathName.Text.Contains("landmarks", StringComparison.OrdinalIgnoreCase))
{
_fillPaint.StrokeWidth = 5;
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, _fillPaint);
brLandmarks.DrawShapedText(shaper, text.Text, vector.X - shapedText.Points[^1].X / 2, vector.Y - 12.5F, _textPaint);
}
else
{
cities.DrawPoint(vector.X, vector.Y, _pathPaint);
cities.DrawBitmap(_cityPinBitmap, vector.X - 50, vector.Y - 90, _imagePaint);
cities.DrawShapedText(shaper, text.Text, vector.X - shapedText.Points[^1].X / 2, vector.Y - 12.5F, _imagePaint);
_fillPaint.StrokeWidth = 10;
var vector = GetMapPosition(worldLocation, _brRadius);
pois.DrawShapedText(shaper, text.Text.ToUpperInvariant(), vector.X - shapedText.Points[^1].X / 2, vector.Y, _fillPaint);
pois.DrawShapedText(shaper, text.Text.ToUpperInvariant(), vector.X - shapedText.Points[^1].X / 2, vector.Y, _textPaint);
}
}
}
CitiesImage = GetImageSource(_citiesBitmap);
LandmarksImage = GetImageSource(_landmarksBitmap);
_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 LoadPatrolPaths()
private async Task LoadPatrolsPath()
{
await _threadWorkerView.Begin(_ =>
{
_patrolPathBitmap = new SKBitmap(_mapBitmap.Width, _mapBitmap.Height, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(_patrolPathBitmap);
_fillPaint.StrokeWidth = 5;
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)
var exports = Utils.LoadExports("/NPCLibrary/LevelOverlays/Apollo_Terrain_NPCLibraryS17_Overlay");
foreach (var export in exports)
{
if (!overlay.TryGetValue(out FSoftObjectPath overlayWorld, "OverlayWorld"))
continue;
if (export is not { } uObject) continue;
if (!uObject.ExportType.Equals("FortAthenaPatrolPath", StringComparison.OrdinalIgnoreCase) ||
!uObject.TryGetValue(out FGameplayTagContainer gameplayTags, "GameplayTags") ||
!uObject.TryGetValue(out FPackageIndex[] patrolPoints, "PatrolPoints")) continue;
var exports = Utils.LoadExports(overlayWorld.AssetPathName.Text.SubstringBeforeLast("."));
foreach (var export in exports)
var displayName = gameplayTags.GameplayTags[0].Text.SubstringAfterLast(".");
if (displayName.Equals("Generic", StringComparison.OrdinalIgnoreCase)) 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 (!(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") ||
if (!Utils.TryGetPackageIndexExport(patrolPoints[i], out uObject) ||
!uObject.TryGetValue(out rootComponent, "RootComponent") ||
!Utils.TryGetPackageIndexExport(rootComponent, out uObject) ||
!uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue;
!uObject.TryGetValue(out relativeLocation, "RelativeLocation")) continue;
var path = new SKPath();
var vector = GetMapPosition(relativeLocation);
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);
path.LineTo(vector.X, vector.Y);
}
path.Close();
c.DrawPath(path, _pathPaint);
c.DrawBitmap(_pinBitmap, vector.X - 50, vector.Y - 90, _imagePaint);
c.DrawText(gameplayTags.GameplayTags[0].Text.SubstringAfterLast("."), vector.X, vector.Y - 12.5F, _imagePaint);
vector = GetMapPosition(relativeLocation, _brRadius);
path.LineTo(vector.X, vector.Y);
}
c.DrawPath(path, _pathPaint);
c.DrawText(displayName, vector.X, vector.Y - 12.5F, _fillPaint);
c.DrawText(displayName, vector.X, vector.Y - 12.5F, _textPaint);
}
PatrolPathImage = GetImageSource(_patrolPathBitmap);
_bitmaps[0]["ApolloGameplay_PatrolsPath"] = new MapLayer {Layer = patrolsPathBitmap, IsEnabled = false};
});
}
private async Task LoadCannonballGame()
{
await _threadWorkerView.Begin(_ =>
{
_fillPaint.StrokeWidth = 5;
var cannonballBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(cannonballBitmap);
var exports = Utils.LoadExports("/PapayaGameplay/LevelOverlays/PapayaGameplay_CannonballGame");
foreach (var export in exports)
{
if (export is not { } uObject) continue;
if (!uObject.ExportType.Equals("BP_CannonballGame_Target_C", StringComparison.OrdinalIgnoreCase) &&
!uObject.ExportType.Equals("CannonballGame_VehicleSpawner_C", StringComparison.OrdinalIgnoreCase)) continue;
if (!uObject.TryGetValue(out FPackageIndex rootComponent, "RootComponent") ||
!Utils.TryGetPackageIndexExport(rootComponent, out uObject) ||
!uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue;
var displayName = Utils.GetLocalizedResource("", "D998BEF44F051E0885C6C58565934BEA", "Cannonball");
var vector = GetMapPosition(relativeLocation, _prRadius);
c.DrawPoint(vector.X, vector.Y, _pathPaint);
c.DrawText(displayName, vector.X, vector.Y - 12.5F, _fillPaint);
c.DrawText(displayName, vector.X, vector.Y - 12.5F, _textPaint);
}
_bitmaps[1]["PapayaGameplay_CannonballGame"] = new MapLayer {Layer = cannonballBitmap, IsEnabled = false};
});
}
private async Task LoadSkydiveGame()
{
await _threadWorkerView.Begin(_ =>
{
_fillPaint.StrokeWidth = 5;
var skydiveBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(skydiveBitmap);
var exports = Utils.LoadExports("/PapayaGameplay/LevelOverlays/PapayaGameplay_SkydiveGame");
foreach (var export in exports)
{
if (export is not { } uObject) continue;
if (!uObject.ExportType.Equals("BP_Waypoint_Papaya_Skydive_Start_C", StringComparison.OrdinalIgnoreCase)) continue;
if (!uObject.TryGetValue(out FPackageIndex rootComponent, "RootComponent") ||
!uObject.TryGetValue(out FText minigameActivityName, "MinigameActivityName") ||
!Utils.TryGetPackageIndexExport(rootComponent, out uObject) ||
!uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue;
var vector = GetMapPosition(relativeLocation, _prRadius);
c.DrawPoint(vector.X, vector.Y, _pathPaint);
c.DrawText(minigameActivityName.Text, vector.X, vector.Y - 12.5F, _fillPaint);
c.DrawText(minigameActivityName.Text, vector.X, vector.Y - 12.5F, _textPaint);
}
_bitmaps[1]["PapayaGameplay_SkydiveGame"] = new MapLayer {Layer = skydiveBitmap, IsEnabled = false};
});
}
private async Task LoadShootingTargets()
{
await _threadWorkerView.Begin(_ =>
{
_fillPaint.StrokeWidth = 5;
var shootingTargetsBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(shootingTargetsBitmap);
var bDone = false;
var exports = Utils.LoadExports("/PapayaGameplay/LevelOverlays/PapayaGameplay_ShootingTargets");
foreach (var export in exports)
{
if (export is not { } uObject) continue;
if (!uObject.ExportType.Equals("PapayaShootingTarget_C", StringComparison.OrdinalIgnoreCase)) continue;
if (!uObject.TryGetValue(out FPackageIndex rootComponent, "RootComponent") ||
!Utils.TryGetPackageIndexExport(rootComponent, out uObject) ||
!uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue;
var vector = GetMapPosition(relativeLocation, _prRadius);
c.DrawPoint(vector.X, vector.Y, _pathPaint);
if (bDone) continue;
bDone = true;
c.DrawText("Shooting Target", vector.X, vector.Y - 12.5F, _fillPaint);
c.DrawText("Shooting Target", vector.X, vector.Y - 12.5F, _textPaint);
}
_bitmaps[1]["PapayaGameplay_ShootingTargets"] = new MapLayer {Layer = shootingTargetsBitmap, IsEnabled = false};
});
}
private async Task LoadWaypoint(EWaypointType type)
{
await _threadWorkerView.Begin(_ =>
{
_fillPaint.StrokeWidth = 5;
var waypointBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(waypointBitmap);
string file;
string name;
switch (type)
{
case EWaypointType.Parkour:
file = "PapayaGameplay_ParkourGame";
name = "Parkour";
break;
case EWaypointType.TimeTrials:
file = "PapayaGameplay_TimeTrials";
name = "Time Trials";
break;
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
var path = new SKPath();
var exports = Utils.LoadExports($"/PapayaGameplay/LevelOverlays/{file}");
foreach (var export in exports)
{
if (export is not { } uObject) continue;
if (!uObject.ExportType.Equals("BP_Waypoint_Parent_Papaya_C", StringComparison.OrdinalIgnoreCase)) continue;
if (!uObject.TryGetValue(out FPackageIndex rootComponent, "RootComponent") ||
!Utils.TryGetPackageIndexExport(rootComponent, out UObject root) ||
!root.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue;
var vector = GetMapPosition(relativeLocation, _prRadius);
if (path.IsEmpty || uObject.TryGetValue(out bool startsTrial, "StartsTrial") && startsTrial)
{
path.MoveTo(vector.X, vector.Y);
c.DrawText(name, vector.X, vector.Y - 12.5F, _fillPaint);
c.DrawText(name, vector.X, vector.Y - 12.5F, _textPaint);
}
else if (uObject.TryGetValue(out bool endsTrial, "EndsTrial") && endsTrial)
{
path.LineTo(vector.X, vector.Y);
c.DrawPath(path, _pathPaint);
path = new SKPath();
}
else path.LineTo(vector.X, vector.Y);
}
_bitmaps[1][file] = new MapLayer {Layer = waypointBitmap, IsEnabled = false};
});
}
private async Task LoadVendingMachines()
{
await _threadWorkerView.Begin(_ =>
{
_fillPaint.StrokeWidth = 5;
var set = new HashSet<string>();
var timeTrialsBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(timeTrialsBitmap);
var exports = Utils.LoadExports("/PapayaGameplay/LevelOverlays/PapayaGameplay_VendingMachines");
foreach (var export in exports)
{
if (export is not { } uObject) continue;
if (!uObject.ExportType.Equals("B_Papaya_VendingMachine_Boat_C", StringComparison.OrdinalIgnoreCase) &&
!uObject.ExportType.Equals("B_Papaya_VendingMachine_BoogieBomb_C", StringComparison.OrdinalIgnoreCase) &&
!uObject.ExportType.Equals("B_Papaya_VendingMachine_Burger_C", StringComparison.OrdinalIgnoreCase) &&
!uObject.ExportType.Equals("B_Papaya_VendingMachine_CrashPad_C", StringComparison.OrdinalIgnoreCase) &&
!uObject.ExportType.Equals("B_Papaya_VendingMachine_FishingPole_C", StringComparison.OrdinalIgnoreCase) &&
!uObject.ExportType.Equals("B_Papaya_VendingMachine_Grappler_C", StringComparison.OrdinalIgnoreCase) &&
!uObject.ExportType.Equals("B_Papaya_VendingMachine_Jetpack_C", StringComparison.OrdinalIgnoreCase) &&
!uObject.ExportType.Equals("B_Papaya_VendingMachine_PaintGrenade_Blue_C", StringComparison.OrdinalIgnoreCase) &&
!uObject.ExportType.Equals("B_Papaya_VendingMachine_PaintGrenade_Red_C", StringComparison.OrdinalIgnoreCase) &&
!uObject.ExportType.Equals("B_Papaya_VendingMachine_PaintLauncher_Blue_C", StringComparison.OrdinalIgnoreCase) &&
!uObject.ExportType.Equals("B_Papaya_VendingMachine_PaintLauncher_Red_C", StringComparison.OrdinalIgnoreCase) &&
!uObject.ExportType.Equals("B_Papaya_VendingMachine_PlungerBow_C", StringComparison.OrdinalIgnoreCase) &&
!uObject.ExportType.Equals("B_Papaya_VendingMachine_Quad_C", StringComparison.OrdinalIgnoreCase) &&
!uObject.ExportType.Equals("B_Papaya_VendingMachine_Tomato_C", StringComparison.OrdinalIgnoreCase)) continue;
if (!uObject.TryGetValue(out FPackageIndex rootComponent, "RootComponent") ||
!Utils.TryGetPackageIndexExport(rootComponent, out UObject root) ||
!root.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue;
var name = uObject.ExportType.SubstringAfter("B_Papaya_VendingMachine_").SubstringBeforeLast("_C");
var vector = GetMapPosition(relativeLocation, _prRadius);
c.DrawPoint(vector.X, vector.Y, _pathPaint);
if (!set.Add(name)) continue;
c.DrawText(name, vector.X, vector.Y - 12.5F, _fillPaint);
c.DrawText(name, vector.X, vector.Y - 12.5F, _textPaint);
}
_bitmaps[1]["PapayaGameplay_VendingMachines"] = new MapLayer {Layer = timeTrialsBitmap, IsEnabled = false};
});
}
private async Task LoadMusicBlocks()
{
await _threadWorkerView.Begin(_ =>
{
_fillPaint.StrokeWidth = 5;
var shootingTargetsBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(shootingTargetsBitmap);
var bDone = false;
var exports = Utils.LoadExports("/PapayaGameplay/LevelOverlays/PapayaGameplay_MusicBlocks");
foreach (var export in exports)
{
if (export is not { } uObject) continue;
if (!uObject.ExportType.Equals("MusicBlock_Piano3_Papaya_C", StringComparison.OrdinalIgnoreCase)) continue;
if (!uObject.TryGetValue(out FPackageIndex rootComponent, "RootComponent") ||
!Utils.TryGetPackageIndexExport(rootComponent, out uObject) ||
!uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue;
var vector = GetMapPosition(relativeLocation, _prRadius);
c.DrawPoint(vector.X, vector.Y, _pathPaint);
if (bDone) continue;
bDone = true;
c.DrawText("Music Blocks", vector.X, vector.Y - 12.5F, _fillPaint);
c.DrawText("Music Blocks", vector.X, vector.Y - 12.5F, _textPaint);
}
_bitmaps[1]["PapayaGameplay_MusicBlocks"] = new MapLayer {Layer = shootingTargetsBitmap, IsEnabled = false};
});
}
private async Task LoadUpgradeBenches()
{
await _threadWorkerView.Begin(_ =>
{
_fillPaint.StrokeWidth = 5;
var upgradeBenchesBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(upgradeBenchesBitmap);
var exports = Utils.LoadExports("/NPCLibrary/LevelOverlays/Apollo_Terrain_NPCLibrary_Stations_UpgradeBenches");
foreach (var export in exports)
{
if (export is not { } uObject) continue;
if (!uObject.ExportType.Equals("B_Athena_Spawner_UpgradeStation_C", StringComparison.OrdinalIgnoreCase)) continue;
var displayName = uObject.Name["B_Athena_Spawner_".Length..];
if (!uObject.TryGetValue(out FPackageIndex rootComponent, "RootComponent") ||
!Utils.TryGetPackageIndexExport(rootComponent, out uObject) ||
!uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue;
var vector = GetMapPosition(relativeLocation, _brRadius);
c.DrawPoint(vector.X, vector.Y, _pathPaint);
c.DrawText(displayName, vector.X, vector.Y - 12.5F, _fillPaint);
c.DrawText(displayName, vector.X, vector.Y - 12.5F, _textPaint);
}
_bitmaps[0]["ApolloGameplay_UpgradeBenches"] = new MapLayer {Layer = upgradeBenchesBitmap, IsEnabled = false};
});
}
private async Task LoadPhonebooths()
{
await _threadWorkerView.Begin(_ =>
{
_fillPaint.StrokeWidth = 5;
var phoneboothsBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(phoneboothsBitmap);
var exports = Utils.LoadExports("/NPCLibrary/LevelOverlays/Apollo_Terrain_NPCLibrary_Stations_Phonebooths");
foreach (var export in exports)
{
if (export is not { } uObject) continue;
if (!uObject.ExportType.Equals("B_Athena_Spawner_Payphone_C", StringComparison.OrdinalIgnoreCase)) continue;
var displayName = uObject.Name["B_Athena_Spawner_".Length..];
if (!uObject.TryGetValue(out FPackageIndex rootComponent, "RootComponent") ||
!Utils.TryGetPackageIndexExport(rootComponent, out uObject) ||
!uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue;
var vector = GetMapPosition(relativeLocation, _brRadius);
c.DrawPoint(vector.X, vector.Y, _pathPaint);
c.DrawText(displayName, vector.X, vector.Y - 12.5F, _fillPaint);
c.DrawText(displayName, vector.X, vector.Y - 12.5F, _textPaint);
}
_bitmaps[0]["ApolloGameplay_Phonebooths"] = new MapLayer {Layer = phoneboothsBitmap, IsEnabled = false};
});
}
private async Task LoadAlienArtifacts()
{
await _threadWorkerView.Begin(_ =>
{
_fillPaint.StrokeWidth = 5;
var alienArtifactsBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(alienArtifactsBitmap);
var exports = Utils.LoadExports("/BattlepassS17/Maps/Apollo_ItemCollect_S17_Overlay");
foreach (var export in exports)
{
if (export is not { } uObject) continue;
if (!uObject.ExportType.Equals("BP_S17_AlienArtifact_Variant1_C", StringComparison.OrdinalIgnoreCase)) continue;
var displayName = uObject.Name;
if (!uObject.TryGetValue(out FPackageIndex rootComponent, "RootComponent") ||
!Utils.TryGetPackageIndexExport(rootComponent, out uObject) ||
!uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue;
var vector = GetMapPosition(relativeLocation, _brRadius);
c.DrawPoint(vector.X, vector.Y, _pathPaint);
c.DrawText(displayName, vector.X, vector.Y - 12.5F, _fillPaint);
c.DrawText(displayName, vector.X, vector.Y - 12.5F, _textPaint);
}
_bitmaps[0]["ApolloGameplay_AlienArtifacts"] = new MapLayer {Layer = alienArtifactsBitmap, IsEnabled = false};
});
}
private async Task LoadTagsLocation()
{
await _threadWorkerView.Begin(_ =>
{
_fillPaint.StrokeWidth = 5;
var tagsLocationBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(tagsLocationBitmap);
if (!Utils.TryLoadObject("FortniteGame/Content/Quests/QuestTagToLocationDataRows.QuestTagToLocationDataRows", out UDataTable locationData))
return;
foreach (var (key, uObject) in locationData.RowMap)
{
if (key.Text.StartsWith("Athena.Location.POI", StringComparison.OrdinalIgnoreCase) ||
key.Text.StartsWith("Athena.Location.Unnamed", StringComparison.OrdinalIgnoreCase) ||
key.Text.Contains(".Tandem.", StringComparison.OrdinalIgnoreCase) ||
!uObject.TryGetValue(out FVector worldLocation, "WorldLocation")) continue;
var parts = key.Text.Split('.');
var displayName = parts[^2];
if (!int.TryParse(parts[^1], out var _))
displayName += " " + parts[^1];
var vector = GetMapPosition(worldLocation, _brRadius);
c.DrawPoint(vector.X, vector.Y, _pathPaint);
c.DrawText(displayName, vector.X, vector.Y - 12.5F, _fillPaint);
c.DrawText(displayName, vector.X, vector.Y - 12.5F, _textPaint);
}
_bitmaps[0]["ApolloGameplay_TagsLocation"] = new MapLayer {Layer = tagsLocationBitmap, IsEnabled = false};
});
}
}
}
}

View File

@@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using CUE4Parse.UE4.Versions;
using CUE4Parse_Conversion.Meshes;
using CUE4Parse_Conversion.Textures;
using FModel.Framework;
using FModel.Services;
using FModel.Settings;
@@ -82,6 +84,27 @@ namespace FModel.ViewModels
get => _selectedCosmeticDisplayAsset;
set => SetProperty(ref _selectedCosmeticDisplayAsset, value);
}
private EMeshFormat _selectedMeshExportFormat;
public EMeshFormat SelectedMeshExportFormat
{
get => _selectedMeshExportFormat;
set => SetProperty(ref _selectedMeshExportFormat, value);
}
private ELodFormat _selectedLodExportFormat;
public ELodFormat SelectedLodExportFormat
{
get => _selectedLodExportFormat;
set => SetProperty(ref _selectedLodExportFormat, value);
}
private ETextureFormat _selectedTextureExportFormat;
public ETextureFormat SelectedTextureExportFormat
{
get => _selectedTextureExportFormat;
set => SetProperty(ref _selectedTextureExportFormat, value);
}
public ReadOnlyObservableCollection<EUpdateMode> UpdateModes { get; private set; }
public ReadOnlyObservableCollection<EGame> UeGames { get; private set; }
@@ -93,6 +116,9 @@ namespace FModel.ViewModels
public ReadOnlyObservableCollection<ECompressedAudio> CompressedAudios { get; private set; }
public ReadOnlyObservableCollection<EIconStyle> CosmeticStyles { get; private set; }
public ReadOnlyObservableCollection<EEnabledDisabled> CosmeticDisplayAssets { get; private set; }
public ReadOnlyObservableCollection<EMeshFormat> MeshExportFormats { get; private set; }
public ReadOnlyObservableCollection<ELodFormat> LodExportFormats { get; private set; }
public ReadOnlyObservableCollection<ETextureFormat> TextureExportFormats { get; private set; }
private readonly FGame _game;
private string _outputSnapshot;
@@ -105,6 +131,9 @@ namespace FModel.ViewModels
private ECompressedAudio _compressedAudioSnapshot;
private EIconStyle _cosmeticStyleSnapshot;
private EEnabledDisabled _cosmeticDisplayAssetSnapshot;
private EMeshFormat _meshExportFormatSnapshot;
private ELodFormat _lodExportFormatSnapshot;
private ETextureFormat _textureExportFormatSnapshot;
public SettingsViewModel(FGame game)
{
@@ -123,6 +152,9 @@ namespace FModel.ViewModels
_compressedAudioSnapshot = UserSettings.Default.CompressedAudioMode;
_cosmeticStyleSnapshot = UserSettings.Default.CosmeticStyle;
_cosmeticDisplayAssetSnapshot = UserSettings.Default.CosmeticDisplayAsset;
_meshExportFormatSnapshot = UserSettings.Default.MeshExportFormat;
_lodExportFormatSnapshot = UserSettings.Default.LodExportFormat;
_textureExportFormatSnapshot = UserSettings.Default.TextureExportFormat;
SelectedUpdateMode = _updateModeSnapshot;
SelectedUeGame = _ueGameSnapshot;
@@ -132,6 +164,9 @@ namespace FModel.ViewModels
SelectedCompressedAudio = _compressedAudioSnapshot;
SelectedCosmeticStyle = _cosmeticStyleSnapshot;
SelectedCosmeticDisplayAsset = _cosmeticDisplayAssetSnapshot;
SelectedMeshExportFormat = _meshExportFormatSnapshot;
SelectedLodExportFormat = _lodExportFormatSnapshot;
SelectedTextureExportFormat = _textureExportFormatSnapshot;
SelectedAesReload = UserSettings.Default.AesReload;
SelectedDiscordRpc = UserSettings.Default.DiscordRpc;
@@ -145,6 +180,9 @@ namespace FModel.ViewModels
CompressedAudios = new ReadOnlyObservableCollection<ECompressedAudio>(new ObservableCollection<ECompressedAudio>(EnumerateCompressedAudios()));
CosmeticStyles = new ReadOnlyObservableCollection<EIconStyle>(new ObservableCollection<EIconStyle>(EnumerateCosmeticStyles()));
CosmeticDisplayAssets = new ReadOnlyObservableCollection<EEnabledDisabled>(new ObservableCollection<EEnabledDisabled>(EnumerateEnabledDisabled()));
MeshExportFormats = new ReadOnlyObservableCollection<EMeshFormat>(new ObservableCollection<EMeshFormat>(EnumerateMeshExportFormat()));
LodExportFormats = new ReadOnlyObservableCollection<ELodFormat>(new ObservableCollection<ELodFormat>(EnumerateLodExportFormat()));
TextureExportFormats = new ReadOnlyObservableCollection<ETextureFormat>(new ObservableCollection<ETextureFormat>(EnumerateTextureExportFormat()));
}
public SettingsOut Save()
@@ -167,6 +205,9 @@ namespace FModel.ViewModels
UserSettings.Default.CompressedAudioMode = SelectedCompressedAudio;
UserSettings.Default.CosmeticStyle = SelectedCosmeticStyle;
UserSettings.Default.CosmeticDisplayAsset = SelectedCosmeticDisplayAsset;
UserSettings.Default.MeshExportFormat = SelectedMeshExportFormat;
UserSettings.Default.LodExportFormat = SelectedLodExportFormat;
UserSettings.Default.TextureExportFormat = SelectedTextureExportFormat;
UserSettings.Default.AesReload = SelectedAesReload;
UserSettings.Default.DiscordRpc = SelectedDiscordRpc;
@@ -185,5 +226,8 @@ namespace FModel.ViewModels
private IEnumerable<ECompressedAudio> EnumerateCompressedAudios() => Enum.GetValues(SelectedCompressedAudio.GetType()).Cast<ECompressedAudio>();
private IEnumerable<EIconStyle> EnumerateCosmeticStyles() => Enum.GetValues(SelectedCosmeticStyle.GetType()).Cast<EIconStyle>();
private IEnumerable<EEnabledDisabled> EnumerateEnabledDisabled() => Enum.GetValues(SelectedCosmeticDisplayAsset.GetType()).Cast<EEnabledDisabled>();
private IEnumerable<EMeshFormat> EnumerateMeshExportFormat() => Enum.GetValues(SelectedMeshExportFormat.GetType()).Cast<EMeshFormat>();
private IEnumerable<ELodFormat> EnumerateLodExportFormat() => Enum.GetValues(SelectedLodExportFormat.GetType()).Cast<ELodFormat>();
private IEnumerable<ETextureFormat> EnumerateTextureExportFormat() => Enum.GetValues(SelectedTextureExportFormat.GetType()).Cast<ETextureFormat>();
}
}

View File

@@ -5,6 +5,7 @@ using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using FModel.Extensions;
using FModel.Settings;
@@ -125,6 +126,13 @@ namespace FModel.ViewModels
}
}
private BitmapScalingMode _imageRender = BitmapScalingMode.Linear;
public BitmapScalingMode ImageRender
{
get => _imageRender;
set => SetProperty(ref _imageRender, value);
}
public bool HasImage => Image != null;
public bool ShouldScroll => !string.IsNullOrEmpty(ScrollTrigger);

View File

@@ -96,6 +96,7 @@ namespace FModel.ViewModels
CurrentCancellationTokenSource = null; // kill token
Log.Error("{Exception}", e);
FLogger.AppendError();
FLogger.AppendText(e.Message, Constants.WHITE, true);
return;

View File

@@ -1,6 +1,7 @@
<adonisControls:AdonisWindow x:Class="FModel.Views.About"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:FModel"
xmlns:adonisControls="clr-namespace:AdonisUI.Controls;assembly=AdonisUI"
WindowStartupLocation="CenterScreen" ResizeMode="NoResize"
IconVisibility="Collapsed" Width="500" SizeToContent="Height">
@@ -18,7 +19,7 @@
</adonisControls:AdonisWindow.Resources>
<StackPanel Margin="30 10">
<StackPanel HorizontalAlignment="Center" Margin="0 0 0 30">
<TextBlock Text="404FMode!" FontSize="15" FontWeight="500" Foreground="#9DA3DD" FontStretch="Expanded" />
<TextBlock Text="{Binding Source={x:Static local:Constants.APP_VERSION}, StringFormat={}FModel {0}}" FontSize="15" FontWeight="500" Foreground="#9DA3DD" FontStretch="Expanded" />
<TextBlock Text="Made with &#x2665; by Asval" FontSize="30" FontWeight="700" Foreground="#DAE5F2" HorizontalAlignment="Center" />
</StackPanel>

View File

@@ -30,6 +30,7 @@ namespace FModel.Views
{
_applicationView.AudioPlayer.Stop();
_applicationView.AudioPlayer.Dispose();
DiscordService.DiscordHandler.UpdateToSavedPresence();
}
private void OnDeviceSwap(object sender, SelectionChangedEventArgs e)

View File

@@ -6,7 +6,7 @@
xmlns:adonisControls="clr-namespace:AdonisUI.Controls;assembly=AdonisUI"
xmlns:adonisExtensions="clr-namespace:AdonisUI.Extensions;assembly=AdonisUI"
WindowStartupLocation="CenterScreen" ResizeMode="NoResize" IconVisibility="Collapsed" SizeToContent="Height" Loaded="OnLoaded"
Width="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.35'}">
Width="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.225'}">
<adonisControls:AdonisWindow.Style>
<Style TargetType="adonisControls:AdonisWindow" BasedOn="{StaticResource {x:Type adonisControls:AdonisWindow}}" >
<Setter Property="Title" Value="Backup Manager" />
@@ -19,68 +19,60 @@
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<GroupBox adonisExtensions:LayerExtension.Layer="2" Margin="10" Padding="{adonisUi:Space 0}" Background="Transparent">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" TextAlignment="Center" TextWrapping="Wrap" HorizontalAlignment="Center" VerticalAlignment="Center"
Text="Backups hold an old state of your game. They are used to show new or modified files between the old and current state of your game. When loading files in those modes, make sure to always select a backup older than what your game currently is, else you will simply see nothing." />
<Grid Column="2" HorizontalAlignment="Stretch" VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="5" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3" Click="OnCreateBackupClick"
Style="{DynamicResource {x:Static adonisUi:Styles.AccentButton}}" Content="Create" />
<Separator Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Style="{StaticResource CustomSeparator}" Tag="PRE-CREATED BACKUP FILES" />
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center"
Text="{Binding SelectedBackup.FileSize, Converter={x:Static converters:SizeToStringConverter.Instance}, FallbackValue='Backup', StringFormat={}Backup ({0})}" />
<ComboBox Grid.Row="2" Grid.Column="2" ItemsSource="{Binding BackupsView}" SelectedItem="{Binding SelectedBackup, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding FileName}" TextTrimming="CharacterEllipsis" />
</DataTemplate>
</ComboBox.ItemTemplate>
<ComboBox.Style>
<Style TargetType="ComboBox" BasedOn="{StaticResource {x:Type ComboBox}}">
<Setter Property="IsEnabled" Value="True" />
<Style.Triggers>
<DataTrigger Binding="{Binding BackupsView.Count}" Value="0">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
<Button Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="3" Content="Download" Click="OnDownloadClick">
<Button.Style>
<Style TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="IsEnabled" Value="True" />
<Style.Triggers>
<DataTrigger Binding="{Binding BackupsView.Count}" Value="0">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</Grid>
</Grid>
</GroupBox>
<Grid Column="2" adonisExtensions:LayerExtension.Layer="2" Margin="10" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="10" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="5" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3" TextAlignment="Center" TextWrapping="Wrap" HorizontalAlignment="Center" VerticalAlignment="Center"
Text="Because it looks like it's pretty hard to understand, you don't load backups, you load your files and compare them with the backup. Also, by clicking the download button you don't create your own backup you download a pre-made one. Kinda crazy we have to explain this, who will even read anyway..." />
<Button Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" Click="OnCreateBackupClick"
Style="{DynamicResource {x:Static adonisUi:Styles.AccentButton}}" Content="Create" />
<Separator Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3" Style="{StaticResource CustomSeparator}" Tag="PRE-CREATED BACKUP FILES" />
<TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center"
Text="{Binding SelectedBackup.FileSize, Converter={x:Static converters:SizeToStringConverter.Instance}, FallbackValue='Backup', StringFormat={}Backup ({0})}" />
<ComboBox Grid.Row="4" Grid.Column="2" ItemsSource="{Binding BackupsView}" SelectedItem="{Binding SelectedBackup, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding FileName}" TextTrimming="CharacterEllipsis" />
</DataTemplate>
</ComboBox.ItemTemplate>
<ComboBox.Style>
<Style TargetType="ComboBox" BasedOn="{StaticResource {x:Type ComboBox}}">
<Setter Property="IsEnabled" Value="True" />
<Style.Triggers>
<DataTrigger Binding="{Binding BackupsView.Count}" Value="0">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
<Button Grid.Row="6" Grid.Column="0" Grid.ColumnSpan="3" Content="Download" Click="OnDownloadClick">
<Button.Style>
<Style TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="IsEnabled" Value="True" />
<Style.Triggers>
<DataTrigger Binding="{Binding BackupsView.Count}" Value="0">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</Grid>
</adonisControls:AdonisWindow>

View File

@@ -1,13 +1,14 @@
<adonisControls:AdonisWindow x:Class="FModel.Views.MapViewer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:FModel"
xmlns:controls="clr-namespace:FModel.Views.Resources.Controls"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
xmlns:adonisControls="clr-namespace:AdonisUI.Controls;assembly=AdonisUI"
WindowStartupLocation="CenterScreen" IconVisibility="Collapsed"
WindowStartupLocation="CenterScreen" IconVisibility="Collapsed" SizeToContent="Width" ResizeMode="CanMinimize" Closing="OnClosing"
Height="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenHeight}, Converter={converters:RatioConverter}, ConverterParameter='0.75'}"
Width="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.50'}">
MinWidth="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.50'}">
<adonisControls:AdonisWindow.Style>
<Style TargetType="adonisControls:AdonisWindow" BasedOn="{StaticResource {x:Type adonisControls:AdonisWindow}}" >
<Setter Property="Title" Value="Map Viewer" />
@@ -18,37 +19,106 @@
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
<controls:OnTagDataTemplateSelector x:Key="TagTemplateSelector" />
<DataTemplate x:Key="BrTemplate">
<StackPanel VerticalAlignment="Center" Margin="25 0">
<CheckBox Content="Points Of Interest" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrPois}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
<CheckBox Content="Landmarks" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrLandmarks}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
<CheckBox Content="Patrols Path" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrPatrolsPath}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
<CheckBox Content="Upgrade Benches" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrUpgradeBenches}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
<CheckBox Content="Phonebooths" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrPhonebooths}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
<CheckBox Content="Alien Artifacts" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrAlienArtifacts}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
<CheckBox Content="Tags Location" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrTagsLocation}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="PrTemplate">
<StackPanel VerticalAlignment="Center" Margin="25 0">
<CheckBox Content="Landmarks" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.PrLandmarks}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
<CheckBox Content="Cannonball" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.PrCannonball}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
<CheckBox Content="Skydive" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.PrSkydive}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
<CheckBox Content="Shooting Targets" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.PrShootingTargets}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
<CheckBox Content="Parkour" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.PrParkour}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
<CheckBox Content="Time Trials" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.PrTimeTrials}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
<CheckBox Content="Vending Machines" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.PrVendingMachines}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
<CheckBox Content="Music Blocks" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.PrMusicBlocks}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
</StackPanel>
</DataTemplate>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" MinWidth="300" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" VerticalAlignment="Center" Margin="50">
<CheckBox Content="Show Cities" IsEnabled="{Binding IsReady}" IsChecked="{Binding MapViewer.ShowCities, Mode=TwoWay}"
Margin="0 0 0 10" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" />
<CheckBox Content="Show Landmarks" IsEnabled="{Binding IsReady}" IsChecked="{Binding MapViewer.ShowLandmarks, Mode=TwoWay}"
Margin="0 0 0 10" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" />
<CheckBox Content="Show Patrol Paths" IsEnabled="{Binding IsReady}" IsChecked="{Binding MapViewer.ShowPatrolPaths, Mode=TwoWay}"
Margin="0 0 0 10" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" />
<Button Content="Save Image" IsEnabled="{Binding IsReady}" Click="OnClick" />
</StackPanel>
<Grid Grid.Column="1">
<Grid Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TreeView Grid.Row="0" Grid.RowSpan="3" x:Name="MapTree" SelectedItemChanged="OnSelectedItemChanged"
Background="{DynamicResource {x:Static adonisUi:Brushes.Layer3BackgroundBrush}}">
<TreeViewItem Tag="BrTemplate" IsSelected="True">
<TreeViewItem.Header>
<StackPanel Orientation="Horizontal">
<Viewbox Width="16" Height="16" HorizontalAlignment="Center" Margin="-20 4 7.5 4">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource GliderIcon}" />
</Canvas>
</Viewbox>
<TextBlock Text="Battle Royale" HorizontalAlignment="Left" VerticalAlignment="Center" />
</StackPanel>
</TreeViewItem.Header>
</TreeViewItem>
<TreeViewItem Tag="PrTemplate">
<TreeViewItem.Header>
<StackPanel Orientation="Horizontal">
<Viewbox Width="16" Height="16" HorizontalAlignment="Center" Margin="-20 4 7.5 4">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource AnchorIcon}" />
</Canvas>
</Viewbox>
<TextBlock Text="Party Royale" HorizontalAlignment="Left" VerticalAlignment="Center" />
</StackPanel>
</TreeViewItem.Header>
</TreeViewItem>
</TreeView>
<Grid Grid.Row="1" HorizontalAlignment="Stretch">
<ContentControl ContentTemplateSelector="{StaticResource TagTemplateSelector}" Content="{Binding SelectedItem.Tag, ElementName=MapTree}" />
</Grid>
<Button Grid.Row="2" Content="Save Image" Margin="5" IsEnabled="{Binding IsReady}" VerticalAlignment="Bottom" Click="OnClick" />
</Grid>
<Grid Grid.Column="1" HorizontalAlignment="Stretch">
<controls:MagnifierManager.Magnifier>
<controls:Magnifier Radius="200" ZoomFactor=".4" BorderBrush="{DynamicResource {x:Static adonisUi:Brushes.AccentBrush}}" BorderThickness="1" />
</controls:MagnifierManager.Magnifier>
<TextBlock Text="Map Viewer is loading, please wait..." HorizontalAlignment="Center" VerticalAlignment="Center"/>
<TextBlock Text="Minimap is loading, please wait..." HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Image UseLayoutRounding="True" Source="{Binding MapViewer.MapImage}" HorizontalAlignment="Right" VerticalAlignment="Center"
Visibility="{Binding IsReady, Converter={StaticResource BoolToVisibilityConverter}}" />
<Image UseLayoutRounding="True" Source="{Binding MapViewer.CitiesImage}" HorizontalAlignment="Right" VerticalAlignment="Center"
Visibility="{Binding MapViewer.ShowCities, Converter={StaticResource BoolToVisibilityConverter}}" />
<Image UseLayoutRounding="True" Source="{Binding MapViewer.LandmarksImage}" HorizontalAlignment="Right" VerticalAlignment="Center"
Visibility="{Binding MapViewer.ShowLandmarks, Converter={StaticResource BoolToVisibilityConverter}}" />
<Image UseLayoutRounding="True" Source="{Binding MapViewer.PatrolPathImage}" HorizontalAlignment="Right" VerticalAlignment="Center"
Visibility="{Binding MapViewer.ShowPatrolPaths, Converter={StaticResource BoolToVisibilityConverter}}" />
<Image UseLayoutRounding="True" Source="{Binding MapViewer.LayerImage}" HorizontalAlignment="Right" VerticalAlignment="Center"
Visibility="{Binding IsReady, Converter={StaticResource BoolToVisibilityConverter}}" />
</Grid>
</Grid>
</adonisControls:AdonisWindow>

View File

@@ -1,5 +1,7 @@
using System.IO;
using System.ComponentModel;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Imaging;
using FModel.Extensions;
using FModel.Services;
@@ -23,6 +25,8 @@ namespace FModel.Views
InitializeComponent();
}
private void OnClosing(object sender, CancelEventArgs e) => DiscordService.DiscordHandler.UpdateToSavedPresence();
private void OnClick(object sender, RoutedEventArgs e)
{
if (_applicationView.MapViewer.MapImage == null) return;
@@ -57,5 +61,21 @@ namespace FModel.Views
FLogger.AppendText("Could not save 'MiniMap.png'", Constants.WHITE, true);
}
}
private void OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
var i = 0;
foreach (var item in MapTree.Items)
{
if (item is not TreeViewItem {IsSelected: true})
{
i++;
continue;
}
_applicationView.MapViewer.MapIndex = i;
break;
}
}
}
}

View File

@@ -0,0 +1,121 @@
using System.Collections.Generic;
using ICSharpCode.AvalonEdit;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Folding;
namespace FModel.Views.Resources.Controls
{
public class JsonFoldingStrategies
{
private readonly BraceFoldingStrategy _strategy;
private readonly FoldingManager _foldingManager;
public JsonFoldingStrategies(TextEditor avalonEditor)
{
_foldingManager = FoldingManager.Install(avalonEditor.TextArea);
_strategy = new BraceFoldingStrategy(avalonEditor);
}
public void UpdateFoldings(TextDocument document)
{
_foldingManager.UpdateFoldings(_strategy.UpdateFoldings(document), -1);
}
public void UnfoldAll()
{
if (_foldingManager.AllFoldings == null)
return;
foreach (var folding in _foldingManager.AllFoldings)
{
folding.IsFolded = false;
}
}
public void FoldToggle(int offset)
{
if (_foldingManager.AllFoldings == null)
return;
var foldSection = _foldingManager.GetFoldingsContaining(offset);
if (foldSection.Count > 0)
foldSection[^1].IsFolded = !foldSection[^1].IsFolded;
}
public void FoldToggleAtLevel(int level = 0)
{
if (_foldingManager.AllFoldings == null)
return;
foreach (var folding in _foldingManager.AllFoldings)
{
if (folding.Tag is not CustomNewFolding realFolding) continue;
if (realFolding.Level == level) folding.IsFolded = !folding.IsFolded;
}
}
}
public class BraceFoldingStrategy
{
public BraceFoldingStrategy(TextEditor editor)
{
UpdateFoldings(editor.Document);
}
public IEnumerable<CustomNewFolding> UpdateFoldings(TextDocument document)
{
return CreateNewFoldings(document);
}
public IEnumerable<CustomNewFolding> CreateNewFoldings(ITextSource document)
{
var newFoldings = new List<CustomNewFolding>();
var startOffsets = new Stack<int>();
var lastNewLineOffset = 0;
var level = -1;
for (var i = 0; i < document.TextLength; i++)
{
var c = document.GetCharAt(i);
switch (c)
{
case '{' or '[':
level++;
startOffsets.Push(i);
break;
case '}' or ']' when startOffsets.Count > 0:
{
var startOffset = startOffsets.Pop();
if (startOffset < lastNewLineOffset)
{
newFoldings.Add(new CustomNewFolding(startOffset, i + 1, level));
}
level--;
break;
}
case '\n' or '\r':
lastNewLineOffset = i + 1;
break;
}
}
newFoldings.Sort((a, b) => a.StartOffset.CompareTo(b.StartOffset));
return newFoldings;
}
}
public class CustomNewFolding : NewFolding
{
public int Level { get; }
public CustomNewFolding(int start, int end, int level) : base(start, end)
{
Level = level;
}
public override string ToString()
{
return $"[{Level}] {StartOffset} -> {EndOffset}";
}
}
}

View File

@@ -23,9 +23,9 @@ namespace FModel.Views.Resources.Controls
public AvalonEditor()
{
CommandBindings.Add(new CommandBinding(NavigationCommands.Search, (s, e) => FindNext(e.Parameter != null)));
CommandBindings.Add(new CommandBinding(NavigationCommands.Search, (_, e) => FindNext(e.Parameter != null)));
InitializeComponent();
YesWeEditor = MyAvalonEditor;
YesWeSearch = WpfSuckMyDick;
MyAvalonEditor.TextArea.TextView.ElementGenerators.Add(new GamePathElementGenerator());
@@ -81,7 +81,7 @@ namespace FModel.Views.Resources.Controls
if (sender is not TextEditor avalonEditor || DataContext is not TabItem tabItem ||
avalonEditor.Document == null || string.IsNullOrEmpty(avalonEditor.Document.Text))
return;
avalonEditor.Document.FileName = tabItem.Directory + '/' + tabItem.Header.SubstringBeforeLast('.');
if (!tabItem.ShouldScroll) return;
@@ -95,9 +95,8 @@ namespace FModel.Views.Resources.Controls
{
if (DataContext is not TabItem tabItem || Keyboard.Modifiers != ModifierKeys.Control)
return;
var fontSize = tabItem.FontSize + e.Delta / 50.0;
tabItem.FontSize = fontSize switch
{
< 6 => 6,

View File

@@ -0,0 +1,13 @@
<UserControl x:Class="FModel.Views.Resources.Controls.Breadcrumb"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
DataContextChanged="OnDataContextChanged">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<StackPanel x:Name="InMeDaddy" Orientation="Horizontal" HorizontalAlignment="Right" Height="24" />
</UserControl>

View File

@@ -0,0 +1,70 @@
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using FModel.Services;
namespace FModel.Views.Resources.Controls
{
public partial class Breadcrumb
{
private const string _NAVIGATE_NEXT = "M9.31 6.71c-.39.39-.39 1.02 0 1.41L13.19 12l-3.88 3.88c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0l4.59-4.59c.39-.39.39-1.02 0-1.41L10.72 6.7c-.38-.38-1.02-.38-1.41.01z";
public Breadcrumb()
{
InitializeComponent();
}
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue is not string pathAtThisPoint) return;
InMeDaddy.Children.Clear();
var folders = pathAtThisPoint.Split('/');
for (var i = 0; i < folders.Length; i++)
{
var textBlock = new TextBlock
{
Text = folders[i],
Background = Brushes.Transparent,
Cursor = Cursors.Hand,
Tag = i + 1,
Margin = new Thickness(0, 3, 0, 0)
};
textBlock.MouseUp += OnMouseClick;
InMeDaddy.Children.Add(textBlock);
if (i >= folders.Length - 1) continue;
InMeDaddy.Children.Add(new Viewbox
{
Width = 16,
Height = 16,
HorizontalAlignment = HorizontalAlignment.Center,
Child = new Canvas
{
Width = 24,
Height = 24,
Children = { new Path
{
Fill = Brushes.White,
Data = Geometry.Parse(_NAVIGATE_NEXT)
}}
}
});
}
}
private void OnMouseClick(object sender, MouseButtonEventArgs e)
{
if (sender is not TextBlock {DataContext: string pathAtThisPoint, Tag: int index}) return;
var directory = string.Join('/', pathAtThisPoint.Split('/').Take(index));
if (pathAtThisPoint.Equals(directory)) return;
ApplicationService.ApplicationView.CustomDirectories.GoToCommand.JumpTo(directory);
}
}
}

View File

@@ -5,7 +5,7 @@
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit"
xmlns:adonisControls="clr-namespace:AdonisUI.Controls;assembly=AdonisUI"
WindowStartupLocation="CenterScreen" IconVisibility="Collapsed"
WindowStartupLocation="CenterScreen" IconVisibility="Collapsed" PreviewKeyDown="OnPreviewKeyDown"
Width="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.40'}">
<adonisControls:AdonisWindow.Resources>
<ResourceDictionary>

View File

@@ -15,6 +15,7 @@ namespace FModel.Views.Resources.Controls
private readonly Regex _hexColorRegex = new("\"Hex\": \"(?'target'[0-9A-Fa-f]{3,8})\"$",
RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
private readonly System.Windows.Controls.ToolTip _toolTip = new();
private JsonFoldingStrategies _manager;
public PropertiesPopout(TabItem contextViewModel)
{
@@ -30,6 +31,8 @@ namespace FModel.Views.Resources.Controls
MyAvalonEditor.ScrollToVerticalOffset(contextViewModel.ScrollPosition);
MyAvalonEditor.TextArea.TextView.ElementGenerators.Add(new GamePathElementGenerator());
MyAvalonEditor.TextArea.TextView.ElementGenerators.Add(new HexColorElementGenerator());
_manager = new JsonFoldingStrategies(MyAvalonEditor);
_manager.UpdateFoldings(MyAvalonEditor.Document);
}
private void OnMouseHover(object sender, MouseEventArgs e)
@@ -64,6 +67,22 @@ namespace FModel.Views.Resources.Controls
_ => fontSize
};
}
private void OnPreviewKeyDown(object sender, KeyEventArgs e)
{
switch (e.Key)
{
case Key.J when Keyboard.IsKeyDown(Key.K) && Keyboard.Modifiers.HasFlag(ModifierKeys.Control):
_manager.UnfoldAll();
break;
case Key.L when Keyboard.IsKeyDown(Key.K) && Keyboard.Modifiers.HasFlag(ModifierKeys.Control):
_manager.FoldToggle(MyAvalonEditor.CaretOffset);
break;
case >= Key.D0 and <= Key.D9 when Keyboard.IsKeyDown(Key.K) && Keyboard.Modifiers.HasFlag(ModifierKeys.Control):
_manager.FoldToggleAtLevel(int.Parse(e.Key.ToString()[1].ToString()));
break;
}
}
private void OnMouseHoverStopped(object sender, MouseEventArgs e)
{

View File

@@ -12,14 +12,16 @@ namespace FModel.Views.Resources.Converters
{
return value switch
{
"Newt" => "Spellbreak",
"Nebula" => "Days Gone",
"Fortnite" => "Fortnite",
"VALORANT" => "Valorant",
"Pewee" => "Rogue Company",
"Rosemallow" => "The Outer Worlds",
"Catnip" => "Borderlands 3",
"AzaleaAlpha" => "The Cycle",
"Snoek" => "State of Decay 2",
"Rosemallow" => "The Outer Worlds",
"WorldExplorersLive" => "Battle Breakers",
"Newt" => "Spellbreak",
"VALORANT" => "Valorant",
"MinecraftDungeons" => "Minecraft Dungeons",
"shoebill" => "Star Wars: Jedi Fallen Order",
"a99769d95d8f400baad1f67ab5dfe508" => "Core",

View File

@@ -7,6 +7,7 @@
xmlns:audioControls="clr-namespace:FModel.Views.Resources.Controls.Aup"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
xmlns:avalonedit="http://icsharpcode.net/sharpdevelop/avalonedit"
xmlns:folding="clr-namespace:ICSharpCode.AvalonEdit.Folding;assembly=ICSharpCode.AvalonEdit"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
xmlns:adonisConverters="clr-namespace:AdonisUI.Converters;assembly=AdonisUI"
xmlns:adonisExtensions="clr-namespace:AdonisUI.Extensions;assembly=AdonisUI"
@@ -57,6 +58,8 @@
<Geometry x:Key="AddAudioIcon">M13 10H3c-.55 0-1 .45-1 1s.45 1 1 1h10c.55 0 1-.45 1-1s-.45-1-1-1zm0-4H3c-.55 0-1 .45-1 1s.45 1 1 1h10c.55 0 1-.45 1-1s-.45-1-1-1zm5 8v-3c0-.55-.45-1-1-1s-1 .45-1 1v3h-3c-.55 0-1 .45-1 1s.45 1 1 1h3v3c0 .55.45 1 1 1s1-.45 1-1v-3h3c.55 0 1-.45 1-1s-.45-1-1-1h-3zM3 16h6c.55 0 1-.45 1-1s-.45-1-1-1H3c-.55 0-1 .45-1 1s.45 1 1 1z</Geometry>
<Geometry x:Key="SavePlaylistIcon">M14 6H4c-.55 0-1 .45-1 1s.45 1 1 1h10c.55 0 1-.45 1-1s-.45-1-1-1zm0 4H4c-.55 0-1 .45-1 1s.45 1 1 1h10c.55 0 1-.45 1-1s-.45-1-1-1zM4 16h6c.55 0 1-.45 1-1s-.45-1-1-1H4c-.55 0-1 .45-1 1s.45 1 1 1zM19 6c-1.1 0-2 .9-2 2v6.18c-.31-.11-.65-.18-1-.18-1.84 0-3.28 1.64-2.95 3.54.21 1.21 1.2 2.2 2.41 2.41 1.9.33 3.54-1.11 3.54-2.95V8h2c.55 0 1-.45 1-1s-.45-1-1-1h-2z</Geometry>
<Geometry x:Key="ImageMergerIcon">M10 12c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zM6 8c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 8c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12-8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm-4 8c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm4-4c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-4-4c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-4-4c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z</Geometry>
<Geometry x:Key="GliderIcon">M12,17c-1.1,0-2-0.9-2-2s0.9-2,2-2s2,0.9,2,2S13.1,17,12,17z M17.95,14c-0.52,0-0.94,0.4-0.99,0.92 c-0.2,2.03-1.05,2.68-1.48,3.02C14.68,18.54,14,19,12,19s-2.68-0.46-3.48-1.06c-0.43-0.34-1.28-0.99-1.48-3.02 C6.99,14.4,6.57,14,6.05,14c-0.59,0-1.06,0.51-1,1.09c0.22,2.08,1.07,3.47,2.24,4.41c0.5,0.4,1.1,0.7,1.7,0.9L9,24h6v-3.6 c0.6-0.2,1.2-0.5,1.7-0.9c1.17-0.94,2.03-2.32,2.24-4.41C19.01,14.51,18.53,14,17.95,14z M12,0C5.92,0,1,1.9,1,4.25v3.49 C1,8.55,1.88,9,2.56,8.57C2.7,8.48,2.84,8.39,3,8.31L5,13h2l1.5-6.28C9.6,6.58,10.78,6.5,12,6.5s2.4,0.08,3.5,0.22L17,13h2l2-4.69 c0.16,0.09,0.3,0.17,0.44,0.26C22.12,9,23,8.55,23,7.74V4.25C23,1.9,18.08,0,12,0z M5.88,11.24L4.37,7.69 c0.75-0.28,1.6-0.52,2.53-0.71L5.88,11.24z M18.12,11.24L17.1,6.98c0.93,0.19,1.78,0.43,2.53,0.71L18.12,11.24z</Geometry>
<Geometry x:Key="AnchorIcon">M13,9V7.82C14.16,7.4,15,6.3,15,5c0-1.65-1.35-3-3-3S9,3.35,9,5c0,1.3,0.84,2.4,2,2.82V9H9c-0.55,0-1,0.45-1,1v0 c0,0.55,0.45,1,1,1h2v8.92c-2.22-0.33-4.59-1.68-5.55-3.37l1.14-1.14c0.22-0.22,0.19-0.57-0.05-0.75L3.8,12.6 C3.47,12.35,3,12.59,3,13v2c0,3.88,4.92,7,9,7s9-3.12,9-7v-2c0-0.41-0.47-0.65-0.8-0.4l-2.74,2.05c-0.24,0.18-0.27,0.54-0.05,0.75 l1.14,1.14c-0.96,1.69-3.33,3.04-5.55,3.37V11h2c0.55,0,1-0.45,1-1v0c0-0.55-0.45-1-1-1H13z M12,4c0.55,0,1,0.45,1,1s-0.45,1-1,1 s-1-0.45-1-1S11.45,4,12,4z</Geometry>
<Style x:Key="TabItemFillSpace" TargetType="TabItem" BasedOn="{StaticResource {x:Type TabItem}}">
<Setter Property="Width">
@@ -382,6 +385,9 @@
<DataTrigger Binding="{Binding Header}" Value="Athena">
<Setter TargetName="TreeImage" Property="Source" Value="/FModel;component/Resources/athena.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header, Converter={x:Static converters:CaseInsensitiveStringEqualsConverter.Instance}, ConverterParameter='StateOfDecay2'}" Value="True">
<Setter TargetName="TreeImage" Property="Source" Value="/FModel;component/Resources/stateofdecay2.png" />
</DataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</Setter.Value>
@@ -642,7 +648,7 @@
<controls:AvalonEditor x:Name="DynamicArea" Grid.Column="0" Grid.ColumnSpan="3" DataContext="{Binding SelectedItem, ElementName=TabControlName}" />
<Image Grid.Column="3" Grid.ColumnSpan="2" VerticalAlignment="Center" HorizontalAlignment="Center" Stretch="Uniform"
Visibility="{Binding SelectedItem.HasImage, ElementName=TabControlName, Converter={StaticResource BoolToVisibilityConverter}}"
Source="{Binding SelectedItem.Image, ElementName=TabControlName}">
Source="{Binding SelectedItem.Image, ElementName=TabControlName}" RenderOptions.BitmapScalingMode="{Binding SelectedItem.ImageRender, ElementName=TabControlName}">
<Image.InputBindings>
<MouseBinding MouseAction="LeftDoubleClick" Command="{Binding SelectedItem.ImageCommand, ElementName=TabControlName}" CommandParameter="Open"/>
</Image.InputBindings>
@@ -1630,4 +1636,11 @@
</Setter>
</Style>
<Style TargetType="{x:Type folding:FoldingMargin}">
<Setter Property="FoldingMarkerBrush" Value="#262630" />
<Setter Property="FoldingMarkerBackgroundBrush" Value="#262630" />
<Setter Property="SelectedFoldingMarkerBrush" Value="#808080" />
<Setter Property="SelectedFoldingMarkerBackgroundBrush" Value="#2A2B34" />
</Style>
</ResourceDictionary>

View File

@@ -38,6 +38,8 @@ namespace FModel.Views
MainWindow.YesWeCats.AssetsListName.ItemsSource = null;
var folder = _applicationView.CustomDirectories.GoToCommand.JumpTo(assetItem.FullPath.SubstringBeforeLast('/'));
if (folder == null) return;
MainWindow.YesWeCats.Activate();
do { await Task.Delay(100); } while (MainWindow.YesWeCats.AssetsListName.Items.Count < folder.AssetsList.Assets.Count);
MainWindow.YesWeCats.LeftTabControl.SelectedIndex = 2; // assets tab

View File

@@ -35,6 +35,7 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
@@ -45,11 +46,11 @@
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Output Directory *" VerticalAlignment="Center" Margin="0 0 0 5" />
<TextBox Grid.Row="0" Grid.Column="2" Text="{Binding OutputDirectory, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 0 0 5" />
<TextBox Grid.Row="0" Grid.Column="2" Text="{Binding OutputDirectory, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Margin="0 0 0 5" />
<Button Grid.Row="0" Grid.Column="4" Content="..." HorizontalAlignment="Right" Click="OnBrowseOutput" Margin="0 0 0 5" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="Game Directory *" VerticalAlignment="Center" Margin="0 0 0 5" />
<TextBox Grid.Row="1" Grid.Column="2" Text="{Binding GameDirectory, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 0 0 5" />
<TextBox Grid.Row="1" Grid.Column="2" Text="{Binding GameDirectory, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Margin="0 0 0 5" />
<Button Grid.Row="1" Grid.Column="4" Content="..." HorizontalAlignment="Right" Click="OnBrowseDirectories" Margin="0 0 0 5" />
<TextBlock Grid.Row="2" Grid.Column="0" Text="Update Mode" VerticalAlignment="Center" Margin="0 0 0 5" ToolTip="Receive updates each time a new release is pushed to GitHub&#10;Receive updates each time a new commit is pushed to GitHub" />
@@ -62,8 +63,8 @@
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Grid.Row="3" Grid.Column="0" Text="UE4 Versions *" VerticalAlignment="Center" Margin="0 0 0 5" ToolTip="Override the UE4 version to use when parsing assets" />
<ComboBox Grid.Row="3" Grid.Column="2" Grid.ColumnSpan="3" ItemsSource="{Binding SettingsView.UeGames}" SelectedItem="{Binding SettingsView.SelectedUeGame, Mode=TwoWay}"
<TextBlock Grid.Row="3" Grid.Column="0" Text="Assets Language" VerticalAlignment="Center" Margin="0 0 0 5" ToolTip="Language used and shown when parsing assets" />
<ComboBox Grid.Row="3" Grid.Column="2" Grid.ColumnSpan="3" ItemsSource="{Binding SettingsView.AssetLanguages}" SelectedItem="{Binding SettingsView.SelectedAssetLanguage, Mode=TwoWay}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.SettingsView}}}" Margin="0 0 0 5">
<ComboBox.ItemTemplate>
<DataTemplate>
@@ -72,8 +73,8 @@
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Grid.Row="4" Grid.Column="0" Text="UE4 Sub-Versions *" VerticalAlignment="Center" Margin="0 0 0 5" ToolTip="Override the UE4 sub-version to use when parsing assets" />
<ComboBox Grid.Row="4" Grid.Column="2" Grid.ColumnSpan="3" ItemsSource="{Binding SettingsView.UeVersions}" SelectedItem="{Binding SettingsView.SelectedUeVersion, Mode=TwoWay}"
<TextBlock Grid.Row="4" Grid.Column="0" Text="Discord Rich Presence" VerticalAlignment="Center" Margin="0 0 0 5" />
<ComboBox Grid.Row="4" Grid.Column="2" Grid.ColumnSpan="3" ItemsSource="{Binding SettingsView.DiscordRpcs}" SelectedItem="{Binding SettingsView.SelectedDiscordRpc, Mode=TwoWay}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.SettingsView}}}" Margin="0 0 0 5">
<ComboBox.ItemTemplate>
<DataTemplate>
@@ -82,8 +83,10 @@
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Grid.Row="5" Grid.Column="0" Text="Assets Language" VerticalAlignment="Center" Margin="0 0 0 5" ToolTip="Language used and shown when parsing assets" />
<ComboBox Grid.Row="5" Grid.Column="2" Grid.ColumnSpan="3" ItemsSource="{Binding SettingsView.AssetLanguages}" SelectedItem="{Binding SettingsView.SelectedAssetLanguage, Mode=TwoWay}"
<Separator Grid.Row="5" Grid.Column="0" Grid.ColumnSpan="5" Style="{StaticResource CustomSeparator}" Tag="ADVANCED"></Separator>
<TextBlock Grid.Row="6" Grid.Column="0" Text="UE4 Versions *" VerticalAlignment="Center" Margin="0 0 0 5" ToolTip="Override the UE4 version to use when parsing assets" />
<ComboBox Grid.Row="6" Grid.Column="2" Grid.ColumnSpan="3" ItemsSource="{Binding SettingsView.UeGames}" SelectedItem="{Binding SettingsView.SelectedUeGame, Mode=TwoWay}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.SettingsView}}}" Margin="0 0 0 5">
<ComboBox.ItemTemplate>
<DataTemplate>
@@ -92,8 +95,8 @@
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Grid.Row="6" Grid.Column="0" Text="Keep Directory Structure" VerticalAlignment="Center" Margin="0 0 0 5" ToolTip="Auto Export &#38; Save assets inside their game directory" />
<ComboBox Grid.Row="6" Grid.Column="2" Grid.ColumnSpan="3" ItemsSource="{Binding SettingsView.DirectoryStructures}" SelectedItem="{Binding SettingsView.SelectedDirectoryStructure, Mode=TwoWay}"
<TextBlock Grid.Row="7" Grid.Column="0" Text="UE4 Sub-Versions *" VerticalAlignment="Center" Margin="0 0 0 5" ToolTip="Override the UE4 sub-version to use when parsing assets" />
<ComboBox Grid.Row="7" Grid.Column="2" Grid.ColumnSpan="3" ItemsSource="{Binding SettingsView.UeVersions}" SelectedItem="{Binding SettingsView.SelectedUeVersion, Mode=TwoWay}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.SettingsView}}}" Margin="0 0 0 5">
<ComboBox.ItemTemplate>
<DataTemplate>
@@ -102,8 +105,8 @@
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Grid.Row="7" Grid.Column="0" Text="Compressed Audio" VerticalAlignment="Center" Margin="0 0 0 5" ToolTip="What to do when encountering a compressed audio file" />
<ComboBox Grid.Row="7" Grid.Column="2" Grid.ColumnSpan="3" ItemsSource="{Binding SettingsView.CompressedAudios}" SelectedItem="{Binding SettingsView.SelectedCompressedAudio, Mode=TwoWay}"
<TextBlock Grid.Row="8" Grid.Column="0" Text="Keep Directory Structure" VerticalAlignment="Center" Margin="0 0 0 5" ToolTip="Auto Export &#38; Save assets inside their game directory" />
<ComboBox Grid.Row="8" Grid.Column="2" Grid.ColumnSpan="3" ItemsSource="{Binding SettingsView.DirectoryStructures}" SelectedItem="{Binding SettingsView.SelectedDirectoryStructure, Mode=TwoWay}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.SettingsView}}}" Margin="0 0 0 5">
<ComboBox.ItemTemplate>
<DataTemplate>
@@ -112,7 +115,17 @@
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Grid.Row="8" Grid.Column="0" Text="AES Reload at Launch" VerticalAlignment="Center" Margin="0 0 0 5"
<TextBlock Grid.Row="9" Grid.Column="0" Text="Compressed Audio" VerticalAlignment="Center" Margin="0 0 0 5" ToolTip="What to do when encountering a compressed audio file" />
<ComboBox Grid.Row="9" Grid.Column="2" Grid.ColumnSpan="3" ItemsSource="{Binding SettingsView.CompressedAudios}" SelectedItem="{Binding SettingsView.SelectedCompressedAudio, Mode=TwoWay}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.SettingsView}}}" Margin="0 0 0 5">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={x:Static converters:EnumToStringConverter.Instance}}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Grid.Row="10" Grid.Column="0" Text="AES Reload at Launch" VerticalAlignment="Center" Margin="0 0 0 5"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.SettingsView}}}">
<TextBlock.Style>
<Style TargetType="TextBlock" BasedOn="{StaticResource {x:Type TextBlock}}">
@@ -125,7 +138,7 @@
</Style>
</TextBlock.Style>
</TextBlock>
<ComboBox Grid.Row="8" Grid.Column="2" Grid.ColumnSpan="3" ItemsSource="{Binding SettingsView.AesReloads}" SelectedItem="{Binding SettingsView.SelectedAesReload, Mode=TwoWay}"
<ComboBox Grid.Row="10" Grid.Column="2" Grid.ColumnSpan="3" ItemsSource="{Binding SettingsView.AesReloads}" SelectedItem="{Binding SettingsView.SelectedAesReload, Mode=TwoWay}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.SettingsView}}}" Margin="0 0 0 5">
<ComboBox.Style>
<Style TargetType="ComboBox" BasedOn="{StaticResource {x:Type ComboBox}}">
@@ -143,16 +156,6 @@
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Grid.Row="9" Grid.Column="0" Text="Discord Rich Presence" VerticalAlignment="Center" Margin="0 0 0 5" />
<ComboBox Grid.Row="9" Grid.Column="2" Grid.ColumnSpan="3" ItemsSource="{Binding SettingsView.DiscordRpcs}" SelectedItem="{Binding SettingsView.SelectedDiscordRpc, Mode=TwoWay}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.SettingsView}}}" Margin="0 0 0 5">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={x:Static converters:EnumToStringConverter.Instance}}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</DataTemplate>
<DataTemplate x:Key="CreatorTemplate">
@@ -225,6 +228,58 @@
</Grid>
</Grid>
</DataTemplate>
<DataTemplate x:Key="ModelsTemplate">
<Grid adonisExtensions:LayerExtension.Layer="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Mesh Export Format" VerticalAlignment="Center" Margin="0 0 0 5" />
<ComboBox Grid.Row="0" Grid.Column="2" ItemsSource="{Binding SettingsView.MeshExportFormats}" SelectedItem="{Binding SettingsView.SelectedMeshExportFormat, Mode=TwoWay}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.SettingsView}}}" Margin="0 0 0 5">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={x:Static converters:EnumToStringConverter.Instance}}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Level Of Detail Export Format" VerticalAlignment="Center" Margin="0 0 0 5" />
<ComboBox Grid.Row="1" Grid.Column="2" ItemsSource="{Binding SettingsView.LodExportFormats}" SelectedItem="{Binding SettingsView.SelectedLodExportFormat, Mode=TwoWay}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.SettingsView}}}" Margin="0 0 0 5">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={x:Static converters:EnumToStringConverter.Instance}}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Texture Export Format" VerticalAlignment="Center" Margin="0 0 0 5" />
<ComboBox Grid.Row="2" Grid.Column="2" ItemsSource="{Binding SettingsView.TextureExportFormats}" SelectedItem="{Binding SettingsView.SelectedTextureExportFormat, Mode=TwoWay}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.SettingsView}}}" Margin="0 0 0 5">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={x:Static converters:EnumToStringConverter.Instance}}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</DataTemplate>
<DataTemplate x:Key="KeybindingsTemplate">
<Grid adonisExtensions:LayerExtension.Layer="2">
<Grid.RowDefinitions>
@@ -287,12 +342,12 @@
<TextBlock Grid.Row="10" Grid.Column="0" Text="Auto Save Textures *" VerticalAlignment="Center" Margin="0 0 0 5" />
<controls:HotkeyTextBox Grid.Row="10" Grid.Column="2" Style="{StaticResource TextBoxDefaultStyle}" Margin="0 0 0 5"
HotKey="{Binding AutoSaveTextures, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" />
<!-- <TextBlock Grid.Row="11" Grid.Column="0" Text="Auto Save Meshes *" VerticalAlignment="Center" Margin="0 0 0 5" /> -->
<!-- <controls:HotkeyTextBox Grid.Row="11" Grid.Column="2" Style="{StaticResource TextBoxDefaultStyle}" Margin="0 0 0 5" -->
<!-- HotKey="{Binding AutoSaveMeshes, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" /> -->
<TextBlock Grid.Row="12" Grid.Column="0" Text="Auto Save Materials *" VerticalAlignment="Center" Margin="0 0 0 5" />
<controls:HotkeyTextBox Grid.Row="12" Grid.Column="2" Style="{StaticResource TextBoxDefaultStyle}" Margin="0 0 0 5"
<TextBlock Grid.Row="11" Grid.Column="0" Text="Auto Save Materials *" VerticalAlignment="Center" Margin="0 0 0 5" />
<controls:HotkeyTextBox Grid.Row="11" Grid.Column="2" Style="{StaticResource TextBoxDefaultStyle}" Margin="0 0 0 5"
HotKey="{Binding AutoSaveMaterials, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" />
<TextBlock Grid.Row="12" Grid.Column="0" Text="Auto Save Meshes *" VerticalAlignment="Center" Margin="0 0 0 5" />
<controls:HotkeyTextBox Grid.Row="12" Grid.Column="2" Style="{StaticResource TextBoxDefaultStyle}" Margin="0 0 0 5"
HotKey="{Binding AutoSaveMeshes, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" />
<TextBlock Grid.Row="13" Grid.Column="0" Text="Auto Open Sounds *" VerticalAlignment="Center" />
<controls:HotkeyTextBox Grid.Row="13" Grid.Column="2" Style="{StaticResource TextBoxDefaultStyle}"
HotKey="{Binding AutoOpenSounds, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" />
@@ -362,6 +417,22 @@
</Style>
</TreeViewItem.Style>
</TreeViewItem>
<TreeViewItem Tag="ModelsTemplate">
<TreeViewItem.Header>
<StackPanel Orientation="Horizontal">
<Viewbox Width="16" Height="16" HorizontalAlignment="Center" Margin="-20 4 7.5 4">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="M2,6c0.55,0,1-0.45,1-1V4c0-0.55,0.45-1,1-1h1c0.55,0,1-0.45,1-1S5.55,1,5,1H4C2.34,1,1,2.34,1,4v1C1,5.55,1.45,6,2,6z" />
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="M5,21H4c-0.55,0-1-0.45-1-1v-1c0-0.55-0.45-1-1-1c-0.55,0-1,0.45-1,1v1c0,1.66,1.34,3,3,3h1c0.55,0,1-0.45,1-1 S5.55,21,5,21z" />
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="M20,1h-1c-0.55,0-1,0.45-1,1s0.45,1,1,1h1c0.55,0,1,0.45,1,1v1c0,0.55,0.45,1,1,1c0.55,0,1-0.45,1-1V4 C23,2.34,21.66,1,20,1z" />
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="M22,18c-0.55,0-1,0.45-1,1v1c0,0.55-0.45,1-1,1h-1c-0.55,0-1,0.45-1,1s0.45,1,1,1h1c1.66,0,3-1.34,3-3v-1 C23,18.45,22.55,18,22,18z" />
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="M19,14.87V9.13c0-0.72-0.38-1.38-1-1.73l-5-2.88c-0.31-0.18-0.65-0.27-1-0.27s-0.69,0.09-1,0.27L6,7.39 C5.38,7.75,5,8.41,5,9.13v5.74c0,0.72,0.38,1.38,1,1.73l5,2.88c0.31,0.18,0.65,0.27,1,0.27s0.69-0.09,1-0.27l5-2.88 C18.62,16.25,19,15.59,19,14.87z M11,17.17l-4-2.3v-4.63l4,2.33V17.17z M12,10.84L8.04,8.53L12,6.25l3.96,2.28L12,10.84z M17,14.87l-4,2.3v-4.6l4-2.33V14.87z" />
</Canvas>
</Viewbox>
<TextBlock Text="Models" HorizontalAlignment="Left" VerticalAlignment="Center" />
</StackPanel>
</TreeViewItem.Header>
</TreeViewItem>
<TreeViewItem Tag="KeybindingsTemplate">
<TreeViewItem.Header>
<StackPanel Orientation="Horizontal">

View File

@@ -31,7 +31,7 @@ namespace FModel.Views
{
var whatShouldIDo = _applicationView.SettingsView.Save();
if (whatShouldIDo == SettingsOut.Restart)
_applicationView.Restart();
_applicationView.RestartWithWarning();
Close();

View File

@@ -18,4 +18,13 @@ To use FModel, you need to have **[.NET 5](https://dotnet.microsoft.com/download
- [@GMatrix](https://github.com/GMatrixGames)
- [@Tiger](https://github.com/XTigerHyperX)
## Support
[![Donate](https://img.shields.io/badge/Paypal-Donate-00457C.svg?logo=paypal)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=EP9SSWG8MW4UC&source=url)
<p>This project is supported by:</p>
<p>
<a href="https://www.jetbrains.com/">
<img src="https://cdn.fmodel.app/i/svg/jetbrains.svg" width="256px">
</a>
</p>