Friday, June 30, 2017

Abiathar API - Configuration

Last time, we looked at the "lifecycle" events that come through the event bus. Some of those have to do with configuration storage in the project file.

All configuration objects need to inherit from FleexConfig. That class provides serialization and deserialization of the public fields. If the class has FleexConfHostAttribute, all public fields will be marked serializable, unless they have FleexConfExemptAttribute. Otherwise, all serializable fields must be marked with FleexConfEntryAttribute. If a field has FleexConfLoadOnlyAttribute, it will be loaded but not saved; this allows for smooth deprecation of old entries. If an entry has FleexConfSaveIfNonNullAttribute, it will only be written if it's not null; this saves space. If it has FleexConfSaveIfNotEqAttribute (which takes a parameter), it will only be written if it doesn't have that value; this also saves space. To have a comment placed before an entry, give it FleexConfCommentAttribute; this is how the guidance in editor.aconf is generated.

Supported types include strings, every value type with a Parse method, lists, and subclasses of FleexConfig.

If your config object should do some special logic after being loaded, override the OnLoad method.

Thursday, June 29, 2017

Abiathar API - Event bus

You might have noticed last time that Abiathar extension objects don't have a whole lot in terms of members. Events are received through the event bus. To make your extension handle an event, create a public function on the event object that receives one of the subclasses of AbiatharEvent, and add the AbiatharEventBusAttribute attribute to the method. This makes Abiathar see the method as an event handler. All event objects have a state accessor that you can use to interact with Abiathar.

Let's look at some "lifecycle" events.

After Abiathar loads all the extensions, it sends AbiatharExtensionsLoadedEvent. Extensions may use the RequestAnother method to ask for another round of these (say, it wants to talk to another extension after the other one has initialized). Therefore, you need to check whether you've already responded appropriately to one of these events so that you don't initialize things too many times.

Soon after, AbiatharSetupMenuStripEvent is sent. This provides the main menu of the window so you can fiddle around with it and add menu items if you please.

When a new project is created, AbiatharRegisterFileConfigEvent is sent. If your extension needs to store some bookkeeping in the project, add an entry to ConfigFiles. Configuration storage will be addressed next time.

When an existing project is loaded, AbiatharLoadFileConfigEvent is sent. Check the Name property and, if the configuration object belongs to your extension, set the ConfigTemplate to a default/empty instance of your configuration type. If a configuration section is not claimed, it will be deleted.

When the application is closing, AbiatharClosingEvent is sent. In response to this, clean up any external resources, like temporary files.

Wednesday, June 28, 2017

Abiathar API - Getting loaded

Welcome to Part 1 of a series on the Abiathar API. This will serve as API documentation so that other programmers can create extensions for Abiathar to add features. Without further ado, let's begin.

Abiathar extensions are .NET DLL files with a .aex extension. These require a reference to Abiathar's Interop.dll. To actually have your DLL recognized as an extension provider, include one public class that implements IAbiatharExtensionServer. That class must be constructable without parameters. Extensions servers have two required methods:

  • LoadExtensions returns a list of IAbiatharExtension. This list can be empty if your extension server determines it can't work on the platform, for instance. If you serve only one extension, it's fine to return a list containing only the extension server if the server also implements IAbiatharExtension.
  • ExtensionLoadFail is called if there's a problem getting the metadata of an extension. It includes the problematic extension and the exception.
The extension object itself only needs one property, Metadata, of type AbiatharExtensionMetadata, which has these fields:
  • Logo is the image that shows up next to your extension in the Extensions menu. It can be null if you don't want an icon.
  • Name is the short name of your extension, for showing the Extensions menu.
  • Version is the version of your extension. It shows up the extension info box.
  • Author is the author of your extension.
  • Description is the brief description of your extension that shows up in crash reports.
  • LongDescription is the more complete description that shows up in the extension info box. If this is empty, the Description is used instead.
  • Measurements is a function that takes a string and returns an object. You can do whatever you want in this function; Abiathar never uses it. It's to allow cooperating extensions to communicate among themselves. This can be null if you don't use that feature.
Drop the compiled AEX next to Abiathar and you'll see your extension in the Extensions list. It doesn't do anything yet, though. Next time, we'll look at adding some functionality.

Tuesday, June 27, 2017

Explorer has trouble with filenames ending in spaces

