Skip to content

Commit

Permalink
Integrate ref-checking with analyzers and app.
Browse files Browse the repository at this point in the history
All analyzers now optionally take a reference checker and will execute it, and the unit tests enforce that every analyzer does support and properly use the parameter.

EasyNPC app now configures the full import expansions for NPCs - that is, all of the possible paths that can cause a build to fail. For now, these are only reported as warnings in the log. They'll need to be turned into a decent UI.

Note: This adds a non-trivial amount of startup time, perhaps between 0.5-1 sec. Consider adding a command-line flag for advanced users to turn it off.

#107
  • Loading branch information
focustense committed Sep 2, 2021
1 parent f6dd9be commit c4e9419
Show file tree
Hide file tree
Showing 13 changed files with 170 additions and 22 deletions.
2 changes: 1 addition & 1 deletion Focus.Analysis/Records/RecordAnalysis.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public ReferenceInfo(IRecordKey key, RecordType type, string editorId)

public override string ToString()
{
return $"{Key.LocalFormIdHex}:{Key.BasePluginName} '{EditorId}'";
return $"{Type} <{Key.LocalFormIdHex}:{Key.BasePluginName}> '{EditorId}'";
}
}

Expand Down
27 changes: 26 additions & 1 deletion Focus.Apps.EasyNpc/Mutagen/MutagenLoadOrderAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using Focus.Analysis.Execution;
using Focus.Analysis.Records;
using Focus.Providers.Mutagen.Analysis;
using Mutagen.Bethesda.Skyrim;
using Serilog;
using System.Linq;

