Giter Club home page Giter Club logo

easymod's Introduction

Discord

EasyMod

Top-level repository for various Bethesda modding tools.

Currently includes the following projects:

Apps

  • EasyNPC: Painlessly mix, merge, and resolve conflicts and compatibility issues for all of your NPC overhauls.

Libraries

easymod's People

Contributors

focustense avatar

Stargazers

TheMerricat avatar Grosa Prap avatar  avatar Mercury71 avatar si avatar Mango avatar  avatar Red Petzen avatar Jacob Mills avatar  avatar Valentin Gurkov avatar  avatar Ryan Middleton avatar  avatar Paulo Santos avatar  avatar Christopher avatar  avatar Chad Manly avatar Wasabi Ice Cream avatar Scorpio SixNine avatar Vrickk avatar ymk avatar  avatar Vernon McCandlish avatar Loïc Reynier avatar Sky avatar Martin Larralde avatar Reg1nleifr avatar  avatar  avatar Theo avatar  avatar  avatar DracoMan671 avatar  avatar  avatar Rui Huang avatar  avatar erri120 avatar  avatar Luca avatar Justin Swanson avatar

Watchers

 avatar Theo avatar Hassaan Abdul Razzaq avatar Scorpio SixNine avatar Alex Ryttel avatar  avatar  avatar F_B_R avatar

Forkers

ladmes sharpsteve

easymod's Issues

v0.2 stuck

The latest version is getting stuck on "Done loading headparts, Building NPC Index"

Log Name: Application
Source: Application Hang
Date: 6/20/2021 9:23:41 PM
Event ID: 1002
Task Category: (101)
Level: Error
Keywords: Classic
User: N/A
Computer: DESKTOP-RNBH10N
Description:
The program EasyNPC.exe version 0.2.0.0 stopped interacting with Windows and was closed. To see if more information about the problem is available, check the problem history in the Security and Maintenance control panel.
Process ID: 6004
Start Time: 01d7664459db922a
Termination Time: 2
Application Path: D:\Modlist\tools\EasyNPC\EasyNPC.exe
Report Id: cb077145-36b1-4f59-9a25-6ffe7827781a
Faulting package full name:
Faulting package-relative application ID:
Hang type: Unknown

Event Xml:



1002
0
2
101
0
0x80000000000000

2406


Application
DESKTOP-RNBH10N



EasyNPC.exe
0.2.0.0
6004
01d7664459db922a
2
D:\Modlist\tools\EasyNPC\EasyNPC.exe
cb077145-36b1-4f59-9a25-6ffe7827781a




Unknown
55006E006B006E006F0077006E0000000000

It becomes unresponsive and then I have to close it through task manager. (I waited 10+ minutes to see if it would open, and it wouldnt) reverting to v0.12 allows me to launch the program.

Missing textures for certain NPCs

Reported on Heimskr from eeekie's Enhanced NPCs, among some others.

Investigation into the game's file accesses showed that it was trying to access "maleheaddetail_rough01", which is referenced in the facegen mesh but not the plugin record. Also various beard textures. The facegen texture scanner is missing these textures.

Undo/redo commands and history management

Everything that happens to a profile is logged for autosave purposes, and those same events can be used to implement undo/redo functionality for the profile. This may not be necessary given the simplicity of tasks being performed in the UI, but then again, why not?