One user noticed some weird behavior of File Explorer when working with items whose names end with a space. Such items can't be deleted; Explorer claims it's no longer located in the parent folder. Renaming it produces an error that the source and destination names are the same. That probably indicates that Explorer trims the file name before doing anything with it.

The only way to get rid of such an item is to use the absolute path syntax \\?\ in a command prompt. The name should be passed in quotes so that the command knows to include the trailing space.

Monday, June 26, 2017

The real account name is not the display name

One user wanted to find the original name of an account after it "being changed from Control Panel." Based on existing answers to that question, I determined that there was some confusion about which name was changed. Accounts have a display name that shows up in most user-facing places. There's also a SAM name: the internal name that you can type into a domain login prompt. It's more challenging to change the SAM name - the only convenient way I know is to use the lusrmgr.msc snap-in - while the standard Control Panel allows the display name to be changed with ease.

While changes to the SAM name produce an event in the event log (4781, to be specific) with the old and new names, that's not the case for the display name. All account properties changes produce event 4738 with the new data, but the old value is not recorded. Therefore, the only way to find the old display name would be to find the previous instance of event 4738, which might have aged out of existence.

Sunday, June 25, 2017

Patch to fix 100% CPU usage and freezing in Clarion 6 applications

A little while back, I noted that Windows 10 build 1703 rewrote the window manager internals. Now, calling PeekMessage sometimes produces more window messages. If a program peeks for messages a few times every spin around its message loop, it can wedge itself into an infinite loop as GetMessage then returns immediately when it sees the internal message, letting the loop go around once more. It so happens that there's a framework that does just that. I worked with a third-party application built on version 6 of Clarion. When run on 1703, the program froze in certain places, pegging the CPU at 100%, but on older Windows versions it was fine.

SoftVelocity no longer supports Clarion 6, so Clarion users are on their own. Fortunately, I managed to create a patch that fixes the problem. The relevant code is in C60RUNX.DLL. At 0xA867B, write these bytes:

8B 44 24 28 ; mov eax, [esp+28h]
E8 D2 42 03 00 ; call fix_detour
83 F8 00 ; cmp eax, 0
75 F2 ; jne -Dh
90 ; nop

We need some extra space for a bit of new logic. Fortunately, there is plenty of padding at the end of the code segment, which is where the call goes. At 0xDC956, write:

50 ; push eax
6A 00 ; push 0
6A 00 ; push 0
6A 00 ; push 0
50 ; push eax
E8 25 46 F2 FF ; call GetMessageA
58 ; pop eax
8B 40 04 ; mov eax, [eax+4]
25 FF FF 00 00 ; and eax, FFFFh
3D 38 07 00 00 ; cmp eax, 738h
0F 94 C0 ; sete al
0F B6 C0 ; movzx eax, al
C3 ; ret

This latter part is a function that takes the address of the buffer for the message in eax and returns whether the message should be ignored (i.e. is the new 0x738 message). Experienced Win32 programmers may notice that this doesn't check the return value of GetMessage, but neither does the original code, so this is no worse. Back up in the first section, the patch replaces the call to GetMessage with a call to this function, and if the message needs to be skipped, it just jumps back and does the call again until it gets something else.

Saturday, June 24, 2017

X509Certificate2.Verify checks the entire chain

Someone was confused about why the Verify method on a self-signed certificate was returning False. More information than just a yes/no is available, though, if you know how to ask. Creating an X509Chain object, calling Build with the certificate, and examining the ChainStatus property shows why the certificate failed to verify. In this case, it was because the root was untrusted, which makes sense since it was a self-signed certificate.

To make it verify, the user needed to add the certificate to the Trusted Root Certification Authorities store. That makes the root trusted, and since everything else about the certificate is good, the certificate verifies.

Friday, June 23, 2017

Policy Plus - POL editing

Today I wrote the rest of the Edit Raw POL dialog. It now supports multi-line strings, forgetting, and deletion. Using the Delete Value(s) button on a value instantly converts it to an explicit "delete this value." Using it on a key brings up a dialog with a choice of deleting everything in the key, adding an instruction to clear the key of other values, and explicitly deleting a single value by name.