namespace Focus.Apps.EasyNpc.Mutagen
{
Expand All @@ -23,7 +25,30 @@ public MutagenLoadOrderAnalyzer(IGroupCache groupCache, ILogger log, bool purgeO
protected override void Configure(AnalysisRunner runner)
{
runner
.Configure(RecordType.Npc, new NpcAnalyzer(groupCache, Log))
.Configure(RecordType.Npc, new NpcAnalyzer(groupCache, new ReferenceChecker<INpcGetter>(groupCache)
.Follow(x => x.HairColor)
.Follow(x => x.HeadParts, headPart => headPart
.Follow(x => x.Model?.AlternateTextures?.Select(t => t.NewTexture))
.Follow(x => x.Color)
.FollowSelf(x => x.ExtraParts)
.Follow(x => x.TextureSet))
.Follow(x => x.HeadTexture)
.Follow(x => x.WornArmor, armor => armor
.Follow(x => x.Armature, addon => addon
.Follow(x => x.AdditionalRaces)
.Follow(x => x.ArtObject, artObject => artObject
.Follow(x => x.Model?.AlternateTextures?.Select(t => t.NewTexture)))
.Follow(x => x.FirstPersonModel, g => g.AlternateTextures?.Select(x => x.NewTexture))
.Follow(x => x.Race)
.Follow(x => x.SkinTexture)
.Follow(x => x.TextureSwapList, swapList => swapList
.Follow(x => x.Items
.Where(x => x.Type == typeof(ITextureSetGetter))
.Select(x => x.FormKey.AsLinkGetter<ITextureSetGetter>())))
.Follow(x => x.WorldModel, g => g.AlternateTextures?.Select(x => x.NewTexture)))
.Follow(x => x.Keywords)
.FollowSelf(x => x.TemplateArmor)
.Follow(x => x.WorldModel, g => g.Model?.AlternateTextures?.Select(t => t.NewTexture)))))
.Configure(RecordType.HeadPart, new HeadPartAnalyzer(groupCache));
}

Expand Down
21 changes: 19 additions & 2 deletions Focus.Apps.EasyNpc/Profiles/ProfileFactory.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using Focus.Analysis.Execution;
using Focus.Analysis.Plugins;
using Focus.Analysis.Records;
using Focus.Apps.EasyNpc.GameData.Files;
using Focus.ModManagers;
using Serilog;
using System;
using System.Collections.Generic;
using System.Linq;
Expand All @@ -19,13 +19,16 @@ Profile RestoreSaved(

public class ProfileFactory : IProfileFactory
{
private readonly ILogger log;
private readonly IModRepository modRepository;
private readonly ISuspendableProfileEventLog profileEventLog;
private readonly IProfilePolicy policy;

public ProfileFactory(
IProfilePolicy policy, IModRepository modRepository, ISuspendableProfileEventLog profileEventLog)
IProfilePolicy policy, IModRepository modRepository, ISuspendableProfileEventLog profileEventLog,
ILogger log)
{
this.log = log;
this.modRepository = modRepository;
this.policy = policy;
this.profileEventLog = profileEventLog;
Expand Down Expand Up @@ -130,10 +133,24 @@ private Profile Create(LoadOrderAnalysis analysis, IProfilePolicy policy, Action
x.Any(r =>
r.Analysis.TemplateInfo is null ||
r.Analysis.TemplateInfo.TargetType == NpcTemplateTargetType.Npc))
.Tap(LogInvalidReferences)
.Select(x => new Npc(x, baseGamePluginNames, modRepository, profileEventLog, policy))
.Where(x => x.HasAvailableFaceCustomizations)
.Tap(defaultAction);
return new Profile(npcs);
}

private void LogInvalidReferences(RecordAnalysisChain<NpcAnalysis> chain)
{
foreach (var record in chain)
{
if (record.Analysis.InvalidPaths.Count == 0)
continue;
log.Warning(
"Record {recordKey} in plugin {pluginName} contains invalid references:\n{referenceList:l}",
chain.Key, record.PluginName,
string.Join("\n", record.Analysis.InvalidPaths.Select(p => $" {p}")));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public class BasicRecordAnalyzerTests : CommonAnalyzerFacts<BasicRecordAnalyzer,
{
public BasicRecordAnalyzerTests()
{
Analyzer = new BasicRecordAnalyzer(Groups, RecordType.Container);
Analyzer = new BasicRecordAnalyzer(Groups, RecordType.Container, ReferenceChecker);
}
}
}
24 changes: 23 additions & 1 deletion Focus.Providers.Mutagen.Tests/Analysis/CommonAnalyzerFacts.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Focus.Analysis.Records;
using Mutagen.Bethesda.Skyrim;
using Serilog;
using System.Collections.Generic;
using Xunit;

namespace Focus.Providers.Mutagen.Tests.Analysis
Expand All @@ -10,9 +11,26 @@ public abstract class CommonAnalyzerFacts<TAnalyzer, TMajorRecord, TAnalysis>
where TAnalysis : RecordAnalysis
where TMajorRecord : class, ISkyrimMajorRecord
{
private static readonly IEnumerable<ReferencePath> DummyInvalidPaths = new[]
{
new ReferencePath(new[]
{
new ReferenceInfo(new RecordKey("dummy.esp", "123456"), RecordType.Armor),
new ReferenceInfo(new RecordKey("dummy.esp", "123457"), RecordType.ArmorAddon),
new ReferenceInfo(new RecordKey("dummy.esp", "123458"), RecordType.TextureSet),
}),
new ReferencePath(new[]
{
new ReferenceInfo(new RecordKey("dummy.esp", "123456"), RecordType.Armor),
new ReferenceInfo(new RecordKey("dummy.esp", "123457"), RecordType.ArmorAddon),
new ReferenceInfo(new RecordKey("dummy.esp", "123459"), RecordType.TextureSet),
})
};

protected TAnalyzer Analyzer { get; set; }
private protected FakeGroupCache Groups { get; set; }
private protected FakeGroupCache Groups { get; private set; }
protected ILogger Logger { get; private set; }
private protected FakeReferenceChecker ReferenceChecker { get; private set; }

public CommonAnalyzerFacts()
{
Expand All @@ -23,6 +41,7 @@ public CommonAnalyzerFacts()
.WriteTo.Debug()
.CreateLogger();
Groups = new FakeGroupCache();
ReferenceChecker = new FakeReferenceChecker { InvalidPaths = DummyInvalidPaths };
}

[Fact]
Expand All @@ -37,6 +56,7 @@ public void WhenRecordNotFound_ReturnsDummyInfo()
Assert.Equal(string.Empty, analysis.EditorId);
Assert.False(analysis.IsInjectedOrInvalid);
Assert.False(analysis.IsOverride);
Assert.Empty(analysis.InvalidPaths);
}

[Fact]
Expand All @@ -56,6 +76,7 @@ public void WhenRecordIsWrongType_ReturnsDummyInfo()
Assert.Equal(string.Empty, analysis.EditorId);
Assert.False(analysis.IsInjectedOrInvalid);
Assert.False(analysis.IsOverride);
Assert.Empty(analysis.InvalidPaths);
}

[Fact]
Expand All @@ -74,6 +95,7 @@ public void WhenRecordFound_ReturnsRecordInfo()
Assert.Equal("record2", analysis.EditorId);
Assert.False(analysis.IsInjectedOrInvalid);
Assert.False(analysis.IsOverride);
Assert.Equal(DummyInvalidPaths, analysis.InvalidPaths);
}

[Fact]
Expand Down
37 changes: 37 additions & 0 deletions Focus.Providers.Mutagen.Tests/Analysis/FakeReferenceChecker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Focus.Analysis.Records;
using Focus.Providers.Mutagen.Analysis;
using Mutagen.Bethesda.Skyrim;
using System.Collections.Generic;

namespace Focus.Providers.Mutagen.Tests.Analysis
{
class FakeReferenceChecker : IReferenceChecker
{
public IEnumerable<ReferencePath> InvalidPaths { get; set; }

public IReferenceChecker<T> Of<T>()
{
return new CheckerOf<T>(this);
}

public IEnumerable<ReferencePath> GetInvalidPaths(ISkyrimMajorRecordGetter record)
{
return InvalidPaths;
}

class CheckerOf<T> : IReferenceChecker<T>
{
private readonly FakeReferenceChecker parentChecker;

public CheckerOf(FakeReferenceChecker parentChecker)
{
this.parentChecker = parentChecker;
}

public IEnumerable<ReferencePath> GetInvalidPaths(T record)
{
return parentChecker.InvalidPaths;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class HeadPartAnalyzerTests : CommonAnalyzerFacts<HeadPartAnalyzer, HeadP

public HeadPartAnalyzerTests()
{
Analyzer = new HeadPartAnalyzer(Groups);
Analyzer = new HeadPartAnalyzer(Groups, ReferenceChecker.Of<IHeadPartGetter>());
}

[Theory]
Expand Down
2 changes: 1 addition & 1 deletion Focus.Providers.Mutagen.Tests/Analysis/NpcAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class NpcAnalyzerTests : CommonAnalyzerFacts<NpcAnalyzer, Npc, NpcAnalysi

public NpcAnalyzerTests()
{
Analyzer = new NpcAnalyzer(Groups, armorAddonHelperMock.Object, Logger);
Analyzer = new NpcAnalyzer(Groups, armorAddonHelperMock.Object, ReferenceChecker.Of<INpcGetter>());
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using RecordType = Focus.Analysis.Records.RecordType;

Expand Down
10 changes: 8 additions & 2 deletions Focus.Providers.Mutagen/Analysis/BasicRecordAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Focus.Analysis.Records;
using Focus.Analysis;
using Focus.Analysis.Records;
using System.Linq;
using RecordType = Focus.Analysis.Records.RecordType;

namespace Focus.Providers.Mutagen.Analysis
Expand All @@ -9,11 +11,14 @@ public class BasicRecordAnalyzer : IRecordAnalyzer<BasicRecordAnalysis>

private readonly IGroupCache groups;
private readonly RecordType recordType;
private readonly IReferenceChecker? referenceChecker;

public BasicRecordAnalyzer(IGroupCache groups, RecordType recordType)
public BasicRecordAnalyzer(
IGroupCache groups, RecordType recordType, IReferenceChecker? referenceChecker = null)
{
this.groups = groups;
this.recordType = recordType;
this.referenceChecker = referenceChecker;
}

public BasicRecordAnalysis Analyze(string pluginName, IRecordKey key)
Expand All @@ -28,6 +33,7 @@ public BasicRecordAnalysis Analyze(string pluginName, IRecordKey key)
LocalFormIdHex = key.LocalFormIdHex,
EditorId = record?.EditorID ?? string.Empty,
Exists = record != null,
InvalidPaths = referenceChecker.SafeCheck(record),
IsInjectedOrInvalid = isOverride && !groups.MasterExists(formKey, recordType),
IsOverride = isOverride,
};
Expand Down
5 changes: 4 additions & 1 deletion Focus.Providers.Mutagen/Analysis/HeadPartAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ public class HeadPartAnalyzer : IRecordAnalyzer<HeadPartAnalysis>
public RecordType RecordType => RecordType.HeadPart;

private readonly IGroupCache groups;
private readonly IReferenceChecker<IHeadPartGetter>? referenceChecker;

public HeadPartAnalyzer(IGroupCache groups)
public HeadPartAnalyzer(IGroupCache groups, IReferenceChecker<IHeadPartGetter>? referenceChecker = null)
{
this.groups = groups;
this.referenceChecker = referenceChecker;
}

public HeadPartAnalysis Analyze(string pluginName, IRecordKey key)
Expand All @@ -35,6 +37,7 @@ public HeadPartAnalysis Analyze(string pluginName, IRecordKey key)
LocalFormIdHex = key.LocalFormIdHex,
EditorId = headPart.EditorID ?? string.Empty,
Exists = true,
InvalidPaths = referenceChecker.SafeCheck(headPart),
IsInjectedOrInvalid = isOverride && !groups.MasterExists(key.ToFormKey(), RecordType),
IsOverride = isOverride,
ExtraPartKeys = headPart.ExtraParts.ToRecordKeys(),
Expand Down
13 changes: 8 additions & 5 deletions Focus.Providers.Mutagen/Analysis/NpcAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,18 @@ private HashSet<FormKey> GetDefaultRaceArmorAddons(IFormLinkGetter<IRaceGetter>

private readonly IArmorAddonHelper armorAddonHelper;
private readonly IGroupCache groups;
private readonly ILogger log;
private readonly IReferenceChecker<INpcGetter>? referenceChecker;

public NpcAnalyzer(IGroupCache groups, ILogger log)
: this(groups, new ArmorAddonHelper(groups), log) { }
public NpcAnalyzer(IGroupCache groups, IReferenceChecker<INpcGetter>? referenceChecker = null)
: this(groups, new ArmorAddonHelper(groups), referenceChecker) { }

public NpcAnalyzer(IGroupCache groups, IArmorAddonHelper armorAddonHelper, ILogger log)
public NpcAnalyzer(
IGroupCache groups, IArmorAddonHelper armorAddonHelper,
IReferenceChecker<INpcGetter>? referenceChecker = null)
{
this.armorAddonHelper = armorAddonHelper;
this.groups = groups;
this.log = log;
this.referenceChecker = referenceChecker;
}

public NpcAnalysis Analyze(string pluginName, IRecordKey key)
Expand All @@ -125,6 +127,7 @@ public NpcAnalysis Analyze(string pluginName, IRecordKey key)
LocalFormIdHex = key.LocalFormIdHex,
EditorId = npc.EditorID ?? string.Empty,
Exists = true,
InvalidPaths = referenceChecker.SafeCheck(npc),
IsInjectedOrInvalid = isOverride && !groups.MasterExists(key.ToFormKey(), RecordType),
IsOverride = isOverride,
CanUseFaceGen = race?.Flags.HasFlag(Race.Flag.FaceGenHead) ?? false,
Expand Down
Loading

0 comments on commit c4e9419

Please sign in to comment.