Overview
The Dynamic Sprites Utility (dsu) is an UberASM Tool resource that enables dynamic sprites to be used.
A dynamic sprite is one that has a number of animation frames larger that could feasibly fit in SP1-SP4 (tiles 0x400-0x5FF in Lunar Magic's 8x8 editor, where all sprite graphics must be available), and thus needs to upload graphics on the fly to get around that space limitation. Because graphics cannot be uploaded directly from sprite code, a separate engine is required. Historically, the Dynamic Sprites Patch (dsx) has been the standard, but it poses a few issues and limitations that dsu hopes to get around.
One glaring point is the region of SP1-SP4 where the graphics are uploaded to. In dsx's system, that region cannot be changed: it's always the bottom half of SP4 (tiles 0x5C0-0x5FF). In the figure below, the red, green, blue and yellow 4x4 tile squares indicate the space that is claimed, in that order, by a new dynamic sprite that comes up on screen, up to a limit of four sprites at once. Therefore, sprites that normally use that region (like Bob-omb, Wiggler and Lakitu) will get their graphics tampered by dynamic sprites, even if they look right in Lunar Magic.
dsu is different from dsx in that:
- dynamic sprites will instead take 8x2 tile rectangles (for a technical reason more than a practical one);
- by default, the first four sprites end up taking the same region as dsx, but unlike in that patch, dsu makes it possible to remap these regions globally and on a per-level basis to anywhere in SP1-SP4 (remapping);
- it is possible to change the number of allowed dynamic sprites on screen globally and on a per-level basis to reduce (or increase) how much graphics space ends up getting tampered (capping);
- although not by default, the original graphics can be set to be restored when dynamic sprites despawn, meaning non-dynamic sprites that use the regions mapped to dynamic sprites can be used as long as they're never spawned together (graphics restoration);
- up to six simultaneous dynamic sprites are supported rather than four (although four is the default). The default mapping is that seen in the figure to the left, with the fifth and sixth sprites (when enabled) corresponding to the cyan and pink regions respectively. An example of dsu's remapping and capping functionalities is shown in the figure to the right.
6).0x580, 0x567, 0x4E0 and 0x4C8) and capping (to 4).The remapping and capping features introduce a lot of versality to dynamic sprite usage, enabling them to be off the bat compatible with any non-dynamic sprite regardless of the graphics region it uses. The graphics restoration feature can enhance this further if the level structure ensures certain non-dynamic sprites will never coexist with dynamic sprites. As for the increased limit of six dynamic sprites on screen, while it does allow for dynamic sprites to be placed more densely, the true potential of this change resides in facilitating and even enabling larger dynamic sprites (that require multiple dynamic slots).
Compatible sprites
It's important to note that dynamic sprites must be programmed accordingly to use dsu. Currently, dynamic sprites in SMW Central's section are compatible with dsx. You can find a list of converted sprites here or in dsu's release thread in SMW Central.
Installation
dsu requires UberASM Tool (version 2.1 or superior) to be installed in the ROM. Furthermore, PIXI (minimal version is unknown, but 1.42 or superior is recommended) must be used to insert the dynamic sprites. Once both tools are set up, dsu's installation is quite straightforward.
- Drag the contents of the uber_files folder inside your UberASM Tool directory.
- In UberASM Tool's list.txt, add the file dsu.asm as every (*) gamemode and run the tool
(if there's already another file inserted as *, separate the filenames with a comma). Your list should look like this:
gamemode:
* dsu.asm - Drag the contents of the pixi_files folder inside your PIXI directory to give support to dynamic sprites made for dsu.
Configuration
This section describes the configuration and usage of dsu's features.
Global settings
In your UberASM Tool directory, find the file dsu/settings.asm. That file has the following settings:
!dsu_mapping_default: The mapping of each dynamic sprite, unless overriden at a given level.!dsu_max_slots_default: The maximum number of allowed sprites on screen, unless overriden at a given level.!dsu_restore_default: If set to0, the graphics restoration setting will be turned off by default and it will be possible to turn it on on a per-level basis. If set to1, the inverse will be true.!unpatch_dsx: If set to1, dsu will spot and uninstall dsx from the ROM when UberASM Tool is ran (nothing will happen if dsx is not installed). Most SA-1 ROMs will have dsx installed as it is a default setting in the SA-1 Pack. Keeping dsx in a ROM with dsu installed shouldn't be harmful, but you're absolutely sure you will not need dsx anymore, you can turn that setting on.
Local settings
To change the mapping of each dynamic sprite in a specific level, write this to UberASM Tool's level list (using level 105 as an example):
105 dsu_remap.asm: ($aaa) ($bbb) ($ccc) ($ddd) ($eee) ($fff)
where $aaa through $fff are the new mappings ($400-$5FF) of each of the six dynamic slots. The number of arguments is variable. In practice, it should match the maximum number of allowed dynamic sprites on screen in that level.
To change the maximum number of allowed dynamic sprites on screen in a specific level, write this to UberASM Tool's level list (using level 105 as an example):
105 dsu_cap.asm: n
where n is the maximum number (0-6).
To toggle the graphics restoration setting (that is, turn it on if !dsu_restore_default is equal to 0, or turn it off if that define is equal to 1), write this to UberASM Tool's level list (using level 105 as an example):
105 dsu_restore.asm
Note that it is possible to use multiple of these files in the same level as well as any other files, for example:
105 dsu_cap.asm: 2, dsu_remap.asm: ($500) ($580), unrelated.asm, dsu_restore.asm
Free RAM
Free RAM settings are at the file dsu/ram.asm in UberASM Tool's directory and at the top of the file asm/ExtraDefines/dsu.asm in PIXI's directory. Both files should match.
As you can see, there are three ranges of free RAM. The first two have separate defines for LoROM and SA-1 and are used regardless of dsu's settings. !dsu_freeram requires 45 (LoROM) or 64 (SA-1) bytes, and !dsu_freeram_nmi requires 54 bytes. Both of these ranges can be in the long access region ($7E2000-$7FFFFF, or $402000-$41FFFF on SA-1), but it is preferable that !dsu_freeram_nmi is in the direct access range ($0100-$1FFF, add $6000 on SA-1), since it is used primarily in the NMI routine and direct accesses are faster.
The third range, !dsu_restore_freeram, is only ever used if the graphics restoration setting is turned on at a given level. It's used to back up the original graphics, so how much of it is taken depends on the maximum number of allowed dynamic sprites on screen: each will take 512 bytes, to a maximum of 3072 bytes. Furthermore, only one define is used because it can use WRAM (banks $7E-$7F) on SA-1 without issue.
You shouldn't normally have to change these addresses. The default !dsu_freeram and !dsu_freeram_restore point to fairly untouched regions even by custom resources, and the default !dsu_freeram_nmi is used exclusively in the overworld and Iggy/Larry's battles (which you usually won't use a dynamic sprite in). If for whatever reason you need to change them, again, make sure to do so in the two files.
Technical information
This section is dedicated to programmers who wish to convert or program new dynamic sprites for dsu.
Upon installation, users will add two things to their PIXI environment: the dsu routines, at routines/Dynamic, and the global defines and macros, made available via the file asm/ExtraDefines/dsu.asm. When programming a sprite for dsu, the availability of these can be assumed.
Importing graphics
One convenient thing about dsx is the very user-friendly expected arrangement of the .bin files containing the frames: they are disposed in 32x32 blocks, typically matching how they are displayed in game. Unfortunately, this arrangement would require four separate DMA uploads for each sprite in dsu's engine. To simultaneously keep dsx's easily editable .bin files arrangement and have DMA-efficient storage in the ROM (see the figures below), a conversion between the two formats is done during assembly.
To perform this conversion, instead of the typical:
incbin graphics.bin
graphics must be imported with the dsu_incbin macro instead:
%dsu_incbin(graphics.bin)
Aside from the mandatory filename, this macro has two optional parameters. The second parameter is the number of frames: if it's not informed, it will be calculated based on the file size, always as a multiple of four. If the actual number of frames is not a multiple of four, one to three empty frames will be inserted (and if the trailing space of the last row has been removed from the .bin file, the macro will try to reference an area that doesn't exist and output an error). In such cases, manually informing the number of frames is recommended for the sake of saving some ROM space.
The third parameter is the hexadecimal offset of the .bin file to start importing graphics from. In a typically formatted file, this offset should be a multiple of 0x800. This parameter is mostly useful when importing graphics larger than 32 kB. This will be talked about further in the section after the next.
A made up example that wishes to import frames 4 to 13 (count starts at 0) of a given .bin file would look like this:
%dsu_incbin(graphics.bin, 10, $0800)
Importing graphics in freedata
Naturally, sprite code is inserted in freecode blocks. Likely because of old assemblers lacking easy freespace search, it has been typical to import the graphics of dynamic sprites in the same block as the sprite itself. By modern standards, it is wasteful to insert data in a freecode block, so it's better practice to include the graphics in a separate freedata block instead.
Say the graphics are in a separate freedata block under the label gfx. First and foremost, any separate freespace block should be protected to prevent it from leaking. The prot command can be used for this, but note that it can only be used at the start of a freespace block, so this should be written at the very start of the sprite code:
prot gfx
The separate freedata block containing the graphics can then be placed anywhere in the sprite code in the following way:
pushpc
freedata
gfx:
%dsu_incbin(graphics.bin)
pullpc
Because it is quite repetitive to write all of this everytime, the dsu_incbin_freedata macro handles all of this internally, including the freespace protection (for that reason, it must be called at the start of the sprite code). The macro has two mandatory parameters: the filename and the label. The third and fourth optional parameters correspond to the second and third optional parameters of the dsu_incbin macro.
So, to do the exact same thing as above, the macro can be called as follows (again, mandatorily at the start of the sprite code):
%dsu_incbin_freedata(graphics.bin, gfx)
Importing large graphics (> 32 kB)
With the deprecation of the file.bin -> label command and the check bankcross off command not functioning, Asar 1.91 essentially does not permit assembling freespace blocks larger than 32 kB. This poses a problem for dynamic sprites with more than 64 frames, of which there are quite a few.
The optional parameters of the dsu_incbin macro were introduced as a workaround for this. Because splitting the .bin file in two would be incovenient, these parameters allow a single .bin file to be inserted in two separate chunks, in two separate freespace blocks.
For example, a .bin file containing 80 frames (40 kB) can be imported across two freespace blocks under two labels. The first block, under the label gfx_1, holds the first 64 frames (32 kB, so a full bank), and the second block, under the label gfx_2, holds the remaining 16 frames (8 kB). Just like above, the labels should be protected at the start of the sprite code:
prot gfx_1
prot gfx_2
The graphics can then be actually imported anywhere in the sprite code in the following way:
pushpc
freedata
gfx_1:
%dsu_incbin(graphics.bin, 64)
freedata
gfx_2:
%dsu_incbin(graphics.bin, 80-64, $8000)
pullpc
Again, to avoid the repetition of writing all of this everytime, the dsu_incbin_large macro handles all of this internally, including the freespace protection. The macro has four mandatory parameters: the filename, the two labels and the total number of frames. For compatibility with dsu's routines, the first block will always hold 64 frames, and the remaining frames will be in the second block.
So, to do the exact same thing as above, the macro can be called as follows (again, mandatorily at the start of the sprite code):
%dsu_incbin_large(graphics.bin, gfx_1, gfx_2, 80)
Dynamic slots
With dsx, a dynamic sprite currently on screen will reupload its graphics every frame in the first "slot" it finds. This is wasteful if the dynamic sprite's frame hasn't changed. To improve upon this, dsu allocates a dynamic slot to a sprite that calls either of the DynamicGetSlot or DynamicAssignSlot routines and is thus able to keep track of where it has been drawn and which frame is currently drawn, preventing unecessary reuploads when the frame hasn't changed.
A sprite's dynamic slot is kept in a sprite table at !sprite_dsu_slot (an empty slot holds the value of 0xFF), though it is typically not necessary to access it directly as the subroutines already handle everything (how all of dsu's RAM works will be detailed later).
The DynamicGetSlot routine is comparable to the GETSLOT routine used in dsx sprites in that it takes the current frame as input, signals a graphics upload (though only when necessary) and outputs the base tile location. There are many key differences, though. For one, dsu does not use a buffer, so the routine does not perform a DMA: it only adjusts the pointers that will be used during the NMI routine. Secondly, because it is a general routine, the pointer(s) to the graphics must be informed on input. And lastly, there is support for animating on a frame rule (at 15 FPS).
A fair warning before getting into it: do not call this routine during a sprite's init! Due to the order of operations of the game loop, it's entirely possible that a sprite spawns in place of a multi slot dynamic sprite (see the next section) that just despawned on the same frame, and if that happens, !sprite_dsu_slot will be in a corrupted format for this routine. By the time the sprite's main runs, dsu will already have taken care of this conflict.
These are the parameters of the routine:
DynamicGetSlot$8A-$8C$8D-$8F0-127).$8AThe dsu_gfx_pointers macro can assist with setting up $8A-$8F (it is merely a series of LDA : STA assuming 8 bits accumulator). The first parameter of this macro is the pointer that goes in $8A-$8C, and the second, optional parameter is the pointer that goes in $8D-$8F for sprites that use more than 64 frames.
Assuming a sprite has more than 64 frames, the graphics are located at pointers gfx_1 (frames 0-63) and gfx_2 (frames 64+), !frame holds the current frame and the frame rule is enabled, this is how the routine would be called:
%dsu_gfx_pointers(gfx_1, gfx_2)
LDA !frame
CLC
%DynamicGetSlot()
BCS no_slot
On output, the accumulator holds the top left tile value: add two for the top right tile, two more for the bottom left tile and two more for the bottom right tile. $8A holds the page bit (the T bit in the YXPPCCCT format).
When a dynamic slot isn't found, the routine itself will despawn the sprite. Despawn, not kill: if the sprite has an entry at the load status table, it will be cleared.
If the frame rule is enabled, a sprite at a given dynamic slot will only upload graphics during a specific state of the two least significant bits of the frame counter ($14). An upload will happen according to the following table:
$14 & 3It's possible to opt out of the frame rule because some sprites rely on updating their graphics at 30 or 60 FPS, but it's strongly recommended to enable it whenever possible to prevent NMI overhead.
The DynamicAssignSlot routine is a simplified version of the previous routine that assigns a dynamic slot to the sprite, but does not set a valid frame or signal an upload. The carry is its only input and output parameter:
DynamicAssignSlotThis routine was primarily created to assist with spawning a dynamic sprite from a shooter, a generator or another sprite. In such cases, it should be called with input carry clear after the search for a free sprite slot. If a dynamic slot isn't found, it shouldn't be allowed to spawn.
There's a second, more niche use for this routine. There are some dsx sprites in which the GETSLOT routine won't be called right away. For example, in Yoshi's Island Wild Piranha, it isn't called if the player is far away, as the bulb does not use dynamic graphics. This means the bulb will spawn if a dynamic slot isn't available, but immediately despawn as soon as the player comes close as it tries to load its dynamic graphics. In such sprites, the DynamicAssignSlot routine should be called under the sprite's init with input carry set.
Multi slot sprites
Large dynamic sprites have existed for dsx for a long time. These sprites would simply take multiple "slots" in dsx by calling GETSLOT multiple times. Because dsu has an actual slot assignment system, DynamicGetSlot cannot simply be called multiple times.
Instead, the DynamicGetMultiSlots routine must be used for such sprites. This routine will format and handle !sprite_dsu_slot bitwise instead of numerically (this will be detailed later). For reasons analogous to those stated in the previous section, this routine should not be called during a sprite's init! These are the parameters of the routine:
DynamicGetMultiSlots$8A-$8C$8D-$8F$04-$090-127).0-5).$04-$09$8A%--987654, with 4 corresponding to the tile number at $04 and so on.The input accumulator determines how much of the $04-$09 scratch RAM range will be used. For example, if only three slots are used (so the input accumulator is 2), only $04-$06 need to be set up on input and read on output, and $07-$09 will not be touched at all.
Using that example, now assuming that the sprite uses 64 frames or less (located at a single pointer gfx) and !frame_n holds the frame to upload at the n-th dynamic slot, this is how the routine would be called:
%dsu_gfx_pointers(gfx)
LDA !frame_1
STA $04
LDA !frame_2
STA $05
LDA !frame_3
STA $06
LDA #$02
%DynamicGetMultiSlots()
BCS no_slots
The top left tile numbers should be handled similarly to the single slot routine (subsequently add two to get the four tiles of the frame). To fetch the page bits, a recommended strategy is to use the code LSR $8A : LSR !15F6,x : ROL !15F6,x after drawing each set of four tiles.
Other routines
This section goes over other routines that are included in dsu's PIXI environment but have more niche uses.
DynamicGetSlotGeneric is a version of DynamicGetSlot that can be used for other sprite types. The original routine expects normal sprite indexing to keep track of the sprite's dynamic slot, so it can't be used for other sprite types. In this routine, Y holds the dynamic slot number plus 1 on input and output, so it must be kept track of within sprite code i.e. in a sprite table. If input Y is equal to 0x00, a dynamic slot search will be performed. Very importantly, the dynamic slot must be freed manually when the sprite despawns! For that, see DynamicClearSlotGeneric.
DynamicGetSlotGeneric$8A-$8C$8D-$8F0-127).0, search for a slot.$8Adsu uses sprite tables to keep track of a normal sprite's dynamic slots to automatically free them up should it despawn. This isn't done for other sprite types. Therefore, the DynamicClearSlotGeneric routine must be called when such sprites despawn, or else their dynamic slot will never be freed. It simply takes the dynamic slot plus 1 as an input.
DynamicClearSlotGenericDynamicTransferSlot transfers the dynamic slot used by the sprite in X to the sprite in Y. This can be used for sprites that spawn other dynamic sprites and only use dynamic graphics themselves during the spawning. If the sprite in X wasn't assigned a dynamic slot in the first place, the output carry will be set. It can only be used for single slot sprites.
DynamicTransferSlotRAM details
The RAM addresses described in this section are globally available in both UberASM Tool's and PIXI's environments.
We're hackers, so one may find uses to manipulating some of them directly (such as reading a sprite's dynamic slot to create a custom frame rule). However, it must be noted that dsu strives for making the most common processes only require calling the enclosed routines and macros, as that facilitates maintenance and sprite programming on itself. With that in mind, if you have suggestions for new routines, macros and functionalities that could be bundled with dsu, please reach out!
!dsu_freeram!dsu_nmi_index0$7F8B4E$409C00!dsu_max_slots1$7F8B4F$419C01!dsu_max_slots_default on level load, but readily overriden if the file dsu_limit.asm is called.!dsu_mapping2$7F8B50$409C02!dsu_mapping_default on level load, but readily overriden if the file dsu_remap.asm is called. Bits are (and are expected to be) masked so the mappings are always in the range 0x0000-0x01FF.!dsu_restore1$7F8B5C$409C0E!dsu_restore_default (which can only be either 0x00 or 0x01) on level load, but readily flipped if the file dsu_restore.asm is called. Set to 0x80 when the graphics are backed up to the buffer to ensure the process is only done once.!dsu_frame15$7F8B5D$409C0F0xFF. 0x00-0x7F are valid frames, and 0x80-0xFE are invalid frames that still indicate a taken slot. Set to 0xFE when the DynamicAssignSlot or DynamicTransferSlot routines are called to mark the slot as taken and force a valid frame to be uploaded when the DynamicGetSlot routine is called. Cleared (to 0xFF) on level load and on frame end when a dynamic sprite is no longer alive or no longer matches !dsu_sprite_num.!sprite_dsu_slot21$7F8B63$409C15!SprSize- If the sprite is not using a dynamic slot, holds the value of
0xFF. - If the sprite is a single slot dynamic sprite, holds the dynamic slot (
0x00-0x05). - If the sprite is a multi slot dynamic sprite, uses the bitwise format
%-m543210, withm=1and bits0-5set if this sprite is occupying that corresponding dynamic slot and clear otherwise.
0xFF) on level load and on frame end when the dynamic sprite is no longer alive or no longer matches !sprite_dsu_num.
!dsu_sprite_num21+!SprSize$7F8B6F$409C2B!SprSize!new_sprite_num table. This is used as a failsafe if a dynamic sprite spawns on the same sprite slot as a different dynamic sprite that just despawned on the same frame. If the sprite numbers don't match, the old dynamic slot is cleared.!dsu_freeram_nmi!dsu_src_bank0$0B06$6B06!dsu_src_16$0B0C$6B0C!dsu_dest_118$0B18$6B18!dsu_src_230$0B24$6B24!dsu_src_1+$0100.!dsu_dest_242$0B20$6B20!dsu_dest_1+$0100.!dsu_gfx_backup, located at $7F8B7B by default, is made equal to !dsu_freeram_restore. It reserves 512 bytes to back up each of up to six mapped regions, up to 3072 bytes.
Acknowledgements
I would like to thank the following people for making dsu possible or supporting its development in one way or another:
- smkdan, edit1754: Creating dsx and introducing dynamic sprites to SMW hacking.
- Vitor Vilela: Offering insight and feedback.
- freeplay: Offering insight and feedback.
- Donut: Proofreading the documentation.
Changelog
Version 1.0.1 (May 22nd, 2026):
- Removed instances of
AND #$01FFwhen fetching the mapping in several files, since dsu always ensures the mapping is in the proper format already.
Version 1.0.0 (May 21st, 2026):
- Added the graphics restoration functionality as a global and per-level setting. On account of this feature, the free RAM usage was bumped up by one extra mandatory byte and 3072 extra optional bytes. On PIXI's side, the
DynamicClearSlotGenericroutine was incremented with this feature. - Reallocated the default SA-1 free RAM, as the region formerly used is actually claimed by the VWF Dialogues patch.
- Reallocated the default LoROM free RAM to before the graphics restoration buffer, so the buffer itself can be located sensibly in both LoROM and SA-1.
- Changed the
dsu_gfx_pointersmacro to use 16 bits accumulator when storing two pointers. - Made note in the readme that the
DynamicGetSlotandDynamicGetMultiSlotroutines should not be called during a sprite's init. Thanks to Donut for pointing this out.
Version 0.7.3 (May 15th, 2025):
- Added the
DynamicGetSlotGenericandDynamicClearSlotGenericroutines to support dynamic sprites of other types.
Version 0.7.2 (May 10th, 2025):
- Added the
DynamicTransferSlotroutine to transfer a sprite's dynamic slot to another. - Fixed an issue in the
DynamicAssignSlotroutine's output where the carry might return clear if a slot wasn't found but the sprite wasn't set to despawn.
Version 0.7.1 (May 7th, 2025):
- Added the
dsu_incbin_freedatamacro to PIXI's environment to facilitate and encourage including files in afreedatablock.
Version 0.7.0 (May 1st, 2026):
- Increased supported maximum number of allowed dynamic sprites from four to six.
- Modified the
DynamicGetMultiSlotsroutine's input and output parameters to give support to the two extra slots. The page bits are now at$8Ainstead of$08. Furthermore, unrelated scratch RAM will no longer get clobbered. - Added the internal functionality of backing up a dynamic sprite's custom sprite number and freeing up a dynamic slot if the sprite number changes to fix a bug where if that sprite despawned and a different dynamic sprite spawned in its place in the same frame, the dynamic slot wouldn't get set up properly.
- Moved the NMI routine from RAM to ROM, keeping only the variable parameters in RAM.
- Split the free RAM in two chunks, so the addresses used in the NMI routine can be in the direct access range (
$0000-$1FFF) by default. RAM in the long access range ($7E2000-$7FFFFF) is still supported for those addresses if necessary. - Added support for unpatching dsx from an SA-1 ROM.
- Renamed dsu_limit.asm to dsu_cap.asm for consistency with how this feature is refered to in the readme.
- Replaced readme.txt and changelog.txt with the much more extensive readme.html (this file).
Version 0.6.0 (April 21st, 2026):
- Added the
DynamicGetMultiSlotsroutine, now supporting sprites that take up multiple dynamic slots. - Modified the
DynamicGetSlotroutine to despawn the sprite when a dynamic slot isn't found. - Modified the
DynamicAssignSlotroutine to optionally despawn the sprite if a dynamic slot isn't found. - Added support in the PIXI routines for frames 65-128 by storing a second graphics pointer to
$8C-$8F. - Modified the checks for free slots, now checking for the value of
!dsu_framebeing specifically0xFFinstead of negative. - Added
dsu_gfx_pointersmacro to PIXI's environment as a quick way to set up the pointers at$8A-$8Fbefore callingDynamicGetSlot.
Version 0.5.1 (April 17th, 2026):
- Moved the execution of dsu_remap.asm from init to load to fix a bug where the default mapping would get clobbered by dynamic sprites that spawn during level load, even if they were remapped for that level. The same was preemptively done in dsu_limit.asm.
- Silently added support to use
$500-$6FFinstead of$000-$1FFfor mappings for easier correlation with the values displayed in Lunar Magic's 8x8 editor.
Version 0.5.0 (April 16th, 2026):
- Beta release.
Version 0.1.0 (April 15th, 2025):
- Alpha release.