That middle one was tough. I had previously assumed that a dictionary kept its keys in the order of insertion, and it usually does, but when the same key is deleted and then re-added, it keeps its place instead of going to the end of the line. Order of application matters because it's possible for a policy to clear a key and then put values in it - lists can do this. I switched to a sorted dictionary for keeping track of the POL entries, which works because **del sorts before normal value names and so will be correctly applied first.


Then I finally decided I'd had enough of the list view's scroll bar jumping all around due to my previous update logic. There isn't really a convenient way of making a list view scroll to a specific location, so I can't recall and then restore that. There is a TopItem property, but it's somewhat buggy and at least for me doesn't always do anything, but it's better than nothing, so I'm going with that.

I also noticed today that policies adding values directly on the Policies key were incorrectly treated as preferences. That's fixed now.

Thursday, June 22, 2017

Policy Plus - Starting POL editing

Today I made some progress on allowing users to edit POL files directly with the Edit Raw POL window in Policy Plus. There are now a handful of buttons across the top to do the editing: Add Key, Add Value, Delete Value(s), Forget, and Edit. Currently only Add Key, Add Value, and Edit do anything.

When a key is selected, Add Key creates a new key under the current one. Add Value creates a new value of the selected key, prompting the user for a kind (e.g. string) and then for the data. Delete Value(s) will do one of two things. If a key is selected, it will allow the user to clear the key or to delete a value with a given name. If a value is selected, it will convert that value to a "delete this value" marker. Forget will remove the selected item and its descendants, making them no longer tracked by the POL file, which is distinct from explicit deletion. Finally, when a value is selected, Edit allows the data to be changed.

Now with actual editing controls
I ran into a bit of a surprise while making the form that edits numeric values (DWord and QWord). Apparently NumericUpDown, despite being designed to work with Decimal numbers (which can get huge, markedly larger than a quad-word), glitches out in hexadecimal mode when its value is greater than the largest signed integer (DWord). Since Policy Plus is supposed to be able to work with unsigned QWords (UInt64), that's a problem. People had run into it before, though, so I was able to adapt some code there into a subclass of NumericUpDown that doesn't bug out.


Since raw POL editing can cause some seriously strange stuff if done poorly, I added a warning message before launching Edit Raw POL for the first time.

FMod - v2.9

Today I published Abiathar v2.9 through the auto-updater and on the PCKF. The only change since yesterday was a small tweak to the Level Inspector's error threshold for level size - it was previously only half of what it should be.

I also published an updated version of the ImfPreview extension. v2.9's removal of the internal ArrayStream class broke ImfPreview, so I had to switch that over to using MemoryStream instead. I also fixed the bug in which too many player windows appeared from a single right-click on a song.

Tuesday, June 20, 2017

FMod - Yet another flood fill fix

The tester for v2.9 noticed a subtle bug. The flood fill tool doesn't fill the very upper-left tile unless that is the tile selected as the start of the fill. The problem is that the flood fill routine stores the list of visited locations as numbers in XXYY format and checks that the value under consideration is greater than zero. I wrote that check to prevent negative values (outside the level bounds) from causing overflows, but it should have been a greater-than-or-equal comparison, not just greater-than. Correcting that fixed the problem. I'll probably publish v2.9 tomorrow.

Monday, June 19, 2017

The Windows 1703 update breaks some AMD drivers

Today I worked with an HP all-in-one with AMD Radeon HD 2300 graphics. It updated to Windows 10 version 1703, after which its extra monitor (attached via a USB-to-VGA adapter) stopped working. The main display also reduced in resolution, with the picture floating in the middle of the screen. Looking in Device Manager showed that the AMD graphics device had a problem.

Apparently, AMD stopped supporting that model a while back; their last released drivers are for Windows 8. HP only has Windows 7 drivers for the machine. Manually installing those drivers worked for a second, but then the driver crashed or something and Windows reverted to the Microsoft basic display driver, falling back to the one monitor at low resolution. Evidently, the 1703 update finally broke support for that AMD driver.

Sunday, June 18, 2017

FMod - Flood fill fix

Yesterday's fix to the level-size-restriction-related crash is working; I received word from the tester today. He discovered a different bug, though. Flood filling in large areas causes Abiathar to vanish. This is actually a crash due to a stack overflow. The flooding is done recursively, so in large uniform areas the recursion can go too deep. This doesn't happen very often in normal levels, but it came up with Rise of the Triad because those levels are a fixed 128x128 (pretty large). So today, I rewrote that section to use a loop instead of recursive calls. That solved the problem.

