Skip to content

Commit

Permalink
Merge branch 'hotfix-0.9.1'
Browse files Browse the repository at this point in the history
  • Loading branch information
focustense committed Aug 25, 2021
2 parents 4422b53 + c3b4a96 commit 5ff0080
Show file tree
Hide file tree
Showing 13 changed files with 134 additions and 46 deletions.
4 changes: 2 additions & 2 deletions Focus.Apps.EasyNpc/Main/LoaderModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ public LoaderTasks Complete()
log.Information("Load order confirmed");
var modRepositoryTask = modRepositoryConfigureTask.ContinueWith(_ => modRepository as IModRepository);
var loadOrderAnalysisTask = AnalyzeLoadOrder();
var profileTask = loadOrderAnalysisTask
.ContinueWith(t => profileFactory.RestoreSaved(t.Result, out _, out _));
var profileTask = Task.WhenAll(loadOrderAnalysisTask, modRepositoryTask)
.ContinueWith(t => profileFactory.RestoreSaved(loadOrderAnalysisTask.Result, out _, out _));

return new LoaderTasks
{
Expand Down
36 changes: 23 additions & 13 deletions Focus.Apps.EasyNpc/Profiles/LineupBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ public interface ILineupBuilder

public class LineupBuilder : ILineupBuilder, IDisposable
{
// Placeholder mod for tracking base game plugins.
// Placeholders for tracking base game content.
private static readonly ModComponentInfo BaseGameComponent = new(
new ModLocatorKey(string.Empty, "Vanilla"), string.Empty, "Vanilla", string.Empty);
private static readonly ModInfo BaseGameMod = new(string.Empty, "Vanilla");

private readonly Subject<bool> disposed = new();
Expand Down Expand Up @@ -66,33 +68,39 @@ public async IAsyncEnumerable<Mugshot> Build(INpcBasicInfo npc, IEnumerable<stri
var modNamesFromPlugins = new HashSet<string>(StringComparer.CurrentCultureIgnoreCase);
var pluginGroups = affectingPlugins
.SelectMany(p => loadOrderGraph.IsImplicit(p) ?
new[] { new { Plugin = p, ModKey = BaseGameMod as IModLocatorKey } } :
modRepository.SearchForFiles(p, false).Select(x => new { Plugin = p, x.ModKey }))
.GroupBy(x => x.ModKey, x => x.Plugin, ModLocatorKeyComparer.Default)
.Select(g => new { ModKey = g.Key, Plugins = g });
new[] { new { Plugin = p, ModComponent = BaseGameComponent, ModKey = BaseGameMod as IModLocatorKey } } :
modRepository.SearchForFiles(p, false).Select(x => new { Plugin = p, x.ModComponent, x.ModKey }))
.GroupBy(x => x.ModKey, ModLocatorKeyComparer.Default)
.Select(g => new
{
ModKey = g.Key,
Plugins = g.Select(x => x.Plugin),
Components = g.Select(x => x.ModComponent)
});
foreach (var pluginGroup in pluginGroups)
{
var mugshotFile = GetMugshotFile(mugshotFiles, pluginGroup.ModKey, npc.IsFemale, modSynonyms);
if (!string.IsNullOrEmpty(mugshotFile.TargetModName))
includedMugshotModNames.Add(mugshotFile.TargetModName);
if (!pluginGroup.ModKey.IsEmpty() && pluginGroup.ModKey != BaseGameMod)
modNamesFromPlugins.Add(pluginGroup.ModKey.Name);
yield return CreateMugshotModel(mugshotFile, pluginGroup.ModKey, pluginGroup.Plugins);
yield return CreateMugshotModel(
mugshotFile, pluginGroup.ModKey, pluginGroup.Components, pluginGroup.Plugins);
}

var faceGenPath = FileStructure.GetFaceMeshFileName(npc.BasePluginName, npc.LocalFormIdHex);
var facegenMods = modRepository.SearchForFiles(faceGenPath, false)
.Select(x => x.ModKey)
var facegenGroups = modRepository.SearchForFiles(faceGenPath, true)
.GroupBy(x => x.ModKey, x => x.ModComponent)
// We shouldn't need to check for synonyms or do anything with IDs here, because the included mugshot
// mod names originally come from the mod repo, and so do the facegen mods here. Their names can't be
// different unless the repo itself is broken, or incorrectly mocked in a test.
.Where(x => !modNamesFromPlugins.Contains(x.Name));
foreach (var facegenMod in facegenMods)
.Where(x => !modNamesFromPlugins.Contains(x.Key.Name));
foreach (var facegenGroup in facegenGroups)
{
var mugshotFile = GetMugshotFile(mugshotFiles, facegenMod, npc.IsFemale, modSynonyms);
var mugshotFile = GetMugshotFile(mugshotFiles, facegenGroup.Key, npc.IsFemale, modSynonyms);
if (!string.IsNullOrEmpty(mugshotFile.TargetModName))
includedMugshotModNames.Add(mugshotFile.TargetModName);
yield return CreateMugshotModel(mugshotFile, facegenMod);
yield return CreateMugshotModel(mugshotFile, facegenGroup.Key, facegenGroup);
}

var extraMugshotFiles = mugshotFiles
Expand All @@ -117,11 +125,13 @@ protected virtual void Dispose(bool disposing)
}

private Mugshot CreateMugshotModel(
MugshotFile file, IModLocatorKey? modKey, IEnumerable<string>? plugins = null)
MugshotFile file, IModLocatorKey? modKey, IEnumerable<ModComponentInfo>? components = null,
IEnumerable<string>? plugins = null)
{
var mod = ResolveMod(modKey);
return new Mugshot
{
InstalledComponents = (components ?? Enumerable.Empty<ModComponentInfo>()).ToList().AsReadOnly(),
InstalledMod = mod,
InstalledPlugins = (plugins ?? Enumerable.Empty<string>()).ToList().AsReadOnly(),
IsPlaceholder = file.IsPlaceholder,
Expand Down
2 changes: 2 additions & 0 deletions Focus.Apps.EasyNpc/Profiles/Mugshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ namespace Focus.Apps.EasyNpc.Profiles
{
public class Mugshot
{
public IReadOnlyList<ModComponentInfo> InstalledComponents { get; init; } =
new List<ModComponentInfo>().AsReadOnly();
public ModInfo? InstalledMod { get; init; }
public IReadOnlyList<string> InstalledPlugins { get; init; } = new List<string>().AsReadOnly();
public bool IsPlaceholder { get; init; }
Expand Down
2 changes: 2 additions & 0 deletions Focus.Apps.EasyNpc/Profiles/MugshotViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;

namespace Focus.Apps.EasyNpc.Profiles
{
Expand All @@ -10,6 +11,7 @@ public class MugshotViewModel : INotifyPropertyChanged
public IReadOnlyList<string> InstalledPlugins => mugshot.InstalledPlugins;
public bool IsFocused { get; set; }
public bool IsHighlighted { get; set; }
public bool IsModDisabled => IsModInstalled && !mugshot.InstalledComponents.Any(x => x.IsEnabled);
public bool IsModInstalled => mugshot.InstalledMod is not null;
public bool IsPluginLoaded => mugshot.InstalledPlugins.Count > 0;
public bool IsSelectedSource { get; set; }
Expand Down
1 change: 1 addition & 0 deletions Focus.Apps.EasyNpc/Profiles/MugshotViewer.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<Run FontWeight="SemiBold" Text="{Binding Path=ModName, Mode=OneWay}"/>
</TextBlock>
<TextBlock Foreground="Red" Text="Mod not installed" Visibility="{Binding IsModInstalled, Converter={StaticResource InvBoolToVisibility}}"/>
<TextBlock Foreground="Red" Text="Mod disabled" Visibility="{Binding IsModDisabled, Converter={StaticResource BoolToVisibility}}"/>
<ItemsControl ItemsSource="{Binding Path=InstalledPlugins}" Visibility="{Binding IsPluginLoaded, Converter={StaticResource BoolToVisibility}}">
<ItemsControl.ItemTemplate>
<DataTemplate>
Expand Down
45 changes: 37 additions & 8 deletions Focus.Apps.EasyNpc/Profiles/Npc.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Focus.Analysis.Plugins;
using Focus.Analysis.Records;
using Focus.Apps.EasyNpc.GameData.Files;
using Focus.ModManagers;
using System;
using System.Collections.Generic;
Expand All @@ -14,6 +15,9 @@ public enum ChangeResult { OK, Invalid, Redundant }
public string BasePluginName => records.Key.BasePluginName;
public string DescriptiveLabel => $"{EditorId} '{Name}' ({LocalFormIdHex}:{BasePluginName})";
public string EditorId => records.Master.EditorId;
public bool HasAvailableFaceCustomizations =>
Options.Any(x => x.Analysis.ComparisonToBase?.ModifiesFace == true) || HasAvailableModdedFaceGens;
public bool HasAvailableModdedFaceGens => hasAvailableModdedFaceGens.Value;
public string LocalFormIdHex => records.Key.LocalFormIdHex;
public string Name => records.Master.Name;
public bool SupportsFaceGen => records.Master.CanUseFaceGen;
Expand All @@ -29,6 +33,7 @@ public enum ChangeResult { OK, Invalid, Redundant }
public string? MissingFacePluginName { get; private set; }
public IReadOnlyList<NpcOption> Options { get; private init; }

private readonly Lazy<bool> hasAvailableModdedFaceGens;
private readonly IModRepository modRepository;
private readonly IProfilePolicy policy;
private readonly IProfileEventLog profileEventLog;
Expand All @@ -43,6 +48,18 @@ public Npc(
this.profileEventLog = profileEventLog;
this.records = records;

hasAvailableModdedFaceGens = new(() =>
{
var masterComponentNames = modRepository.SearchForFiles(records.Master.BasePluginName, false)
.Select(x => x.ModComponent.Name)
.ToHashSet(StringComparer.CurrentCultureIgnoreCase);
var faceGenPath = FileStructure.GetFaceMeshFileName(this);
// Vanilla BSAs aren't in the mod directory, so all results below are actually modded.
return modRepository.SearchForFiles(faceGenPath, true)
.Where(x => !masterComponentNames.Contains(x.ModComponent.Name))
.Any();
}, true);

Options = records
.Select(x => new NpcOption(x, baseGamePluginNames.Contains(x.PluginName)))
.ToList()
Expand Down Expand Up @@ -104,13 +121,18 @@ public bool IsFacePlugin(string pluginName)
return FaceOption.PluginName.Equals(pluginName, StringComparison.CurrentCultureIgnoreCase);
}

public ChangeResult SetDefaultOption(string pluginName)
public ChangeResult SetDefaultOption(string pluginName, bool asFallback = false)
{
var option = FindOption(pluginName);
if (option is not null && option != DefaultOption)
if (option is not null && (option != DefaultOption || !string.IsNullOrEmpty(MissingDefaultPluginName)))
{
LogProfileEvent(NpcProfileField.DefaultPlugin, DefaultOption.PluginName, option.PluginName);
var oldPluginName = !string.IsNullOrEmpty(MissingDefaultPluginName) ?
MissingDefaultPluginName : DefaultOption.PluginName;
if (!asFallback)
LogProfileEvent(NpcProfileField.DefaultPlugin, oldPluginName, option.PluginName);
DefaultOption = option;
if (!asFallback)
MissingDefaultPluginName = null;
return ChangeResult.OK;
}
else if (option is null)
Expand All @@ -122,21 +144,28 @@ public ChangeResult SetFaceMod(string modName)
{
var mod = ModLocatorKey.TryParse(modName, out var key) ?
modRepository.FindByKey(key) : modRepository.GetByName(modName);
if (mod is null)
if (mod is null || !modRepository.ContainsFile(mod, FileStructure.GetFaceMeshFileName(this), true))
return ChangeResult.Invalid;
if (modRepository.ContainsFile(mod, FaceOption.PluginName, false))
if ((FaceGenOverride is not null && FaceGenOverride.IncludesName(modName)) ||
modRepository.ContainsFile(mod, FaceOption.PluginName, false))
return ChangeResult.Redundant;
var bestOption = Options.LastOrDefault(x => modRepository.ContainsFile(mod, x.PluginName, false));
return bestOption is not null ? SetFaceOption(bestOption.PluginName) : SetFaceGenOverride(mod);
}

public ChangeResult SetFaceOption(string pluginName, bool keepFaceGenMod = false)
public ChangeResult SetFaceOption(string pluginName, bool keepFaceGenMod = false, bool asFallback = false)
{
var option = FindOption(pluginName);
if (option is not null && option != FaceOption)
if (option is not null &&
(option != FaceOption || FaceGenOverride is not null || !string.IsNullOrEmpty(MissingFacePluginName)))
{
LogProfileEvent(NpcProfileField.FacePlugin, FaceOption.PluginName, option.PluginName);
var oldPluginName = !string.IsNullOrEmpty(MissingFacePluginName) ?
MissingFacePluginName : DefaultOption.PluginName;
if (!asFallback)
LogProfileEvent(NpcProfileField.FacePlugin, oldPluginName, option.PluginName);
FaceOption = option;
if (!asFallback)
MissingFacePluginName = null;
if (!keepFaceGenMod)
SetFaceGenOverride(null);
return ChangeResult.OK;
Expand Down
35 changes: 24 additions & 11 deletions Focus.Apps.EasyNpc/Profiles/NpcViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public class NpcViewModel : INotifyPropertyChanged, INpcBasicInfo
// Selected option refers to selection in the UI view, and has no effect on the profile itself.
public NpcOptionViewModel? SelectedOption { get; set; }

private readonly bool isInitialized;
private readonly Npc npc;

public NpcViewModel(Npc npc, IAsyncEnumerable<Mugshot> mugshots)
Expand All @@ -44,8 +45,9 @@ public NpcViewModel(Npc npc, IAsyncEnumerable<Mugshot> mugshots)
Options = npc.Options.Select(x => CreateOption(x)).ToList().AsReadOnly();
DefaultOption = GetOption(npc.DefaultOption.PluginName);
FaceOption = GetOption(npc.FaceOption.PluginName);
Mugshots = mugshots.Select(x => new MugshotViewModel(x, FaceOptionIsAny(x.InstalledPlugins)))
Mugshots = mugshots.Select(x => new MugshotViewModel(x, IsSelectedMugshot(x.ModName, x.InstalledPlugins)))
.ToObservableCollection();
isInitialized = true;
}

public bool TrySetDefaultPlugin(string pluginName, [MaybeNullWhen(false)] out NpcOptionViewModel option)
Expand All @@ -66,7 +68,12 @@ public bool TrySetFaceMod(string modName, [MaybeNullWhen(false)] out NpcOptionVi
// If the selected mod ends up being an override (no option/plugin), the "option" parameter will be the
// previously-selected option, and the FaceOption won't change. This is the expected behavior. There is
// no guarantee that the output option always corresponds to the mod name.
FaceOption = option = GetOption(npc.FaceOption.PluginName);
option = GetOption(npc.FaceOption.PluginName);
if (FaceOption != option)
FaceOption = option;
else
// Change handler won't run if they're the same, need to explicitly update mugshot states.
UpdateMugshotAssignments();
FaceModNames = npc.GetFaceModNames().ToHashSet(StringComparer.CurrentCultureIgnoreCase);
}
else
Expand Down Expand Up @@ -106,13 +113,17 @@ protected void OnFaceOptionChanged(object before, object after)

protected void OnSelectedMugshotChanged()
{
if (!isInitialized)
return;
foreach (var mugshot in Mugshots)
mugshot.IsHighlighted = false;
UpdateHighlights(SelectedMugshot?.InstalledPlugins ?? Enumerable.Empty<string>());
}

protected void OnSelectedOptionChanged(object before, object after)
{
if (!isInitialized)
return;
foreach (var option in Options)
option.IsHighlighted = false;
if (before is NpcOptionViewModel previousOption)
Expand All @@ -129,23 +140,25 @@ private NpcOptionViewModel CreateOption(NpcOption option)
return optionViewModel;
}

private bool FaceOptionIsAny(IEnumerable<string> pluginNames)
{
return
FaceOption is not null &&
pluginNames.Contains(FaceOption.PluginName, StringComparer.CurrentCultureIgnoreCase);
}

private NpcOptionViewModel GetOption(string pluginName)
{
// The way in which this method is used should always succeed - i.e. we get the plugin name from the model
// itself, and then use it to find the option we generated from the model's options.
return Options.Single(x => x.PluginName == pluginName);
}

private bool IsSelectedMugshot(string modName, IEnumerable<string> installedPlugins)
{
if (npc.FaceGenOverride is not null)
return npc.FaceGenOverride.IncludesName(modName);
return
FaceOption is not null &&
installedPlugins.Contains(FaceOption.PluginName, StringComparer.CurrentCultureIgnoreCase);
}

private void Option_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (sender is not NpcOptionViewModel option)
if (!isInitialized || sender is not NpcOptionViewModel option)
return;
switch (e.PropertyName)
{
Expand Down Expand Up @@ -182,7 +195,7 @@ private void UpdateHighlights(IEnumerable<string> pluginNames)
private void UpdateMugshotAssignments()
{
foreach (var mugshot in Mugshots)
mugshot.IsSelectedSource = FaceOptionIsAny(mugshot.InstalledPlugins);
mugshot.IsSelectedSource = IsSelectedMugshot(mugshot.ModName, mugshot.InstalledPlugins);
}
}
}
Loading

0 comments on commit 5ff0080

Please sign in to comment.