This tool really needs a name, because typing out "my P/Invoke command-line tool" is going to get dry pretty quick.
Today I added a copyslot instruction that (surprise!) copies the contents of one slot to another.
newslot int oneInt = 3;
newslot int anotherInt = 2;
copyslot anotherInt = oneInt;
readslot anotherInt
That outputs 3. The instruction also has the option of specifying a field ID in a structure:
newslot block someBlock = int 4, byte 5;
newslot byte oneByte = 0;
copyslot oneByte = someBlock field 1;
readslot oneByte
That outputs 5, because field 1 of the block (numbering starting at zero) contains the byte 5. Finally, the instruction can use a raw byte offset:
newslot block someBlock = int 4, byte 5;
newslot byte oneByte = 0;
copyslot oneByte = someBlock offset 4;
readslot oneByte
That again outputs 5, because at byte 4 (zero-based), after four bytes' worth of int, there's the byte.
Unrelatedly, there's now a GUID kind that (no surprise) produces GUID structures.
Various technical articles, IT-related tutorials, software information, and development journals
Monday, July 31, 2017
Sunday, July 30, 2017
More operations on slots for the P/Invoke tool
Yesterday and today, I made a couple more additions to my P/Invoke command-line tool. The call instruction now takes an optional /into switch specifying the name of a slot. If the slot doesn't exist, it is created with the kind specified by the /return switch.
For a few Windows API functions, it's necessary to pass both the address of a field and its length. Allocated slots have the allocsize keyword, but that applies to the buffer pointed to by the slot's data, not the slot's data itself. Now there's also a slotsize keyword that produces the size of a slot's data, supporting the as and in modifiers just like allocsize.
Previously it was only possible to create slots of standard kinds. I introduced the block psuedo-kind that, in the newslot command, creates a custom kind based on a list of fields. This allows structures to be returned.
For a few Windows API functions, it's necessary to pass both the address of a field and its length. Allocated slots have the allocsize keyword, but that applies to the buffer pointed to by the slot's data, not the slot's data itself. Now there's also a slotsize keyword that produces the size of a slot's data, supporting the as and in modifiers just like allocsize.
Previously it was only possible to create slots of standard kinds. I introduced the block psuedo-kind that, in the newslot command, creates a custom kind based on a list of fields. This allows structures to be returned.
Saturday, July 29, 2017
FMod - v2.9.2
I discovered today that Abiathar simply crashes when viewing the Level Format tab of the advanced New Project Wizard when there are no extended formats installed. I'm not sure how I managed to miss that when testing v2.9. Apparently nobody else noticed, but this bug made the advanced NPW completely unusable, so I made a one-line change and published it as v2.9.2.
Friday, July 28, 2017
Buffer allocation for the P/Invoke tool
Some Windows functions require a buffer to be allocated before calling the function so that a string (or other chunk of data) of variable length can be returned. To make my P/Invoke command-line tool work with these functions, I introduced an instruction to create a slot of a pointer-typed kind, allocate a buffer of a given length, and fill the slot with the pointer to the buffer. Slots can be allocated with variable size, i.e. with a length provided by an existing slot's data. The length of a slot's buffer can be passed in a call using another new instruction. Since some functions actually care about character length rather than raw byte length, a unit - "bytes" or "chars" can be supplied.
This command (broken across lines for readability) takes advantage of these features to get the computer's name.
newslot int size = 0;
call kernel32.dll!GetComputerNameW (nullptr, slotptr size);
allocslot lpwstr name: size chars;
call kernel32.dll!GetComputerNameW (slotdata name, slotptr size);
readslot name
This command (broken across lines for readability) takes advantage of these features to get the computer's name.
newslot int size = 0;
call kernel32.dll!GetComputerNameW (nullptr, slotptr size);
allocslot lpwstr name: size chars;
call kernel32.dll!GetComputerNameW (slotdata name, slotptr size);
readslot name
Thursday, July 27, 2017
FMod - v2.9.1
A new user was having trouble saving their new Abiathar project. I am still unable to reproduce the issue, and there's no debugging information in that particular error dialog, so I'm stuck when it comes to helping this person. To get some data on what's happening here, I added an error logging procedure to the saving method. It produces the same kind of log as loading failures do.
A few days ago, I also added a little configuration option that controls the background color that appears behind the level and the tileset. This is useful in case people's color settings make one of the 16 EGA colors difficult to see on the Windows default beige (like f.lux does for me for yellow).
I published these changes earlier today as v2.9.1.
A few days ago, I also added a little configuration option that controls the background color that appears behind the level and the tileset. This is useful in case people's color settings make one of the 16 EGA colors difficult to see on the Windows default beige (like f.lux does for me for yellow).
I published these changes earlier today as v2.9.1.
Wednesday, July 26, 2017
Further progress on the P/Invoke command-line tool
Continuing on from yesterday, I added more features to my P/Invoke command-line tool that expanded the set of functions it can handle. First I added support for null-terminated strings, both ANSI and Unicode, under the type names lpstr and lpwstr.
Then I went about supporting "out" parameters, those that take a pointer and put something in it. To do that, I introduced the concept of "slots," which can be allocated, passed to a function, and then printed to the screen. Of course, multiple-instruction commands required a way to pass multiple logical lines on the command line, so that required a little parser adjustment to treat a semicolon like a line separator. To implement the definition of slots, I refactored the other data-type select statements into a "kind" system. A kind object provides information on how to display and store values of that type.
Slots can be created with the newslot command (which takes the kind, name, and optionally an initial value):
newslot int quota
Slots can be used in calls. To pass the address of a slot (i.e. its pointer), use the slotptr keyword. To pass its current value, use slotdata.
call kernel32.dll!GetSystemRegistryQuota (nullptr, slotptr quota)
Finally, a slot can be printed with the readslot command:
readslot quota
Chaining all those together with semicolons retrieves and displays the current size of the Registry.
To see why a function is failing, it's useful to get the last Win32 error code. Unfortunately, dynamically generated P/Invoke methods don't seem to have a way of setting the last error for ease of managed access. So I just P/Invoke GetLastError after every call and store it in case the user issues a lasterror to retrieve it.
Then I went about supporting "out" parameters, those that take a pointer and put something in it. To do that, I introduced the concept of "slots," which can be allocated, passed to a function, and then printed to the screen. Of course, multiple-instruction commands required a way to pass multiple logical lines on the command line, so that required a little parser adjustment to treat a semicolon like a line separator. To implement the definition of slots, I refactored the other data-type select statements into a "kind" system. A kind object provides information on how to display and store values of that type.
Slots can be created with the newslot command (which takes the kind, name, and optionally an initial value):
newslot int quota
Slots can be used in calls. To pass the address of a slot (i.e. its pointer), use the slotptr keyword. To pass its current value, use slotdata.
call kernel32.dll!GetSystemRegistryQuota (nullptr, slotptr quota)
Finally, a slot can be printed with the readslot command:
readslot quota
Chaining all those together with semicolons retrieves and displays the current size of the Registry.
To see why a function is failing, it's useful to get the last Win32 error code. Unfortunately, dynamically generated P/Invoke methods don't seem to have a way of setting the last error for ease of managed access. So I just P/Invoke GetLastError after every call and store it in case the user issues a lasterror to retrieve it.
Tuesday, July 25, 2017
Starting a PInvoke command-line tool
It frustrates me that people continue to misuse rundll32 to try to run arbitrary functions. Unfortunately, there's no convenient alternative - nobody wants to compile a full C/C++ program just to run one function, and using P/Invoke in PowerShell is gnarly. If it's easy to do the wrong thing and hard to do the right thing, people will probably do the wrong thing, so the solution here is to make the right thing easy.
Yesterday I started poking around with .NET reflection and P/Invoke; I managed to get my program invoking an arbitrary parameterless function. Today I built on that. I now have these features working:
Yesterday I started poking around with .NET reflection and P/Invoke; I managed to get my program invoking an arbitrary parameterless function. Today I built on that. I now have these features working:
- Numeric parameters are accepted in hex or decimal, from 16 to 64 bits long. The native specifier corresponds to the .NET IntPtr type in that it becomes a 32- or 64-bit field as appropriate for the system.
- A return value can be accepted if its type is given (default is none). Currently only 32-bit or wider numeric types are supported for returns.
- The calling convention can be specified: stdcall (default), cdecl, or thiscall.
- Pointers to structures can be easily passed with the blockptr specifier. blockptr takes a list of parameters just like a function, creates a structure containing them, gets a pointer to that structure, and passes that to the main function. These can be nested.
For example, this command line sets the window text color to a pretty blue and returns whether the function worked:
user32.dll!SetSysColors /return int (int 1, blockptr(int 8), blockptr(int 0xAA4400))
Monday, July 24, 2017
DefinePInvokeMethod functions will unbalance the stack without PreserveSig
Today I worked on a program that adds a P/Invoke method to a dynamically generated module using DefinePInvokeMethod. Building the module worked just fine, but calling that method produced an error about a stack imbalance. I triple-checked the calling convention and parameters (the lack thereof, actually), but the error remained.
Eventually I came across an Important box in the Examples section of DefinePInvokeMethod's documentation. There is a PreserveSig flag that needs to be set for the return value to get back to managed code, but apparently the flag does other things too. After I set it, the stack imbalance went away.
dynMethod.SetImplementationFlags( _
dynMethod.GetMethodImplementationFlags() Or MethodImplAttributes.PreserveSig)
Eventually I came across an Important box in the Examples section of DefinePInvokeMethod's documentation. There is a PreserveSig flag that needs to be set for the return value to get back to managed code, but apparently the flag does other things too. After I set it, the stack imbalance went away.
dynMethod.SetImplementationFlags( _
dynMethod.GetMethodImplementationFlags() Or MethodImplAttributes.PreserveSig)
Sunday, July 23, 2017
FMod - Mouse holding and arrow keys
Quite a few versions back now, I made sure that pressing the arrow keys to scroll the Abiathar level viewer notified tools of the new cursor position. I noticed while making some of my own levels, though, that the mouse button state is not considered, so drag-click translation doesn't happen. For example, holding down the left mouse button to tessellate with the Essential Manipulator while moving with the keyboard didn't work - the preview moved, but no tiles were placed.
So today I went about fixing that. At first, it seemed to work, but in some circumstances Abiathar would crash when loading a project because tools placed tiles before the undo stack was set up. So I reordered things, but then I found that tiles got placed by the click from the Open dialog, which is undesirable. I fixed most such cases, but couldn't completely figure out how I had apparently broken things. Then I found that moving the mouse during initialization has always caused an extra tile to be placed, so at least there's no regression.
So today I went about fixing that. At first, it seemed to work, but in some circumstances Abiathar would crash when loading a project because tools placed tiles before the undo stack was set up. So I reordered things, but then I found that tiles got placed by the click from the Open dialog, which is undesirable. I fixed most such cases, but couldn't completely figure out how I had apparently broken things. Then I found that moving the mouse during initialization has always caused an extra tile to be placed, so at least there's no regression.
Saturday, July 22, 2017
Not all settings from Get-IISAppPool can be committed
IIS app pools (along with other IIS configuration) can be managed with PowerShell. One user wanted to change the logging options of an app pool (probably acquired from Get-IISAppPool), but couldn't figure out how. The LogEventOnRecycle property of the Recycling member on the app pool object seems to be the relevant part, but changes don't stick.
Fortunately, there's an alternate way of working with IIS from PowerShell. If the WebAdministration module is imported, the IIS:\ drive contains IIS objects that can be read and changed with Get-ItemProperty and Set-ItemProperty. To change logging settings, get the recycling property on the app pool, revise the logEventOnRecycle member of that object, then set the recycling property with the altered configuration section.
Fortunately, there's an alternate way of working with IIS from PowerShell. If the WebAdministration module is imported, the IIS:\ drive contains IIS objects that can be read and changed with Get-ItemProperty and Set-ItemProperty. To change logging settings, get the recycling property on the app pool, revise the logEventOnRecycle member of that object, then set the recycling property with the altered configuration section.
Friday, July 21, 2017
Preventing a single shortcut from tracking its target
Windows shortcuts will attempt to find their target if it doesn't exist in the path it knew about. In some use cases, though, that might not be the most desirable behavior. There doesn't seem to be a way to disable the tracking behavior in the GUI - even stopping the Distributed Link Tracking Client service won't stop ye olde search through adjacent/ancestor/descendant directories.
Someone mentioned the shortcut utility from an old Resource Kit. Unfortunately, it's not easily available. The mention that it could work that change on one shortcut told me that something in the shortcut file could disable the tracking. So I skimmed the official format spec on the LNK format. On page 12, it introduces the LinkFlags field, a four-byte value with 25 different flags defined. A few of those in the third byte (byte 0x16 from the beginning) looked relevant, so I threw together a PowerShell script that set them. Sure enough, it worked.
$linkfile = Resolve-Path $args[0]
$bytes = [IO.File]::ReadAllBytes($linkfile)
$bytes[0x16] = $bytes[0x16] -bor 0x36
[IO.File]::WriteAllBytes($linkfile, $bytes)
Weirdly, if a shortcut altered like this is broken (i.e. its target goes missing), no dialog is created to tell you about that, at least on Windows 10.
Someone mentioned the shortcut utility from an old Resource Kit. Unfortunately, it's not easily available. The mention that it could work that change on one shortcut told me that something in the shortcut file could disable the tracking. So I skimmed the official format spec on the LNK format. On page 12, it introduces the LinkFlags field, a four-byte value with 25 different flags defined. A few of those in the third byte (byte 0x16 from the beginning) looked relevant, so I threw together a PowerShell script that set them. Sure enough, it worked.
$linkfile = Resolve-Path $args[0]
$bytes = [IO.File]::ReadAllBytes($linkfile)
$bytes[0x16] = $bytes[0x16] -bor 0x36
[IO.File]::WriteAllBytes($linkfile, $bytes)
Weirdly, if a shortcut altered like this is broken (i.e. its target goes missing), no dialog is created to tell you about that, at least on Windows 10.
Thursday, July 20, 2017
FMod - Foreground highlight fix
Today I stumbled upon yet another small UI glitch in Abiathar. When the foreground highlight is enabled in the advanced tile property overlay settings, the highlight renders perfectly fine, but when edits are made, the highlight doesn't appear on the altered tiles. This is because the tile update handler of the properties render plane sets the graphics compositing mode to source-copy to erase the previous image, but doesn't put it back to source-over. The drawing of the blocking lines removes the foreground highlight.
I adjusted that method to switch back to source-copy after clearing the tile. That fixed the problem.
I adjusted that method to switch back to source-copy after clearing the tile. That fixed the problem.
Wednesday, July 19, 2017
FMod - Tile Property Modifier number format
While investigating a tileinfo problem today, I noticed a UI inconsistency in Abiathar's Tile Property Modifier. A few releases back, I went through the Level Inspector's messages and made sure it complies with the user's number format preferences. Apparently I missed the inspection mode of the TPM, though, because it's warning messages always show tile IDs in decimal. That discrepancy makes it very difficult to find the tiles that it's referring to because most people (including myself) cannot quickly convert arbitrary numbers from decimal to hexadecimal.
The Tile Property Modifier now uses the recommended Abiathar API method (GetNumberDisplay) to format the tile IDs.
The Tile Property Modifier now uses the recommended Abiathar API method (GetNumberDisplay) to format the tile IDs.
Tuesday, July 18, 2017
Access control lists for power configuration
Changes to power settings are written through the cooperation of the Power service. That service, like most, runs as SYSTEM, so there has to be some other means of access control than just normal Registry key ACLs. And sure enough, there is in this key:
HKLM\SYSTEM\CurrentControlSet\Control\Power\SecurityDescriptors
Each value appears to be the GUID of a setting type. "Default" likely applies to all the other operations The data is the ACL in SDDL format. The specific permissions available are Registry permissions (KR for read, KW for write, KA for all). Changes to these ACLs require a reboot to take effect.
HKLM\SYSTEM\CurrentControlSet\Control\Power\SecurityDescriptors
Each value appears to be the GUID of a setting type. "Default" likely applies to all the other operations The data is the ACL in SDDL format. The specific permissions available are Registry permissions (KR for read, KW for write, KA for all). Changes to these ACLs require a reboot to take effect.
Monday, July 17, 2017
Policy Plus - Fill in gpt.ini
While testing Policy Plus on a fresh Windows 10 Pro VM yesterday, I noticed that the policy changes I made didn't take effect. They didn't even get written to the Registry, despite the save operation claiming to complete successfully.
It turns out that the POL files were written correctly, but gpt.ini had only the section header, no Version line. That piece of data was the key. Policy Plus only checks whether gpt.ini exists, and if it does, it looks for and updates the Version line, but if there is no such line, the file is re-written with no alterations. So I adjusted the POL source saving routine to keep track of whether it's seen the necessary lines and, if it hasn't by the end of the file, add them.
These changes were released yesterday in this GitHub commit.
It turns out that the POL files were written correctly, but gpt.ini had only the section header, no Version line. That piece of data was the key. Policy Plus only checks whether gpt.ini exists, and if it does, it looks for and updates the Version line, but if there is no such line, the file is re-written with no alterations. So I adjusted the POL source saving routine to keep track of whether it's seen the necessary lines and, if it hasn't by the end of the file, add them.
These changes were released yesterday in this GitHub commit.
Sunday, July 16, 2017
Policy Plus - Non-English improvements
Policy Plus received it's first official GitHub issue report a day or so ago. On French Windows 10, the program gives an error at startup, saying that it cannot load policy definitions. Upon investigation in a French Windows 10 VM, I found that this comes down to a globalization problem. In French, the decimal separator is apparently the comma. In English (and in the ADMX and ADML formats), it's a period. Policy Plus used the normal .NET parsing method to comprehend the decimal number in the version field of those files, but .NET used the user's culture settings, so the decimal point was not recognized and the parsing failed. I fixed that problem by using the invariant culture to parse decimal numbers.
Then I found that Policy Plus errored again, being unable to find the ADML file for a certain ADMX, while the LGPE had no problem. Evidently, the LGPE falls back to English ADML files if another language's ADML associated with an ADMX is not present. Policy Plus now does this too. These two changes, now live on GitHub, fixed that user's problem.
Then I found that Policy Plus errored again, being unable to find the ADML file for a certain ADMX, while the LGPE had no problem. Evidently, the LGPE falls back to English ADML files if another language's ADML associated with an ADMX is not present. Policy Plus now does this too. These two changes, now live on GitHub, fixed that user's problem.
Saturday, July 15, 2017
Hyper-V dynamic memory causes problems with Windows setup
Windows 10 1703's incarnation of Hyper-V has a Quick Create feature that lets you, well, quickly create a VM from an ISO setup disk. I have sometimes run into problems, though, with the OS installation phase when using that creation mode. Quick Create enables dynamic memory with a minimum RAM of 512 MB. This is apparently not always enough memory for Windows setup, because setup can fail with an inability to display the license terms. If this happens, the solution is to disable dynamic memory and try again.
Friday, July 14, 2017
The three locations in IDA
In the lower left of the disassembly window of IDA, there are three different indicators showing the current location. I'm currently browsing a Keen 5 disassembly and I see these values:
00008E2D | 0000622D: sub616D+C0
The leftmost value is the location in the EXE, the address you would give to your hex editor if you wanted to see that spot.
The one in the next section of the status bar is the address in memory, distinct from the EXE address because the EXE format has a header with various information before the actual code and data. This address is very important because it's the one you should give to CKPatch to alter the instructions in memory.
The last isn't really a distinct address; it's a restatement of the previous. It gives the current location relative to the last name or the current segment, depending on where you are in the file. In this case, I'm 0xC0 bytes into the function auto-named sub616D. If it's showing a segment (evidenced by the presence of another colon), you can use the Segmentation window to find the Base of the segment and use that to get an RL value for CKPatch. Otherwise, this alternate value display isn't super useful for patching purposes.
00008E2D | 0000622D: sub616D+C0
The leftmost value is the location in the EXE, the address you would give to your hex editor if you wanted to see that spot.
The one in the next section of the status bar is the address in memory, distinct from the EXE address because the EXE format has a header with various information before the actual code and data. This address is very important because it's the one you should give to CKPatch to alter the instructions in memory.
The last isn't really a distinct address; it's a restatement of the previous. It gives the current location relative to the last name or the current segment, depending on where you are in the file. In this case, I'm 0xC0 bytes into the function auto-named sub616D. If it's showing a segment (evidenced by the presence of another colon), you can use the Segmentation window to find the Base of the segment and use that to get an RL value for CKPatch. Otherwise, this alternate value display isn't super useful for patching purposes.
Thursday, July 13, 2017
FMod - Lone Editing and hidden planes
While working on my machine-assisted Keen 5 level pack, I noticed two things about Abiathar: the Lone Editing mode is fantastic, but also it has a tiny bug. Lone Editing makes sure only one plane is active at a time, so switching to editing only, say, the infoplane is a quick tap of the 3 key with no need to manually disable the previous plane first. Planes can also be hidden, which is a distinct state from being disabled/locked/visible. Sometimes I need to hide one plane to clearly see one behind it. Re-enabling a hidden plane, though, doesn't enforce the "one plane active at a time" rule. This confuses the paste preview plane, which proceeds to render extra planes in the translucent preview (but fortunately it places the correct ones).
The plane visibility toggle now considers Lone Editing mode. If there are any active planes when a hidden one's visibility is toggled, that plane becomes locked instead of active.
Additionally, I just now noticed a minor UI glitch. The "switch to tileset" method, which automatically updates plane states in Lone Editing mode, directly edited the plane state array instead of calling the appropriate function, so the checked state of the plane control menu items didn't get updated. That's fixed too.
The plane visibility toggle now considers Lone Editing mode. If there are any active planes when a hidden one's visibility is toggled, that plane becomes locked instead of active.
Additionally, I just now noticed a minor UI glitch. The "switch to tileset" method, which automatically updates plane states in Lone Editing mode, directly edited the plane state array instead of calling the appropriate function, so the checked state of the plane control menu items didn't get updated. That's fixed too.
Wednesday, July 12, 2017
Markeen level polishing project
Markeen is getting pretty decent at generating Keen 5 levels. As of yet, though, it hasn't actually been used for anything other than making pretty pictures for some people to look at. So I figured polishing up some Markeen-generated levels would be a good way to create my "own" levels without spending huge amounts of time building from nothing.
I started by profiling the default Keen 5 levels to a depth of 3, then using editprof noadj to discourage Markeen from placing certain obviously incompatible pairs of tiles next to each other. Then I used that profile to generate 100 starting points. Since Markeen is far from perfect, a lot of those levels are a mess, so I went through and deleted all the ones that would take too much time to salvage. That left me with 22 candidates. By default, Keen 5 only has 13 levels, so I could easily get away with making fewer than 22 of my own. One of my goals is to preserve as much of Markeen's "creativity" as possible, so I want to avoid having to place the fuse machines myself. Unfortunately, only 4 of the 5 were used anywhere in the 100 levels, but a couple of those 4 were used multiple times, so I'll probably use one of the levels containing an extra and just swap out the duplicate machine for the missing one.
So far, I've polished one level. It started like this:
And now looks like this:
With its linearity and lack of switches, doors, and keys, it's a good choice for a first level. This refinement only took about two hours including all the fine-tuning of the secret areas.
I started by profiling the default Keen 5 levels to a depth of 3, then using editprof noadj to discourage Markeen from placing certain obviously incompatible pairs of tiles next to each other. Then I used that profile to generate 100 starting points. Since Markeen is far from perfect, a lot of those levels are a mess, so I went through and deleted all the ones that would take too much time to salvage. That left me with 22 candidates. By default, Keen 5 only has 13 levels, so I could easily get away with making fewer than 22 of my own. One of my goals is to preserve as much of Markeen's "creativity" as possible, so I want to avoid having to place the fuse machines myself. Unfortunately, only 4 of the 5 were used anywhere in the 100 levels, but a couple of those 4 were used multiple times, so I'll probably use one of the levels containing an extra and just swap out the duplicate machine for the missing one.
So far, I've polished one level. It started like this:
And now looks like this:
With its linearity and lack of switches, doors, and keys, it's a good choice for a first level. This refinement only took about two hours including all the fine-tuning of the secret areas.
Tuesday, July 11, 2017
KeenGraph only checks for the old palette patch
Today I helped a PCKF member extract graphics from a Keen 1 mod that had an alternate color palette patched in. KeenGraph was able to extract the graphics, but it didn't process the palette patch, so the colors in the exported graphics didn't match the colors shown in the game. KeenGraph documentation claims that it can scan the patch file for an altered palette, so I was confused.
After trying various things (including changing the decimal values in the patch file to hexadecimal) I consulted the KeenWiki page on the palette patch. There, I learned that there are two versions of the palette patch, one old and one new. The only relevant different for patch file scanning purposes is that the new edition stores the color table in a different location. Apparently the version of KeenGraph I was using only knew about the old palette patch, and so didn't realize that the new version (used by this mod) affected the palette. Altering the patch file to use the old offset made KeenGraph use the right colors.
After trying various things (including changing the decimal values in the patch file to hexadecimal) I consulted the KeenWiki page on the palette patch. There, I learned that there are two versions of the palette patch, one old and one new. The only relevant different for patch file scanning purposes is that the new edition stores the color table in a different location. Apparently the version of KeenGraph I was using only knew about the old palette patch, and so didn't realize that the new version (used by this mod) affected the palette. Altering the patch file to use the old offset made KeenGraph use the right colors.
Monday, July 10, 2017
Abiathar API - Level sets
To provide an Abiathar level format, you have to implement INextGenLevelSet, from FMod.dll. This is made somewhat more tricky by the possibility of including tileinfo in some maps files, and by VeriMaps signatures. Level set implementations have these members:
- Levels is exactly what it claims to be: a dictionary of the levels in the level set, identified by their ID.
- HasTileinfo returns whether there is currently a tileinfo resource in the level set.
- TileSize is the side length of a tile in pixels. Abiathar only supports 16 pixels per tile at the moment.
- Tileinfo is the embedded tileinfo resource. If your level set format doesn't support tileinfo embedding, discard the provided object in this property's setter.
- IsVeriMapsSigned gets the VeriMaps status of the levels file. If your format doesn't handle VeriMaps, just return CantSign and do nothing in all the other VeriMaps-related members. NotSigned indicates that the set supports signing, but is not currently signed. Signed means that there is a signature and that Abiathar should try to verify it.
- Signer returns the username of the person who allegedly signed the levels file.
- SignedHash gets the signature on the level data that was stored when the file was saved.
- Hash gets the hash of the level data. You should use the Sha512 method in your loading method to compute this. It's up to you which parts of the file to authenticate.
- SigningCert is the certificate to use when saving a signed file. If this is set when your saving function is called, use the SignHash method to compute the signature. If this is null then, do not sign the levels.
Sunday, July 9, 2017
Abiathar API - Other event bus stops
There are three Abiathar event bus stops not previous covered in this API series.
AbiatharPlaneStateChangeEvent is sent when the user changes the state of a plane (e.g. from Active to Hidden). It includes the ID of the affected plane and the new plane state.
AbiatharSelTileChangeEvent is sent when the selected tile for a plane changes. This includes only the ID of the affected plane. You can get the new selected tile from the SelTiles array on the state manager.
AbiatharPatchGenerationEvent is sent when the auto-generated section of the patches is being written. To add patches, call AddLine for every line. Do not include the starting and ending commands; Abiathar handles these for you.
AbiatharPlaneStateChangeEvent is sent when the user changes the state of a plane (e.g. from Active to Hidden). It includes the ID of the affected plane and the new plane state.
AbiatharSelTileChangeEvent is sent when the selected tile for a plane changes. This includes only the ID of the affected plane. You can get the new selected tile from the SelTiles array on the state manager.
AbiatharPatchGenerationEvent is sent when the auto-generated section of the patches is being written. To add patches, call AddLine for every line. Do not include the starting and ending commands; Abiathar handles these for you.
Saturday, July 8, 2017
Abiathar API - Level formats
As of v2.9, Abiathar allows extensions to define level formats so that the editor can support other games. Implementing level formats is at a lower level than most extensions' work. To provide a format, an extension must have a reference to FMod.dll.
Abiathar internally uses an INextGenLevelSet to store the levels, so an extension that provides level formats must have a class that implements that interface. A level format object implements ILevelFormat, which has these members:
Abiathar internally uses an INextGenLevelSet to store the levels, so an extension that provides level formats must have a class that implements that interface. A level format object implements ILevelFormat, which has these members:
- ID gets the internal name of the level format. By convention, this should describe the storage format of the maps in some way. For example, the normal format is called Carmack.
- DisplayName gets the user-friendly name that shows up in the Level Format tab of the New Project Wizard.
- SizeRestrictions gets a LevelSizeRestrictions object describing the limits of the map format.
- WizardResources gets an array of up to three LevelResourceDescription objects. These objects set the file description (e.g. "GameMaps") and whether the setting refers to a real file (which is used to determine whether Browse should be enabled). If this is null, the level format uses a custom resource browser, and the NPW replaces the text fields with a single button to launch that browser.
- SupportedExtraData gets an array of extended data field IDs that the level objects support.
- LevelGenre gets a string that identifies the kind of level in some way. For example, "3PlaneKeen" is the genre for Keen Galaxy and Dreams.
- DisplayCustomLevelResourcesDialog instructs the format to display its custom resource browser. This will only be called if no wizard resources are supplied. The input and output arrays may contain up to three strings. Return null if the user cancels the dialog; return data if the user makes changes and saves them.
- CanContinue returns whether the NPW can advance given the current resource values. For plain file resources, Abiathar already checks for existence if necessary.
- SaveLevels saves the level set to disk using the resource configuration provided by the user.
- CreateLevels generates an empty level set.
- CreateLevel generates a blank level with the given dimensions. The return value may be a subclass of GalaxyLevel.
- LoadLevels loads a level set from disk.
- GetLevelExtraData returns an extended data field's value for the given level.
- SetLevelExtraData sets an extended data field's value for the given level.
To register a level format, call AddFormat on an AbiatharRegisterLevelFormatEvent in the event bus.
Friday, July 7, 2017
Abiathar API - Render planes
The image of a level or tileset in Abiathar is composed of planes stacked on top of each other. Though planes are not removed from the list of planes once added, they choose to render or not based on the user's instructions. Classic planes keep an image, which Abiathar's renderer draws where appropriate. Ephemeral planes draw directly on a graphics context. Planes implement either ILevelRenderPlane or ITilesetRenderPlane as appropriate. Both those interfaces are inherited from IRenderPlane, which has these members:
- RenderNow gets whether the plane should be asked to show itself currently.
- Image gets the image to render. This is not used for ephemeral planes.
- ConsiderOffset gets whether Abiathar needs to position the plane according to the scroll position. This is not used for ephemeral planes.
- PlaneName is the ID of the plane. By convention, IDs take the form of the extension name, a colon, and a plane title.
- NeedsUpdate gets whether the plane should be asked to completely re-render itself at the next draw. Ephemeral planes should always return false.
- HasTileinfo returns whether the plane displays tileinfo for the given simple plane (0 or 1).
- SetStateAccessor is called so Abiathar can give the plane a state manager.
- Render instructs the plane to prepare its Image. This is not used for ephemeral planes.
- MarkDirty instructs the plane to mark itself as in need of a full update. This can be ignored by ephemeral planes.
- IsEphemeral gets whether the plane uses ephemeral rendering.
- RenderEphemeral instructs the plane to draw its contents on the given graphics with the given zoom level and offset. This is only used for ephemeral planes.
Level planes also have these members:
- LevelPlane gets the simple plane (0 to 2) that the plane draws, or some other value if it does not correspond to any particular level plane.
- SetRenderLevel is called to provide the plane with the level object it should render.
- UpdateTile is called when a single tile is marked dirty. Return whether any changes were made to the image.
Tileset planes have these members in addition to the common plane members:
- TilePlane is a historical artifact and is not used.
- SetRenderPalette provides the plane with its tileset object.
- UpdateSelectedTile is called when the selected tile for a plane changes. It provides the simple plane ID and the new tile ID.
- TileViewSettingsChanged is not called by Abiathar.
To register a plane, add it to the Planes list of an AbiatharLevelViewStateInitializeEvent (for level planes) or an AbiatharTileViewStateInitializeEvent (for tileset planes) in the event bus.
Thursday, July 6, 2017
Abiathar API - Tools
Tools are the preferred method of providing functionality to Abiathar. Tool objects implement either IAbiatharLevelTool (for level tools) or IAbiatharTilesetTool (for tileset tools). These both inherit from IAbiatharTool, which has these members:
- Name is the tool's name for display in the Tools menu.
- Icon is the image to place on the Tools menu. This can be null if you do not want an icon.
- ShortcutKey specifies the tool's keyboard shortcut. Key numbers are Windows Forms key codes. This can be null if the tool does not have a shortcut.
- ShortcutText is the text to show on the right of the item in the Tools menu.
- HandlesKeyPress returns whether the tool should receive key events while active.
- KeyPress is called when a key is pressed. Return whether the press should be considered handled. This is only called if the tool handles key presses.
- CanBeDefault returns whether it's acceptable for the user to set this tool as their default. For example, the Tile Placer can be the default level tool, but the Level Inspector cannot.
Level tools also have these members:
- StartUse is called when the tool is switched to. It provides the current level.
- SwitchLevel is called when the current level changes while the tool is active. It provides the new level.
- Canceled is called when the tool is switched away from. It includes whether the cancel was initiated by the CancelTool method.
- HandlesInLevelClick returns whether the tool should receive click events for the level.
- InLevelClick is called when the user clicks in the level. It includes the coordinates and whether it is a right-click. This is only called if the tool handles clicks.
- HandlesInLevelMouseMove returns whether the tool should receive mouse movement events.
- InLevelMouseMove is called when the mouse is moved in the level. It includes the current mouse coordinates in tiles and whether the left and right buttons are being held down. This is only called if the tool handles mouse movement.
Tileset tools have these members in addition to the generic tool members, very similar to the level tool members but with tileset parameters instead of levels:
- StartUse is like a level tool's StartUse.
- SwitchSet is like a level tool's SwitchLevel.
- Canceled is like a level tool's Canceled.
- HandlesTileClick is like a level tool's HandlesInLevelClick.
- TileClick is like a level tool's InLevelClick.
- HandlesTileMouseMove is like a level tool's HandlesInLevelMouseMove.
- TileMouseMove is like a level tool's InLevelMouseMove.
To register a tool, add it to the Tools list of an AbiatharRegisterToolsEvent (of IAbiatharLevelTool or IAbiatharTilesetTool, as appropriate) in the event bus.
Wednesday, July 5, 2017
Abiathar API - Undo/redo
A very important feature of Abiathar is the undo/redo history. This allows the user to reverse any mistakes they made without discarding all uncommitted changes. Extensions that alter levels, tileinfo, or project configuration must consider the undo history.
After making any change, extensions must either clear the undo history (ClearUndoStack on the state manager) or add the action to the undo stack with PushUndoStack. If these methods are not used, the undo stack may become corrupted, leading to inconsistencies and crashes. An undoable action object must implement IUndoableAction, which has these members:
After making any change, extensions must either clear the undo history (ClearUndoStack on the state manager) or add the action to the undo stack with PushUndoStack. If these methods are not used, the undo stack may become corrupted, leading to inconsistencies and crashes. An undoable action object must implement IUndoableAction, which has these members:
- Undo undoes the action.
- Redo redoes the action.
- Description gets a one-line description of the action for display in the Time Machine.
For your convenience, there is a SimpleUndoableAction class that combines Undo and Redo into one Perform method that takes a Boolean indicating whether to redo.
Tuesday, July 4, 2017
Abiathar API - Interacting with the UI
The ViewState property on the Abiathar state is very important for showing changes on the screen. It has these members:
- GetTileImage returns the 16x16 image of the given tile in the given plane.
- RerenderSelTiles refreshes the selected tile bay.
- MarkLevelDirty marks the entire given level as in need of a re-render.
- MarkPlaneDirty marks the given plane of the given level as in need of a re-render.
- MarkTileDirty takes a level ID, plane, X, Y, new tile ID, and old tile ID, using them to update just that spot if necessary. This is much faster than invalidating an entire plane or level.
- RerenderViewer updates the level/tileset viewer.
- SetInfoText sets the tool status text in the lower right.
- MoveToExtantLevel switches to a level that exists. Call this after deleting a level.
- ShowingTileset returns the current tileset, or nothing if no tileset is visible at the moment.
- ShowingLevel returns whether a level is visible.
- LevelViewOffset returns the X and Y scroll position in the given level. This might throw an exception if the level is not cached.
- TileViewOffset returns the Y scroll position for the given tileset.
- VariableTilesetWidth is a historical artifact and always returns false.
- Config returns the editor configuration. This is probably not useful to any extension.
- GetPlane returns the plane object for a given level ID and plane name.
- GetTilePlane does likewise, but for tilesets.
- ShowTip puts up a "did you know?" in the lower right.
- SetContextHelpText sets the contextual help text.
- RefreshMouse creates a mouse movement event at the location of the mouse cursor.
- SetMouseLocHighlight enables or disables highlighting the tile under the mouse. This is ignored if the user doesn't have that particular Keen:Next nostalgia setting on.
- GetNumberDisplay formats a number in accordance with the user's numeric base preference for that kind of data.
- ZipToPoint places the given level position under the mouse cursor. If StoreAsLast is true, the Zip To window will suggest it automatically next time that window is shown.
The UI processing state object (returned by UiProcessingState on the state manager) has these properties:
- TranslatedClick returns whether the current click event is actually a translation of some other process.
- DragTranslationActive returns whether the current level or tile tool has some kind of click translation enabled.
Drag translation is activated with the SetDragTranslationMode method on the state manager. You have these options:
- NoTranslation doesn't do any automatic translation of drags into clicks.
- ClickOnEveryStep produces a click on every tile the mouse drags over.
- ClickOnDragEndpoints produces at most two clicks for every drag: one at the start and one at the end.
Monday, July 3, 2017
Abiathar API - Interacting with tilesets
Abiathar supports four tilesets at the moment: background, foreground, infoplane, and combined foreground/infoplane. The IDs for these are set by the Tileset enumeration. Tileset objects returned by the CurTileset and TilePalette properties of the state manager have these members:
- TileCount returns the number of tiles in the palette.
- Width and Height are the dimensions of the palette in tiles.
- TileIDAt returns the ID of the tile at the given X and Y, or nothing if there's no tile there.
- TilePos returns the X and Y in tiles of the given tile ID, or nothing if that tile isn't in the tileset.
- TechnicalCount returns how many tiles the actual graphics source has of this type. Since there are only two kinds - unmasked and masked - all sets except the background have the same technical count.
- ViewStart is the ID of the first tile in the tileset.
- Includes returns whether the tileset has tiles useful for the given normal plane (0 to 2).
- ID returns the ID of the tileset.
The tileinfo registry (acquired through TileinfoAccessor on the state manager) has these properties:
- ForegroundTileProp gets or sets the Abiathar Tile Property ID for the given foreground tile. To interpret these values, use the TileinfoHelper class. To create such values, use PropertyAssemblyHelper.AssembleForegroundProperty.
- BackgroundTileProp does likewise, but for the background. To interpret, use the BackgroundAnimHelper class. To create, use PropertyAssemblyHelper.AssembleBackgroundProperty.
- EditableTileinfo returns whether the current project's tileinfo is editable. If it's not, changes to tileinfo will be ignored.
Sunday, July 2, 2017
Abiathar API - Interacting with levels
The LevelGen property on IAbiatharState allows extensions to create levels. The level generator has these functions:
- NewLevel takes the width, height, and name and produces a level object with those dimensions.
- NewFromProps creates a new level with the same dimensions and properties as an existing one.
- NewFromMetadata creates a new blank level with the given width and height, but the other properties provided by an existing level.
You are responsible for making sure the dimensions you supply are within the size restrictions returned by the SizeRestrictions property of the state manager.
Level objects have these properties:
- Data gets or sets the tile ID at the given X, Y, and plane.
- Name is the level's name.
- Signature is the signature written into the level file next to the level. This might be ignored, depending on the level format.
- Width and Height are exactly what they claim to be.
- ExtraData gets or sets the value of an extended data field. This is only useful on extended level formats.
Level sets (as returned by the state manager property Levels) have these members:
- Count is the number of present levels.
- Add adds a new level object with the given ID.
- Delete removes the level with the given ID.
- At gets or sets the level at the given ID. Instead of using this to replace a level, use ReplaceLevel on the state manager.
- Clear deletes all levels.
- List gets a list of all present IDs.
- Create returns a new level object with the given width and height. Use the level generator instead.
- Exists returns whether there is a level with the given ID.
Saturday, July 1, 2017
Abiathar API - Interacting with Abiathar
Almost all interactions an extension has with Abiathar provide an IAbiatharState object. This lets your extension work with Abiathar objects. Here are all the members of that interface:
- LevelGen provides a level generator, which you can use to create new level objects.
- Levels provides the level set, which has methods that allow you to inspect, add, and remove level objects.
- TilePalette produces a tileset object for the given tileset ID.
- CurTileset returns the currently active tileset object.
- ViewState returns the view state manager, which you can use to control the UI.
- TileinfoAccessor provides the tileinfo manager, which you can use to get and set tile's properties.
- FastMode returns whether Abiathar is in High Speed Mode. In this mode, you should skip fancy graphical effects and not bother with tracking the undo/redo history.
- CurLevelID returns the current level ID.
- CurLevel returns the current level object.
- SelTiles returns the three-long array of selected tile IDs, one for each plane.
- PlaneStates returns the three-long array of plane states (Active, Locked, Hidden).
- CurTool returns the current level tool.
- CurTileTool returns the current tileset tool.
- FileConfig returns the dictionary of configuration sections.
- UiProcessingState returns an object that lets you query the current UI settings.
- SizeRestrictions gets the level format's level size restrictions.
- ExtraDataFields returns the level format's list of extra data fields. This will only be useful on extended level formats.
- LevelGenre returns a level-format-defined string.
- MarkChangesMade sets the levels as modified so Abiathar will warn before exiting.
- PushUndoStack adds an undo history entry to the timeline. This does nothing if High Speed Mode is on.
- ClearUndoStack purges the undo timeline.
- DestroyLevelViewState deletes the cached images of the level with the given ID. Call this when removing a level.
- InitializeLevelViewState sets up all the planes of the level with the given ID. Call this when adding a new level.
- ReplaceLevel replaces the level with the given ID with the level object provided.
- CancelTool cancels either the current level tool or the current tileset tool (depending on whether TileTool is true). The tool may be restarted but left active if HardAbort is false. Call this when a tool has finished an operation.
- SetTool sets the current tool (level or tileset, as indicated by TileTool) to the given tool object.
- NotifySelTileChange broadcasts a change in the selected tile for the given plane. Call this after changing the array of selected tiles.
- NotifyTileinfoChange broadcasts a change in a plane's tileinfo, 0 for background, 1 for foreground.
- SetDragTranslationMode sets how Abiathar will translate mouse drags for your tool. Supply whether your tool is a tileset tool.
- DoVisibleLongOperation executes an action on a separate thread and shows a dialog to the user while it runs.
- IsLevelCurrent returns whether the given level object is the current level.
- GetExtension returns a loaded extension by name, or null if it cannot be found.
The properties that have to do with the current level or tileset only make sense if a level or tileset is being shown. Therefore, it is only safe to call them from a tool of that type, since a level tool can only run while a level is shown, and likewise for tileset tools and tilesets.
Subscribe to:
Posts (Atom)