Saturday, June 17, 2017

FMod - Level size fixes

I sent the Abiathar v2.9 release candidate along with the Rise of the Triad extension off to a tester a couple days ago. I received feedback that it works, but there is one bug. Adding a new level crashed Abiathar. The tester was very helpful: he attached the crash log, so I saw exactly what happened. When the Level | Add menu item is selected, the level properties dialog is set up with some defaults, specifically a default size of 40x40. For Rise of the Triad, that's not an acceptable level size, so those fields were already configured with a minimum and maximum, causing an error when attempting to set those values.

Fixing the crash was a simple matter of clamping the default of 40 to each dimension's acceptable range. I also happened to notice that the Import feature (for single-level files) didn't check the dimensions of the level against the size restrictions of the level format. It does now, so that there will be an error immediately instead of at save time.

Friday, June 16, 2017

Policy Plus - POL viewing

Today I finally got a start on raw POL editing features for Policy Plus. There is now an Edit Raw POL entry under File. When clicked, it prompts for a section (user or computer) and then opens a dialog with a tree of keys and values. Windows Forms doesn't actually have a combined tree/details control, so I rigged up a decent-looking structure by using the IndentCount property on ListView. There currently aren't any editing capabilities, but it does indeed show the contents of the POL file independent of the policies.


The tree starts up at SOFTWARE and there are a few other high-up keys in the tree, so the ones shown are indented a lot. Apparently empty keys appear because there is, for reasons unclear to me, a zero-length entry with a zero-length name in some places.

While working on this, I discovered two other problems. The code in PolicySource to translate DWord and QWord numbers to and from the raw bytes had errors that led to incorrect numbers being reported and crashes when inputting large numbers. That's fixed.

More subtly, I discovered while exploring this new form that the behavior of check boxes with implicit Registry effects is not quite what I assumed. I thought such a checkbox would produce a 1 when checked and nothing when unchecked, but the LGPE sets an active deletion on the value when unchecked. To accommodate that, I updated PolicyProcessing's saving routine and the basic state heuristics to not consider a deletion of a checkbox's value as evidence for the policy being disabled.

Thursday, June 15, 2017

Abiathar Online - Render links

Sometimes I open up levels in Abiathar just to see where the switches and doors go. I've been missing that feature in Abiathar Online. So today, I updated it to have a "draw links" checkbox, which when checked causes infoplane values greater than 255 to render as links. It uses the traditional Abiathar line style and color.


As far as I can tell, it renders identically to the real Abiathar links except that the connecting lines are antialiased.

Updating the level when links are showing is much slower because the entire level has to be rerendered (there might be a link crossing) due to the lack of planes. I will see about the possibility of having multiple canvases on top of each other.

There is currently no convenient way to create a link, but showing links is progress. The changes are live - check it out online!

Wednesday, June 14, 2017

FMod - Extensible level formats

I don't plan to add explicit support for a bunch of different level formats to Abiathar. The main goal is just to be an amazing Keen Galaxy editor, and including other different formats and all the exceptions that come with them makes things more tricky. Still, I want to allow other sufficiently determined people to make Abiathar work with those levels. Therefore, I decided to allow extensions to provide level formats, and that's what I've been working on yesterday and today.

At startup, extensions are given the opportunity to register level formats. When there are extended formats present, a dropdown for them appears on the Level Format page of the New Project Wizard. The formats then dictate the map resources (e.g. "GameMaps" or "MapTemp") and whether they're simple files that can be browsed for. Alternatively, formats can decline to use the traditional NPW, in which case the Level Files interface is replaced with a single button that launches the format's custom configuration dialog.

At dependency file opening time, the format is responsible for creating or loading a level set object. When levels are added, the format has a chance to return a subclass of GalaxyLevel if it wants, or it can return nothing to let Abiathar do the default. To expose extended level features, Abiathar level wrappers now have an ExtraData property, reads and writes of which eventually make their way to the level format. This is how I implemented the "pushwalls always active" flag in Rise of the Triad.

There are a lot of Abiathar features that only make sense for Galaxy levels, so I also introduced a "genre" field on level formats, the meaning of which is intentionally left somewhat vague. Basically, the default genre activates all the normal Abiathar features, while unknown/new genres indicate to tools that they can't make Galaxy assumptions. Currently nothing actually checks this, but it seemed like something good to have for the future.

