Skip to content

Commit

Permalink
Include NPCs with FaceGen overrides in profile.
Browse files Browse the repository at this point in the history
Ensures that standalone facegens are counted toward both startup and profile filters when checking for at least one, or more than one face override.

Since all standalone facegens indexed from the mod directory are an alternative option, just one is enough for inclusion. And since facegen overrides are never picked by default, these should be shown even if there is only one facegen override, even when filtering to "multiple" choices.

To avoid some of the confusion this may cause, the mugshot viewer and accompanying data now reports if a mod is disabled, aside from simply saying "plugin not loaded". This isn't perfect, but helps at least somewhat in determining if a facegen override is actually a facegen override as opposed to a missing or unloaded plugin.

Fixes #101
  • Loading branch information
focustense committed Aug 25, 2021
1 parent 5d609cd commit c3b4a96
Show file tree
Hide file tree
Showing 7 changed files with 50 additions and 17 deletions.
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
16 changes: 16 additions & 0 deletions Focus.Apps.EasyNpc/Profiles/Npc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,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 @@ -30,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 @@ -44,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
8 changes: 5 additions & 3 deletions Focus.Apps.EasyNpc/Profiles/ProfileFactory.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using Focus.Analysis.Execution;
using Focus.Analysis.Plugins;
using Focus.Analysis.Records;
using Focus.Apps.EasyNpc.GameData.Files;
using Focus.ModManagers;
using System;
using System.Collections.Generic;
Expand Down Expand Up @@ -121,15 +123,15 @@ private Profile Create(LoadOrderAnalysis analysis, IProfilePolicy policy, Action
.ExtractChains<NpcAnalysis>(RecordType.Npc)
.AsParallel()
.Where(x =>
x.Master.CanUseFaceGen && !x.Master.IsChild && !x.Master.IsAudioTemplate && x.Count > 1 &&
x.Master.CanUseFaceGen && !x.Master.IsChild && !x.Master.IsAudioTemplate &&
// Template NPCs that are based on another NPC should be included but treated as "read only".
// If ALL POSSIBILITIES point only to Leveled NPC or unknown/invalid (not standard NPC) targets,
// then there is effectively nothing useful we can do with it and it should be excluded entirely.
x.Any(r =>
r.Analysis.TemplateInfo is null ||
r.Analysis.TemplateInfo.TargetType == NpcTemplateTargetType.Npc) &&
x.Any(r => r.Analysis.ComparisonToBase?.ModifiesFace == true))
r.Analysis.TemplateInfo.TargetType == NpcTemplateTargetType.Npc))
.Select(x => new Npc(x, baseGamePluginNames, modRepository, profileEventLog, policy))
.Where(x => x.HasAvailableFaceCustomizations)
.Tap(defaultAction);
return new Profile(npcs);
}
Expand Down
2 changes: 1 addition & 1 deletion Focus.Apps.EasyNpc/Profiles/ProfileViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ private void ApplyFilters()
// TODO: Also check for invalid facegen override plugin?
filteredNpcs = filteredNpcs.Where(x => x.HasMissingPlugins);
filteredNpcs = filteredNpcs
.Where(x => x.GetOverrideCount(!Filters.NonDlc) >= minOverrideCount);
.Where(x => x.GetOverrideCount(!Filters.NonDlc) >= minOverrideCount || x.HasAvailableModdedFaceGens);
filteredNpcs = filteredNpcs
// This is only the default ordering; grid ordering is independent.
.OrderBy(x => pluginOrder.GetOrDefault(x.BasePluginName))
Expand Down

0 comments on commit c3b4a96

Please sign in to comment.