A more advanced version of this is to support either large-scale rollbacks or individual record rollbacks from the profile history (#4). That could be a lot more useful, for example if someone accidentally loaded an old/bad profile, used a Reset function and didn't like the result, etc.

It's easy to underestimate the number of scenarios leading to "I can't remember what I did, and now things are broken" - they're hard to predict but happen often in practice, so let's have a safety valve.

Detect when previous merge is already in the load order

There is currently nothing to prevent loading a previous merge and making that a dependency of the new merge - probably by accident.

Plugins that are actually merges previously created by the app should either be disabled in the load order screen, or simply ignored. This is similar to how a Bashed Patch can't be imported as its own dependency.

Update documentation and in-app messaging for Vortex

Now that it's essentially supported as a mod manager (#9), there need to be a few refinements to docs and messaging, which is pretty MO-centric. At this point, it is really the only thing blocking a beta (Nexus) launch.

This will have to include an instruction to restart Vortex, if I can't find any other way to get it to immediately recognize the new mod in the staging directory.

Report missing plugins in pre-build checks

If a profile is set up with certain plugins, and then those plugins are removed from the load order and the app started again, then fallback plugins will be selected for that session, since every NPC must have both a default and face plugin selected.

This behavior doesn't affect the autosave - if the plugin is subsequently re-enabled and the app restarted, without making any explicit changes, then the originally-selected plugins become selected again. However, while in the incomplete state, this can potentially generate confusing warnings if someone tries to build. The actual warning generate will be a FaceModPluginMismatch, which is technically true for that session but not really representative of what's happening.

Since the original selection information has not been lost, the build checker should report a totally different error, explicitly indicating that the plugin is no longer available or enabled, providing a clear course of action to fix that warning, i.e. re-enabling it.

Warn on/safely handle invalid mod directories

A vortex user somehow got E:\Vortex Mods\{game} as the mod directory. Presumably that string came from the Vortex extension, so that's worth looking into on the extension side, but more important is not allowing users to get into this situation which is hard to get out of.

The Settings screen should warn prominently if the mod directory is invalid, and the rest of the app should treat an invalid mod directory the same as no mod directory at all - so that at least the app starts, and users can change the setting later.

Try to identify and clearly label patches

Although there isn't really any such thing as a "patch type" - it's just a normal plugin - there are a few signals that might be usable.

  1. A lot of patch plugins actually do contain the word "Patch". These are pretty obvious. Also words like "fix".
  2. Logic similar to #49 - anything at all that depends on an NPC overhaul, especially if it's an NPC overhaul + some master that the overhaul itself does not depend on - is almost certainly a patch.
  3. Modifies NPCs, but doesn't have a BSA or any loose files in the corresponding mod.
  4. Some things are always patches, like Synthesis.esp, DynDOLOD.esp and so on; these are rarely implicated in issues but it doesn't hurt to have a list of them.
  5. Some other, weaker signals that could potentially be combined are:
    • Item 2 above but for any master. A typical pattern is depending on 2 or more non-vanilla masters that neither of those original ones depend on. In other words, it depends on mod X and mod Y, but X does not depend on Y and Y does not depend on X. A lot of ordinary mods can do this too, but it's not actually that common.
    • Is in a mod (not the plugin itself) with "patch" or "fix" in the name.
    • Only edits one record type.
    • ESL flags - very weak signal but can strengthen other signals.
    • Maybe others?

I'm not sure that the last, highly-variable item is worth introducing unless all the previous items just aren't good enough.

Clearly identifying patches - like say, with a bandaid next to the plugin name and maybe even some other more noticeable identifiers like red text or build warnings (with optional "not a patch/this patch is OK" whitelist), would help avoid these patches being accidentally set as default/face plugins by the user, even if they're not automatically selected by the tool.

Support "global replacers" that change vanilla head parts (VHR, SUEMR, etc.)

There are a number of mods out that do nothing but replace vanilla NPC hair, including many for the actual vanilla NPCs (Shiva's replacer, Vanilla Hair Replacer) but also mod-originating NPCs (ApachiiHair3DNPC, KSHair for CRF), and many more.

While the value of creating mugshots for all of these to use as face mods/overrides is highly questionable, there is a use case which is probably closer to what most players actually want, and which may be possible to implement using Nifly and the experienced gained from de-wiggification. The logic would be a global setting and look something like:

  1. Identify hair mods at startup. Should be mostly auto-detectable, allow users to override.
  2. Set up a priority order for identified hair mods based on load order, again allowing this to be overridden.
  3. During build, if an NPC is not touched by any other face override (i.e. still uses their vanilla face), we can just treat the hair mod as a regular face override, use its headparts and facegen file. This is the easy part.
  4. If an NPC is modified but still uses vanilla hair - i.e. uses a custom face, makeup, etc. but references the same hair-headpart as the original master - then use NIF injection to mash-up the winning hair replacer and the selected face.
  5. If a modified NPC does not use vanilla hair, then ignore the hair replacer - but allow this to be overridden at some level, probably either globally or per-hair-mod.

If we think about the niche that hair replacers tend to fill - providing a quick and easy "overhaul lite" for characters that haven't been fully overhauled - then I believe these steps best reflect the intent of both the mod creators and the modders themselves.

It does need to be stated that this is not as simple as it may sound at first. Two major obstacles I've uncovered are:

  1. At least some (maybe most?) hair replacers "cheat" the facegen system by providing only facegens, and using the vanilla names for the NiNodes, and don't make any record edits at all. Either they have no ESP, or the ESP is just a dummy plugin to get a BSA to load. While this doesn't make the simple case, where we just copy the facegen file, any more difficult, it makes the NIF injection more complex because it creates ambiguity over the proper headpart names.

    • It's probably (?) safe to assume that they are the originally headparts used by the original master, otherwise they probably (?) wouldn't work in game. However, it's not a sure thing. Without checking each and every one of these files, it's possible that some of them use vanilla headpart names that are not the original vanilla headpart names used by those NPCs. This part of the facegen system is still somewhat of a mystery and many mod creators use trial and error to get through it.
    • If they are in fact using the true original headpart names - i.e. the customized-hair facegens exactly match the headpart list in the master record - then this is relatively simple: just revert any custom hair in the merged plugin back to the vanilla hair, and inject the "not really vanilla" hair from the replacer into the NIF.
    • However, if they are not using the original headparts, just some arbitrary vanilla headparts, then this may become considerably more complicated, as it would be necessary to actually parse the replaced facegens to figure out which nodes are hair/hairline, which is difficult as meshes don't contain any of this metadata.
  2. Other replacers, most notably VHR, work by editing vanilla head parts, which is an invasive edit (like editing vanilla races) that requires extensive patching to avoid glitches and whose steps can actually break NPC mods that use custom geometry (morphs).

    • We can absolutely do this patching as well, and probably automate the entire process above, without inheriting any of its problems, but the signal for it is different. The app has to actually understand when plugins modify vanilla head parts and inject the modified head parts into any modified NPCs who are still using the original head parts. While this isn't necessarily any more difficult than anything else the app does (especially e.g. de-wigging), it is an entirely different category of functionality that simply doesn't exist and doesn't share much overlap with existing functions.

Bottom line: This is likely to be a highly valuable feature if supported, which can save people many hours or even days of patching time, but it is also going to be one of the most difficult and potentially finicky implementations. Getting this into the beta would be nice, but may get pushed back if it doesn't go smoothly.

Crash due to "Item with the same key" error for BSA files on load

Copied from the Nexus comments:

System.AggregateException: One or more errors occurred. (An item with the same key has already been added. Key: FortDawnguardImmersive - Textures.bsa)
 ---> System.ArgumentException: An item with the same key has already been added. Key: FortDawnguardImmersive - Textures.bsa
   at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
   at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
   at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector, IEqualityComparer`1 comparer)
   at Focus.Apps.EasyNpc.GameData.Files.ModPluginMap.<>c__DisplayClass3_0.<ForDirectory>b__4()
   at System.Threading.Tasks.Task.InnerInvoke()
   at System.Threading.Tasks.Task.<>c.<.cctor>b__277_0(Object obj)
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread)
   --- End of inner exception stack trace ---
   at System.Threading.Tasks.Task.WaitAllCore(Task[] tasks, Int32 millisecondsTimeout, CancellationToken cancellationToken)
   at System.Threading.Tasks.Task.WaitAll(Task[] tasks)
   at System.Threading.Tasks.Parallel.Invoke(ParallelOptions parallelOptions, Action[] actions)
--- End of stack trace from previous location ---
   at System.Threading.Tasks.Parallel.ThrowSingleCancellationExceptionOrOtherException(ICollection exceptions, CancellationToken cancelToken, Exception otherException)
   at System.Threading.Tasks.Parallel.Invoke(ParallelOptions parallelOptions, Action[] actions)
   at System.Threading.Tasks.Parallel.Invoke(Action[] actions)
   at Focus.Apps.EasyNpc.GameData.Files.ModPluginMap.ForDirectory(String modRootDirectory, IModResolver modResolver, IEnumerable`1 pluginNames, IEnumerable`1 archiveNames)
   at Focus.Apps.EasyNpc.Mutagen.MutagenModPluginMapFactory.CreateForDirectory(String modRootDirectory)
   at Focus.Apps.EasyNpc.GameData.Files.ModPluginMapFactoryExtensions.DefaultMap(IModPluginMapFactory modPluginMapFactory)
   at Focus.Apps.EasyNpc.Profile.NpcConfiguration`1.SetFacePlugin(NpcOverrideConfiguration`1 faceConfig, Boolean detectFaceMod)
   at Focus.Apps.EasyNpc.Profile.NpcConfiguration`1.SetFacePlugin(String pluginName, Boolean detectFaceMod)
   at Focus.Apps.EasyNpc.Profile.NpcConfiguration`1.Reset(Boolean defaults, Boolean faces)
   at Focus.Apps.EasyNpc.Profile.NpcConfiguration`1..ctor(INpc`1 npc, IModPluginMapFactory modPluginMapFactory, IProfileRuleSet ruleSet)
   at Focus.Apps.EasyNpc.Profile.ProfileViewModel`1..ctor(IEnumerable`1 npcs, IModPluginMapFactory modPluginMapFactory, IEnumerable`1 loadedPluginNames, IEnumerable`1 masterNames, ProfileEventLog profileEventLog)
   at Focus.Apps.EasyNpc.Main.MainViewModel`1.<>c__DisplayClass55_0.<.ctor>b__1()
   at Focus.Apps.EasyNpc.Main.LoaderViewModel`1.ConfirmPlugins()
   at System.Threading.Tasks.Task.<>c.<ThrowAsync>b__140_0(Object state)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.DispatcherOperation.InvokeImpl()
   at MS.Internal.CulturePreservingExecutionContext.CallbackWrapper(Object obj)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at MS.Internal.CulturePreservingExecutionContext.Run(CulturePreservingExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Windows.Threading.DispatcherOperation.Invoke()
   at System.Windows.Threading.Dispatcher.ProcessQueue()
   at System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.Dispatcher.LegacyInvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
   at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
   at MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
   at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.Run()
   at System.Windows.Application.RunDispatcher(Object ignore)
   at System.Windows.Application.RunInternal(Window window)
   at System.Windows.Application.Run()
   at Focus.Apps.EasyNpc.App.Main()

Some head parts are not fully standalone

This was observed on two elements: the Hair Color (HCLF) for an NPC, and the Texture Set that is referenced by the alternate textures in any head part (but not the "main" head part texture set, which is fine).

Improve Mod Organizer integration

Certain quirks of the app are due to using Mod Organizer's mod directory, but not knowing all of the things that Mod Organizer knows, especially the order of mods and whether or not they are enabled. This leads to some false-positive warnings, such as flagging backups or disabled mods as duplicates. It also means some false negatives - for example, it can't report that a mod is not really enabled when selecting a mugshot for that mod, even though there is no matching plugin. It doesn't know if an NPC or resource mod is supposed to have its textures overridden by some other mod; and so on.

Although it is slightly cumbersome to navigate, Mod Organizer's configuration can be entirely traversed using just the root .ini, including the mod directory, selected profile, and so on. We can discover everything about the current mod-list state by pointing to that .ini, and solve a number of these problems.

This is mainly a QoL feature, not essential core functionality.

Repeated Crash while loading in plugins

Hello! I keep getting an error while my plugins are loading in to begin the selection process for NPCS. I suspect its related to the amount of plugins I'm loading in because I'm trying to use this program to reduce my plugin count to below the allowed amount (287 esps->254 esps). I'm not 100% sure though which is why I'm uploading this report. Please feel free to ask any questions or close this if previously addressed. Looking forward to hearing back.
Log_20210628_114548_7048429.txt

Profile history view

Since the autosave already tracks every change made to a profile, there's no particular reason why we shouldn't be able to see that full history in the app. While the autosave is technically human-readable (JSON records), it doesn't make it that easy to answer simple questions like "why did this NPC change and when?" For one thing, the autosave only has the Form ID and not the Editor ID or NPC name.

Should be simple to add a page or sub-view with all the change history, filterable by various fields like it is in the Profile itself. For now this would be read-only, though it is a stepping stone into full undo/redo.

Add a "jump to" feature

"jump to" right click integration for the "check for problems" section to jump straight to the npc in question where problems are reported would save a lot of time, especially for people with a lot of these(which would be expected on a first run) like me.

Support for Skyrim VR

image

  1. I run the EasyNPC utility from MO2 as described
  2. Expected result - the list of actually enabled plugins for the currently selected game appears.
  3. Actual result - The list of plugins seems to point at the SkyrimSE directory, without the ability to change targets.

As a secondary attempt (separate drives for isntalled mods and MO2 data: the mods are on an SSD, the same as the Skyrim installation and the MO2 data on a separate HDD), I created a symlink in the MO2 BASEDIR and pointed EasyNPC to that instead, with the same resuilt.

Pride of Skyrim not being detected

Hello, I am back once again. Thanks for the prior big fix as it solved the issue I was having before. The new issue that I am facing is that Pride of Skyrim isn't being properly detected by EasyNPC.
image
This image shows how the AIO is installed but is showing up under a different name than the choice with the mugshot. I think it might have something to do with how the AIO has a bunch of esls and not one esp? I could also try renaming my mod if that would solve the issue.

Improve dependency injection

Hacking away at this project has been fun, but clocking in now at over 10k LOC, this project needs to have much better DI in order to have any hope of being maintained long term.

This is an all-encompassing task that is more than just adding a container like AutoFac or Ninject. The dependencies have to be tamed somehow. 9 dependencies is too many. Although that's a more extreme case, the typical constructor of a major type like a view model takes 5 or more. SRP has been thrown to the wind in order to get this shipped, but now that it is (about to be) shipped, a serious refactoring effort is high priority. The current problems also affect the ability to write unit tests.

Without creating a zillion bugs for every possible refactoring, these are the broad areas that should be looked at:

  1. Division of View Models into sub-models.

    The current view models are effectively monolithic, per page, and from a pure UI point of view, they can be - there are currently no plans to reuse parts of one page in any other page, except for a few small cases that have already been handled such as the log viewer. But this isn't scaling well as the pages themselves become more complex. Even if reuse isn't planned, it is still a major hindrance for testability and overall maintenance.

    Some examples of where I think this could be done:

    • Extracting a true NPC view model for the Profile, instead of the half-baked NpcConfiguration in use right now
    • Extracting the relationships between mods and plugins into a standalone model
    • Moving Mugshots into their own component and view model
    • Essentially the entire Profile page should be broken down into subcomponents, each maintaining their own state, while the page view model can simply deal with the list of NPCs, the currently-selected one, and maybe filters.
  2. Extract stateless tasks into services.

    One very specific and targeted fix already made was to refactor the BuildChecker out of the BuildViewModel, which did eliminate a couple of dependencies, although only a couple. Anything that doesn't need to maintain its own state, i.e. just produces some output based on some external input and maybe a few other services, should be converted into a standalone service and registered with IoC.

    Some possible candidates for services include:

    • Loading/saving profiles
    • Making MergedFolder not static - a lot of the dependency insanity is for this class alone, and even though it doesn't have that much actual code, it has 9 dependencies. Really it's performing discrete tasks such as: resolving file lists, copying files, finding dependencies in files, the experimental de-wiggifier, etc. The only state that it really needs to track is the progress and file lists; almost all of these individual steps can be offloaded to services.
  3. Add an IoC container for the service injection. Self-explanatory.

Despite the wall of text above, most of it is not really that bad - yet. There aren't classes with a thousand LOC - yet. That is why I think now is the time to start thinking more clearly about app architecture, when problems are just starting to emerge but are still relatively easy to fix.

I anticipate one other fringe benefit of doing this, which is that some of the extractable processes, such as file and record copiers, could be useful to other modding tools planned for the future.

"An item with the same key has already been added" for BSA during build

This is similar to #28, but happens on build, not load.

2021-07-19 09:18:20.760 +10:00 [INF] [Merged Folder] - Building file index
2021-07-19 09:18:21.000 +10:00 [ERR] Exception was not handled
System.ArgumentException: An item with the same key has already been added. Key: C:\Users\User\Desktop\Games\The Elder Scrolls - Skyrim - Special Edition\Data\Weapons Armor Clothing & Clutter Fixes - Textures.bsa
   at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
   at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
   at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector, IEqualityComparer`1 comparer)
   at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector)
   at Focus.Apps.EasyNpc.Build.MergedFolder.<>c__DisplayClass1_0`1.<Build>b__2(String modName)
   at System.Linq.Enumerable.SelectListIterator`2.ToList()
   at Focus.Apps.EasyNpc.Build.MergedFolder.Build[TKey](IReadOnlyList`1 npcs, MergedPluginResult mergeInfo, IArchiveProvider archiveProvider, IFaceGenEditor faceGenEditor, ModPluginMap modPluginMap, IModResolver modResolver, BuildSettings`1 buildSettings, ProgressViewModel progress, ILogger logger)
   at Focus.Apps.EasyNpc.Build.BuildViewModel`1.<BeginBuild>b__79_0()
   at System.Threading.Tasks.Task.InnerInvoke()
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread)
--- End of stack trace from previous location ---
   at Focus.Apps.EasyNpc.Build.BuildViewModel`1.BeginBuild()
   at System.Threading.Tasks.Task.<>c.<ThrowAsync>b__140_0(Object state)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.DispatcherOperation.InvokeImpl()
   at MS.Internal.CulturePreservingExecutionContext.CallbackWrapper(Object obj)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at MS.Internal.CulturePreservingExecutionContext.Run(CulturePreservingExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Windows.Threading.DispatcherOperation.Invoke()
   at System.Windows.Threading.Dispatcher.ProcessQueue()
   at System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.Dispatcher.LegacyInvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
   at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
   at MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
   at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.Run()
   at System.Windows.Application.RunDispatcher(Object ignore)
   at System.Windows.Application.RunInternal(Window window)
   at System.Windows.Application.Run()
   at Focus.Apps.EasyNpc.App.Main()

Vortex failures on load: ArgumentNullException in ModPluginMap

Trace:

2021-07-17 15:15:12.705 -04:00 [INF] All NPCs loaded.
2021-07-17 15:15:12.767 -04:00 [ERR] Exception was not handled
System.AggregateException: One or more errors occurred. (Value cannot be null. (Parameter 'key')) (Value cannot be null. (Parameter 'key'))
 ---> System.ArgumentNullException: Value cannot be null. (Parameter 'key')
   at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
   at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
   at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](List`1 source, Func`2 keySelector, Func`2 elementSelector, IEqualityComparer`1 comparer)
   at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector, IEqualityComparer`1 comparer)
   at Focus.Apps.EasyNpc.GameData.Files.ModPluginMap.<>c__DisplayClass3_0.<ForDirectory>b__3()
   at System.Threading.Tasks.Task.InnerInvoke()
   at System.Threading.Tasks.Task.<>c.<.cctor>b__277_0(Object obj)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread)
   --- End of inner exception stack trace ---
   at System.Threading.Tasks.Task.WaitAllCore(Task[] tasks, Int32 millisecondsTimeout, CancellationToken cancellationToken)
   at System.Threading.Tasks.Task.WaitAll(Task[] tasks)
   at System.Threading.Tasks.Parallel.Invoke(ParallelOptions parallelOptions, Action[] actions)
--- End of stack trace from previous location ---
   at System.Threading.Tasks.Parallel.ThrowSingleCancellationExceptionOrOtherException(ICollection exceptions, CancellationToken cancelToken, Exception otherException)
   at System.Threading.Tasks.Parallel.Invoke(ParallelOptions parallelOptions, Action[] actions)
   at System.Threading.Tasks.Parallel.Invoke(Action[] actions)
   at Focus.Apps.EasyNpc.GameData.Files.ModPluginMap.ForDirectory(String modRootDirectory, IModResolver modResolver, IEnumerable`1 pluginNames, IEnumerable`1 archiveNames)
   at Focus.Apps.EasyNpc.Mutagen.MutagenModPluginMapFactory.CreateForDirectory(String modRootDirectory)
   at Focus.Apps.EasyNpc.GameData.Files.ModPluginMapFactoryExtensions.DefaultMap(IModPluginMapFactory modPluginMapFactory)
   at Focus.Apps.EasyNpc.Profile.NpcConfiguration`1.SetFacePlugin(NpcOverrideConfiguration`1 faceConfig, Boolean detectFaceMod)
   at Focus.Apps.EasyNpc.Profile.NpcConfiguration`1.SetFacePlugin(String pluginName, Boolean detectFaceMod)
   at Focus.Apps.EasyNpc.Profile.NpcConfiguration`1.Reset(Boolean defaults, Boolean faces)
   at Focus.Apps.EasyNpc.Profile.NpcConfiguration`1..ctor(INpc`1 npc, IModPluginMapFactory modPluginMapFactory, IProfileRuleSet ruleSet)
   at Focus.Apps.EasyNpc.Profile.ProfileViewModel`1..ctor(IEnumerable`1 npcs, IModPluginMapFactory modPluginMapFactory, IEnumerable`1 loadedPluginNames, IEnumerable`1 masterNames, ProfileEventLog profileEventLog)
   at Focus.Apps.EasyNpc.Main.MainViewModel`1.<>c__DisplayClass55_0.<.ctor>b__1()
   at Focus.Apps.EasyNpc.Main.LoaderViewModel`1.ConfirmPlugins()
   at System.Threading.Tasks.Task.<>c.<ThrowAsync>b__140_0(Object state)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.DispatcherOperation.InvokeImpl()
   at MS.Internal.CulturePreservingExecutionContext.CallbackWrapper(Object obj)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at MS.Internal.CulturePreservingExecutionContext.Run(CulturePreservingExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Windows.Threading.DispatcherOperation.Invoke()
   at System.Windows.Threading.Dispatcher.ProcessQueue()
   at System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.Dispatcher.LegacyInvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
   at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
   at MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
   at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.Run()
   at System.Windows.Application.RunDispatcher(Object ignore)
   at System.Windows.Application.RunInternal(Window window)
   at System.Windows.Application.Run()
   at Focus.Apps.EasyNpc.App.Main()
 ---> (Inner Exception #1) System.ArgumentNullException: Value cannot be null. (Parameter 'key')
   at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
   at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
   at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](List`1 source, Func`2 keySelector, Func`2 elementSelector, IEqualityComparer`1 comparer)
   at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector, IEqualityComparer`1 comparer)
   at Focus.Apps.EasyNpc.GameData.Files.ModPluginMap.<>c__DisplayClass3_0.<ForDirectory>b__5()
   at System.Threading.Tasks.Task.InnerInvoke()
   at System.Threading.Tasks.Task.<>c.<.cctor>b__277_0(Object obj)
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread)<---

Block builds on invalid mod selections

As it came out in #48, and at least one informal bug report before that, if any NPCs in a profile point to invalid mods - including mods that were removed or renamed later on - the build is always going to crash.

These should already be reported as build warnings, but this isn't really a warning, it's an error. We know that the build cannot complete in this state and therefore it requires a different UI. Arguably, this should be shown prominently before even starting/showing any other build checks, and be required to be corrected.

Making this easy to correct would also be helpful, such as a filter on the Profile page by mod name (not plugin, because in this case the plugin was still valid).

Add "invalid only" option to Reset panel

We have the option to reset NPC defaults/faces in the Maintenance tab, but now that additional checks are being added for missing plugins (i.e. face/default referencing a plugin that no longer exists), there is some likelihood of needing a large-scale but not full-scale "reset".

More specifically, if I've made direct changes to 500 NPCs, and then remove a fairly large mod like HPNPC or WICO or RS Children, that modified another 1500 NPCs, there's currently no way to fix everything for 1500 invalid entries without losing my 500 good ones.

With the detection in place, it would make sense to add an "Only invalid entries" checkbox to the Maintenance tab - or just a top level "Fix invalid" button - which does exactly this. Go through the list of NPCs, find any with temporary assignments due to missing plugins, and reset the default/face selections as needed, making these selections permanent (autosaved) and updating the mod selections to match.

Error during build: Sequence contains more than one element

Trace:

2021-07-18 01:00:38.270 +02:00 [ERR] Exception was not handled
System.InvalidOperationException: Sequence contains more than one matching element
   at System.Linq.ThrowHelper.ThrowMoreThanOneMatchException()
   at System.Linq.Enumerable.SingleOrDefault[TSource](IEnumerable`1 source, Func`2 predicate)
   at Focus.Apps.EasyNpc.Build.MergedFolder.Build[TKey](IReadOnlyList`1 npcs, MergedPluginResult mergeInfo, IArchiveProvider archiveProvider, IFaceGenEditor faceGenEditor, ModPluginMap modPluginMap, IModResolver modResolver, BuildSettings`1 buildSettings, ProgressViewModel progress, ILogger logger)
   at Focus.Apps.EasyNpc.Build.BuildViewModel`1.<BeginBuild>b__79_0()
   at System.Threading.Tasks.Task.InnerInvoke()
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread)
--- End of stack trace from previous location ---
   at Focus.Apps.EasyNpc.Build.BuildViewModel`1.BeginBuild()
   at System.Threading.Tasks.Task.<>c.<ThrowAsync>b__140_0(Object state)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.DispatcherOperation.InvokeImpl()
   at MS.Internal.CulturePreservingExecutionContext.CallbackWrapper(Object obj)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at MS.Internal.CulturePreservingExecutionContext.Run(CulturePreservingExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Windows.Threading.DispatcherOperation.Invoke()
   at System.Windows.Threading.Dispatcher.ProcessQueue()
   at System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.Dispatcher.LegacyInvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
   at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
   at MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
   at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.Run()
   at System.Windows.Application.RunDispatcher(Object ignore)
   at System.Windows.Application.RunInternal(Window window)
   at System.Windows.Application.Run()
   at Focus.Apps.EasyNpc.App.Main()

Support for Vortex mod manager

Vortex has a staging directory that looks a lot like Mod Organizer's mod directory, although the subdirectories are named differently.

In particular, multiple files or versions from the same mod are not merged in Vortex, so you can end up with:

  • ModName-{NexusID}-{VersionInfo}
  • ModName-{NexusID}-{OtherVersionInfo}
  • OptionalFileName-{NexusID}-{VersionInfo}

etc.

Thus it is possible, albeit quirky, to infer the concept of a "mod". We have to group by the Nexus ID and perhaps try to figure out the "primary" mod name in a context-dependent way, or perhaps some combination of heuristics such as total size (could be tricked by a 4K texture addon) or the presence of ESPs/BSAs (tricked by updates/patches) and other checks. Individually none of them are reliable, but put together, they might be useful. Alternatively, maybe there is some metadata in the file system that could tell us without having to use the Nexus API, or maybe there is some other Vortex file we could read to get that info. I'm not a Vortex user so I don't know offhand.

But there is probably some way to do this, quirky though it may be, and if so, then there is no reason why EasyNPC needs to be a Mod Organizer exclusive.

The final question is whether the generated mod directory will actually show up in Vortex. Possibly, it happens automatically or after a refresh/restart, as Vortex does support "offline" mods. Or possibly it has to be registered somehow and instructions need to be provided in the docs. Will require testing and/or input from Vortex users to figure out.

Ideally this would be made available with the first Nexus release, since Vortex is obviously prevalent there.

Vortex launch fails due to non-numeric mod ID

As reported on the Nexus page:

Newtonsoft.Json.JsonReaderException: Could not convert string to integer: skse64. Path 'files.skse64_2_00_19.modId', line 1, position 16438.
   at Newtonsoft.Json.JsonReader.ReadInt32String(String s)
   at Newtonsoft.Json.JsonTextReader.FinishReadQuotedNumber(ReadType readType)
   at Newtonsoft.Json.JsonTextReader.ReadNumberValue(ReadType readType)
   at Newtonsoft.Json.JsonTextReader.ReadAsInt32()
   at Newtonsoft.Json.JsonReader.ReadForType(JsonContract contract, Boolean hasConverter)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateDictionary(IDictionary dictionary, JsonReader reader, JsonDictionaryContract contract, JsonProperty containerProperty, String id)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.SetPropertyValue(JsonProperty property, JsonConverter propertyConverter, JsonContainerContract containerContract, JsonProperty containerProperty, JsonReader reader, Object target)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
   at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
   at Newtonsoft.Json.JsonSerializer.Deserialize(JsonReader reader, Type objectType)
   at Newtonsoft.Json.JsonSerializer.Deserialize[T](JsonReader reader)
   at Focus.ModManagers.Vortex.VortexModResolver.LoadManifest(String path)
   at Focus.ModManagers.Vortex.VortexModResolver..ctor(IModResolver defaultResolver, String manifestPath)
   at Focus.Apps.EasyNpc.App.CreateModResolver(StartupInfo startupInfo, CommandLineOptions options)
   at Focus.Apps.EasyNpc.App.Start(CommandLineOptions options)
   at CommandLine.ParserResultExtensions.WithParsed[T](ParserResult`1 result, Action`1 action)
   at Focus.Apps.EasyNpc.App.Application_Startup(Object sender, StartupEventArgs e)
   at System.Windows.Application.OnStartup(StartupEventArgs e)
   at System.Windows.Application.<.ctor>b__1_0(Object unused)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.DispatcherOperation.InvokeImpl()
   at System.Windows.Threading.DispatcherOperation.InvokeInSecurityContext(Object state)
   at MS.Internal.CulturePreservingExecutionContext.CallbackWrapper(Object obj)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at MS.Internal.CulturePreservingExecutionContext.Run(CulturePreservingExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Windows.Threading.DispatcherOperation.Invoke()
   at System.Windows.Threading.Dispatcher.ProcessQueue()
   at System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.Dispatcher.LegacyInvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
   at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
   at MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
   at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.Run()
   at System.Windows.Application.RunDispatcher(Object ignore)
   at System.Windows.Application.RunInternal(Window window)
   at System.Windows.Application.Run()
   at Focus.Apps.EasyNpc.App.Main()

Nothing depends on the ID being a number, so the type just needs to be changed.

More Profile filters

The default, record-based filters are good enough for most basic tasks, but aren't particularly helpful for certain troubleshooting, cleanup or "evolution" flows. Specifically what I have in mind here is filtering by:

  • Default or face override (i.e. show me everything referencing a plugin)
    • This is particularly important for debugging "unwanted master" issues, but also good for migrating away from a plugin that someone has decided they don't like anymore.
  • NPCs with wigs, since these tend to cause a lot of problems.
  • Conflicting selections where mod and plugin don't match.
  • Invalid mod selections.
  • NPCs with missing plugins (#2).

These probably belong in the header area but aren't going to fit, so either all filters or just the new filters would have to be turned into a dropdown.

Some DLC changes are reverted on the default settings

image
image

This problem seems to mostly (almost exclusively) affect some of the Vampires in the Dawnguard DLC. It looks like the changing of the head part causes the DLC to be labeled as a face mod, overriding the perk, voice, actor effects and faction changes in these NPCs.

Maybe some logic could be added to check if the mod(or DLC in this case) makes any changes to the NPCs relative to its own masters. Though I am not sure how viable this is performance-wise

Improve mugshot messaging for facegen-only edits

As identified in this comment, if a mod edits an NPC by only providing a facegen, but does not contain any record edits, the default message that gets displayed ("plugin not loaded") is confusing and misleading.

These types of edits often aren't safe and are also flagged by the build checker, so displaying a warning is still a good thing. It should just be a clearer warning, if possible.

I can't think of any consistent way to tell the difference between "plugin exists but isn't enabled" and "there actually is no plugin that edits this NPC" because, obviously, EasyNPC can't read the mod author's mind or know about every plugin that might be in a mod - at least not without massive scope creep in the form of some huge database about every mod and plugin and version. So we might have to settle for just replacing the message with one that allows for a little more ambiguity.

Still not sure about exact wording.

Incremental/differential builds

Actually a two-parter:

  1. With each mod, embed the profile and settings that were used to create it (including load order, installed/found mods), so that it can easily be recreated. This is "somewhat" useful in and of itself, for troubleshooting, but really is an enabler of:

  2. Allow updating an existing merge instead of creating a new one, based only on the changes made to the profile. Build times could be described as "decent" (under 5 minutes, usually), but shaving that 5 minutes down to 5 seconds would be extra awesome.

This will actually require the metadata to be fairly detailed if we want to be able to keep the merge clean. For example, if we've copied a bunch of head parts and related assets for a specific subset of NPC choices that have all been changed, then those records and files are all orphans and need to be deleted. The metadata therefore needs to track everything that was added, and the reason (i.e. originating record or file) why it was added, in a tree or more likely graph structure, since diamonds are clearly possible (mod X includes NPCs A and B who use headparts C and D which both reference the same texture). This graph then has to be traversed and updated in order to prune obsolete dependencies.

What about file assets? These may have changed, e.g. a bugfix for a particular mod was installed which just updated a single mesh or texture. We might be able to use simple MD5 or CRC32 to check for changes. This requires reading all the files as they currently are. Is this faster than just copying them all again? Uncertain - copying sure does take a long time, especially the facegen/facetint files, but checksumming them might take almost as long, and if a large number have changed, then that time is added to the copy time. (Note - we don't have to recompute the hashes for the previous merge, we can just store those when it's created the first time.)

What about BSAs? BSAs are immutable... or are they? They're immutable with the way the API currently works, but theoretically there actually is a low-O mutation algorithm if only a small number of files have changed:

  1. Compute new header (index) size
  2. Append new files to the end of the archive
  3. Move file blobs from the beginning of the files section to the end of the file until the "hole" is greater than the difference between previous and current header size
  4. Either do nothing with the additional empty space (between end of new header and beginning of "moved" files section), if this proves to be stable in-game, or zero it out and add a "padding file" to the index. It's a generated patch, so it makes no difference if there's one garbage file in there.
  5. Rewrite the header entirely (probably fast enough) or update it with the new/changed offsets
  6. On next inc/diff build, fill in the padding area if it's large enough to hold the new/changed files, or expand it the same way as above if not.

A few problems: (a) this crosses multiple libraries and is totally untested, (b) pre-BA2 structure containing directory offsets probably throws a wrench in the gears, although this isn't unsolvable with the right algorithms; and (c) this significantly raises the complexity of keeping archive sizes under 2 GB - a possible workaround is to only expand the last archive in the sequence, and use the same uncompressed-size rule to decide when it's too large.

The complexity of incremental/differential BSAing is extremely high. Even though it seems technically possible, and maybe technically interesting, it may not be worth the effort here. A lower-tech option that is likely to work for many people is to do the diffs/additions as loose files, and if the loose file list seems to be growing unreasonably large (say, over 1 GB), then give the option to "compact" i.e. rebuild the BSAs using loose files as overrides. This could even be set up as a user-defined build rule ("compact when loose files > X"), with a reasonable default, and many/most users would either not really notice or never even encounter it.

Definitely leaning toward the low-tech option for now, with high-tech being left for some way-off-in-the-future release. The biggest risk with low-tech is someone changing the loose files and invalidating the hashes, but we can say that, metaphorically speaking, tampering with the output voids the warranty. And in any case, the issue can always be fixed by just generating a brand-new output mod.

Blah blah blah, tl;dr - this is doable in the short term, hopefully by beta or shortly after, but is liable to be a little bit on the ugly or "brute force" side.

Custom body carry-over

While it's going to be far better for compatibility to not carry over custom bodies, in the spirit of allowing users to be in full control, this should still be allowed.

In principle, not very difficult - copy the WNAM and make it standalone like we do with HDPTs.

Questions abound on how best to expose the feature:

  • Global override?
  • Per mod/plugin?
  • Per NPC?
  • Do we need body previews?
  • Do we need to warn (e.g. about armor clipping, physics issues)?
  • Use as automatic fallback for wig conversion (wigs also use WNAM)?
  • Only if the body would otherwise be vanilla (i.e. load order doesn't include body mods)?

And probably more. Most of the work will go into the UI design and mind-reading logic, rather than the record-copying itself.

Folder redirects for face previews

The documentation currently has some guidance on renaming mugshot folders if they don't quite match up, but this is limited and a little user-unfriendly. A better way to handle this is name-based redirects.

In other words, let me set up a mapping from "Bijin NPCs" to "Bijin NPCs SE" to tell EasyNPC that they're the same mod, and to use mugshots of one for the other. This isn't just for renamed mods, it's also going to be very useful if a mod author releases an AIO version of previous standalone mods, as was the case with Pride of Skyrim AIO. If someone has a few of the older PoS overhauls but doesn't want to download the whole AIO, they should be able to use the mugshots from the AIO. The app will still know that the plugin isn't installed, so this shouldn't trick anyone into making invalid selections.

This could go on the Settings page, as a simple grid of source->destination text or combo boxes.

Add unit tests

Some things are going to be very hard to test, especially those things that depend on the idiosyncracies and complex interactions of different kinds of mods. But that's a relatively small part of the app.

At the moment, the real reason this hasn't been done is because the code structure is making it difficult. With some or all of #26 out of the way, we should be able to begin to write useful tests.

Some areas that are in particularly dire need of concrete specs are:

  1. The rules for detection/reset of plugins per NPC
  2. Detection of ITM, ITPO and whether records change face or behavior
  3. NPC filters in the UI, and "unusual" interactions like forcing an NPC to be visible when coming from a build warning
  4. Many of the "little" interactions, like exactly which parts of the screen are supposed to update, and how, when some other property is changed. Example is what updates (or doesn't update) when an NPC is selected, when a mugshot is highlighted, vs. mugshot double clicked, etc.
  5. Everything that possibly can be tested about the Build process. Mutagen interactions may be the most complicated, but some things like file copying and even BSA creation should be pretty simple after a little refactoring.

Show clearer warnings for "Trim" operation

Trimming is a destructive operation that potentially makes it much more difficult to get the profile back into a "clean" state, if there are problems with the current configuration.

Specifically, if a mod has been removed, especially a large one like High Poly NPC Overhaul, the result will be a profile with many NPCs permanently pointing to the fallback face plugin (possibly Skyrim.esm), but still pointing to the original mod, because the mod has not actually disappeared (it's just disabled).

I see two areas of improvement here:

  1. Warn more generally that trimming is usually not necessary, can make temporary problems permanent, and should only be done in extreme cases such as autosaves that are tens or hundreds of MB.

  2. Warn more specifically when trimming would cause inconsistencies between the plugin and the mod, and advise correcting those problems first, e.g. by using Reset with "missing only" checked.

Apply visual customizations from a different NPC

When you use many visual overhaul mods, there will be many eliminated faces from them. Although eliminated but their quality is still very high, is it possible to add features to record them and then use them on other NPC mod that do not have a good visual overhaul

Error in File.Delete when starting build

Log file from report:

2021-07-17 19:48:19.196 +02:00 [INF] All NPCs loaded.
2021-07-17 19:49:36.644 +02:00 [INF] [Pack Archive] - Waiting for merged folder
2021-07-17 19:49:36.644 +02:00 [INF] [Merged Plugin] - Not started
2021-07-17 19:49:36.644 +02:00 [INF] [Merged Folder] - Waiting for merged plugin
2021-07-17 19:49:36.648 +02:00 [ERR] Exception was not handled
System.ArgumentNullException: Value cannot be null. (Parameter 'path')
   at System.IO.File.Delete(String path)
   at Focus.Apps.EasyNpc.Build.BuildViewModel`1.<BeginBuild>b__79_0()
   at System.Threading.Tasks.Task.InnerInvoke()
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread)
--- End of stack trace from previous location ---
   at Focus.Apps.EasyNpc.Build.BuildViewModel`1.BeginBuild()
   at System.Threading.Tasks.Task.<>c.<ThrowAsync>b__140_0(Object state)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.DispatcherOperation.InvokeImpl()
   at MS.Internal.CulturePreservingExecutionContext.CallbackWrapper(Object obj)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at MS.Internal.CulturePreservingExecutionContext.Run(CulturePreservingExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Windows.Threading.DispatcherOperation.Invoke()
   at System.Windows.Threading.Dispatcher.ProcessQueue()
   at System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.Dispatcher.LegacyInvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
   at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
   at MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
   at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.Run()
   at System.Windows.Application.RunDispatcher(Object ignore)
   at System.Windows.Application.RunInternal(Window window)
   at System.Windows.Application.Run()
   at Focus.Apps.EasyNpc.App.Main()

This looks to be the build report for Vortex. It's probably failing because it doesn't exist when running from Mod Organizer.

Mistaken assumption that File.Delete would have an implicit "if exists". It doesn't.

Dynamic previews

I know it's a big ask and not going to happen any time soon, and my C# is rusty at best so I probably won't be much help short-term, but I thought it worthwhile to at least open an issue to discuss what it would take to make this happen. For modding guides on the scale of Lexy's and mine, a true visual NPC picker that can do what EasyNPC does would be a game-changer, so I'm willing to put in as much legwork as I can to make it happen.

Crash at Initial Opening of Program

Program crashed when I tried to open it for the first time. Appears to be a problem with the Sands of Time mod. Log follows:

Log_20210718_043859_4027903.txt

2021-07-18 04:38:59.540 -04:00 [INF] Initialized
2021-07-18 04:41:40.933 -04:00 [ERR] Exception was not handled
RecordException SOTFull.esp => 000D62:SOTFull.esp<Race>: Could not resolve record    at Mutagen.Bethesda.IFormLinkExt.Resolve[TMajor](IFormLinkGetter`1 link, ILinkCache cache)
   at Focus.Apps.EasyNpc.Mutagen.MutagenAdapter.<GetValidRaces>b__41_0(IFormLinkGetter`1 x)
   at System.Linq.Utilities.<>c__DisplayClass2_0`3.<CombineSelectors>b__0(TSource x)
   at System.Linq.Enumerable.SelectEnumerableIterator`2.MoveNext()
   at System.Collections.Generic.HashSet`1.UnionWith(IEnumerable`1 other)
   at System.Collections.Generic.HashSet`1..ctor(IEnumerable`1 collection, IEqualityComparer`1 comparer)
   at System.Linq.Enumerable.ToHashSet[TSource](IEnumerable`1 source, IEqualityComparer`1 comparer)
   at System.Linq.Enumerable.ToHashSet[TSource](IEnumerable`1 source)
   at Focus.Apps.EasyNpc.Mutagen.MutagenAdapter.<ReadHairRecords>b__34_1(IHeadPartGetter x)
   at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
   at System.Linq.Enumerable.SelectManySingleSelectorIterator`2.MoveNext()
   at System.Linq.Enumerable.WhereEnumerableIterator`1.ToList()
   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   at Focus.Apps.EasyNpc.Main.LoaderViewModel`1.ConfirmPlugins()
   at System.Threading.Tasks.Task.<>c.<ThrowAsync>b__140_0(Object state)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.DispatcherOperation.InvokeImpl()
   at MS.Internal.CulturePreservingExecutionContext.CallbackWrapper(Object obj)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at MS.Internal.CulturePreservingExecutionContext.Run(CulturePreservingExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Windows.Threading.DispatcherOperation.Invoke()
   at System.Windows.Threading.Dispatcher.ProcessQueue()
   at System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.Dispatcher.LegacyInvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
   at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
   at MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
   at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.Run()
   at System.Windows.Application.RunDispatcher(Object ignore)
   at System.Windows.Application.RunInternal(Window window)
   at System.Windows.Application.Run()
   at Focus.Apps.EasyNpc.App.Main()

Automatic outfit merging

There are a wide variety of mods that edit NPC outfits: that is, the DOFT - Default Outfit or SOFT - Sleep Outfit references.

and no doubt many more.

EasyNPC does "support" outfits in the sense that they are currently part of the Default/Behavior category. Installing CRF, Opulent Outfits, and the OO-CRF patch, and referencing OO-CRF as the Default, will get the outfit, along with any face edits from other overhauls, without messing up CRF. However, our goal is to eliminate thousands of compatibility patches, and we could do a lot better here.

In a similar vein to hair mods (#6), there is likely a happy-path that most modders intend on, and that less-experienced modders probably intuit should just work:

  • If an overhaul changes an NPC's outfit, then the overhaul wins.
  • If an overhaul does not change an NPC's outfit, but some outfit mod does, then the outfit mod wins.
  • In some cases explicitly specified by the player, outfit mods should take precedence over the overhaul mod.

The solution is therefore to detect, catalog, and allow users to override and prioritize "outfit changers", and incorporate these into the merge using logic similar to the above. This will eliminate the need for outfit compatibility patches, just like it already eliminates the need for overhaul compatibility patches.

Some open questions:

  • How do we reliably identify an "outfit changer mod"?
    • One way is to try to go by record types, i.e. look for just ARMR, ARMA, and NPC_, maybe also TXST and so on. But outfits can imply a lot more records - magic effects, crafting/tempering recipes, even worldspace locations containing cheat chests for those outfits.
    • A simpler, dumber but probably more stable way is to mark any mod that changes an NPCs outfit as an outfit-changer mod. This could put out a bloated list, but maybe that's OK.
  • Should the outfits be cloned into the merge, or the outfit mods be used as masters?
    • The wide variety of record types that could be used by outfit mods tend to point to the master option. Better to just add a dependency up front than to risk a half-baked implementation that copies some top-level records and still needs the master for something deep in the bowels of the mod.
    • However, this does nerf the "standalone" claim a bit.
  • If body carryover (#7) is also in use, should outfit mods be assumed to be meant for the default body and therefore incompatible and ignored? Is it even possible to define a rule that will always give the correct result here?
  • Should this be visually-integrated in any way, i.e. do we need outfit previews along with NPCs or in some other section?

Masters problem

Easynpc is adding some npc overhauls as a master (such as the bijin series), meaning i cant disable the plugins/mods without having a missing masters issue.

Report defaults (masters)

This has some overlap with #5 but in a different context. Since building the NPC mod is an expensive process, it will be helpful to inform people which plugins are going to end up as masters before they actually start the build. This could maybe be shown on the build confirmation screen, just before clicking the "Build" button, since that screen has a good amount of empty space already.

Integrating with #5 when implemented would also be a good idea - i.e. if a suspicious master is present, they could double-click on it and be taken to a filtered Profile view showing every NPC that includes it as the master (default).

I think this would be the most straightforward, lowest-effort (for users) method of fixing any mistakes that EasyNPC made in its initial scan, or stray edits during the profile build.

Idea: Detect patches by master dependency on flagged overhauls

The whole "disable patches first" thing is proving to be very difficult for at least some subset of users, either due to confusion over what constitutes an "overhaul patch" or just general difficulty with the mechanics of it, especially in Vortex.

In some interim version, probably 0.3, the detection on overhaul mods was improved so that overhauls themselves could never be detected as default plugins. The detection uses some heuristics (% of faces, % of only faces, etc.) but seems to be quite reliable.

One thing that was never looked at as a follow-up to that was extending this into patches. It just hadn't come up as a major issue at the time. However, if a visual overhaul should never be selected as default, then it logically follows that a patch for that overhaul should not be selected as the default either.

I doubt the detection here is perfect... but it might be just good enough to drop the whole "no patches" thing if it catches 99% or 95% of the problem-plugins. The whole reason for anti-patching is just to prevent them from ending up as defaults, and if we can prevent it a different way that doesn't require any manual work, then the process can be made a lot easier. Which is of course the whole idea.

If the detection is almost perfect but not quite, a few broad-targeted manual overrides could help smooth over the rest. For example, in addition to implementing #17, add a feature somewhere to "remove from defaults" that would cut out an entire plugin from all NPCs using it. Or, some type of "soft reset" feature that could allow users to identify patches in the load order or just any mods they want removed - i.e. rather than doing it one at a time. It's not good if these hack-fixes become a core part of the workflow, but if they just deal with a few strange edge cases, they're more palatable design-wise.

Immersive weapons crash

Crashes on loading immersive weapons with:

2021-07-18 18:53:45.867 -05:00 [ERR] Exception was not handled
System.InvalidOperationException: Nullable object must have a value.
   at System.Nullable`1.get_Value()
   at Focus.Apps.EasyNpc.Mutagen.MutagenAdapter.<>c.<ReadFaceTints>b__52_0(ITintLayerGetter x)
   at System.Linq.Enumerable.SelectEnumerableIterator`2.ToArray()
   at Focus.Apps.EasyNpc.Mutagen.MutagenAdapter.ReadFaceTints(INpcGetter npc)
   at Focus.Apps.EasyNpc.Mutagen.MutagenAdapter.ReadFaceData(INpcGetter npc)
   at Focus.Apps.EasyNpc.Mutagen.MutagenAdapter.GetFaceOverrides(INpcGetter npc, INpcGetter comparison, Boolean& affectsFaceGen)
   at Focus.Apps.EasyNpc.Mutagen.MutagenAdapter.ReadNpcRecords(String pluginName, IDictionary`2 cache)
   at Focus.Apps.EasyNpc.Main.LoaderViewModel`1.GetNpcs()
   at System.Threading.Tasks.Task`1.InnerInvoke()
   at System.Threading.Tasks.Task.<>c.<.cctor>b__277_0(Object obj)
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread)
--- End of stack trace from previous location ---
   at Focus.Apps.EasyNpc.Main.LoaderViewModel`1.ConfirmPlugins()
   at System.Threading.Tasks.Task.<>c.<ThrowAsync>b__140_0(Object state)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.DispatcherOperation.InvokeImpl()
   at MS.Internal.CulturePreservingExecutionContext.CallbackWrapper(Object obj)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at MS.Internal.CulturePreservingExecutionContext.Run(CulturePreservingExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Windows.Threading.DispatcherOperation.Invoke()
   at System.Windows.Threading.Dispatcher.ProcessQueue()
   at System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.Dispatcher.LegacyInvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
   at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
   at MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
   at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.Run()
   at System.Windows.Application.RunDispatcher(Object ignore)
   at System.Windows.Application.RunInternal(Window window)
   at System.Windows.Application.Run()
   at Focus.Apps.EasyNpc.App.Main()

Build Warning: FaceModMissingFaceGen

So after running the built-in check for problems feature, the Build Warnings were all reporting FaceModMissingFaceGen, the weird thing was that the warnings were all connected to actors that were using seemingly unrelated mods for the face sources.
e.g: JaySus swords. See the Word Doc attached for the full list.
EasyNPC Problems.docx

So I posted this concern on the Nexus Page, which got a very informative response. I then tried going ahead with the build disregarding the warnings and then got the App Error message and link to Crash Log.

Please see the attached crash log.

Thank you for the assistance.

Log_20210720_153606_9196696.txt
@focustense

"Path" error in Vortex extension

Some users have reported this one coming from Vortex when trying to launch the app:

The "path" argument must be of type string. Received undefined.

Image

Seems to be the result of one or more tools registered with no path. The extension just needs to ignore these.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.