Using all this, I have a working Rise of the Triad extension. After some more testing and polishing, these changes will be released as v2.9.

Tuesday, June 13, 2017

FMod - Level size checks

I noticed a couple days ago that Abiathar crashes if the row/column adjusters are used to reduce a dimension of a level to zero. It doesn't really make sense to have a zero-area level, and the level Properties window doesn't let you make one anyway, so today I added checks to those tools to make them not expand or contract the level beyond the limits. While doing that, I removed incorrect checks intended to prevent too-large levels but that actually consulted the opposite dimension they needed to.

While debugging that problem, I found yet another. Mousing over a location greater than 255 made Abiathar completely freeze. It was trying to format the coordinates in hexadecimal, padded to a certain length, but because 256 is 100 in hex, the string was already more than long enough. The check was only for equality as opposed to exceeding-or-equality, so into an infinite loop it went. That's fixed now.

Finally, I added that size restriction data to the API. For normal level sets it's just from 1 to 255, but other formats (yet to be supported) have different limits.

Monday, June 12, 2017

Windows 10 1703 sometimes sends messages in response to PeekMessage

I spent rather a lot of time last week debugging a third-party application that froze up in some places on Windows 10 1703 but worked perfectly on previous versions. Long story short, this application calls PeekMessage many times on every trip around its message loop. As of version 1703, it appears that the window message 0x738 is occasionally sent in the internals of PeekMessage. That message goes into the queue, so GetMessage returns immediately, and the loop goes around again. That pegs the CPU at 100% and stops things from proceeding as they should.

The solution was to patch that component to check whether the message ID is 0x738 and if it is, ignore the message by jumping back to right before the GetMessage invocation.

Sunday, June 11, 2017

FMod - Advanced settings and the Back button

While testing Abiathar on Rise of the Triad levels a few days ago, I noticed a little glitch in the New Project Wizard. The NPW's first page offers a list of templates as well as the option to ignore the templates and enter all the settings manually. There is, though, a checkbox under the template option that lets the user fiddle all the advanced settings anyway. Once the user gets to the first non-advanced page using the latter option, the Back button goes to the start of the NPW instead of the last advanced page. This happens because the button just checks whether there's a template in use, which does not fully describe whether advanced settings can be shown.

To fix the glitch, I introduced a new variable - "can view advanced settings" - that has the correct semantics here. The Back button's decision is now made based on that.

Storing files inside PowerShell scripts

I recently wrote a Super User answer where I needed to stash an image in a PowerShell script and manipulate it a bit. The easiest way of storing a lot of non-text data is to encode it in Base64.

[Convert]::ToBase64String((gc .\myfile.bin -Encoding Byte))

Pipe that into scb to copy it to your clipboard. Then paste that into a string variable in your script. At runtime, that can be decoded back into the raw bytes.

$bin = [Convert]::FromBase64String($b64)

To open the byte array as a stream (suitable for many .NET APIs), create a MemoryStream. I used the constructor overload with an extra parameter because otherwise PowerShell will think each byte in the array should be its own parameter.

$ms = New-Object System.IO.MemoryStream $bin, $false

Then you can, for example, load that stream as an image.

Friday, June 9, 2017

FMod - EGA object tables not required

Normal EGA resources for Keen Galaxy-ish games start with a few tables (for pictures, masked pictures, and sprites) that catalog later chunks and give important information on them, like their dimensions. I assumed those are always there, therefore Abiathar requires the tile start chunk ID to be at least 3.

When investigating the possibility of adding support for Rise of the Triad, I found that the EGA resources provided (which are used only for level editing, not by the actual game), start their tiles right at the first chunk. They don't need the tables because they have no pictures or sprites. So to accommodate that, I reduced the minimum tile chunk ID to zero. This allows Abiathar to successfully open those graphics.

Wednesday, June 7, 2017

Profiling a process on a machine without Visual Studio

Today I needed to create a performance profile of a certain program. Usually I would just fire up Visual Studio, but VS is very large and wasn't on that computer. Fortunately, the profiler can be installed without the full IDE. The setup package can be found in this folder on the newest version of Visual Studio:

C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Team Tools\Performance Tools\Setups

Copy the installer to the other computer and set it up. The tools get installed to that same path, minus the Setups part. Then run the profiler on your application from an administrative command prompt:

vsperf /launch:"C:\path\to\myapp.exe" /file:filetitle

Once the launched process exits, the profiler saves the report to filetitle.vspx. Copy that file back to a computer with Visual Studio. Open it with File | Open | File and you'll get the analysis.

Tuesday, June 6, 2017

Checking who can do what to a table in SQL Anywhere 11

Today I was investigating the security of an SQL Anywhere 11 database and needed to see who had the authority to update a certain table. I managed to find the systableperm system table that holds this information. This query returns a list of users/groups and which actions they can perform on the table:

select user_name, selectauth, insertauth, deleteauth, updateauth 
from sys.systableperm
inner join sys.systable on systable.table_id = systableperm.stable_id
inner join sys.sysuser on sysuser.user_id = systableperm.grantee
where table_name = 'MyTable'

Monday, June 5, 2017

Abiathar Online - Enter ID

The desktop Abiathar program has long had the ability to enter a tile ID to select it. That's a fairly simple feature and one that comes in handy occasionally, so today I added it to Abiathar Online. Clicking the selected tile image for a plane brings up the ID entry form. A "hexadecimal" check box controls whether the text box's contents should be treated as hex.

I also removed the copy-pasted plane control HTML in favor of generating in with JavaScript. That way, it'll be easier and less repetitive to add more plane-related features.

Sunday, June 4, 2017

Abiathar Online - Graphics fixed on Chrome

The last time I worked on Abiathar Online, I found that its graphics were messed up on Chrome - the first time a tile was requested, it wouldn't necessarily display until the level was refreshed. I'm still not sure why it was doing that, or why it worked before, but today I made a change that fixed the problem. This Stack Overflow answer taught me that I don't have to make an Image out of the temporary canvas before using it to draw on the main one. Canvases can be drawn on other canvases just fine. So I switched to caching canvases, only converting them to Base64 URLs when displaying them in the selected tile bay. Abiathar Online now appears as it should in both Chrome and Edge.

Saturday, June 3, 2017

Changing the desktop background color with PowerShell

There is a Registry value under HKCU\Control Panel\Colors to control the background color of the desktop, but writing that value doesn't change the current desktop immediately. For a program trying to update the color, the SetSysColors Windows function is the proper way. That's not so easy to handle from the command line, though.

Fortunately, PowerShell can compile and run C# code, which can use P/Invoke to run native Windows functions. This one-liner does the job of defining a type to invoke SetSysColors:
add-type -typedefinition "using System;`n using System.Runtime.InteropServices;`n public class PInvoke { [DllImport(`"user32.dll`")] public static extern bool SetSysColors(int cElements, int[] lpaElements, int[] lpaRgbValues); }"

That function takes a count and then two arrays of that length, one with setting IDs and one with their values. We're only interested in one, the desktop background color (1). The value is the new color in 0xRRGGBB format.

[PInvoke]::SetSysColors(1, @(1), @(0xAA40C0))

This doesn't affect the Registry, so if you want the change to stick, you need to also write the new data to Registry yourself.

Based on my Super User answer.

Policy Plus - Select list items with Enter

A long time ago, I added a bit of code to the Policy Plus main window to make the Enter key open/activate the currently selected object in the right pane. That greatly improves keyboard navigability and intuitiveness. I found myself missing that on the three newer list forms: Loaded ADMX Files, All Products, and All Support Definitions. So today, I added that functionality.

The changes are live on GitHub.

Thursday, June 1, 2017

Policy Plus - List all support definitions and products

Today I added two dialogs to Policy Plus: All Products and All Support Definitions. The products list allows the user to explore all the defined products. Strictly speaking I only needed to show the top-level products, since the Product Details window can explore the tree of one of those, but I decided it would be weird to have a window named "all products" only show some product entries, so here we are. Selecting an entry in a higher list populates the lower list with its subproducts. Double-clicking any product in any list opens its details.


The support definitions list is just that. It also includes a text box to filter the support definitions because there are so many of them. For example, typing "windows xp" narrows the list to definitions that have names containing that. An ADMX File column shows at a glance which administrative template is responsible for the definition. I didn't bother doing that for products because most if not all of them are from WindowsProducts.admx.


The changes are live on GitHub.