From a54e83ccec0330a29f41f466b32b9537a029cbcd Mon Sep 17 00:00:00 2001 From: Mykyta Onipchenko <44075969+nenikitov@users.noreply.github.com> Date: Mon, 8 Jan 2024 21:15:12 -0500 Subject: [PATCH 01/14] Create LICENSE (#31) --- LICENSE | 674 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 674 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. From 50607a172af1888e6b6176afb597aee035da55a2 Mon Sep 17 00:00:00 2001 From: Mykyta Onipchenko <44075969+nenikitov@users.noreply.github.com> Date: Wed, 17 Jan 2024 10:03:03 -0500 Subject: [PATCH 02/14] chore: update readme with new instructions for getting the files (#33) --------- Co-authored-by: Angel Pineda <57047985+UserIsntAvailable@users.noreply.github.com> --- README.md | 128 ++++++++++++------------------------------------------ 1 file changed, 28 insertions(+), 100 deletions(-) diff --git a/README.md b/README.md index 0e3d76f..35e9b66 100644 --- a/README.md +++ b/README.md @@ -2,71 +2,40 @@ A playable source port of an obscure NGage game Ashen -## How to run? +## Usage -- Place the ROM - ```sh - mkdir ./rom/ - cp /path/to/packfile.dat ./rom/packfile.dat - ``` -- Parse files (output will be in `output/parsed/`) +### Setup + +1. Place the ROM ```sh - cargo test --release -- --ignored parse_rom_packfile - cargo test --release -- --ignored parse_rom_asset + mkdir rom/ + cp /path/to/packfile.dat rom/packfile.dat ``` -## File structure - -### Overview - -- File is composed out of 3 parts - - Header - - File declarations - - Data -- Some data is compressed using `zlib` algorithm, some isn't -- All data is stored in Little-endian - -### Header - -| Size (bytes) | Purpose | -| ------------ | ---------------- | -| `4` | Signature `PMAN` | -| `4` | Number of files | -| `56` | Copyright | - -### File declarations - -- This structure is repeated for every file in the packfile - -| Size (bytes) | Purpose | -| ------------ | ------------------------------ | -| `4` | Padding? Always `00 00 00 00` | -| `4` | Offset of the file in packfile | -| `4` | Size of the file | -| `4` | Padding? Always `00 00 00 00` | - -### Data - -- If data is compressed using zlib +### Extracting files -| Size (bytes) | Purpose | -| ------------ | ---------------------- | -| `2` | Signature `ZL` | -| `3` | Size when uncompressed | -| `*` | Zlib stream | +File parsing is in test suite only, for now. -- If data is not compressed, just data stream +- Unpack game resources + 1. Run deflate test + ```sh + cargo test --release -- --ignored parse_rom_packfile + ``` + - This will split and deflate game files into `output/deflated/` directory + - Files are named with the address as they appear in the [asset table](#file-structure) +- Parse resources (make sure to unpack first) + 1. Run parsing tests + ```sh + cargo test --release -- --ignored parse_rom_asset + ``` + - This will parse select few game files into `output/parsed/` directory -### Known file declarations - -**⚠️ WARNING ⚠️** - -I use the packfile that comes with Ashen 1.06. -Your packfile may have different offsets to files, I didn't test with different versions. +## File structure -Notes: -- **⚠️** - Unknown file -- **🔎** - Unseen content +> [!IMPORTANT] +> +> I use the packfile that comes with Ashen 1.06. +> Your packfile may have different offsets to files, I didn't test with different versions. | Address (HEX) | Asset | |---------------|---------------------------------| @@ -229,56 +198,15 @@ Notes: | `9C` | stringtable german | | `9D` | stringtable spanish | -### Known file formats - -### Text bank - -- Starts with `9B 01 00 00` -- The rest is encoded in UTF16 -- All text chunks are separated with `\r\r` -- There are weird characters like `20 20` which are probably to control in-game scripts - -### Color palette - -- Is a collection of 4 byte integers -- Every integer is the 12-bit color -- If transformed into 256x32 image, shows colored arcs similar to the Quake color palette - -### Entity - -- Groups texture, model, and possibly animation data together -- Textures are stored as 8 bit integers that are indeces of the color on the color palette - -### Music - -- Another collection of Zlib files concatinated together -- Music is probably stored as OGG, instruments separate from composition -- **TODO** Document this better -- Segments -> files -- 1st segment - OST - - 7 - - 1 - - 8 - - 3 - - 5 - - 4 - - 2 - - 7 - - victory - - concept - - death - - load - - main menu - ## Discoveries - Test level for enemies - There is a pool with fish enemies, which I don't remember seeing in the game -- Test level ??? +- Test level for doors - Collision by itself works with other level geometry, geometry by itself also works, but not together ## Resources - [Post by HoRRoR](http://www.emu-land.net/forum/index.php?topic=49753.0) - [Debug Windows build with symbols](https://archive.org/details/Nokia_N-Gage_Ashen_v1.0.6_Windows_Build) - +- [Manual](https://ia804704.us.archive.org/25/items/n-gage-user-manuals/User%20Manual%20Ashen.pdf) From 26d0f55fc4696fb722331b1d1b40548f24893886 Mon Sep 17 00:00:00 2001 From: Mykyta Onipchenko <44075969+nenikitov@users.noreply.github.com> Date: Wed, 17 Jan 2024 11:50:05 -0500 Subject: [PATCH 03/14] feat: add parsing for `models`. * feat: implement texture parsing from a model * feat: add parsing of model triangles * feat: added sequence parsing * feat: added frame parsing and some temporary output * feat: added scaling * feat: added uv to exported script * refactor: improved formatting functions * feat: made texture loading automatic * feat: added parsing of triangle normals * feat: added separate animations as shape keys * feat: add origin to transform macro * fix: properly centered the model * chore: add todos --- Cargo.lock | 54 +++++ engine/Cargo.toml | 2 + engine/src/asset/color_map.rs | 18 +- engine/src/asset/mod.rs | 2 + engine/src/asset/model/dat/frame.rs | 131 ++++++++++++ engine/src/asset/model/dat/header.rs | 51 +++++ engine/src/asset/model/dat/mod.rs | 4 + engine/src/asset/model/dat/sequence.rs | 36 ++++ engine/src/asset/model/dat/triangle.rs | 39 ++++ engine/src/asset/model/mod.rs | 284 +++++++++++++++++++++++++ engine/src/asset/pack_file.rs | 2 +- engine/src/asset/skybox.rs | 13 +- engine/src/asset/sound/mod.rs | 8 +- engine/src/utils/nom.rs | 27 ++- 14 files changed, 659 insertions(+), 12 deletions(-) create mode 100644 engine/src/asset/model/dat/frame.rs create mode 100644 engine/src/asset/model/dat/header.rs create mode 100644 engine/src/asset/model/dat/mod.rs create mode 100644 engine/src/asset/model/dat/sequence.rs create mode 100644 engine/src/asset/model/dat/triangle.rs create mode 100644 engine/src/asset/model/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 0b240b3..3b32c22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,18 @@ dependencies = [ "engine", ] +[[package]] +name = "az" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" + +[[package]] +name = "bytemuck" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" + [[package]] name = "byteorder" version = "1.5.0" @@ -74,6 +86,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "either" version = "1.9.0" @@ -86,11 +104,13 @@ version = "0.1.0" dependencies = [ "const_format", "eyre", + "fixed", "flate2", "itertools", "lewton", "loom", "nom", + "paste", ] [[package]] @@ -103,6 +123,18 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fixed" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02c69ce7e7c0f17aa18fdd9d0de39727adb9c6281f2ad12f57cbe54ae6e76e7d" +dependencies = [ + "az", + "bytemuck", + "half", + "typenum", +] + [[package]] name = "flate2" version = "1.0.28" @@ -126,6 +158,16 @@ dependencies = [ "windows", ] +[[package]] +name = "half" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc52e53916c08643f1b56ec082790d1e86a32e58dc5268f897f313fbae7b4872" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "indenter" version = "0.3.3" @@ -254,6 +296,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -446,6 +494,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-ident" version = "1.0.12" diff --git a/engine/Cargo.toml b/engine/Cargo.toml index 87e8d6e..86fba7f 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -5,11 +5,13 @@ edition = "2021" [dependencies] const_format = "0.2.32" +fixed = "1.24.0" flate2 = "1.0.28" itertools = "0.12.0" lewton = "0.10.2" loom = { version = "0.7.1", optional = true } nom = "7.1.3" +paste = "1.0.14" [dev-dependencies] eyre = "0.6.8" diff --git a/engine/src/asset/color_map.rs b/engine/src/asset/color_map.rs index 3fc1b3c..7d2ce50 100644 --- a/engine/src/asset/color_map.rs +++ b/engine/src/asset/color_map.rs @@ -1,6 +1,6 @@ use super::{Asset, AssetChunk, Extension, Kind}; use crate::{error, utils::nom::*}; -use std::mem; +use std::{mem, ops::Deref}; const COLORS_COUNT: usize = 256; const SHADES_COUNT: usize = 32; @@ -160,3 +160,19 @@ mod tests { Ok(()) } } + +pub trait PaletteTexture { + fn with_palette(&self, palette: &[Color]) -> Vec>; +} +// impl for any 2D array like data structure. +impl PaletteTexture for Outer +where + Outer: Deref, + Inner: AsRef<[u8]>, +{ + fn with_palette(&self, palette: &[Color]) -> Vec> { + self.iter() + .map(|c| c.as_ref().iter().map(|c| palette[*c as usize]).collect()) + .collect() + } +} diff --git a/engine/src/asset/mod.rs b/engine/src/asset/mod.rs index 322a23d..a1ce525 100644 --- a/engine/src/asset/mod.rs +++ b/engine/src/asset/mod.rs @@ -4,6 +4,7 @@ mod pack_info; pub mod color_map; pub mod gamma_table; +pub mod model; pub mod pack_file; pub mod skybox; pub mod sound; @@ -16,6 +17,7 @@ pub enum Kind { SoundCollection, StringTable, Skybox, + Model, } #[derive(Clone, Debug, Default, PartialEq, Eq)] diff --git a/engine/src/asset/model/dat/frame.rs b/engine/src/asset/model/dat/frame.rs new file mode 100644 index 0000000..f418beb --- /dev/null +++ b/engine/src/asset/model/dat/frame.rs @@ -0,0 +1,131 @@ +use crate::{asset::AssetChunk, utils::nom::*}; + +// TODO(nenikitov): Should probably be a fancy utility class +// With generics for data type and dimension +#[derive(Debug)] +pub struct Vec3 { + pub x: f32, + pub y: f32, + pub z: f32, +} + +impl AssetChunk for Vec3 { + fn parse(input: &[u8]) -> Result { + let (input, x) = number::le_i16f16(input)?; + let (input, y) = number::le_i16f16(input)?; + let (input, z) = number::le_i16f16(input)?; + + Ok(( + input, + Self { + x: x.to_num(), + y: y.to_num(), + z: z.to_num(), + }, + )) + } +} + +pub struct ModelVertex { + pub x: u8, + pub y: u8, + pub z: u8, + // TODO(nenikitov): For now, no clue what this is for + pub light_normal_index: u8, +} + +impl AssetChunk for ModelVertex { + fn parse(input: &[u8]) -> Result { + let (input, x) = number::le_u8(input)?; + let (input, y) = number::le_u8(input)?; + let (input, z) = number::le_u8(input)?; + let (input, light_normal_index) = number::le_u8(input)?; + + Ok(( + input, + Self { + x, + y, + z, + light_normal_index, + }, + )) + } +} + +impl ModelVertex { + const UNITS_PER_METER: f32 = 32.0; + + fn to_parsed(&self, scale: &Vec3, scale_origin: &Vec3) -> ModelVertexParsed { + macro_rules! transform { + ($coordinate: ident) => { + (scale.$coordinate * self.$coordinate as f32 / -256.0 - scale_origin.$coordinate) + / Self::UNITS_PER_METER + }; + } + ModelVertexParsed { + x: transform!(x), + y: transform!(y), + z: transform!(z), + normal_index: self.light_normal_index, + } + } +} + +pub struct ModelVertexParsed { + pub x: f32, + pub y: f32, + pub z: f32, + pub normal_index: u8, +} + +pub struct ModelFrame { + pub bounding_sphere_radius: f32, + pub vertices: Vec, + pub triangle_normal_indexes: Vec, +} + +impl ModelFrame { + pub fn parse( + vertex_count: usize, + triangle_count: usize, + frame_size: usize, + ) -> impl Fn(&[u8]) -> Result { + move |input| { + let (input, scale) = Vec3::parse(input)?; + let (input, scale_origin) = Vec3::parse(input)?; + + let (input, bounding_sphere_radius) = number::le_i24f8(input)?; + + let (input, vertices) = multi::count!(ModelVertex::parse, vertex_count)(input)?; + let vertices = vertices + .into_iter() + .map(|v| v.to_parsed(&scale, &scale_origin)) + .collect(); + + let (input, triangle_normal_indexes) = + multi::count!(number::le_u8, triangle_count)(input)?; + + // This ugly formula calculates the padding after the frame data until next frame data + // ``` + // frame_size + // - sizeof(scale) + // - sizeof(scale_origin) + // - sizeof(bounding_sphere_radius) + // - sizeof(vertices) // sizeof(ModelVertex) * vertex_count + // - sizeof(triangle_normalindexes) // sizeof(u8) triangle_count + // ``` + let (input, _) = + bytes::take(frame_size - 28 - 4 * vertex_count - triangle_count)(input)?; + + Ok(( + input, + Self { + bounding_sphere_radius: bounding_sphere_radius.to_num(), + vertices, + triangle_normal_indexes, + }, + )) + } + } +} diff --git a/engine/src/asset/model/dat/header.rs b/engine/src/asset/model/dat/header.rs new file mode 100644 index 0000000..5bf02b8 --- /dev/null +++ b/engine/src/asset/model/dat/header.rs @@ -0,0 +1,51 @@ +use crate::{asset::AssetChunk, utils::nom::*}; + +pub struct ModelHeader { + pub triangle_count: u32, + pub vertex_count: u32, + pub texture_width: u32, + pub texture_height: u32, + pub frame_count: u32, + pub frame_size: u32, + pub sequence_count: u32, + pub offset_texture: u32, + pub offset_triangles: u32, + pub offset_frames: u32, + pub offset_sequences: u32, + pub locator_nodes: [u8; 16], +} + +impl AssetChunk for ModelHeader { + fn parse(input: &[u8]) -> Result { + let (input, triangle_count) = number::le_u32(input)?; + let (input, vertex_count) = number::le_u32(input)?; + let (input, texture_width) = number::le_u32(input)?; + let (input, texture_height) = number::le_u32(input)?; + let (input, frame_count) = number::le_u32(input)?; + let (input, frame_size) = number::le_u32(input)?; + let (input, sequence_count) = number::le_u32(input)?; + let (input, offset_texture) = number::le_u32(input)?; + let (input, offset_triangles) = number::le_u32(input)?; + let (input, offset_frames) = number::le_u32(input)?; + let (input, offset_sequences) = number::le_u32(input)?; + let (input, locator_nodes) = multi::count!(number::le_u8)(input)?; + + Ok(( + input, + Self { + triangle_count, + vertex_count, + texture_width, + texture_height, + frame_count, + frame_size, + sequence_count, + offset_texture, + offset_triangles, + offset_frames, + offset_sequences, + locator_nodes, + }, + )) + } +} diff --git a/engine/src/asset/model/dat/mod.rs b/engine/src/asset/model/dat/mod.rs new file mode 100644 index 0000000..cd41926 --- /dev/null +++ b/engine/src/asset/model/dat/mod.rs @@ -0,0 +1,4 @@ +pub mod frame; +pub mod header; +pub mod sequence; +pub mod triangle; diff --git a/engine/src/asset/model/dat/sequence.rs b/engine/src/asset/model/dat/sequence.rs new file mode 100644 index 0000000..12fb561 --- /dev/null +++ b/engine/src/asset/model/dat/sequence.rs @@ -0,0 +1,36 @@ +use crate::{asset::AssetChunk, utils::nom::*}; + +pub struct ModelSequence { + pub frame_count: u32, + pub offset: u32, +} + +impl AssetChunk for ModelSequence { + fn parse(input: &[u8]) -> Result { + let (input, frame_count) = number::le_u32(input)?; + let (input, offset) = number::le_u32(input)?; + + Ok(( + input, + Self { + frame_count, + offset, + }, + )) + } +} + +#[derive(Debug)] +pub struct ModelSequenceParsed { + pub frames: Vec, +} + +impl ModelSequenceParsed { + pub fn parse<'a>(input: &'a [u8], header: &ModelSequence) -> Result<'a, Self> { + let (_, frames) = multi::count!(number::le_u32, header.frame_count as usize)( + &input[header.offset as usize..], + )?; + + Ok((&[], Self { frames })) + } +} diff --git a/engine/src/asset/model/dat/triangle.rs b/engine/src/asset/model/dat/triangle.rs new file mode 100644 index 0000000..314262c --- /dev/null +++ b/engine/src/asset/model/dat/triangle.rs @@ -0,0 +1,39 @@ +use crate::utils::nom::*; + +pub struct ModelPoint { + pub vertex_index: u16, + pub u: f32, + pub v: f32, +} + +impl ModelPoint { + pub fn parse(texture_width: u32, texture_height: u32) -> impl Fn(&[u8]) -> Result { + move |input| { + let (input, vertex_index) = number::le_u16(input)?; + + let (input, u) = number::le_u16(input)?; + let u = (u as f32 + 0.5) / texture_width as f32; + + let (input, v) = number::le_u16(input)?; + // Y coordinates need to be flipped + let v = 1f32 - (v as f32 + 0.5) / texture_height as f32; + + Ok((input, Self { vertex_index, u, v })) + } + } +} + +pub struct ModelTriangle { + pub points: [ModelPoint; 3], +} + +impl ModelTriangle { + pub fn parse(texture_width: u32, texture_height: u32) -> impl Fn(&[u8]) -> Result { + move |input| { + let (input, points) = + multi::count!(ModelPoint::parse(texture_width, texture_height))(input)?; + + Ok((input, Self { points })) + } + } +} diff --git a/engine/src/asset/model/mod.rs b/engine/src/asset/model/mod.rs new file mode 100644 index 0000000..6aadba6 --- /dev/null +++ b/engine/src/asset/model/mod.rs @@ -0,0 +1,284 @@ +mod dat; + +use super::{Asset, AssetChunk, Extension, Kind}; +use crate::{error, utils::nom::*}; +use dat::{ + frame::ModelFrame, + header::ModelHeader, + sequence::{ModelSequence, ModelSequenceParsed}, + triangle::ModelTriangle, +}; + +use itertools::Itertools; + +pub struct Model { + pub texture: Vec>, + pub triangles: Vec, + pub sequences: Vec, + pub frames: Vec, +} + +impl Asset for Model { + fn kind() -> super::Kind { + Kind::Model + } + + fn parse(input: &[u8], extension: Extension) -> Result { + match extension { + Extension::Dat => { + let (_, header) = ModelHeader::parse(input)?; + + let (_, triangles) = multi::count!( + ModelTriangle::parse(header.texture_width, header.texture_height), + header.triangle_count as usize + )(&input[header.offset_triangles as usize..])?; + + let (_, texture) = multi::count!( + number::le_u8, + (header.texture_width * header.texture_height) as usize + )(&input[header.offset_texture as usize..])?; + let texture = texture + .into_iter() + .chunks(header.texture_width as usize) + .into_iter() + .map(Iterator::collect) + .collect(); + + let (_, sequences) = multi::count!( + ModelSequence::parse, + header.sequence_count as usize + )(&input[header.offset_sequences as usize..])?; + let sequences = sequences + .into_iter() + .map(|s| ModelSequenceParsed::parse(input, &s).map(|(_, d)| d)) + .collect::, _>>()?; + + let (_, frames) = multi::count!( + ModelFrame::parse( + header.vertex_count as usize, + header.triangle_count as usize, + header.frame_size as usize + ), + header.frame_count as usize + )(&input[header.offset_frames as usize..])?; + + Ok(( + &[0], + Self { + texture, + triangles, + sequences, + frames, + }, + )) + } + _ => Err(error::ParseError::unsupported_extension(input, extension).into()), + } + } +} + +#[cfg(test)] +mod tests { + use super::{ + dat::{frame::ModelVertexParsed, triangle::ModelPoint}, + *, + }; + use crate::{ + asset::color_map::{Color, ColorMap, PaletteTexture}, + utils::test::*, + }; + use std::{ + cell::LazyCell, + fmt::{Display, Formatter}, + path::PathBuf, + }; + + const COLOR_MAP_DATA: LazyCell> = deflated_file!("01.dat"); + const MODEL_DATA: LazyCell> = deflated_file!("0E.dat"); + + #[test] + #[ignore = "uses Ashen ROM files"] + fn parse_rom_asset() -> eyre::Result<()> { + let (_, model) = Model::parse(&MODEL_DATA, Extension::Dat)?; + let palette = { + let (_, color_map) = ColorMap::parse(&COLOR_MAP_DATA, Extension::Dat)?; + color_map.shades[15] + }; + + let output_dir = PathBuf::from(parsed_file_path!("models/hunter/")); + + output_file(output_dir.join("hunter.py"), model.to_py(&palette))?; + + Ok(()) + } + + // TODO(nenikitov): Move this to `utils::format` module + pub trait ModelPythonFile { + fn to_py(&self, palette: &[Color]) -> String; + } + + impl ModelPythonFile for Model { + fn to_py(&self, palette: &[Color]) -> String { + // TODO(nenikitov): Move a common part of the script into a separate `.py` file + format!( + r#"import bpy + +# Data +texture_width = {} +texture_height = {} +texture = [ +{} +] +frames = [ +{} +] +triangles = [ +{} +] +sequences = [ +{} +] + +# Clean up +for obj in bpy.data.objects: + bpy.data.objects.remove(obj, do_unlink=True) + +# Mesh +mesh = bpy.data.meshes.new("Mesh") +object = bpy.data.objects.new("Model", mesh) +mesh.from_pydata( + [ + (v["x"], v["y"], v["z"]) + for v in frames[0] + ], + [], + [ + (t["points"][0]["vertex_index"], t["points"][1]["vertex_index"], t["points"][2]["vertex_index"]) + for t in triangles + ] +) +# UV +uv = mesh.uv_layers.new(name="UV") +for loop in mesh.loops: + i = loop.index + triangle_point = triangles[i // 3]["points"][i % 3] + uv.data[i].uv = (triangle_point["u"], triangle_point["v"]) +# Texture +image = bpy.data.images.new("Texture", texture_width, texture_height) +image.pixels = texture +image.update() +# Material +material = bpy.data.materials.new(name="Material") +material.use_nodes = True +material_bsdf = material.node_tree.nodes["Principled BSDF"] +material_bsdf.inputs["Roughness"].default_value = 1.0 +material_texture = material.node_tree.nodes.new("ShaderNodeTexImage") +material_texture.image = image +material_texture.interpolation = "Closest" +material.node_tree.links.new(material_texture.outputs["Color"], material_bsdf.inputs["Base Color"]) +mesh.materials.append(material) +# Shape keys +shape_keys = [] +for i, f in enumerate(frames): + shape_keys.append(object.shape_key_add(name=f"Key {{i}}")) + for v_i, v in enumerate(f): + shape_keys[i].data[v_i].co = (v["x"], v["y"], v["z"]) +# Actions +actions = [] +mesh.shape_keys.animation_data_create() +for i, s in enumerate(sequences): + actions.append(bpy.data.actions.new(f"Action {{i}}")) + actions[i].use_fake_user = True + actions[i].frame_end = len(s["frames"]) + actions[i].use_frame_range = True + mesh.shape_keys.animation_data.action = actions[i] + for f_i, frame in enumerate(s["frames"]): + for s_i, shape_key in enumerate(shape_keys): + shape_key.value = 1.0 if s_i == frame else 0.0 + shape_key.keyframe_insert(data_path="value", frame=f_i + 1) + +# Finalize +bpy.context.collection.objects.link(object) +bpy.context.view_layer.objects.active = object +object.select_set(True) + "#, + self.texture[0].len(), + self.texture.len(), + self.texture + .with_palette(&palette) + .into_iter() + // In blender, y axis of textures is reversed + .rev() + .map(|r| format!(" {}", r.into_iter().map(|c| c.to_string()).join(", "))) + .join(",\n"), + self.frames + .iter() + .map(|f| format!( + " [{}]", + f.vertices.iter().map(|v| v.to_string()).join(", ") + )) + .join(",\n"), + self.triangles + .iter() + .map(|v| format!(" {v}")) + .join(",\n"), + self.sequences + .iter() + .map(|s| format!(" {s}")) + .join(",\n") + ) + } + } + + impl Display for Color { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + r#"{}, {}, {}, 1.0"#, + self.r as f32 / 255.0, + self.g as f32 / 255.0, + self.b as f32 / 255.0 + ) + } + } + + impl Display for ModelVertexParsed { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + r#"{{ "x": {}, "y": {}, "z": {}, "lightmap": {} }}"#, + self.x, self.y, self.z, self.normal_index + ) + } + } + + impl Display for ModelTriangle { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + r#"{{ "points": [{}, {}, {}] }}"#, + self.points[0], self.points[1], self.points[2], + ) + } + } + + impl Display for ModelPoint { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + r#"{{ "vertex_index": {}, "u": {}, "v": {} }}"#, + self.vertex_index, self.u, self.v + ) + } + } + + impl Display for ModelSequenceParsed { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + r#"{{ "frames": [{}] }}"#, + self.frames.iter().map(u32::to_string).join(", ") + ) + } + } +} diff --git a/engine/src/asset/pack_file.rs b/engine/src/asset/pack_file.rs index 8d613f8..9713a48 100644 --- a/engine/src/asset/pack_file.rs +++ b/engine/src/asset/pack_file.rs @@ -198,7 +198,7 @@ mod tests { fn parse_rom_packfile() -> eyre::Result<()> { let (_, pack_file) = PackFile::new(&ROM_DATA)?; - let mut output_dir = PathBuf::from(workspace_file_path!(DEFLATED_PATH)); + let output_dir = PathBuf::from(workspace_file_path!(DEFLATED_PATH)); pack_file .entries diff --git a/engine/src/asset/skybox.rs b/engine/src/asset/skybox.rs index 4f2176c..b037d77 100644 --- a/engine/src/asset/skybox.rs +++ b/engine/src/asset/skybox.rs @@ -6,7 +6,8 @@ const COLOR_COUNT: usize = 256; #[derive(Debug)] pub struct Skybox { - pub texture: Vec>, + pub palette: Vec, + pub texture: Vec>, } impl Asset for Skybox { @@ -28,13 +29,12 @@ impl Asset for Skybox { multi::count!(number::le_u8, (width * height) as usize)(input)?; let texture = texture .into_iter() - .map(|c| palette[c as usize]) .chunks(width as usize) .into_iter() .map(Iterator::collect) .collect(); - Ok((&[], Self { texture })) + Ok((&[], Self { palette, texture })) } _ => Err(error::ParseError::unsupported_extension(input, extension).into()), } @@ -44,7 +44,10 @@ impl Asset for Skybox { #[cfg(test)] mod tests { use super::*; - use crate::utils::{format::*, test::*}; + use crate::{ + asset::color_map::PaletteTexture, + utils::{format::*, test::*}, + }; use std::cell::LazyCell; const SKYBOX_DATA: LazyCell> = deflated_file!("3C.dat"); @@ -56,7 +59,7 @@ mod tests { output_file( parsed_file_path!("skyboxes/level-1.ppm"), - skybox.texture.to_ppm(), + skybox.texture.with_palette(&skybox.palette).to_ppm(), )?; Ok(()) diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 519dc06..865e6ec 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -10,7 +10,7 @@ use crate::{ asset::sound::dat::{ asset_header::SoundAssetHeader, chunk_header::SoundChunkHeader, t_song::TSong, }, - error::{self}, + error, utils::nom::*, }; @@ -78,7 +78,7 @@ impl Asset for SoundAssetCollection { #[cfg(test)] mod tests { use super::*; - use crate::utils::{format::WaveFile, test::*}; + use crate::utils::{format::*, test::*}; use std::{cell::LazyCell, path::PathBuf}; const SOUND_DATA: LazyCell> = deflated_file!("97.dat"); @@ -88,7 +88,7 @@ mod tests { fn parse_rom_asset() -> eyre::Result<()> { let (_, asset) = SoundAssetCollection::parse(&SOUND_DATA, Extension::Dat)?; - let mut output_dir = PathBuf::from(parsed_file_path!("sounds/songs/")); + let output_dir = PathBuf::from(parsed_file_path!("sounds/songs/")); asset.songs.iter().enumerate().try_for_each(|(i, song)| { let file = output_dir.join(format!("{i:0>2X}.wav")); @@ -101,7 +101,7 @@ mod tests { ) })?; - let mut output_dir = PathBuf::from(parsed_file_path!("sounds/effects/")); + let output_dir = PathBuf::from(parsed_file_path!("sounds/effects/")); asset .effects diff --git a/engine/src/utils/nom.rs b/engine/src/utils/nom.rs index 0930d34..1c28390 100644 --- a/engine/src/utils/nom.rs +++ b/engine/src/utils/nom.rs @@ -12,10 +12,35 @@ macro_rules! re_export { }; } +/// Re-exports all `nom::number` items. +pub mod number { + pub use nom::number::complete::*; + + use super::Result; + use nom::number; + use paste::paste; + + macro_rules! parser_for_fixed { + ($type: ty, $bits: expr) => { + paste! { + pub fn [](input: &[u8]) -> Result<$type> { + let (input, value) = number::complete::[](input)?; + Ok((input, $type::from_bits(value))) + } + } + }; + } + + use fixed::types::{I16F16, I24F8, I8F24}; + + parser_for_fixed!(I8F24, 32); + parser_for_fixed!(I16F16, 32); + parser_for_fixed!(I24F8, 32); +} + re_export!(bits); re_export!(bytes); re_export!(character); -re_export!(number); /// Re-exports all `nom::multi` items. pub mod multi { From c468436b811da6795f783cfef53bbcb69cce109a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jan 2024 00:14:15 -0500 Subject: [PATCH 04/14] chore(deps): bump actions/cache from 3 to 4 (#36) Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ce93bcb..b619b39 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,7 @@ jobs: submodules: true - name: Restore cached downloaded game id: cache-ashen - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: rom key: ${{ runner.os }}-ashen From 660f35d3f408d99f78e57b37f9820e221ef394ee Mon Sep 17 00:00:00 2001 From: Mykyta Onipchenko <44075969+nenikitov@users.noreply.github.com> Date: Thu, 18 Jan 2024 20:29:03 -0500 Subject: [PATCH 05/14] fix!: made packfile not auto deflate files (#38) * chore: add note about this change in README --- README.md | 6 ++-- engine/src/asset/model/mod.rs | 2 +- engine/src/asset/pack_file.rs | 52 ++++++++++++++------------------ engine/src/asset/sound/mod.rs | 27 ++--------------- engine/src/asset/string_table.rs | 2 +- engine/src/utils/compression.rs | 39 ++++++++++++++++++++++++ engine/src/utils/mod.rs | 1 + 7 files changed, 72 insertions(+), 57 deletions(-) create mode 100644 engine/src/utils/compression.rs diff --git a/README.md b/README.md index 35e9b66..046c5a8 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,10 @@ File parsing is in test suite only, for now. ```sh cargo test --release -- --ignored parse_rom_packfile ``` - - This will split and deflate game files into `output/deflated/` directory - - Files are named with the address as they appear in the [asset table](#file-structure) + - This will split and deflate game files into `output/deflated/` directory. + - Files are named with the address as they appear in the [asset table](#file-structure). + - Files that begin with decompression signature are automatically decompressed and named with `-deflated` suffix. + This automatic decompression doesn't work well for collection assets such as textures. - Parse resources (make sure to unpack first) 1. Run parsing tests ```sh diff --git a/engine/src/asset/model/mod.rs b/engine/src/asset/model/mod.rs index 6aadba6..8cf4176 100644 --- a/engine/src/asset/model/mod.rs +++ b/engine/src/asset/model/mod.rs @@ -94,7 +94,7 @@ mod tests { }; const COLOR_MAP_DATA: LazyCell> = deflated_file!("01.dat"); - const MODEL_DATA: LazyCell> = deflated_file!("0E.dat"); + const MODEL_DATA: LazyCell> = deflated_file!("0E-deflated.dat"); #[test] #[ignore = "uses Ashen ROM files"] diff --git a/engine/src/asset/pack_file.rs b/engine/src/asset/pack_file.rs index 9713a48..b459b1e 100644 --- a/engine/src/asset/pack_file.rs +++ b/engine/src/asset/pack_file.rs @@ -1,8 +1,6 @@ //! This only works with the last version of Ashen :). use crate::utils::nom::*; -use flate2::read::ZlibDecoder; -use std::io::Read; #[derive(Debug, PartialEq)] struct EntryHeader { @@ -74,22 +72,10 @@ impl PackFile { ) -> Result<'a, Vec> { fn entry(input: &[u8], entry_header: &EntryHeader) -> EntryData { let bytes = &input[entry_header.offset as usize..][..entry_header.size as usize]; - let bytes = if let [b'Z', b'L', s1, s2, s3, bytes @ ..] = bytes { - let size = u32::from_le_bytes([*s1, *s2, *s3, 0]); - - let mut decoder = ZlibDecoder::new(bytes); - let mut data = Vec::with_capacity(size as usize); - decoder - .read_to_end(&mut data) - .expect("Data should be a valid zlib stream"); - // TODO(nenikitov): Check if `data.len() == size` - - data - } else { - bytes.to_vec() - }; - - EntryData { bytes } + + EntryData { + bytes: bytes.to_vec(), + } } let entries = entry_headers.iter().map(|h| entry(input, h)).collect(); @@ -100,9 +86,9 @@ impl PackFile { #[cfg(test)] mod tests { - use std::{cell::LazyCell, path::PathBuf}; + use std::{cell::LazyCell, io, path::PathBuf}; - use crate::utils::test::*; + use crate::utils::{compression::decompress, test::*}; use super::*; @@ -160,16 +146,13 @@ mod tests { // File 1 b'A', b's', b'h', b'e', b'n', // File 2 - b'Z', b'L', // Asset Zlib signature - 0x06, 0x00, 0x00, // Stream size - 0x78, 0xDA, // Actual Zlib signature - 0x73, 0x2C, 0xCE, 0x48, 0xCD, 0xE3, 0x02, 0x00, 0x07, 0x80, 0x01, 0xFA, + b'Z', b'L', ], &[ EntryHeader { offset: 0, size: 5 }, EntryHeader { offset: 5, - size: 19, + size: 2, }, ], )?; @@ -181,7 +164,7 @@ mod tests { bytes: b"Ashen".to_vec(), }, EntryData { - bytes: b"Ashen\n".to_vec(), + bytes: b"ZL".to_vec(), } ] ); @@ -204,9 +187,20 @@ mod tests { .entries .iter() .enumerate() - .try_for_each(|(i, entry)| { - let file = output_dir.join(format!("{i:0>2X}.dat")); - output_file(file, &entry.bytes) + .try_for_each(|(i, entry)| -> io::Result<()> { + let compressed = &entry.bytes; + let decompressed = &decompress(&entry.bytes); + + output_file(output_dir.join(format!("{i:0>2X}.dat")), &entry.bytes)?; + + if compressed != decompressed { + output_file( + output_dir.join(format!("{i:0>2X}-deflated.dat")), + &decompress(&entry.bytes), + )?; + } + + Ok(()) })?; Ok(()) diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 865e6ec..4f9c08d 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -1,8 +1,5 @@ mod dat; -use flate2::read::ZlibDecoder; -use std::io::Read; - use self::dat::t_effect::TEffect; use super::{Asset, AssetChunk, Extension, Kind}; @@ -11,27 +8,9 @@ use crate::{ asset_header::SoundAssetHeader, chunk_header::SoundChunkHeader, t_song::TSong, }, error, - utils::nom::*, + utils::{compression::decompress, nom::*}, }; -// TODO(nenikitov): Move to utils -// TODO(nenikitov): Return `Result` -fn deflate(input: &[u8]) -> Vec { - if let [b'Z', b'L', s1, s2, s3, bytes @ ..] = input { - let size = u32::from_le_bytes([*s1, *s2, *s3, 0]); - - let mut decoder = ZlibDecoder::new(bytes); - let mut data = Vec::with_capacity(size as usize); - decoder - .read_to_end(&mut data) - .expect("Data should be a valid zlib stream"); - - data - } else { - input.to_vec() - } -} - pub struct SoundAssetCollection { songs: Vec, effects: Vec, @@ -56,7 +35,7 @@ impl Asset for SoundAssetCollection { let songs = songs .infos .into_iter() - .map(|s| deflate(&input[s])) + .map(|s| decompress(&input[s])) .map(|s| TSong::parse(s.as_slice()).map(|(_, d)| d)) .collect::, _>>()?; @@ -64,7 +43,7 @@ impl Asset for SoundAssetCollection { let effects = effects .infos .into_iter() - .map(|s| deflate(&input[s])) + .map(|s| decompress(&input[s])) .map(|s| TEffect::parse(s.as_slice()).map(|(_, d)| d)) .collect::, _>>()?; diff --git a/engine/src/asset/string_table.rs b/engine/src/asset/string_table.rs index 4248349..60919a1 100644 --- a/engine/src/asset/string_table.rs +++ b/engine/src/asset/string_table.rs @@ -39,7 +39,7 @@ mod tests { use crate::utils::test::*; use std::cell::LazyCell; - const STRING_TABLE_DATA: LazyCell> = deflated_file!("98.dat"); + const STRING_TABLE_DATA: LazyCell> = deflated_file!("98-deflated.dat"); #[test] #[ignore = "uses Ashen ROM files"] diff --git a/engine/src/utils/compression.rs b/engine/src/utils/compression.rs new file mode 100644 index 0000000..13f2f28 --- /dev/null +++ b/engine/src/utils/compression.rs @@ -0,0 +1,39 @@ +use std::io::Read; + +use flate2::bufread::ZlibDecoder; + +// TODO(nenikitov): Return `Result` +pub fn decompress(bytes: &[u8]) -> Vec { + match bytes { + [b'Z', b'L', s1, s2, s3, bytes @ ..] => { + let size = u32::from_le_bytes([*s1, *s2, *s3, 0]); + let mut decoder = ZlibDecoder::new(bytes); + let mut data = Vec::with_capacity(size as usize); + decoder + .read_to_end(&mut data) + .expect("Data should be a valid zlib stream"); + + // TODO(nenikitov): Check if `data.len() == size` + + data + } + _ => bytes.to_vec(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn decompress_zlib_works() { + let data = [ + b'Z', b'L', // Asset Zlib signature + 0x06, 0x00, 0x00, // Stream size + 0x78, 0xDA, // Actual Zlib signature + 0x73, 0x2C, 0xCE, 0x48, 0xCD, 0xE3, 0x02, 0x00, 0x07, 0x80, 0x01, 0xFA, + ]; + + assert_eq!("Ashen\n".bytes().collect::>(), decompress(&data)) + } +} diff --git a/engine/src/utils/mod.rs b/engine/src/utils/mod.rs index e4bcaf3..17a7b5a 100644 --- a/engine/src/utils/mod.rs +++ b/engine/src/utils/mod.rs @@ -1,3 +1,4 @@ +pub mod compression; pub mod format; pub mod nom; #[cfg(test)] From b9688c9a274510cbb0cc490724d1617f9fa062b2 Mon Sep 17 00:00:00 2001 From: Mykyta Onipchenko <44075969+nenikitov@users.noreply.github.com> Date: Fri, 19 Jan 2024 13:24:27 -0500 Subject: [PATCH 06/14] fix: made ci not ouptut parsed files (#40) --- .github/workflows/test.yml | 8 +++++--- engine/src/utils/test.rs | 22 ++++++++++++++++++---- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b619b39..0657cf0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,14 +1,14 @@ -permissions: - contents: read +name: test on: push: branches: [main] pull_request: +permissions: + contents: read # Spend CI time only on latest ref: https://github.com/jonhoo/rust-ci-conf/pull/5 concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true -name: test jobs: required: runs-on: ubuntu-latest @@ -54,6 +54,8 @@ jobs: run: cargo test --locked -- --ignored parse_rom_packfile - name: ROM - Assets run: cargo test --locked -- --ignored parse_rom_asset + env: + SKIP_TEST_WRITE: true minimal: runs-on: ubuntu-latest name: ubuntu / nightly / minimal-versions diff --git a/engine/src/utils/test.rs b/engine/src/utils/test.rs index ff903d2..ab01e90 100644 --- a/engine/src/utils/test.rs +++ b/engine/src/utils/test.rs @@ -1,4 +1,4 @@ -use std::{fs, io, path::Path}; +use std::{env, fs, io, path::Path}; pub const PARSED_PATH: &'static str = "output/parsed/"; pub const DEFLATED_PATH: &'static str = "output/deflated/"; @@ -29,6 +29,16 @@ macro_rules! deflated_file { }; } +pub fn should_skip_write() -> bool { + match env::var("SKIP_TEST_WRITE") + .map(|value| value.to_lowercase()) + .as_deref() + { + Ok("true") | Ok("1") => true, + _ => false, + } +} + /// Writes to a file creating the directory automatically. pub fn output_file(path: P, contents: C) -> io::Result<()> where @@ -36,9 +46,13 @@ where C: AsRef<[u8]>, { fn inner(path: &Path, contents: &[u8]) -> io::Result<()> { - let parent = path.parent().ok_or(io::ErrorKind::InvalidFilename)?; - fs::create_dir_all(parent)?; - fs::write(path, contents) + if !should_skip_write() { + let parent = path.parent().ok_or(io::ErrorKind::InvalidFilename)?; + fs::create_dir_all(parent)?; + fs::write(path, contents) + } else { + Ok(()) + } } inner(path.as_ref(), contents.as_ref()) From 474184b03bb72b0e33cd6a2a1b3786f1f3703ae3 Mon Sep 17 00:00:00 2001 From: Mykyta Onipchenko <44075969+nenikitov@users.noreply.github.com> Date: Fri, 19 Jan 2024 22:36:36 -0500 Subject: [PATCH 07/14] refactor!: new `AssetParser` trait instead of `Asset` (#37) * refactor: add context to asset parsing * refactor: remove `AssetChunk` * fix: reverted back to ignore doc test This causes problems with the `doc test` CI action. * fix: slice returned after model parsing --------- Co-authored-by: UserIsntAvailable --- Cargo.lock | 52 +++- engine/Cargo.toml | 2 + engine/src/asset/color_map.rs | 92 ++++--- engine/src/asset/gamma_table.rs | 55 ++-- engine/src/asset/mod.rs | 128 ++++++--- engine/src/asset/model/dat/frame.rs | 145 ++++++----- engine/src/asset/model/dat/header.rs | 69 ++--- engine/src/asset/model/dat/sequence.rs | 43 ++- engine/src/asset/model/dat/triangle.rs | 29 ++- engine/src/asset/model/mod.rs | 115 ++++---- engine/src/asset/pack_file.rs | 6 +- engine/src/asset/pack_info.rs | 20 +- engine/src/asset/skybox.rs | 44 ++-- engine/src/asset/sound/dat/asset_header.rs | 36 +-- engine/src/asset/sound/dat/chunk_header.rs | 14 +- engine/src/asset/sound/dat/mod.rs | 3 +- engine/src/asset/sound/dat/t_effect.rs | 62 +++-- engine/src/asset/sound/dat/t_instrument.rs | 191 +++++++------- engine/src/asset/sound/dat/t_song.rs | 289 ++++++++++----------- engine/src/asset/sound/mod.rs | 61 ++--- engine/src/asset/string_table.rs | 27 +- engine/src/directory.rs | 12 +- engine/src/error.rs | 11 +- engine/src/lib.rs | 10 +- engine/src/utils/compression.rs | 3 +- engine/src/utils/nom.rs | 5 +- 26 files changed, 786 insertions(+), 738 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3b32c22..93e230f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,6 +111,8 @@ dependencies = [ "loom", "nom", "paste", + "sealed", + "thiserror", ] [[package]] @@ -168,6 +170,12 @@ dependencies = [ "crunchy", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "indenter" version = "0.3.3" @@ -310,18 +318,18 @@ checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -382,6 +390,18 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "sealed" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a8caec23b7800fb97971a1c6ae365b6239aaeddfb934d6265f8505e795699d" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -399,15 +419,35 @@ checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "syn" -version = "2.0.38" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.7" diff --git a/engine/Cargo.toml b/engine/Cargo.toml index 86fba7f..f3873b2 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -12,6 +12,8 @@ lewton = "0.10.2" loom = { version = "0.7.1", optional = true } nom = "7.1.3" paste = "1.0.14" +sealed = "0.5.0" +thiserror = "1.0.56" [dev-dependencies] eyre = "0.6.8" diff --git a/engine/src/asset/color_map.rs b/engine/src/asset/color_map.rs index 7d2ce50..01f71bf 100644 --- a/engine/src/asset/color_map.rs +++ b/engine/src/asset/color_map.rs @@ -1,4 +1,4 @@ -use super::{Asset, AssetChunk, Extension, Kind}; +use super::{extension::*, AssetParser}; use crate::{error, utils::nom::*}; use std::{mem, ops::Deref}; @@ -15,6 +15,7 @@ pub struct Color { impl Color { pub fn from_12_bit(color: u16) -> Self { + // TODO(nenikitov): return result. assert!(color <= 0xFFF, "12 bit color is smaller than 0xFFF"); let r = (color & 0xF00) >> 8; @@ -33,68 +34,62 @@ impl Color { } } -impl AssetChunk for Color { - fn parse(input: &[u8]) -> Result { - let (input, color) = number::le_u32(input)?; - let color = Self::from_12_bit(color as u16); - Ok((input, color)) +impl AssetParser for Color { + fn parser((): Self::Context<'_>) -> impl FnParser { + move |input| { + let (input, color) = number::le_u32(input)?; + Ok((input, Self::from_12_bit(color as u16))) + } } } -// TODO(Unavailable): derive +// TODO(Unavailable): derive ??? pub struct ColorMap { // TODO(nenikitov): This probably shouldn't be `pub` and should have an // accessor that will hide the "ugly" internal 2D-array structure. pub shades: Box<[[Color; COLORS_COUNT]; SHADES_COUNT]>, } -impl Asset for ColorMap { - fn kind() -> Kind { - Kind::ColorMap - } - - fn parse(input: &[u8], extension: Extension) -> Result { - fn colors(input: &[u8]) -> Result<[Color; COLORS_COUNT]> { - multi::count!(Color::parse)(input) - } - - match extension { - Extension::Dat => { - error::ensure_bytes_length( - input, - mem::size_of::() * COLORS_COUNT * SHADES_COUNT, - "Incorrect `ColorMap` format (256x32 array of 12-bit [padded to 32-bit] colors)", - )?; - - let (input, colors) = multi::count!(colors, SHADES_COUNT)(input)?; - - let colors = { - let colors = colors.into_boxed_slice(); - // Ensure the original box is not dropped. - let mut colors = mem::ManuallyDrop::new(colors); - // SAFETY: [_] and [_; N] has the same memory layout as long - // as the slice contains exactly N elements. - unsafe { Box::from_raw(colors.as_mut_ptr().cast()) } - }; - - Ok((input, Self { shades: colors })) - } - _ => Err(error::ParseError::unsupported_extension(input, extension).into()), +impl AssetParser for ColorMap { + fn parser((): Self::Context<'_>) -> impl FnParser { + move |input| { + error::ensure_bytes_length( + input, + mem::size_of::() * COLORS_COUNT * SHADES_COUNT, + "Incorrect `ColorMap` format (256x32 array of 12-bit [padded to 32-bit] colors)", + )?; + + let (input, colors) = multi::count!( + |input| -> Result<[Color; COLORS_COUNT]> { + multi::count!(Color::parser(()))(input) + }, + SHADES_COUNT + )(input)?; + + let colors = { + let colors = colors.into_boxed_slice(); + // Ensure the original box is not dropped. + let mut colors = mem::ManuallyDrop::new(colors); + // SAFETY: [_] and [_; N] has the same memory layout as long + // as the slice contains exactly N elements. + unsafe { Box::from_raw(colors.as_mut_ptr().cast()) } + }; + + Ok((input, Self { shades: colors })) } } } #[cfg(test)] mod tests { - use std::cell::LazyCell; - use super::*; use crate::utils::{format::*, test::*}; + use std::cell::LazyCell; #[test] fn shade_works() -> eyre::Result<()> { assert_eq!( - Color::parse(&u32::to_le_bytes(0x100))?.1, + Color::parser(())(&u32::to_le_bytes(0x100))?.1, Color { r: 0x11, g: 0, @@ -102,7 +97,7 @@ mod tests { }, ); assert_eq!( - Color::parse(&u32::to_le_bytes(0x011))?.1, + Color::parser(())(&u32::to_le_bytes(0x011))?.1, Color { r: 0, g: 0x11, @@ -110,7 +105,7 @@ mod tests { }, ); assert_eq!( - Color::parse(&u32::to_le_bytes(0x001))?.1, + Color::parser(())(&u32::to_le_bytes(0x001))?.1, Color { r: 0, g: 0, @@ -118,7 +113,7 @@ mod tests { }, ); assert_eq!( - Color::parse(&u32::to_le_bytes(0x220))?.1, + Color::parser(())(&u32::to_le_bytes(0x220))?.1, Color { r: 0x22, g: 0x22, @@ -126,7 +121,7 @@ mod tests { }, ); assert_eq!( - Color::parse(&u32::to_le_bytes(0x022))?.1, + Color::parser(())(&u32::to_le_bytes(0x022))?.1, Color { r: 0, g: 0x22, @@ -134,7 +129,7 @@ mod tests { }, ); assert_eq!( - Color::parse(&u32::to_le_bytes(0x333))?.1, + Color::parser(())(&u32::to_le_bytes(0x333))?.1, Color { r: 0x33, g: 0x33, @@ -150,7 +145,7 @@ mod tests { #[test] #[ignore = "uses Ashen ROM files"] fn parse_rom_asset() -> eyre::Result<()> { - let (_, color_map) = ColorMap::parse(&COLOR_MAP_DATA, Extension::Dat)?; + let (_, color_map) = >::parser(())(&COLOR_MAP_DATA)?; output_file( parsed_file_path!("color-map/monsters.ppm"), @@ -164,6 +159,7 @@ mod tests { pub trait PaletteTexture { fn with_palette(&self, palette: &[Color]) -> Vec>; } + // impl for any 2D array like data structure. impl PaletteTexture for Outer where diff --git a/engine/src/asset/gamma_table.rs b/engine/src/asset/gamma_table.rs index a8976f8..fecdc3b 100644 --- a/engine/src/asset/gamma_table.rs +++ b/engine/src/asset/gamma_table.rs @@ -1,4 +1,4 @@ -use super::{Asset, Extension, Kind}; +use super::{extension::*, AssetParser}; use crate::{error, utils::nom::*}; use std::mem; @@ -11,37 +11,32 @@ pub struct GammaTable { pub lookups: Box<[[u8; ROWS_COUNT]; COLS_COUNT]>, } -impl Asset for GammaTable { - fn kind() -> Kind { - Kind::GammaTable - } - - fn parse(input: &[u8], extension: Extension) -> Result { - match extension { - Extension::Dat => { - error::ensure_bytes_length( - input, - GAMMA_TABLE_LENGTH, - "Incorrect `GammaTable` format (256x101 array of u8s)", - )?; +impl AssetParser for GammaTable { + fn parser((): Self::Context<'_>) -> impl FnParser { + move |input| { + error::ensure_bytes_length( + input, + GAMMA_TABLE_LENGTH, + "Incorrect `GammaTable` format (256x101 array of u8s)", + )?; - // Technically this can't never fail. - let (input, bytes) = bytes::take(GAMMA_TABLE_LENGTH)(input)?; + // Technically this can't never fail. + let (input, bytes) = bytes::take(GAMMA_TABLE_LENGTH)(input)?; - // SAFETY: bytes::take() should return exactly `ROWS_COUNT * COLS_COUNT` - // bytes; also slices and arrays are guaranteed to have the same memory - // layout. - let lookups = - unsafe { mem::transmute_copy::<_, &[[u8; ROWS_COUNT]; COLS_COUNT]>(&bytes) }; + // SAFETY(Unavailable): Dont transmute references!!!!!!! + // + // SAFETY: bytes::take() should return exactly `ROWS_COUNT * COLS_COUNT` + // bytes; also slices and arrays are guaranteed to have the same memory + // layout. + let lookups = + unsafe { mem::transmute_copy::<_, &[[u8; ROWS_COUNT]; COLS_COUNT]>(&bytes) }; - Ok(( - input, - Self { - lookups: Box::new(*lookups), - }, - )) - } - _ => Err(error::ParseError::unsupported_extension(input, extension).into()), + Ok(( + input, + Self { + lookups: Box::new(*lookups), + }, + )) } } } @@ -60,7 +55,7 @@ mod tests { #[test] #[ignore = "uses Ashen ROM files"] fn parse_rom_asset() -> eyre::Result<()> { - let (_, gamma_table) = GammaTable::parse(&GAMMA_TABLE_DATA, Extension::Dat)?; + let (_, gamma_table) = >::parser(())(&GAMMA_TABLE_DATA)?; let gamma_table = gamma_table .lookups diff --git a/engine/src/asset/mod.rs b/engine/src/asset/mod.rs index a1ce525..2050ae3 100644 --- a/engine/src/asset/mod.rs +++ b/engine/src/asset/mod.rs @@ -1,63 +1,107 @@ -use crate::utils::nom::Result; - -mod pack_info; - pub mod color_map; pub mod gamma_table; pub mod model; pub mod pack_file; +mod pack_info; pub mod skybox; pub mod sound; pub mod string_table; -#[derive(Clone, Copy, Debug)] -pub enum Kind { - GammaTable, - ColorMap, - SoundCollection, - StringTable, - Skybox, - Model, -} +use crate::{error::ParseError, utils::nom::FnParser}; +use extension::*; + +/// Definition for all available extensions that the engine can parse. +pub mod extension { + #[sealed::sealed] + pub trait Extension: AsRef + for<'str> TryFrom<&'str str> {} + + #[derive(Debug, thiserror::Error)] + #[error("The provided extension is invalid '{}'", self.0)] + pub struct ExtensionMismatchError(String); + + macro_rules! impl_extension { + ($(#[$docs:meta])+ $name:ident => $ext:literal) => { + $(#[$docs])+ + pub struct $name; + + impl AsRef for $name { + fn as_ref(&self) -> &str { + $ext + } + } + + impl TryFrom<&str> for $name { + type Error = ExtensionMismatchError; -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub enum Extension { - #[default] - Dat, - Custom(String), + fn try_from(value: &str) -> Result { + if value == $ext { + Ok(Self) + } else { + Err(ExtensionMismatchError(value.to_owned())) + } + } + } + + #[sealed::sealed] + impl Extension for $name {} + }; + } + + impl_extension!( + /// Wildcard + Wildcard => "*" + ); + + impl_extension!( + /// Extension that implies that the asset comes from ashen's files (packfile). + Pack => "pack" + ); } -pub trait Asset +pub trait AssetParser where Self: Sized, { - // TODO(Unavailable): Replace `kind()` with: - // - // ``` - // // rename to id? - // fn kind() -> std::any::TypeId { - // std::any::TypeId::of::() - // } - // ``` - // - // the only disadvantage is that `Self` also needs to be `'static` which - // prevents us for implementing `Asset` for `&`-ed types. Right now is not - // clear that we might need that, so I would hold this changes until then. - - /// Returns this Asset's kind. - fn kind() -> Kind; - - /// Tries to parse this `Asset`'s kind. + /// The kind of asset generated by this parser. + /// + /// _Most_ of the time this would be equal to `Self`. See [`Self::Output`] + /// for more details. + type Item = Self; + + /// The final value that would be returned by [`parser`]. /// - /// # Errors + /// DOCS(Unavailable): /// - /// If the `input` is invalid for the provided `extension`. - fn parse(input: &[u8], extension: Extension) -> Result; + /// A hypothetical `TextureCollection` would return `Self` as its output, + /// however its item "kind" would be `Texture`, because that is the overall + /// kind of values that it would hold. + /// + /// [`parser`]: Self::parser + type Output = Self::Item; + + /// Extra information passed down to the parser. + type Context<'ctx> = (); + + /// Generates a new parser with the provided context. + fn parser(context: Self::Context<'_>) -> impl FnParser; } -pub(crate) trait AssetChunk +// For the moment we only have implementaions of `AssetParser` where `Ext = Dat` +// that means that we can trivially generate a `AssetParser` impl without +// too much code boilerplate. +// +// However, this is gonna break coherence once we start adding `Custom` exts. +// Hopefully `negative_impls` will become stable before 2030 :). +impl AssetParser for T where - Self: Sized, + T: AssetParser = ()>, { - fn parse(input: &[u8]) -> Result; + type Context<'ctx> = &'ctx str; + + fn parser(context: Self::Context<'_>) -> impl FnParser { + move |input| match context { + "pack" => >::parser(())(input), + _ => Err(ParseError::unsupported_extension(input, context).into()), + } + } } diff --git a/engine/src/asset/model/dat/frame.rs b/engine/src/asset/model/dat/frame.rs index f418beb..029d28d 100644 --- a/engine/src/asset/model/dat/frame.rs +++ b/engine/src/asset/model/dat/frame.rs @@ -1,4 +1,7 @@ -use crate::{asset::AssetChunk, utils::nom::*}; +use crate::{ + asset::{extension::*, AssetParser}, + utils::nom::*, +}; // TODO(nenikitov): Should probably be a fancy utility class // With generics for data type and dimension @@ -9,102 +12,102 @@ pub struct Vec3 { pub z: f32, } -impl AssetChunk for Vec3 { - fn parse(input: &[u8]) -> Result { - let (input, x) = number::le_i16f16(input)?; - let (input, y) = number::le_i16f16(input)?; - let (input, z) = number::le_i16f16(input)?; - - Ok(( - input, - Self { - x: x.to_num(), - y: y.to_num(), - z: z.to_num(), - }, - )) +impl AssetParser for Vec3 { + fn parser((): Self::Context<'_>) -> impl FnParser { + move |input| { + let (input, x) = number::le_i16f16(input)?; + let (input, y) = number::le_i16f16(input)?; + let (input, z) = number::le_i16f16(input)?; + + Ok(( + input, + Self { + x: x.to_num(), + y: y.to_num(), + z: z.to_num(), + }, + )) + } } } pub struct ModelVertex { - pub x: u8, - pub y: u8, - pub z: u8, + pub x: f32, + pub y: f32, + pub z: f32, // TODO(nenikitov): For now, no clue what this is for - pub light_normal_index: u8, -} - -impl AssetChunk for ModelVertex { - fn parse(input: &[u8]) -> Result { - let (input, x) = number::le_u8(input)?; - let (input, y) = number::le_u8(input)?; - let (input, z) = number::le_u8(input)?; - let (input, light_normal_index) = number::le_u8(input)?; - - Ok(( - input, - Self { - x, - y, - z, - light_normal_index, - }, - )) - } + pub normal_index: u8, } impl ModelVertex { const UNITS_PER_METER: f32 = 32.0; +} + +pub struct VertexTransform { + scale: Vec3, + origin: Vec3, +} - fn to_parsed(&self, scale: &Vec3, scale_origin: &Vec3) -> ModelVertexParsed { +impl AssetParser for ModelVertex { + type Context<'ctx> = VertexTransform; + + fn parser(transform: Self::Context<'_>) -> impl FnParser { macro_rules! transform { ($coordinate: ident) => { - (scale.$coordinate * self.$coordinate as f32 / -256.0 - scale_origin.$coordinate) + (transform.scale.$coordinate * $coordinate as f32 / -256.0 + - transform.origin.$coordinate) / Self::UNITS_PER_METER }; } - ModelVertexParsed { - x: transform!(x), - y: transform!(y), - z: transform!(z), - normal_index: self.light_normal_index, + + move |input| { + let (input, x) = number::le_u8(input)?; + let (input, y) = number::le_u8(input)?; + let (input, z) = number::le_u8(input)?; + let (input, normal_index) = number::le_u8(input)?; + + Ok(( + input, + Self { + x: transform!(x), + y: transform!(y), + z: transform!(z), + normal_index, + }, + )) } } } -pub struct ModelVertexParsed { - pub x: f32, - pub y: f32, - pub z: f32, - pub normal_index: u8, -} - pub struct ModelFrame { pub bounding_sphere_radius: f32, - pub vertices: Vec, + pub vertices: Vec, pub triangle_normal_indexes: Vec, } -impl ModelFrame { - pub fn parse( - vertex_count: usize, - triangle_count: usize, - frame_size: usize, - ) -> impl Fn(&[u8]) -> Result { +pub struct ModelSpecs { + pub vertex_count: u32, + pub triangle_count: u32, + pub frame_size: u32, +} + +impl AssetParser for ModelFrame { + type Context<'ctx> = ModelSpecs; + + fn parser(model_specs: Self::Context<'_>) -> impl FnParser { move |input| { - let (input, scale) = Vec3::parse(input)?; - let (input, scale_origin) = Vec3::parse(input)?; + let (input, scale) = Vec3::parser(())(input)?; + let (input, origin) = Vec3::parser(())(input)?; let (input, bounding_sphere_radius) = number::le_i24f8(input)?; - let (input, vertices) = multi::count!(ModelVertex::parse, vertex_count)(input)?; - let vertices = vertices - .into_iter() - .map(|v| v.to_parsed(&scale, &scale_origin)) - .collect(); + let (input, vertices) = multi::count!( + ModelVertex::parser(VertexTransform { scale, origin }), + model_specs.vertex_count as usize + )(input)?; let (input, triangle_normal_indexes) = - multi::count!(number::le_u8, triangle_count)(input)?; + multi::count!(number::le_u8, model_specs.triangle_count as usize)(input)?; // This ugly formula calculates the padding after the frame data until next frame data // ``` @@ -115,8 +118,12 @@ impl ModelFrame { // - sizeof(vertices) // sizeof(ModelVertex) * vertex_count // - sizeof(triangle_normalindexes) // sizeof(u8) triangle_count // ``` - let (input, _) = - bytes::take(frame_size - 28 - 4 * vertex_count - triangle_count)(input)?; + let (input, _) = bytes::take( + model_specs.frame_size + - 28 + - 4 * model_specs.vertex_count + - model_specs.triangle_count, + )(input)?; Ok(( input, diff --git a/engine/src/asset/model/dat/header.rs b/engine/src/asset/model/dat/header.rs index 5bf02b8..b3991ce 100644 --- a/engine/src/asset/model/dat/header.rs +++ b/engine/src/asset/model/dat/header.rs @@ -1,4 +1,7 @@ -use crate::{asset::AssetChunk, utils::nom::*}; +use crate::{ + asset::{extension::*, AssetParser}, + utils::nom::*, +}; pub struct ModelHeader { pub triangle_count: u32, @@ -15,37 +18,39 @@ pub struct ModelHeader { pub locator_nodes: [u8; 16], } -impl AssetChunk for ModelHeader { - fn parse(input: &[u8]) -> Result { - let (input, triangle_count) = number::le_u32(input)?; - let (input, vertex_count) = number::le_u32(input)?; - let (input, texture_width) = number::le_u32(input)?; - let (input, texture_height) = number::le_u32(input)?; - let (input, frame_count) = number::le_u32(input)?; - let (input, frame_size) = number::le_u32(input)?; - let (input, sequence_count) = number::le_u32(input)?; - let (input, offset_texture) = number::le_u32(input)?; - let (input, offset_triangles) = number::le_u32(input)?; - let (input, offset_frames) = number::le_u32(input)?; - let (input, offset_sequences) = number::le_u32(input)?; - let (input, locator_nodes) = multi::count!(number::le_u8)(input)?; +impl AssetParser for ModelHeader { + fn parser((): Self::Context<'_>) -> impl FnParser { + move |input| { + let (input, triangle_count) = number::le_u32(input)?; + let (input, vertex_count) = number::le_u32(input)?; + let (input, texture_width) = number::le_u32(input)?; + let (input, texture_height) = number::le_u32(input)?; + let (input, frame_count) = number::le_u32(input)?; + let (input, frame_size) = number::le_u32(input)?; + let (input, sequence_count) = number::le_u32(input)?; + let (input, offset_texture) = number::le_u32(input)?; + let (input, offset_triangles) = number::le_u32(input)?; + let (input, offset_frames) = number::le_u32(input)?; + let (input, offset_sequences) = number::le_u32(input)?; + let (input, locator_nodes) = multi::count!(number::le_u8)(input)?; - Ok(( - input, - Self { - triangle_count, - vertex_count, - texture_width, - texture_height, - frame_count, - frame_size, - sequence_count, - offset_texture, - offset_triangles, - offset_frames, - offset_sequences, - locator_nodes, - }, - )) + Ok(( + input, + Self { + triangle_count, + vertex_count, + texture_width, + texture_height, + frame_count, + frame_size, + sequence_count, + offset_texture, + offset_triangles, + offset_frames, + offset_sequences, + locator_nodes, + }, + )) + } } } diff --git a/engine/src/asset/model/dat/sequence.rs b/engine/src/asset/model/dat/sequence.rs index 12fb561..986db7e 100644 --- a/engine/src/asset/model/dat/sequence.rs +++ b/engine/src/asset/model/dat/sequence.rs @@ -1,36 +1,25 @@ -use crate::{asset::AssetChunk, utils::nom::*}; +use crate::{ + asset::{extension::*, AssetParser}, + utils::nom::*, +}; pub struct ModelSequence { - pub frame_count: u32, - pub offset: u32, + pub frames: Vec, } -impl AssetChunk for ModelSequence { - fn parse(input: &[u8]) -> Result { - let (input, frame_count) = number::le_u32(input)?; - let (input, offset) = number::le_u32(input)?; - - Ok(( - input, - Self { - frame_count, - offset, - }, - )) - } -} +impl AssetParser for ModelSequence { + type Context<'ctx> = &'ctx [u8]; -#[derive(Debug)] -pub struct ModelSequenceParsed { - pub frames: Vec, -} + fn parser(full_input: Self::Context<'_>) -> impl FnParser { + move |input| { + let (input, frame_count) = number::le_u32(input)?; + let (input, offset) = number::le_u32(input)?; -impl ModelSequenceParsed { - pub fn parse<'a>(input: &'a [u8], header: &ModelSequence) -> Result<'a, Self> { - let (_, frames) = multi::count!(number::le_u32, header.frame_count as usize)( - &input[header.offset as usize..], - )?; + let (_, frames) = multi::count!(number::le_u32, frame_count as usize)( + &full_input[offset as usize..], + )?; - Ok((&[], Self { frames })) + Ok((input, Self { frames })) + } } } diff --git a/engine/src/asset/model/dat/triangle.rs b/engine/src/asset/model/dat/triangle.rs index 314262c..404b7d0 100644 --- a/engine/src/asset/model/dat/triangle.rs +++ b/engine/src/asset/model/dat/triangle.rs @@ -1,4 +1,7 @@ -use crate::utils::nom::*; +use crate::{ + asset::{extension::*, AssetParser}, + utils::nom::*, +}; pub struct ModelPoint { pub vertex_index: u16, @@ -6,17 +9,24 @@ pub struct ModelPoint { pub v: f32, } -impl ModelPoint { - pub fn parse(texture_width: u32, texture_height: u32) -> impl Fn(&[u8]) -> Result { +pub struct TextureDimensions { + pub width: u32, + pub height: u32, +} + +impl AssetParser for ModelPoint { + type Context<'ctx> = &'ctx TextureDimensions; + + fn parser(texture_dimensions: Self::Context<'_>) -> impl FnParser { move |input| { let (input, vertex_index) = number::le_u16(input)?; let (input, u) = number::le_u16(input)?; - let u = (u as f32 + 0.5) / texture_width as f32; + let u = (u as f32 + 0.5) / texture_dimensions.width as f32; let (input, v) = number::le_u16(input)?; // Y coordinates need to be flipped - let v = 1f32 - (v as f32 + 0.5) / texture_height as f32; + let v = 1f32 - (v as f32 + 0.5) / texture_dimensions.height as f32; Ok((input, Self { vertex_index, u, v })) } @@ -27,11 +37,12 @@ pub struct ModelTriangle { pub points: [ModelPoint; 3], } -impl ModelTriangle { - pub fn parse(texture_width: u32, texture_height: u32) -> impl Fn(&[u8]) -> Result { +impl AssetParser for ModelTriangle { + type Context<'ctx> = TextureDimensions; + + fn parser(texture_dimensions: Self::Context<'_>) -> impl FnParser { move |input| { - let (input, points) = - multi::count!(ModelPoint::parse(texture_width, texture_height))(input)?; + let (input, points) = multi::count!(ModelPoint::parser(&texture_dimensions))(input)?; Ok((input, Self { points })) } diff --git a/engine/src/asset/model/mod.rs b/engine/src/asset/model/mod.rs index 8cf4176..4087eb1 100644 --- a/engine/src/asset/model/mod.rs +++ b/engine/src/asset/model/mod.rs @@ -1,78 +1,67 @@ mod dat; -use super::{Asset, AssetChunk, Extension, Kind}; -use crate::{error, utils::nom::*}; +use super::{extension::*, AssetParser}; +use crate::utils::nom::*; use dat::{ - frame::ModelFrame, - header::ModelHeader, - sequence::{ModelSequence, ModelSequenceParsed}, - triangle::ModelTriangle, + frame::ModelFrame, frame::ModelSpecs, header::ModelHeader, sequence::ModelSequence, + triangle::ModelTriangle, triangle::TextureDimensions, }; - use itertools::Itertools; pub struct Model { pub texture: Vec>, pub triangles: Vec, - pub sequences: Vec, + pub sequences: Vec, pub frames: Vec, } -impl Asset for Model { - fn kind() -> super::Kind { - Kind::Model - } - - fn parse(input: &[u8], extension: Extension) -> Result { - match extension { - Extension::Dat => { - let (_, header) = ModelHeader::parse(input)?; +impl AssetParser for Model { + fn parser((): Self::Context<'_>) -> impl FnParser { + move |input| { + let (_, header) = ModelHeader::parser(())(input)?; - let (_, triangles) = multi::count!( - ModelTriangle::parse(header.texture_width, header.texture_height), - header.triangle_count as usize - )(&input[header.offset_triangles as usize..])?; + let (_, triangles) = multi::count!( + ModelTriangle::parser(TextureDimensions { + width: header.texture_width, + height: header.texture_height + }), + header.triangle_count as usize + )(&input[header.offset_triangles as usize..])?; - let (_, texture) = multi::count!( - number::le_u8, - (header.texture_width * header.texture_height) as usize - )(&input[header.offset_texture as usize..])?; - let texture = texture - .into_iter() - .chunks(header.texture_width as usize) - .into_iter() - .map(Iterator::collect) - .collect(); + let (_, texture) = multi::count!( + number::le_u8, + (header.texture_width * header.texture_height) as usize + )(&input[header.offset_texture as usize..])?; + let texture = texture + .into_iter() + .chunks(header.texture_width as usize) + .into_iter() + .map(Iterator::collect) + .collect(); - let (_, sequences) = multi::count!( - ModelSequence::parse, - header.sequence_count as usize - )(&input[header.offset_sequences as usize..])?; - let sequences = sequences - .into_iter() - .map(|s| ModelSequenceParsed::parse(input, &s).map(|(_, d)| d)) - .collect::, _>>()?; + let (_, sequences) = multi::count!( + ModelSequence::parser(input), + header.sequence_count as usize + )(&input[header.offset_sequences as usize..])?; - let (_, frames) = multi::count!( - ModelFrame::parse( - header.vertex_count as usize, - header.triangle_count as usize, - header.frame_size as usize - ), - header.frame_count as usize - )(&input[header.offset_frames as usize..])?; + let (_, frames) = multi::count!( + ModelFrame::parser(ModelSpecs { + vertex_count: header.vertex_count, + triangle_count: header.triangle_count, + frame_size: header.frame_size + }), + header.frame_count as usize + )(&input[header.offset_frames as usize..])?; - Ok(( - &[0], - Self { - texture, - triangles, - sequences, - frames, - }, - )) - } - _ => Err(error::ParseError::unsupported_extension(input, extension).into()), + Ok(( + &[], + Self { + texture, + triangles, + sequences, + frames, + }, + )) } } } @@ -80,7 +69,7 @@ impl Asset for Model { #[cfg(test)] mod tests { use super::{ - dat::{frame::ModelVertexParsed, triangle::ModelPoint}, + dat::{frame::ModelVertex, triangle::ModelPoint}, *, }; use crate::{ @@ -99,9 +88,9 @@ mod tests { #[test] #[ignore = "uses Ashen ROM files"] fn parse_rom_asset() -> eyre::Result<()> { - let (_, model) = Model::parse(&MODEL_DATA, Extension::Dat)?; + let (_, model) = >::parser(())(&MODEL_DATA)?; let palette = { - let (_, color_map) = ColorMap::parse(&COLOR_MAP_DATA, Extension::Dat)?; + let (_, color_map) = >::parser(())(&COLOR_MAP_DATA)?; color_map.shades[15] }; @@ -242,7 +231,7 @@ object.select_set(True) } } - impl Display for ModelVertexParsed { + impl Display for ModelVertex { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, @@ -272,7 +261,7 @@ object.select_set(True) } } - impl Display for ModelSequenceParsed { + impl Display for ModelSequence { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, diff --git a/engine/src/asset/pack_file.rs b/engine/src/asset/pack_file.rs index b459b1e..996bb44 100644 --- a/engine/src/asset/pack_file.rs +++ b/engine/src/asset/pack_file.rs @@ -86,11 +86,9 @@ impl PackFile { #[cfg(test)] mod tests { - use std::{cell::LazyCell, io, path::PathBuf}; - - use crate::utils::{compression::decompress, test::*}; - use super::*; + use crate::utils::{compression::decompress, test::*}; + use std::{cell::LazyCell, io, path::PathBuf}; #[test] fn header_works() -> eyre::Result<()> { diff --git a/engine/src/asset/pack_info.rs b/engine/src/asset/pack_info.rs index 62fbc15..214fd24 100644 --- a/engine/src/asset/pack_info.rs +++ b/engine/src/asset/pack_info.rs @@ -1,4 +1,4 @@ -use super::AssetChunk; +use super::{extension::*, AssetParser}; use crate::utils::nom::*; use std::ops::Index; @@ -8,17 +8,19 @@ pub struct PackInfo { pub size: u32, } -impl AssetChunk for PackInfo { - fn parse(input: &[u8]) -> Result { - let (input, offset) = number::le_u32(input)?; +impl AssetParser for PackInfo { + fn parser((): Self::Context<'_>) -> impl FnParser { + move |input| { + let (input, offset) = number::le_u32(input)?; - let (input, size) = number::le_u32(input)?; + let (input, size) = number::le_u32(input)?; - let (input, padding) = number::le_u32(input)?; - // TODO(nenikitov): Make it return `Result` - assert_eq!(padding, 0); + let (input, padding) = number::le_u32(input)?; + // TODO(nenikitov): Make it return `Result` + assert_eq!(padding, 0); - Ok((input, PackInfo { offset, size })) + Ok((input, PackInfo { offset, size })) + } } } diff --git a/engine/src/asset/skybox.rs b/engine/src/asset/skybox.rs index b037d77..0808779 100644 --- a/engine/src/asset/skybox.rs +++ b/engine/src/asset/skybox.rs @@ -1,5 +1,5 @@ -use super::{Asset, Extension, Kind}; -use crate::{asset::color_map::Color, error, utils::nom::*}; +use super::{extension::*, AssetParser}; +use crate::{asset::color_map::Color, utils::nom::*}; use itertools::Itertools; const COLOR_COUNT: usize = 256; @@ -10,33 +10,25 @@ pub struct Skybox { pub texture: Vec>, } -impl Asset for Skybox { - fn kind() -> Kind { - Kind::Skybox - } - - fn parse(input: &[u8], extension: Extension) -> crate::utils::nom::Result { - match extension { - Extension::Dat => { - let (input, width) = number::le_u32(input)?; +impl AssetParser for Skybox { + fn parser((): Self::Context<'_>) -> impl FnParser { + move |input| { + let (input, width) = number::le_u32(input)?; - let (input, height) = number::le_u32(input)?; + let (input, height) = number::le_u32(input)?; - let (input, palette) = multi::count!(number::le_u16, 256)(input)?; - let palette: Vec<_> = palette.into_iter().map(Color::from_12_bit).collect(); + let (input, palette) = multi::count!(number::le_u16, 256)(input)?; + let palette: Vec<_> = palette.into_iter().map(Color::from_12_bit).collect(); - let (input, texture) = - multi::count!(number::le_u8, (width * height) as usize)(input)?; - let texture = texture - .into_iter() - .chunks(width as usize) - .into_iter() - .map(Iterator::collect) - .collect(); + let (input, texture) = multi::count!(number::le_u8, (width * height) as usize)(input)?; + let texture = texture + .into_iter() + .chunks(width as usize) + .into_iter() + .map(Iterator::collect) + .collect(); - Ok((&[], Self { palette, texture })) - } - _ => Err(error::ParseError::unsupported_extension(input, extension).into()), + Ok((&[], Self { palette, texture })) } } } @@ -55,7 +47,7 @@ mod tests { #[test] #[ignore = "uses Ashen ROM files"] fn parse_rom_asset() -> eyre::Result<()> { - let (_, skybox) = Skybox::parse(&SKYBOX_DATA, Extension::Dat)?; + let (_, skybox) = >::parser(())(&SKYBOX_DATA)?; output_file( parsed_file_path!("skyboxes/level-1.ppm"), diff --git a/engine/src/asset/sound/dat/asset_header.rs b/engine/src/asset/sound/dat/asset_header.rs index 5c89ee8..02e201d 100644 --- a/engine/src/asset/sound/dat/asset_header.rs +++ b/engine/src/asset/sound/dat/asset_header.rs @@ -1,5 +1,5 @@ use crate::{ - asset::{pack_info::PackInfo, AssetChunk}, + asset::{extension::*, pack_info::PackInfo, AssetParser}, utils::nom::*, }; @@ -14,23 +14,25 @@ impl SoundAssetHeader { const HEADER: &'static str = "TSND"; } -impl AssetChunk for SoundAssetHeader { - fn parse(input: &[u8]) -> Result { - let (input, _) = bytes::tag(Self::HEADER)(input)?; +impl AssetParser for SoundAssetHeader { + fn parser((): Self::Context<'_>) -> impl FnParser { + move |input| { + let (input, _) = bytes::tag(Self::HEADER)(input)?; - let (input, songs) = PackInfo::parse(input)?; - let (input, effects) = PackInfo::parse(input)?; - let (input, emitters) = PackInfo::parse(input)?; - let (input, maps) = PackInfo::parse(input)?; + let (input, songs) = PackInfo::parser(())(input)?; + let (input, effects) = PackInfo::parser(())(input)?; + let (input, emitters) = PackInfo::parser(())(input)?; + let (input, maps) = PackInfo::parser(())(input)?; - Ok(( - input, - Self { - songs, - effects, - emitters, - maps, - }, - )) + Ok(( + input, + Self { + songs, + effects, + emitters, + maps, + }, + )) + } } } diff --git a/engine/src/asset/sound/dat/chunk_header.rs b/engine/src/asset/sound/dat/chunk_header.rs index 4372e95..f9588c2 100644 --- a/engine/src/asset/sound/dat/chunk_header.rs +++ b/engine/src/asset/sound/dat/chunk_header.rs @@ -1,5 +1,5 @@ use crate::{ - asset::{pack_info::PackInfo, AssetChunk}, + asset::{extension::*, pack_info::PackInfo, AssetParser}, utils::nom::*, }; @@ -7,11 +7,13 @@ pub struct SoundChunkHeader { pub infos: Vec, } -impl AssetChunk for SoundChunkHeader { - fn parse(input: &[u8]) -> Result { - let (input, count) = number::le_u32(input)?; - let (input, infos) = multi::count!(PackInfo::parse, count as usize)(input)?; +impl AssetParser for SoundChunkHeader { + fn parser((): Self::Context<'_>) -> impl FnParser { + move |input| { + let (input, count) = number::le_u32(input)?; + let (input, infos) = multi::count!(PackInfo::parser(()), count as usize)(input)?; - Ok((input, Self { infos })) + Ok((input, Self { infos })) + } } } diff --git a/engine/src/asset/sound/dat/mod.rs b/engine/src/asset/sound/dat/mod.rs index 6e632de..2b19080 100644 --- a/engine/src/asset/sound/dat/mod.rs +++ b/engine/src/asset/sound/dat/mod.rs @@ -1,6 +1,5 @@ -use std::io::Cursor; - use lewton::inside_ogg::OggStreamReader; +use std::io::Cursor; pub mod asset_header; pub mod chunk_header; diff --git a/engine/src/asset/sound/dat/t_effect.rs b/engine/src/asset/sound/dat/t_effect.rs index 8ea403a..1cfb91a 100644 --- a/engine/src/asset/sound/dat/t_effect.rs +++ b/engine/src/asset/sound/dat/t_effect.rs @@ -1,17 +1,16 @@ -use crate::{ - asset::{sound::dat::t_instrument::TSampleParsed, AssetChunk}, - utils::nom::*, -}; - use super::{ mixer::Mixer, t_instrument::{TInstrument, TSample}, uncompress, }; +use crate::{ + asset::{extension::*, AssetParser}, + utils::nom::*, +}; pub struct TEffect { instrument: TInstrument, - sample: TSampleParsed, + sample: TSample, } // It should be separated @@ -23,23 +22,18 @@ impl TEffect { } } -impl AssetChunk for TEffect { - fn parse(input: &[u8]) -> crate::utils::nom::Result { - let (_, pointers) = TEffectPointers::parse(input)?; - - let (_, instrument) = TInstrument::parse(&input[pointers.instrument as usize..])?; +impl AssetParser for TEffect { + fn parser((): Self::Context<'_>) -> impl FnParser { + move |input| { + let (_, pointers) = TEffectPointers::parser(())(input)?; - let sample = { - let data = uncompress(&input[pointers.sample_data as usize..]); - let (_, sample) = TSample::parse(&input[pointers.sample as usize..])?; + let (_, instrument) = TInstrument::parser(())(&input[pointers.instrument as usize..])?; - TSampleParsed::parse( - &sample, - &data[sample.sample as usize..sample.loop_end as usize], - ) - }; + let sample = uncompress(&input[pointers.sample_data as usize..]); + let (_, sample) = TSample::parser(&sample)(&input[pointers.sample as usize..])?; - Ok((&[], Self { instrument, sample })) + Ok((&[], Self { instrument, sample })) + } } } @@ -50,19 +44,21 @@ struct TEffectPointers { sample_data: u32, } -impl AssetChunk for TEffectPointers { - fn parse(input: &[u8]) -> crate::utils::nom::Result { - let (input, instrument) = number::le_u32(input)?; - let (input, sample) = number::le_u32(input)?; - let (input, sample_data) = number::le_u32(input)?; +impl AssetParser for TEffectPointers { + fn parser((): Self::Context<'_>) -> impl FnParser { + move |input| { + let (input, instrument) = number::le_u32(input)?; + let (input, sample) = number::le_u32(input)?; + let (input, sample_data) = number::le_u32(input)?; - Ok(( - input, - Self { - instrument, - sample, - sample_data, - }, - )) + Ok(( + input, + Self { + instrument, + sample, + sample_data, + }, + )) + } } } diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index 634597f..2c49e58 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -1,4 +1,7 @@ -use crate::{asset::AssetChunk, utils::nom::*}; +use crate::{ + asset::{extension::*, AssetParser}, + utils::nom::*, +}; #[derive(Debug)] pub struct TInstrument { @@ -26,102 +29,64 @@ pub struct TInstrument { pub samples: Box<[u8; 96]>, } -impl AssetChunk for TInstrument { - fn parse(input: &[u8]) -> Result { - let (input, flags) = number::le_u8(input)?; - - let (input, _) = bytes::take(1usize)(input)?; - - let (input, volume_begin) = number::le_u16(input)?; - let (input, volume_end) = number::le_u16(input)?; - let (input, volume_sustain) = number::le_u16(input)?; - let (input, volume_envelope_border) = number::le_u16(input)?; - let (input, volume_envelope) = multi::count!(number::le_u8)(input)?; - - let (input, pan_begin) = number::le_u16(input)?; - let (input, pan_end) = number::le_u16(input)?; - let (input, pan_sustain) = number::le_u16(input)?; - let (input, pan_envelope_border) = number::le_u16(input)?; - let (input, pan_envelope) = multi::count!(number::le_u8)(input)?; - - let (input, _) = bytes::take(1usize)(input)?; - - let (input, vibrato_depth) = number::le_u8(input)?; - let (input, vibrato_speed) = number::le_u8(input)?; - let (input, vibrato_sweep) = number::le_u8(input)?; - - let (input, fadeout) = number::le_u32(input)?; - let (input, vibrato_table) = number::le_u32(input)?; - - let (input, samples) = multi::count!(number::le_u8)(input)?; - - Ok(( - input, - Self { - flags, - volume_begin, - volume_end, - volume_sustain, - volume_envelope_border, - volume_envelope: Box::new(volume_envelope), - pan_begin, - pan_end, - pan_sustain, - pan_envelope_border, - pan_envelope: Box::new(pan_envelope), - vibrato_depth, - vibrato_speed, - vibrato_sweep, - fadeout, - vibrato_table, - samples: Box::new(samples), - }, - )) +impl AssetParser for TInstrument { + fn parser((): Self::Context<'_>) -> impl FnParser { + move |input| { + let (input, flags) = number::le_u8(input)?; + + let (input, _) = bytes::take(1usize)(input)?; + + let (input, volume_begin) = number::le_u16(input)?; + let (input, volume_end) = number::le_u16(input)?; + let (input, volume_sustain) = number::le_u16(input)?; + let (input, volume_envelope_border) = number::le_u16(input)?; + let (input, volume_envelope) = multi::count!(number::le_u8)(input)?; + + let (input, pan_begin) = number::le_u16(input)?; + let (input, pan_end) = number::le_u16(input)?; + let (input, pan_sustain) = number::le_u16(input)?; + let (input, pan_envelope_border) = number::le_u16(input)?; + let (input, pan_envelope) = multi::count!(number::le_u8)(input)?; + + let (input, _) = bytes::take(1usize)(input)?; + + let (input, vibrato_depth) = number::le_u8(input)?; + let (input, vibrato_speed) = number::le_u8(input)?; + let (input, vibrato_sweep) = number::le_u8(input)?; + + let (input, fadeout) = number::le_u32(input)?; + let (input, vibrato_table) = number::le_u32(input)?; + + let (input, samples) = multi::count!(number::le_u8)(input)?; + + Ok(( + input, + Self { + flags, + volume_begin, + volume_end, + volume_sustain, + volume_envelope_border, + volume_envelope: Box::new(volume_envelope), + pan_begin, + pan_end, + pan_sustain, + pan_envelope_border, + pan_envelope: Box::new(pan_envelope), + vibrato_depth, + vibrato_speed, + vibrato_sweep, + fadeout, + vibrato_table, + samples: Box::new(samples), + }, + )) + } } } #[derive(Debug)] pub struct TSample { - pub flags: u8, - pub volume: u8, - pub panning: u8, - pub align: u8, - pub finetune: u32, - pub loop_length: u32, - pub loop_end: u32, - pub sample: u32, -} - -impl AssetChunk for TSample { - fn parse(input: &[u8]) -> Result { - let (input, flags) = number::le_u8(input)?; - let (input, volume) = number::le_u8(input)?; - let (input, panning) = number::le_u8(input)?; - let (input, align) = number::le_u8(input)?; - let (input, finetune) = number::le_u32(input)?; - let (input, loop_length) = number::le_u32(input)?; - let (input, loop_end) = number::le_u32(input)?; - let (input, sample) = number::le_u32(input)?; - - Ok(( - input, - Self { - flags, - volume, - panning, - align, - finetune, - loop_length, - // The game uses offset for `i16`, but it's much more conventient to just use indeces - loop_end: loop_end / 2, - sample: sample / 2, - }, - )) - } -} - -#[derive(Debug)] -pub struct TSampleParsed { pub flags: u8, pub volume: u8, pub panning: u8, @@ -131,19 +96,41 @@ pub struct TSampleParsed { pub data: Vec, } -impl TSampleParsed { - pub fn parse(header: &TSample, sample_data: &[i16]) -> Self { - Self { - flags: header.flags, - volume: header.volume, - panning: header.panning, - align: header.align, - finetune: header.finetune, - loop_length: header.loop_length, - data: sample_data.to_vec(), +impl AssetParser for TSample { + type Context<'ctx> = &'ctx [i16]; + + fn parser(sample_data: Self::Context<'_>) -> impl FnParser { + move |input| { + let (input, flags) = number::le_u8(input)?; + let (input, volume) = number::le_u8(input)?; + let (input, panning) = number::le_u8(input)?; + let (input, align) = number::le_u8(input)?; + let (input, finetune) = number::le_u32(input)?; + let (input, loop_length) = number::le_u32(input)?; + let (input, loop_end) = number::le_u32(input)?; + let (input, sample_offset) = number::le_u32(input)?; + + // The game uses offset for `i16`, but it's much more conventient to just use indeces + let loop_end = loop_end / 2; + let sample_offset = sample_offset / 2; + + Ok(( + input, + Self { + flags, + volume, + panning, + align, + finetune, + loop_length, + data: sample_data[sample_offset as usize..loop_end as usize].to_vec(), + }, + )) } } +} +impl TSample { pub fn sample_full(&self) -> &[i16] { &self.data } diff --git a/engine/src/asset/sound/dat/t_song.rs b/engine/src/asset/sound/dat/t_song.rs index a38fe55..c5a24b0 100644 --- a/engine/src/asset/sound/dat/t_song.rs +++ b/engine/src/asset/sound/dat/t_song.rs @@ -1,11 +1,10 @@ +use super::{mixer::SoundEffect, t_instrument::*, uncompress}; use crate::{ - asset::{sound::dat::mixer::Mixer, AssetChunk}, + asset::{extension::*, sound::dat::mixer::Mixer, AssetParser}, utils::nom::*, }; use itertools::Itertools; -use super::{mixer::SoundEffect, t_instrument::*, uncompress}; - #[derive(Debug)] pub struct TSong { bpm: u8, @@ -15,7 +14,7 @@ pub struct TSong { /// Reusable and repeatable sequence -> Row -> Channel (`None` to play nothing) patterns: Vec>>>, instruments: Vec, - samples: Vec, + samples: Vec, } impl TSong { @@ -56,83 +55,75 @@ impl TSong { } } -impl AssetChunk for TSong { - fn parse(input: &[u8]) -> Result { - let (header, pointers) = { - let (input, header) = TSongHeader::parse(input)?; - let (input, pointers) = TSongPointers::parse(input)?; - (header, pointers) - }; +impl AssetParser for TSong { + fn parser((): Self::Context<'_>) -> impl FnParser { + move |input| { + let (header, pointers) = { + let (input, header) = TSongHeader::parser(())(input)?; + let (input, pointers) = TSongPointers::parser(())(input)?; + (header, pointers) + }; - let (_, orders) = multi::count!(number::le_u8, header.song_length as usize)( - &input[pointers.orders as usize..], - )?; - - let patterns: Vec<_> = { - let (_, lengths) = multi::count!(number::le_u8, header.pattern_count as usize)( - &input[pointers.pattern_lengths as usize..], + let (_, orders) = multi::count!(number::le_u8, header.song_length as usize)( + &input[pointers.orders as usize..], )?; - multi::count!(number::le_u32, header.pattern_count as usize)( - &input[pointers.patterns as usize..], - )? - .1 - .into_iter() - .map(|position| position + pointers.pattern_data) - .map(|position| &input[position as usize..]) - .zip(lengths) - .map(|(input, length)| { - multi::count!( - >::parse, - header.channel_count as usize * length as usize - )(input) - }) - .map(|patterns| patterns.map(|(_, p)| p)) - .map(|patterns| { - patterns.map(|p| -> Vec> { - p.into_iter() - .chunks(header.channel_count as usize) - .into_iter() - .map(Iterator::collect) - .collect() - }) - }) - .collect::>()? - }; + let patterns: Vec<_> = { + let (_, lengths) = multi::count!(number::le_u8, header.pattern_count as usize)( + &input[pointers.pattern_lengths as usize..], + )?; - let (_, instruments) = multi::count!(TInstrument::parse, header.instrument_count as usize)( - &input[pointers.instruments as usize..], - )?; + multi::count!(number::le_u32, header.pattern_count as usize)( + &input[pointers.patterns as usize..], + )? + .1 + .into_iter() + .map(|position| position + pointers.pattern_data) + .map(|position| &input[position as usize..]) + .zip(lengths) + .map(|(input, length)| { + multi::count!( + >::parser(()), + header.channel_count as usize * length as usize + )(input) + }) + .map(|patterns| patterns.map(|(_, p)| p)) + .map(|patterns| { + patterns.map(|p| -> Vec> { + p.into_iter() + .chunks(header.channel_count as usize) + .into_iter() + .map(Iterator::collect) + .collect() + }) + }) + .collect::>()? + }; - let samples: Vec<_> = { - let data = uncompress(&input[pointers.sample_data as usize..]); + let (_, instruments) = multi::count!( + TInstrument::parser(()), + header.instrument_count as usize + )(&input[pointers.instruments as usize..])?; - multi::count!(TSample::parse, header.sample_count as usize)( - &input[pointers.samples as usize..], - )? - .1 - .into_iter() - .map(|sample| { - TSampleParsed::parse( - &sample, - &data[sample.sample as usize..sample.loop_end as usize], - ) - }) - .collect() - }; + let samples = uncompress(&input[pointers.sample_data as usize..]); + let (_, samples) = multi::count!( + TSample::parser(&samples), + header.sample_count as usize + )(&input[pointers.samples as usize..])?; - Ok(( - input, - Self { - bpm: header.bpm, - speed: header.speed, - restart_order: header.restart_order, - orders, - patterns, - instruments, - samples, - }, - )) + Ok(( + input, + Self { + bpm: header.bpm, + speed: header.speed, + restart_order: header.restart_order, + orders, + patterns, + instruments, + samples, + }, + )) + } } } @@ -148,30 +139,32 @@ struct TSongHeader { bpm: u8, } -impl AssetChunk for TSongHeader { - fn parse(input: &[u8]) -> Result { - let (input, song_length) = number::le_u8(input)?; - let (input, restart_order) = number::le_u8(input)?; - let (input, channel_count) = number::le_u8(input)?; - let (input, pattern_count) = number::le_u8(input)?; - let (input, instrument_count) = number::le_u8(input)?; - let (input, sample_count) = number::le_u8(input)?; - let (input, speed) = number::le_u8(input)?; - let (input, bpm) = number::le_u8(input)?; +impl AssetParser for TSongHeader { + fn parser((): Self::Context<'_>) -> impl FnParser { + move |input| { + let (input, song_length) = number::le_u8(input)?; + let (input, restart_order) = number::le_u8(input)?; + let (input, channel_count) = number::le_u8(input)?; + let (input, pattern_count) = number::le_u8(input)?; + let (input, instrument_count) = number::le_u8(input)?; + let (input, sample_count) = number::le_u8(input)?; + let (input, speed) = number::le_u8(input)?; + let (input, bpm) = number::le_u8(input)?; - Ok(( - input, - Self { - song_length, - restart_order, - channel_count, - pattern_count, - instrument_count, - sample_count, - speed, - bpm, - }, - )) + Ok(( + input, + Self { + song_length, + restart_order, + channel_count, + pattern_count, + instrument_count, + sample_count, + speed, + bpm, + }, + )) + } } } @@ -186,28 +179,30 @@ struct TSongPointers { sample_data: u32, } -impl AssetChunk for TSongPointers { - fn parse(input: &[u8]) -> Result { - let (input, orders) = number::le_u32(input)?; - let (input, patterns) = number::le_u32(input)?; - let (input, pattern_lengths) = number::le_u32(input)?; - let (input, pattern_data) = number::le_u32(input)?; - let (input, instruments) = number::le_u32(input)?; - let (input, samples) = number::le_u32(input)?; - let (input, sample_data) = number::le_u32(input)?; +impl AssetParser for TSongPointers { + fn parser((): Self::Context<'_>) -> impl FnParser { + move |input| { + let (input, orders) = number::le_u32(input)?; + let (input, patterns) = number::le_u32(input)?; + let (input, pattern_lengths) = number::le_u32(input)?; + let (input, pattern_data) = number::le_u32(input)?; + let (input, instruments) = number::le_u32(input)?; + let (input, samples) = number::le_u32(input)?; + let (input, sample_data) = number::le_u32(input)?; - Ok(( - input, - Self { - orders, - patterns, - pattern_lengths, - pattern_data, - instruments, - samples, - sample_data, - }, - )) + Ok(( + input, + Self { + orders, + patterns, + pattern_lengths, + pattern_data, + instruments, + samples, + sample_data, + }, + )) + } } } @@ -221,37 +216,41 @@ struct TPattern { effect_2: u16, } -impl AssetChunk for TPattern { - fn parse(input: &[u8]) -> Result { - let (input, flags) = number::le_u8(input)?; - let (input, note) = number::le_u8(input)?; - let (input, instrument) = number::le_u8(input)?; - let (input, volume) = number::le_u8(input)?; - let (input, effect_1) = number::le_u16(input)?; - let (input, effect_2) = number::le_u16(input)?; +impl AssetParser for TPattern { + fn parser((): Self::Context<'_>) -> impl FnParser { + move |input| { + let (input, flags) = number::le_u8(input)?; + let (input, note) = number::le_u8(input)?; + let (input, instrument) = number::le_u8(input)?; + let (input, volume) = number::le_u8(input)?; + let (input, effect_1) = number::le_u16(input)?; + let (input, effect_2) = number::le_u16(input)?; - Ok(( - input, - Self { - flags, - note: (note != 0).then_some(note), - instrument, - volume, - effect_1, - effect_2, - }, - )) + Ok(( + input, + Self { + flags, + note: (note != 0).then_some(note), + instrument, + volume, + effect_1, + effect_2, + }, + )) + } } } -impl AssetChunk for Option { - fn parse(input: &[u8]) -> Result { - let (after_flags, flags) = number::le_u8(input)?; - if (flags & 0x20) != 0 { - Ok((after_flags, None)) - } else { - let (input, pattern) = TPattern::parse(input)?; - Ok((input, Some(pattern))) +impl AssetParser for Option { + fn parser((): Self::Context<'_>) -> impl FnParser { + move |input| { + let (after_flags, flags) = number::le_u8(input)?; + if (flags & 0x20) != 0 { + Ok((after_flags, None)) + } else { + let (input, pattern) = TPattern::parser(())(input)?; + Ok((input, Some(pattern))) + } } } } diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 4f9c08d..e51e6ab 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -1,13 +1,11 @@ mod dat; -use self::dat::t_effect::TEffect; - -use super::{Asset, AssetChunk, Extension, Kind}; +use super::{extension::*, AssetParser}; use crate::{ asset::sound::dat::{ - asset_header::SoundAssetHeader, chunk_header::SoundChunkHeader, t_song::TSong, + asset_header::SoundAssetHeader, chunk_header::SoundChunkHeader, t_effect::TEffect, + t_song::TSong, }, - error, utils::{compression::decompress, nom::*}, }; @@ -21,35 +19,28 @@ impl SoundAssetCollection { const CHANNEL_COUNT: usize = 1; } -impl Asset for SoundAssetCollection { - fn kind() -> Kind { - Kind::SoundCollection - } - - fn parse(input: &[u8], extension: Extension) -> Result { - match extension { - Extension::Dat => { - let (_, header) = SoundAssetHeader::parse(input)?; - - let (_, songs) = SoundChunkHeader::parse(&input[header.songs])?; - let songs = songs - .infos - .into_iter() - .map(|s| decompress(&input[s])) - .map(|s| TSong::parse(s.as_slice()).map(|(_, d)| d)) - .collect::, _>>()?; - - let (_, effects) = SoundChunkHeader::parse(&input[header.effects])?; - let effects = effects - .infos - .into_iter() - .map(|s| decompress(&input[s])) - .map(|s| TEffect::parse(s.as_slice()).map(|(_, d)| d)) - .collect::, _>>()?; - - Ok((&[], SoundAssetCollection { songs, effects })) - } - _ => Err(error::ParseError::unsupported_extension(input, extension).into()), +impl AssetParser for SoundAssetCollection { + fn parser((): Self::Context<'_>) -> impl FnParser { + move |input| { + let (_, header) = SoundAssetHeader::parser(())(input)?; + + let (_, songs) = SoundChunkHeader::parser(())(&input[header.songs])?; + let songs = songs + .infos + .into_iter() + .map(|s| decompress(&input[s])) + .map(|s| TSong::parser(())(s.as_slice()).map(|(_, d)| d)) + .collect::, _>>()?; + + let (_, effects) = SoundChunkHeader::parser(())(&input[header.effects])?; + let effects = effects + .infos + .into_iter() + .map(|s| decompress(&input[s])) + .map(|s| TEffect::parser(())(s.as_slice()).map(|(_, d)| d)) + .collect::, _>>()?; + + Ok((&[], SoundAssetCollection { songs, effects })) } } } @@ -65,7 +56,7 @@ mod tests { #[test] #[ignore = "uses Ashen ROM files"] fn parse_rom_asset() -> eyre::Result<()> { - let (_, asset) = SoundAssetCollection::parse(&SOUND_DATA, Extension::Dat)?; + let (_, asset) = >::parser(())(&SOUND_DATA)?; let output_dir = PathBuf::from(parsed_file_path!("sounds/songs/")); diff --git a/engine/src/asset/string_table.rs b/engine/src/asset/string_table.rs index 60919a1..4854a91 100644 --- a/engine/src/asset/string_table.rs +++ b/engine/src/asset/string_table.rs @@ -1,5 +1,5 @@ -use super::{Asset, Extension, Kind}; -use crate::{error, utils::nom::*}; +use super::{extension::*, AssetParser}; +use crate::utils::nom::*; pub struct StringTable { table: Vec, @@ -14,21 +14,14 @@ fn utf_16_string(input: &[u8]) -> Result { }) } -impl Asset for StringTable { - fn kind() -> Kind { - Kind::StringTable - } - - fn parse(input: &[u8], extension: super::Extension) -> Result { - match extension { - Extension::Dat => { - let (input, count) = number::le_u32(input)?; - // TODO(Unavailable): Find out what the "catholic" characters are. - let (input, table) = multi::count!(utf_16_string, count as usize)(input)?; +impl AssetParser for StringTable { + fn parser((): Self::Context<'_>) -> impl FnParser { + move |input| { + let (input, count) = number::le_u32(input)?; + // TODO(Unavailable): Find out what the "catholic" characters are. + let (input, table) = multi::count!(utf_16_string, count as usize)(input)?; - Ok((input, StringTable { table })) - } - _ => Err(error::ParseError::unsupported_extension(input, extension).into()), + Ok((input, StringTable { table })) } } } @@ -44,7 +37,7 @@ mod tests { #[test] #[ignore = "uses Ashen ROM files"] fn parse_rom_asset() -> eyre::Result<()> { - let (_, string_table) = StringTable::parse(&STRING_TABLE_DATA, Extension::Dat)?; + let (_, string_table) = >::parser(())(&STRING_TABLE_DATA)?; output_file( parsed_file_path!("strings/english-uk.txt"), diff --git a/engine/src/directory.rs b/engine/src/directory.rs index 5f63548..75091a4 100644 --- a/engine/src/directory.rs +++ b/engine/src/directory.rs @@ -1,7 +1,5 @@ -use crate::asset::Asset; - -pub trait Directory { - // TODO(nenikitov): Return `Option` or `Result`. - fn get(&self, id: &str) -> A; - fn get_all(&self) -> Vec; -} +// pub trait Directory { +// // TODO(nenikitov): Return `Option` or `Result`. +// fn get(&self, id: &str) -> A; +// fn get_all(&self) -> Vec; +// } diff --git a/engine/src/error.rs b/engine/src/error.rs index 5ba9707..8d77a62 100644 --- a/engine/src/error.rs +++ b/engine/src/error.rs @@ -1,4 +1,4 @@ -use crate::{asset::Extension, utils::nom::Input}; +use crate::utils::nom::Input; use nom::error::ErrorKind as NomErrorKind; use std::num::NonZeroUsize; @@ -26,13 +26,16 @@ pub struct ParseError { impl ParseError { /// Creates a new `ParseError` with a kind of `UnsupportedExtension`. - pub(crate) fn unsupported_extension(bytes: B, unsupported: Extension) -> Self + pub(crate) fn unsupported_extension(bytes: B, unsupported: I) -> Self where + I: Into, B: AsRef<[u8]>, { Self { bytes: bytes.as_ref().to_vec().into_boxed_slice(), - kind: ErrorKind::UnsupportedExtension { unsupported }, + kind: ErrorKind::UnsupportedExtension { + unsupported: Into::into(unsupported), + }, custom: None, } } @@ -53,7 +56,7 @@ impl ParseError { pub enum ErrorKind { /// The requested extension is not supported. UnsupportedExtension { - unsupported: Extension, + unsupported: String, /* TODO(Unavailable): supported: Box<[Extension]> */ }, /// Not enough bytes where provided to parse the `Asset`. diff --git a/engine/src/lib.rs b/engine/src/lib.rs index b2b9bfe..2705f9b 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -4,7 +4,11 @@ clippy::default_trait_access, clippy::module_name_repetitions, clippy::must_use_candidate, - clippy::wildcard_imports + clippy::wildcard_imports, + // TODO(Unavailable): Consider reactivating these. + clippy::cast_lossless, + clippy::cast_possible_truncation, + clippy::cast_precision_loss, )] #![warn(unused_imports)] #![feature( @@ -13,7 +17,9 @@ maybe_uninit_uninit_array_transpose, let_chains, lazy_cell, - io_error_more + io_error_more, + associated_type_defaults, + trait_alias, )] pub mod asset; diff --git a/engine/src/utils/compression.rs b/engine/src/utils/compression.rs index 13f2f28..71b90b7 100644 --- a/engine/src/utils/compression.rs +++ b/engine/src/utils/compression.rs @@ -1,6 +1,5 @@ -use std::io::Read; - use flate2::bufread::ZlibDecoder; +use std::io::Read; // TODO(nenikitov): Return `Result` pub fn decompress(bytes: &[u8]) -> Vec { diff --git a/engine/src/utils/nom.rs b/engine/src/utils/nom.rs index 1c28390..6eeed85 100644 --- a/engine/src/utils/nom.rs +++ b/engine/src/utils/nom.rs @@ -55,7 +55,7 @@ pub mod multi { /// array with a inferred `N` (count) would be returned e.g: /// /// ```ignore - /// use engine::utils::nom::{Result, multi, number}; + /// use crate::utils::nom::{Result, multi, number}; /// /// fn parse_u32s(input: &[u8]) -> Result<[u32; COUNT]> { /// // N is infered by the function return type ([u32; COUNT]). @@ -138,3 +138,6 @@ impl<'a, T, O> Parser<'a, O> for T where T: nom::Parser, O, crate::err /// Holds the result of parsing functions. pub type Result<'a, O> = nom::IResult, O, crate::error::ParseError>; + +// DOCS(Unavailable): +pub trait FnParser = Fn(Input) -> Result; From 21881a8558b1dd78505801750147279f27bab7db Mon Sep 17 00:00:00 2001 From: Mykyta Onipchenko <44075969+nenikitov@users.noreply.github.com> Date: Mon, 22 Jan 2024 20:06:49 -0500 Subject: [PATCH 08/14] chore(rustup): add overwrite toolchain version (#42) --- rust-toolchain.toml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 rust-toolchain.toml diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..b295b1d --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly" +components = [ "clippy", "miri" ] From 98ebcb236e31584cceaf62d7c67924414199a4e3 Mon Sep 17 00:00:00 2001 From: Mykyta Onipchenko <44075969+nenikitov@users.noreply.github.com> Date: Mon, 22 Jan 2024 20:11:34 -0500 Subject: [PATCH 09/14] chore: update readme with roadmap (#41) --- README.md | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 046c5a8..018b8ad 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ -# Ashen source port +# Ashen reverse engineering -A playable source port of an obscure NGage game Ashen +Reverse engineering of an N-Gage game [Ashen](https://en.wikipedia.org/wiki/Ashen_(2004_video_game)) +- Can extract game resources ## Usage @@ -31,13 +32,67 @@ File parsing is in test suite only, for now. cargo test --release -- --ignored parse_rom_asset ``` - This will parse select few game files into `output/parsed/` directory + - Directions on how to opened parsed files are available [here](#file-formats-and-extraction) + +## Roadmap + +### File formats and extraction + +- [x] Gamma table + - **Purpose** + - Look up table for brightness when changing gamma in settings + - **Output format** + - PPM image +- [x] Color map + - **Purpose** + - Look up table for 12-bit RGB colors, because textures are stored in 256 indexed color format + - There is also ghost version for when you use Ghost Vision Goggles in game + - There are unique color maps for monsters, pick-ups (and some other models), UI sprites, and levels + - **Output format** + - PPM image +- [x] Model + - **Purpose** + - Texture, geometry, and model data + - **TO DO** + - Figure out what to do with pre-calculated face and vertex normals + - **Output format** + - Blender Python script that can be executed with + ```sh + blender -P /path/to/script.py + ``` + - Automatically sets up Cycles material and animations as shape keys in dope sheet's shape key editor +- [x] Sky + - **Purpose** + - Color map and skybox texture + - **Output format** + - PPM image +- [ ] Skins +- [ ] Level geometry +- [ ] Level collision +- [ ] Level waypoints +- [ ] Sprite +- [x] Texture + - **Purpose** + - Texture info - offsets, texture dimensions + - Texture data - color indeces + - **TO DO** + - Re-integrate output + - **Output format** + - PPM image +- [x] String table + - **Purpose** + - Printable strings in UI + - **TO DO** + - Figure out weird non-text characters (probably for controlling in-game events) + - **Output format** + - Plain text ## File structure > [!IMPORTANT] > > I use the packfile that comes with Ashen 1.06. -> Your packfile may have different offsets to files, I didn't test with different versions. +> Your packfile may have different order, I didn't test with different versions. | Address (HEX) | Asset | |---------------|---------------------------------| From 7ff97f0741a889abbe34e056c6b75bbd04f1004f Mon Sep 17 00:00:00 2001 From: Mykyta Onipchenko <44075969+nenikitov@users.noreply.github.com> Date: Tue, 23 Jan 2024 11:03:40 -0500 Subject: [PATCH 10/14] chore: update readme with roadmap for sound effects (#44) --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 018b8ad..b68e48d 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,16 @@ File parsing is in test suite only, for now. - Re-integrate output - **Output format** - PPM image +- [x] Music and sound effects + - **Purpose** + - Sound effects + - Music + - **TO DO** + - Improve mixer with pitch, pan, and other sound effects + - Support sustained instruments + - Set correct tempo + - **Output format** + - WAV audio file - [x] String table - **Purpose** - Printable strings in UI From 210759d011271e56c5cf05e0fb8d31de09ecad5a Mon Sep 17 00:00:00 2001 From: Mykyta Onipchenko <44075969+nenikitov@users.noreply.github.com> Date: Wed, 24 Jan 2024 13:42:26 -0500 Subject: [PATCH 11/14] refactor(test): output `png`s instead of `ppm` images. Windows image viewer doesn't support PPM... * ci: use `direct-minimal-versions` for `minimal versions` action. --- .github/workflows/test.yml | 8 +- Cargo.lock | 369 ++++++++++++++++++++++++++++++++ README.md | 8 +- engine/Cargo.toml | 1 + engine/src/asset/color_map.rs | 4 +- engine/src/asset/gamma_table.rs | 2 +- engine/src/asset/skybox.rs | 4 +- engine/src/utils/format.rs | 43 ++-- engine/src/utils/mod.rs | 1 + 9 files changed, 410 insertions(+), 30 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0657cf0..4f77990 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -58,17 +58,17 @@ jobs: SKIP_TEST_WRITE: true minimal: runs-on: ubuntu-latest - name: ubuntu / nightly / minimal-versions + name: ubuntu / nightly / direct-minimal-versions steps: - uses: actions/checkout@v4 with: submodules: true - - name: Install nightly for -Zminimal-versions + - name: Install nightly for -Z direct-minimal-versions uses: dtolnay/rust-toolchain@nightly - name: rustup default nightly run: rustup default nightly - - name: cargo update -Zminimal-versions - run: cargo update -Zminimal-versions + - name: cargo update -Z direct-minimal-versions + run: cargo update -Z direct-minimal-versions - name: cargo test run: cargo test --locked --all-features --all-targets os-check: diff --git a/Cargo.lock b/Cargo.lock index 93e230f..0640e2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,12 +24,36 @@ dependencies = [ "engine", ] +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "az" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + [[package]] name = "bytemuck" version = "1.14.0" @@ -57,6 +81,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "const_format" version = "0.2.32" @@ -86,6 +116,31 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + [[package]] name = "crunchy" version = "0.2.2" @@ -106,6 +161,7 @@ dependencies = [ "eyre", "fixed", "flate2", + "image", "itertools", "lewton", "loom", @@ -115,6 +171,22 @@ dependencies = [ "thiserror", ] +[[package]] +name = "exr" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279d3efcc55e19917fff7ab3ddd6c14afb6a90881a0078465196fe2f99d08c56" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "eyre" version = "0.6.8" @@ -125,6 +197,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + [[package]] name = "fixed" version = "1.24.0" @@ -147,6 +228,31 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + [[package]] name = "generator" version = "0.7.5" @@ -160,6 +266,29 @@ dependencies = [ "windows", ] +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "half" version = "2.3.1" @@ -176,6 +305,24 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "image" +version = "0.24.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034bbe799d1909622a74d1193aa50147769440040ff36cb2baa947609b0a4e23" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-traits", + "png", + "qoi", + "tiff", +] + [[package]] name = "indenter" version = "0.3.3" @@ -191,12 +338,36 @@ dependencies = [ "either", ] +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" +dependencies = [ + "rayon", +] + +[[package]] +name = "js-sys" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + [[package]] name = "lewton" version = "0.10.2" @@ -214,6 +385,16 @@ version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.20" @@ -261,6 +442,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", + "simd-adler32", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", ] [[package]] @@ -283,6 +474,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + [[package]] name = "ogg" version = "0.8.0" @@ -310,12 +510,45 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +[[package]] +name = "png" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f6c3c3e617595665b8ea2ff95a86066be38fb121ff920a9c0eb282abcd1da5a" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "proc-macro2" version = "1.0.76" @@ -325,6 +558,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + [[package]] name = "quote" version = "1.0.35" @@ -334,6 +576,26 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rayon" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "regex" version = "1.10.2" @@ -390,6 +652,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "sealed" version = "0.5.0" @@ -411,12 +679,27 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "smallvec" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "syn" version = "2.0.48" @@ -458,6 +741,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -558,6 +852,72 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" + +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + [[package]] name = "winapi" version = "0.3.9" @@ -645,3 +1005,12 @@ name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] diff --git a/README.md b/README.md index b68e48d..96a8c02 100644 --- a/README.md +++ b/README.md @@ -42,14 +42,14 @@ File parsing is in test suite only, for now. - **Purpose** - Look up table for brightness when changing gamma in settings - **Output format** - - PPM image + - PNG image - [x] Color map - **Purpose** - Look up table for 12-bit RGB colors, because textures are stored in 256 indexed color format - There is also ghost version for when you use Ghost Vision Goggles in game - There are unique color maps for monsters, pick-ups (and some other models), UI sprites, and levels - **Output format** - - PPM image + - PNG image - [x] Model - **Purpose** - Texture, geometry, and model data @@ -65,7 +65,7 @@ File parsing is in test suite only, for now. - **Purpose** - Color map and skybox texture - **Output format** - - PPM image + - PNG image - [ ] Skins - [ ] Level geometry - [ ] Level collision @@ -78,7 +78,7 @@ File parsing is in test suite only, for now. - **TO DO** - Re-integrate output - **Output format** - - PPM image + - PNG image - [x] Music and sound effects - **Purpose** - Sound effects diff --git a/engine/Cargo.toml b/engine/Cargo.toml index f3873b2..3cd7453 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -17,6 +17,7 @@ thiserror = "1.0.56" [dev-dependencies] eyre = "0.6.8" +image = "0.24.8" [features] loom = ["dep:loom"] diff --git a/engine/src/asset/color_map.rs b/engine/src/asset/color_map.rs index 01f71bf..5cb937f 100644 --- a/engine/src/asset/color_map.rs +++ b/engine/src/asset/color_map.rs @@ -148,8 +148,8 @@ mod tests { let (_, color_map) = >::parser(())(&COLOR_MAP_DATA)?; output_file( - parsed_file_path!("color-map/monsters.ppm"), - color_map.shades.as_slice().to_ppm(), + parsed_file_path!("color-map/monsters.png"), + color_map.shades.as_slice().to_png(), )?; Ok(()) diff --git a/engine/src/asset/gamma_table.rs b/engine/src/asset/gamma_table.rs index fecdc3b..bb1c115 100644 --- a/engine/src/asset/gamma_table.rs +++ b/engine/src/asset/gamma_table.rs @@ -72,7 +72,7 @@ mod tests { }) .collect::>(); - output_file(parsed_file_path!("gamma-table.ppm"), gamma_table.to_ppm())?; + output_file(parsed_file_path!("gamma-table.png"), gamma_table.to_png())?; Ok(()) } diff --git a/engine/src/asset/skybox.rs b/engine/src/asset/skybox.rs index 0808779..c0d8a1f 100644 --- a/engine/src/asset/skybox.rs +++ b/engine/src/asset/skybox.rs @@ -50,8 +50,8 @@ mod tests { let (_, skybox) = >::parser(())(&SKYBOX_DATA)?; output_file( - parsed_file_path!("skyboxes/level-1.ppm"), - skybox.texture.with_palette(&skybox.palette).to_ppm(), + parsed_file_path!("skyboxes/level-1.png"), + skybox.texture.with_palette(&skybox.palette).to_png(), )?; Ok(()) diff --git a/engine/src/utils/format.rs b/engine/src/utils/format.rs index ad12091..d1d7c48 100644 --- a/engine/src/utils/format.rs +++ b/engine/src/utils/format.rs @@ -1,29 +1,38 @@ use crate::asset::color_map::Color; -use std::ops::Deref; +use image::{ImageOutputFormat, RgbImage}; +use std::{io::Cursor, ops::Deref}; -pub trait PpmFile { - fn to_ppm(&self) -> Vec; +pub trait PngFile { + fn to_png(&self) -> Vec; } // impl for any 2D array like data structure. -impl PpmFile for Outer +impl PngFile for Outer where Outer: Deref, Inner: AsRef<[Color]>, { - fn to_ppm(&self) -> Vec { - let width = self[0].as_ref().len(); - let height = self.len(); - - let mut bytes = format!("P6 {width} {height} 255\n").into_bytes(); - bytes.extend(self.iter().flat_map(|slice| { - slice - .as_ref() - .iter() - .flat_map(|color| [color.r, color.g, color.b]) - })); - - bytes + fn to_png(&self) -> Vec { + let width = self[0].as_ref().len() as u32; + let height = self.len() as u32; + + let data: Vec = self + .iter() + .flat_map(|slice| { + slice + .as_ref() + .iter() + .flat_map(|color| [color.r, color.g, color.b]) + }) + .collect(); + + let mut image = + RgbImage::from_vec(width, height, data).expect("Generated image data must be valid"); + + let mut data = Cursor::new(vec![]); + image.write_to(&mut data, ImageOutputFormat::Png); + + data.into_inner() } } diff --git a/engine/src/utils/mod.rs b/engine/src/utils/mod.rs index 17a7b5a..91e6738 100644 --- a/engine/src/utils/mod.rs +++ b/engine/src/utils/mod.rs @@ -1,4 +1,5 @@ pub mod compression; +#[cfg(test)] pub mod format; pub mod nom; #[cfg(test)] From 0c529f83f2a132d9c2ccb9a918f671c7d0d56654 Mon Sep 17 00:00:00 2001 From: Mykyta Onipchenko <44075969+nenikitov@users.noreply.github.com> Date: Wed, 24 Jan 2024 13:49:55 -0500 Subject: [PATCH 12/14] fix: remove `associated_type_defaults` and `trait_alias` features. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It was confusing `rust-analyzer` and we start to have {unknown}s everywhere. --- engine/src/asset/color_map.rs | 12 +++++-- engine/src/asset/gamma_table.rs | 6 +++- engine/src/asset/mod.rs | 41 ++++------------------ engine/src/asset/model/dat/frame.rs | 14 ++++++-- engine/src/asset/model/dat/header.rs | 6 +++- engine/src/asset/model/dat/sequence.rs | 9 +++-- engine/src/asset/model/dat/triangle.rs | 8 +++-- engine/src/asset/model/mod.rs | 6 +++- engine/src/asset/pack_info.rs | 6 +++- engine/src/asset/skybox.rs | 6 +++- engine/src/asset/sound/dat/asset_header.rs | 6 +++- engine/src/asset/sound/dat/chunk_header.rs | 6 +++- engine/src/asset/sound/dat/t_effect.rs | 12 +++++-- engine/src/asset/sound/dat/t_instrument.rs | 10 ++++-- engine/src/asset/sound/dat/t_song.rs | 30 +++++++++++++--- engine/src/asset/sound/mod.rs | 7 +++- engine/src/asset/string_table.rs | 6 +++- engine/src/lib.rs | 2 -- engine/src/utils/nom.rs | 3 -- 19 files changed, 129 insertions(+), 67 deletions(-) diff --git a/engine/src/asset/color_map.rs b/engine/src/asset/color_map.rs index 5cb937f..495145b 100644 --- a/engine/src/asset/color_map.rs +++ b/engine/src/asset/color_map.rs @@ -35,7 +35,11 @@ impl Color { } impl AssetParser for Color { - fn parser((): Self::Context<'_>) -> impl FnParser { + type Output = Self; + + type Context<'ctx> = (); + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (input, color) = number::le_u32(input)?; Ok((input, Self::from_12_bit(color as u16))) @@ -51,7 +55,11 @@ pub struct ColorMap { } impl AssetParser for ColorMap { - fn parser((): Self::Context<'_>) -> impl FnParser { + type Output = Self; + + type Context<'ctx> = (); + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { error::ensure_bytes_length( input, diff --git a/engine/src/asset/gamma_table.rs b/engine/src/asset/gamma_table.rs index bb1c115..cadb9d9 100644 --- a/engine/src/asset/gamma_table.rs +++ b/engine/src/asset/gamma_table.rs @@ -12,7 +12,11 @@ pub struct GammaTable { } impl AssetParser for GammaTable { - fn parser((): Self::Context<'_>) -> impl FnParser { + type Output = Self; + + type Context<'ctx> = (); + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { error::ensure_bytes_length( input, diff --git a/engine/src/asset/mod.rs b/engine/src/asset/mod.rs index 2050ae3..78ea09c 100644 --- a/engine/src/asset/mod.rs +++ b/engine/src/asset/mod.rs @@ -7,8 +7,7 @@ pub mod skybox; pub mod sound; pub mod string_table; -use crate::{error::ParseError, utils::nom::FnParser}; -use extension::*; +use crate::utils::nom::{Input, Result}; /// Definition for all available extensions that the engine can parse. pub mod extension { @@ -62,46 +61,18 @@ pub trait AssetParser where Self: Sized, { - /// The kind of asset generated by this parser. - /// - /// _Most_ of the time this would be equal to `Self`. See [`Self::Output`] - /// for more details. - type Item = Self; - /// The final value that would be returned by [`parser`]. /// - /// DOCS(Unavailable): + /// _Most_ of the time this would be equal to `Self`. /// - /// A hypothetical `TextureCollection` would return `Self` as its output, - /// however its item "kind" would be `Texture`, because that is the overall - /// kind of values that it would hold. + /// A hypothetical `TextureCollection` would return `Vec` as its output. /// /// [`parser`]: Self::parser - type Output = Self::Item; + type Output; /// Extra information passed down to the parser. - type Context<'ctx> = (); + type Context<'ctx>; /// Generates a new parser with the provided context. - fn parser(context: Self::Context<'_>) -> impl FnParser; -} - -// For the moment we only have implementaions of `AssetParser` where `Ext = Dat` -// that means that we can trivially generate a `AssetParser` impl without -// too much code boilerplate. -// -// However, this is gonna break coherence once we start adding `Custom` exts. -// Hopefully `negative_impls` will become stable before 2030 :). -impl AssetParser for T -where - T: AssetParser = ()>, -{ - type Context<'ctx> = &'ctx str; - - fn parser(context: Self::Context<'_>) -> impl FnParser { - move |input| match context { - "pack" => >::parser(())(input), - _ => Err(ParseError::unsupported_extension(input, context).into()), - } - } + fn parser(context: Self::Context<'_>) -> impl Fn(Input) -> Result; } diff --git a/engine/src/asset/model/dat/frame.rs b/engine/src/asset/model/dat/frame.rs index 029d28d..3c6f024 100644 --- a/engine/src/asset/model/dat/frame.rs +++ b/engine/src/asset/model/dat/frame.rs @@ -13,7 +13,11 @@ pub struct Vec3 { } impl AssetParser for Vec3 { - fn parser((): Self::Context<'_>) -> impl FnParser { + type Output = Self; + + type Context<'ctx> = (); + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (input, x) = number::le_i16f16(input)?; let (input, y) = number::le_i16f16(input)?; @@ -49,9 +53,11 @@ pub struct VertexTransform { } impl AssetParser for ModelVertex { + type Output = Self; + type Context<'ctx> = VertexTransform; - fn parser(transform: Self::Context<'_>) -> impl FnParser { + fn parser(transform: Self::Context<'_>) -> impl Fn(Input) -> Result { macro_rules! transform { ($coordinate: ident) => { (transform.scale.$coordinate * $coordinate as f32 / -256.0 @@ -92,9 +98,11 @@ pub struct ModelSpecs { } impl AssetParser for ModelFrame { + type Output = Self; + type Context<'ctx> = ModelSpecs; - fn parser(model_specs: Self::Context<'_>) -> impl FnParser { + fn parser(model_specs: Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (input, scale) = Vec3::parser(())(input)?; let (input, origin) = Vec3::parser(())(input)?; diff --git a/engine/src/asset/model/dat/header.rs b/engine/src/asset/model/dat/header.rs index b3991ce..74bcb28 100644 --- a/engine/src/asset/model/dat/header.rs +++ b/engine/src/asset/model/dat/header.rs @@ -19,7 +19,11 @@ pub struct ModelHeader { } impl AssetParser for ModelHeader { - fn parser((): Self::Context<'_>) -> impl FnParser { + type Output = Self; + + type Context<'ctx> = (); + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (input, triangle_count) = number::le_u32(input)?; let (input, vertex_count) = number::le_u32(input)?; diff --git a/engine/src/asset/model/dat/sequence.rs b/engine/src/asset/model/dat/sequence.rs index 986db7e..067a25c 100644 --- a/engine/src/asset/model/dat/sequence.rs +++ b/engine/src/asset/model/dat/sequence.rs @@ -8,9 +8,14 @@ pub struct ModelSequence { } impl AssetParser for ModelSequence { - type Context<'ctx> = &'ctx [u8]; + type Output = Self; - fn parser(full_input: Self::Context<'_>) -> impl FnParser { + // TODO(nenikitov): Maybe refactor it to not accept full input. + // In other asset parts, it's parent's responsability to cut input into slices + // for asset parts to parse. + type Context<'ctx> = Input<'ctx>; + + fn parser(full_input: Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (input, frame_count) = number::le_u32(input)?; let (input, offset) = number::le_u32(input)?; diff --git a/engine/src/asset/model/dat/triangle.rs b/engine/src/asset/model/dat/triangle.rs index 404b7d0..d9f5764 100644 --- a/engine/src/asset/model/dat/triangle.rs +++ b/engine/src/asset/model/dat/triangle.rs @@ -15,9 +15,11 @@ pub struct TextureDimensions { } impl AssetParser for ModelPoint { + type Output = Self; + type Context<'ctx> = &'ctx TextureDimensions; - fn parser(texture_dimensions: Self::Context<'_>) -> impl FnParser { + fn parser(texture_dimensions: Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (input, vertex_index) = number::le_u16(input)?; @@ -38,9 +40,11 @@ pub struct ModelTriangle { } impl AssetParser for ModelTriangle { + type Output = Self; + type Context<'ctx> = TextureDimensions; - fn parser(texture_dimensions: Self::Context<'_>) -> impl FnParser { + fn parser(texture_dimensions: Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (input, points) = multi::count!(ModelPoint::parser(&texture_dimensions))(input)?; diff --git a/engine/src/asset/model/mod.rs b/engine/src/asset/model/mod.rs index 4087eb1..a7ded1d 100644 --- a/engine/src/asset/model/mod.rs +++ b/engine/src/asset/model/mod.rs @@ -16,7 +16,11 @@ pub struct Model { } impl AssetParser for Model { - fn parser((): Self::Context<'_>) -> impl FnParser { + type Output = Self; + + type Context<'ctx> = (); + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (_, header) = ModelHeader::parser(())(input)?; diff --git a/engine/src/asset/pack_info.rs b/engine/src/asset/pack_info.rs index 214fd24..c357f23 100644 --- a/engine/src/asset/pack_info.rs +++ b/engine/src/asset/pack_info.rs @@ -9,7 +9,11 @@ pub struct PackInfo { } impl AssetParser for PackInfo { - fn parser((): Self::Context<'_>) -> impl FnParser { + type Output = Self; + + type Context<'ctx> = (); + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (input, offset) = number::le_u32(input)?; diff --git a/engine/src/asset/skybox.rs b/engine/src/asset/skybox.rs index c0d8a1f..71a4bfa 100644 --- a/engine/src/asset/skybox.rs +++ b/engine/src/asset/skybox.rs @@ -11,7 +11,11 @@ pub struct Skybox { } impl AssetParser for Skybox { - fn parser((): Self::Context<'_>) -> impl FnParser { + type Output = Self; + + type Context<'ctx> = (); + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (input, width) = number::le_u32(input)?; diff --git a/engine/src/asset/sound/dat/asset_header.rs b/engine/src/asset/sound/dat/asset_header.rs index 02e201d..51fdad0 100644 --- a/engine/src/asset/sound/dat/asset_header.rs +++ b/engine/src/asset/sound/dat/asset_header.rs @@ -15,7 +15,11 @@ impl SoundAssetHeader { } impl AssetParser for SoundAssetHeader { - fn parser((): Self::Context<'_>) -> impl FnParser { + type Output = Self; + + type Context<'ctx> = (); + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (input, _) = bytes::tag(Self::HEADER)(input)?; diff --git a/engine/src/asset/sound/dat/chunk_header.rs b/engine/src/asset/sound/dat/chunk_header.rs index f9588c2..3990ea4 100644 --- a/engine/src/asset/sound/dat/chunk_header.rs +++ b/engine/src/asset/sound/dat/chunk_header.rs @@ -8,7 +8,11 @@ pub struct SoundChunkHeader { } impl AssetParser for SoundChunkHeader { - fn parser((): Self::Context<'_>) -> impl FnParser { + type Output = Self; + + type Context<'ctx> = (); + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (input, count) = number::le_u32(input)?; let (input, infos) = multi::count!(PackInfo::parser(()), count as usize)(input)?; diff --git a/engine/src/asset/sound/dat/t_effect.rs b/engine/src/asset/sound/dat/t_effect.rs index 1cfb91a..4413163 100644 --- a/engine/src/asset/sound/dat/t_effect.rs +++ b/engine/src/asset/sound/dat/t_effect.rs @@ -23,7 +23,11 @@ impl TEffect { } impl AssetParser for TEffect { - fn parser((): Self::Context<'_>) -> impl FnParser { + type Output = Self; + + type Context<'ctx> = (); + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (_, pointers) = TEffectPointers::parser(())(input)?; @@ -45,7 +49,11 @@ struct TEffectPointers { } impl AssetParser for TEffectPointers { - fn parser((): Self::Context<'_>) -> impl FnParser { + type Output = Self; + + type Context<'ctx> = (); + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (input, instrument) = number::le_u32(input)?; let (input, sample) = number::le_u32(input)?; diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index 2c49e58..ca32f3e 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -30,7 +30,11 @@ pub struct TInstrument { } impl AssetParser for TInstrument { - fn parser((): Self::Context<'_>) -> impl FnParser { + type Output = Self; + + type Context<'ctx> = (); + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (input, flags) = number::le_u8(input)?; @@ -97,9 +101,11 @@ pub struct TSample { } impl AssetParser for TSample { + type Output = Self; + type Context<'ctx> = &'ctx [i16]; - fn parser(sample_data: Self::Context<'_>) -> impl FnParser { + fn parser(sample_data: Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (input, flags) = number::le_u8(input)?; let (input, volume) = number::le_u8(input)?; diff --git a/engine/src/asset/sound/dat/t_song.rs b/engine/src/asset/sound/dat/t_song.rs index c5a24b0..f465d9c 100644 --- a/engine/src/asset/sound/dat/t_song.rs +++ b/engine/src/asset/sound/dat/t_song.rs @@ -56,7 +56,11 @@ impl TSong { } impl AssetParser for TSong { - fn parser((): Self::Context<'_>) -> impl FnParser { + type Output = Self; + + type Context<'ctx> = (); + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (header, pointers) = { let (input, header) = TSongHeader::parser(())(input)?; @@ -140,7 +144,11 @@ struct TSongHeader { } impl AssetParser for TSongHeader { - fn parser((): Self::Context<'_>) -> impl FnParser { + type Output = Self; + + type Context<'ctx> = (); + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (input, song_length) = number::le_u8(input)?; let (input, restart_order) = number::le_u8(input)?; @@ -180,7 +188,11 @@ struct TSongPointers { } impl AssetParser for TSongPointers { - fn parser((): Self::Context<'_>) -> impl FnParser { + type Output = Self; + + type Context<'ctx> = (); + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (input, orders) = number::le_u32(input)?; let (input, patterns) = number::le_u32(input)?; @@ -217,7 +229,11 @@ struct TPattern { } impl AssetParser for TPattern { - fn parser((): Self::Context<'_>) -> impl FnParser { + type Output = Self; + + type Context<'ctx> = (); + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (input, flags) = number::le_u8(input)?; let (input, note) = number::le_u8(input)?; @@ -242,7 +258,11 @@ impl AssetParser for TPattern { } impl AssetParser for Option { - fn parser((): Self::Context<'_>) -> impl FnParser { + type Output = Self; + + type Context<'ctx> = (); + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (after_flags, flags) = number::le_u8(input)?; if (flags & 0x20) != 0 { diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index e51e6ab..0753068 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -20,7 +20,12 @@ impl SoundAssetCollection { } impl AssetParser for SoundAssetCollection { - fn parser((): Self::Context<'_>) -> impl FnParser { + // TODO(nenikitov): Make it output vecs somehow to follow collection convention + type Output = Self; + + type Context<'ctx> = (); + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (_, header) = SoundAssetHeader::parser(())(input)?; diff --git a/engine/src/asset/string_table.rs b/engine/src/asset/string_table.rs index 4854a91..577fdeb 100644 --- a/engine/src/asset/string_table.rs +++ b/engine/src/asset/string_table.rs @@ -15,7 +15,11 @@ fn utf_16_string(input: &[u8]) -> Result { } impl AssetParser for StringTable { - fn parser((): Self::Context<'_>) -> impl FnParser { + type Output = Self; + + type Context<'ctx> = (); + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (input, count) = number::le_u32(input)?; // TODO(Unavailable): Find out what the "catholic" characters are. diff --git a/engine/src/lib.rs b/engine/src/lib.rs index 2705f9b..3a4394f 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -18,8 +18,6 @@ let_chains, lazy_cell, io_error_more, - associated_type_defaults, - trait_alias, )] pub mod asset; diff --git a/engine/src/utils/nom.rs b/engine/src/utils/nom.rs index 6eeed85..560fa4b 100644 --- a/engine/src/utils/nom.rs +++ b/engine/src/utils/nom.rs @@ -138,6 +138,3 @@ impl<'a, T, O> Parser<'a, O> for T where T: nom::Parser, O, crate::err /// Holds the result of parsing functions. pub type Result<'a, O> = nom::IResult, O, crate::error::ParseError>; - -// DOCS(Unavailable): -pub trait FnParser = Fn(Input) -> Result; From 9d74305057f5e11f5db6e9449d06df250e083218 Mon Sep 17 00:00:00 2001 From: Mykyta Onipchenko <44075969+nenikitov@users.noreply.github.com> Date: Fri, 26 Jan 2024 10:45:31 -0500 Subject: [PATCH 13/14] feat: add parsing for `textures`. * chore: update readme --- README.md | 3 +- engine/src/asset/mod.rs | 1 + engine/src/asset/texture/dat/mod.rs | 3 + engine/src/asset/texture/dat/offset.rs | 45 +++++++ engine/src/asset/texture/dat/size.rs | 26 ++++ engine/src/asset/texture/dat/texture.rs | 62 ++++++++++ engine/src/asset/texture/mod.rs | 151 ++++++++++++++++++++++++ engine/src/utils/format.rs | 87 +++++++++++--- 8 files changed, 361 insertions(+), 17 deletions(-) create mode 100644 engine/src/asset/texture/dat/mod.rs create mode 100644 engine/src/asset/texture/dat/offset.rs create mode 100644 engine/src/asset/texture/dat/size.rs create mode 100644 engine/src/asset/texture/dat/texture.rs create mode 100644 engine/src/asset/texture/mod.rs diff --git a/README.md b/README.md index 96a8c02..205f8d3 100644 --- a/README.md +++ b/README.md @@ -75,10 +75,9 @@ File parsing is in test suite only, for now. - **Purpose** - Texture info - offsets, texture dimensions - Texture data - color indeces - - **TO DO** - - Re-integrate output - **Output format** - PNG image + - GIF image for animated textures - [x] Music and sound effects - **Purpose** - Sound effects diff --git a/engine/src/asset/mod.rs b/engine/src/asset/mod.rs index 78ea09c..2f0838c 100644 --- a/engine/src/asset/mod.rs +++ b/engine/src/asset/mod.rs @@ -6,6 +6,7 @@ mod pack_info; pub mod skybox; pub mod sound; pub mod string_table; +pub mod texture; use crate::utils::nom::{Input, Result}; diff --git a/engine/src/asset/texture/dat/mod.rs b/engine/src/asset/texture/dat/mod.rs new file mode 100644 index 0000000..f4c3dfb --- /dev/null +++ b/engine/src/asset/texture/dat/mod.rs @@ -0,0 +1,3 @@ +pub mod offset; +pub mod size; +pub mod texture; diff --git a/engine/src/asset/texture/dat/offset.rs b/engine/src/asset/texture/dat/offset.rs new file mode 100644 index 0000000..c241c80 --- /dev/null +++ b/engine/src/asset/texture/dat/offset.rs @@ -0,0 +1,45 @@ +use crate::{ + asset::{extension::*, AssetParser}, + utils::nom::*, +}; + +pub struct TextureOffset { + pub width: u16, + pub height: u16, + pub offset: u32, + pub size_compressed: u32, + pub size_decompressed: u32, + pub animation_frames: u32, + pub next_animation_texture_id: u32, +} + +impl AssetParser for TextureOffset { + type Context<'ctx> = (); + + type Output = Self; + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { + move |input| { + let (input, width) = number::le_u16(input)?; + let (input, height) = number::le_u16(input)?; + let (input, offset) = number::le_u32(input)?; + let (input, size_compressed) = number::le_u32(input)?; + let (input, size_decompressed) = number::le_u32(input)?; + let (input, animation_frames) = number::le_u32(input)?; + let (input, next_animation_texture_id) = number::le_u32(input)?; + + Ok(( + input, + Self { + width, + height, + offset, + size_compressed, + size_decompressed, + animation_frames, + next_animation_texture_id, + }, + )) + } + } +} diff --git a/engine/src/asset/texture/dat/size.rs b/engine/src/asset/texture/dat/size.rs new file mode 100644 index 0000000..9325d3c --- /dev/null +++ b/engine/src/asset/texture/dat/size.rs @@ -0,0 +1,26 @@ +use std::ops::Div; + +#[derive(Clone, Copy)] +pub struct TextureSize { + pub width: u16, + pub height: u16, +} + +impl Div for TextureSize { + type Output = Self; + + fn div(self, rhs: u16) -> Self::Output { + &self / rhs + } +} + +impl Div for &TextureSize { + type Output = TextureSize; + + fn div(self, rhs: u16) -> Self::Output { + TextureSize { + width: self.width / rhs, + height: self.height / rhs, + } + } +} diff --git a/engine/src/asset/texture/dat/texture.rs b/engine/src/asset/texture/dat/texture.rs new file mode 100644 index 0000000..068236b --- /dev/null +++ b/engine/src/asset/texture/dat/texture.rs @@ -0,0 +1,62 @@ +use super::size::TextureSize; +use crate::{ + asset::{extension::*, AssetParser}, + utils::nom::*, +}; +use itertools::Itertools; + +#[derive(Clone)] +pub struct Texture { + pub colors: Vec>, +} + +impl AssetParser for Texture { + type Output = Self; + + type Context<'ctx> = &'ctx TextureSize; + + fn parser(size: Self::Context<'_>) -> impl Fn(Input) -> Result { + let width = size.width as usize; + let height = size.height as usize; + + move |input| { + let (input, colors) = multi::count!(number::le_u8, width * height)(input)?; + + let colors = colors + .into_iter() + .chunks(width) + .into_iter() + .map(Iterator::collect) + .collect(); + + Ok((input, Self { colors })) + } + } +} + +#[derive(Clone)] +pub struct MippedTexture { + pub mips: [Texture; 4], +} + +impl AssetParser for MippedTexture { + type Output = Self; + + type Context<'ctx> = TextureSize; + + fn parser(size: Self::Context<'_>) -> impl Fn(Input) -> Result { + move |input| { + let (input, mip_1) = Texture::parser(&size)(input)?; + let (input, mip_2) = Texture::parser(&(size / 2))(input)?; + let (input, mip_3) = Texture::parser(&(size / 4))(input)?; + let (input, mip_4) = Texture::parser(&(size / 8))(input)?; + + Ok(( + &[], + Self { + mips: [mip_1, mip_2, mip_3, mip_4], + }, + )) + } + } +} diff --git a/engine/src/asset/texture/mod.rs b/engine/src/asset/texture/mod.rs new file mode 100644 index 0000000..afc2621 --- /dev/null +++ b/engine/src/asset/texture/mod.rs @@ -0,0 +1,151 @@ +mod dat; + +use self::dat::texture::Texture; + +use super::{extension::*, AssetParser}; +use crate::utils::{compression::decompress, nom::*}; +use dat::{offset::TextureOffset, size::TextureSize, texture::MippedTexture}; + +pub enum TextureMipKind { + NonMipped(Texture), + Mipped(MippedTexture), +} + +pub enum TextureAnimationKind { + Static(TextureMipKind), + Animated(Vec), +} + +pub struct TextureOffsetCollection; + +impl AssetParser for TextureOffsetCollection { + type Output = Vec; + + type Context<'ctx> = (); + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { + move |input| { + let (_, offsets) = multi::many0(TextureOffset::parser(()))(input)?; + + Ok((&[], offsets)) + } + } +} + +pub struct MippedTextureCollection; + +impl AssetParser for MippedTextureCollection { + type Output = Vec; + + type Context<'ctx> = &'ctx [TextureOffset]; + + fn parser(offsets: Self::Context<'_>) -> impl Fn(Input) -> Result { + move |input| { + let textures = offsets + .iter() + .map(|o| { + let input = &input[o.offset as usize..][..o.size_compressed as usize]; + let input = decompress(input); + + MippedTexture::parser(TextureSize { + width: o.width, + height: o.height, + })(&input) + .map(|(_, d)| (d, o)) + }) + .collect::, _>>()?; + + let textures = textures + .iter() + .cloned() + .map(|(texture, offset)| { + if offset.animation_frames == 0 { + TextureAnimationKind::Static(TextureMipKind::Mipped(texture)) + } else { + let mut frames = Vec::with_capacity(offset.animation_frames as usize); + + (0..offset.animation_frames).fold( + (texture, offset), + |(texture, offset), _| { + frames.push(TextureMipKind::Mipped(texture)); + textures[offset.next_animation_texture_id as usize].clone() + }, + ); + + TextureAnimationKind::Animated(frames) + } + }) + .collect::>(); + + Ok((&[], textures)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + asset::color_map::{ColorMap, PaletteTexture}, + utils::{format::*, test::*}, + }; + use std::{cell::LazyCell, path::PathBuf}; + + const COLOR_MAP_DATA: LazyCell> = deflated_file!("4F.dat"); + const TEXTURE_INFO_DATA: LazyCell> = deflated_file!("93.dat"); + const TEXTURE_DATA: LazyCell> = deflated_file!("95.dat"); + + #[test] + #[ignore = "uses Ashen ROM files"] + fn parse_rom_asset() -> eyre::Result<()> { + let (_, color_map) = >::parser(())(&COLOR_MAP_DATA)?; + let color_map = &color_map.shades[15]; + let (_, offsets) = + >::parser(())(&TEXTURE_INFO_DATA)?; + let (_, textures) = + >::parser(&offsets)(&TEXTURE_DATA)?; + + let output_dir = PathBuf::from(parsed_file_path!("textures/")); + + textures + .iter() + .enumerate() + .try_for_each(|(i, texture)| match texture { + TextureAnimationKind::Static(TextureMipKind::NonMipped(_)) => { + unreachable!("World textures are always mipped") + } + TextureAnimationKind::Static(TextureMipKind::Mipped(t)) => { + t.mips.iter().enumerate().try_for_each(|(m, mip)| { + let file = &output_dir.join(format!("{i:0>3X}-mip-{m}.png")); + output_file(file, mip.colors.with_palette(color_map).to_png()) + }) + } + TextureAnimationKind::Animated(t) => { + let frames = t.iter().map(|t| match t { + TextureMipKind::NonMipped(_) => { + unreachable!("World textures are always mipped") + } + TextureMipKind::Mipped(t) => t, + }); + + let mut data = vec![]; + for (f, frame) in frames.enumerate() { + for (m, mip) in frame.mips.iter().enumerate() { + if data.len() <= m { + data.push(vec![]); + } + + data[m].push(mip.colors.with_palette(color_map)) + } + } + + data.iter().enumerate().try_for_each(|(m, mip)| { + let file = &output_dir.join(format!("{i:0>3X}-mip-{m}.gif")); + output_file(file, mip.to_gif()) + }) + } + }); + + Ok(()) + } +} diff --git a/engine/src/utils/format.rs b/engine/src/utils/format.rs index d1d7c48..6f38cae 100644 --- a/engine/src/utils/format.rs +++ b/engine/src/utils/format.rs @@ -1,6 +1,12 @@ use crate::asset::color_map::Color; -use image::{ImageOutputFormat, RgbImage}; -use std::{io::Cursor, ops::Deref}; +use image::{ + codecs::{ + gif::{GifEncoder, Repeat}, + png::PngEncoder, + }, + Frame, ImageEncoder, RgbaImage, +}; +use std::ops::Deref; pub trait PngFile { fn to_png(&self) -> Vec; @@ -16,23 +22,74 @@ where let width = self[0].as_ref().len() as u32; let height = self.len() as u32; - let data: Vec = self - .iter() - .flat_map(|slice| { - slice - .as_ref() + let mut data = vec![]; + let mut encoder = PngEncoder::new(&mut data); + + encoder + .write_image( + &self .iter() - .flat_map(|color| [color.r, color.g, color.b]) - }) - .collect(); + .flat_map(|slice| { + slice + .as_ref() + .iter() + .flat_map(|color| [color.r, color.g, color.b]) + }) + .collect::>(), + width, + height, + image::ColorType::Rgb8, + ) + .expect("Generated image data must be valid"); + + data + } +} + +pub trait GifFile { + fn to_gif(&self) -> Vec; +} + +impl GifFile for Outer +where + Outer: Deref, + Inner1: Deref, + Inner2: AsRef<[Color]>, +{ + fn to_gif(&self) -> Vec { + let width = self[0][0].as_ref().len() as u32; + let height = self[0].len() as u32; + + let mut data = vec![]; + let mut encoder = GifEncoder::new_with_speed(&mut data, 10); + + encoder + .encode_frames(self.iter().map(|f| { + Frame::new( + RgbaImage::from_vec( + width, + height, + f.iter() + .flat_map(|slice| { + slice + .as_ref() + .iter() + .flat_map(|color| [color.r, color.g, color.b, 255]) + }) + .collect(), + ) + .expect("Generated image data must be valid"), + ) + })) + .expect("Generated image frames must be valid"); - let mut image = - RgbImage::from_vec(width, height, data).expect("Generated image data must be valid"); + encoder + .set_repeat(Repeat::Infinite) + .expect("Generated image frames must loop"); - let mut data = Cursor::new(vec![]); - image.write_to(&mut data, ImageOutputFormat::Png); + drop(encoder); - data.into_inner() + data } } From bedbd087e448bb706d37bef1f49f309c9ac1b894 Mon Sep 17 00:00:00 2001 From: Mykyta Onipchenko <44075969+nenikitov@users.noreply.github.com> Date: Fri, 26 Jan 2024 18:02:38 -0500 Subject: [PATCH 14/14] refactor: reuse `Texture` parsing for `SkyBox` and `Model`. --- engine/src/asset/model/mod.rs | 28 ++++++++--------- engine/src/asset/skybox.rs | 21 +++++++------ engine/src/asset/texture/dat/size.rs | 16 +++------- engine/src/asset/texture/dat/texture.rs | 40 ++++++++++++++++++------- engine/src/asset/texture/mod.rs | 9 +++--- 5 files changed, 61 insertions(+), 53 deletions(-) diff --git a/engine/src/asset/model/mod.rs b/engine/src/asset/model/mod.rs index a7ded1d..af4a079 100644 --- a/engine/src/asset/model/mod.rs +++ b/engine/src/asset/model/mod.rs @@ -1,15 +1,18 @@ mod dat; -use super::{extension::*, AssetParser}; +use super::{ + extension::*, + texture::dat::{size::TextureSize, texture::Texture}, + AssetParser, +}; use crate::utils::nom::*; use dat::{ frame::ModelFrame, frame::ModelSpecs, header::ModelHeader, sequence::ModelSequence, triangle::ModelTriangle, triangle::TextureDimensions, }; -use itertools::Itertools; pub struct Model { - pub texture: Vec>, + pub texture: Texture, pub triangles: Vec, pub sequences: Vec, pub frames: Vec, @@ -32,16 +35,10 @@ impl AssetParser for Model { header.triangle_count as usize )(&input[header.offset_triangles as usize..])?; - let (_, texture) = multi::count!( - number::le_u8, - (header.texture_width * header.texture_height) as usize - )(&input[header.offset_texture as usize..])?; - let texture = texture - .into_iter() - .chunks(header.texture_width as usize) - .into_iter() - .map(Iterator::collect) - .collect(); + let (_, texture) = Texture::parser(TextureSize { + width: header.texture_width as usize, + height: header.texture_height as usize, + })(&input[header.offset_texture as usize..])?; let (_, sequences) = multi::count!( ModelSequence::parser(input), @@ -80,6 +77,7 @@ mod tests { asset::color_map::{Color, ColorMap, PaletteTexture}, utils::test::*, }; + use itertools::Itertools; use std::{ cell::LazyCell, fmt::{Display, Formatter}, @@ -195,8 +193,8 @@ bpy.context.collection.objects.link(object) bpy.context.view_layer.objects.active = object object.select_set(True) "#, - self.texture[0].len(), - self.texture.len(), + self.texture.width(), + self.texture.height(), self.texture .with_palette(&palette) .into_iter() diff --git a/engine/src/asset/skybox.rs b/engine/src/asset/skybox.rs index 71a4bfa..d347b10 100644 --- a/engine/src/asset/skybox.rs +++ b/engine/src/asset/skybox.rs @@ -1,13 +1,15 @@ -use super::{extension::*, AssetParser}; +use super::{ + extension::*, + texture::dat::{size::TextureSize, texture::Texture}, + AssetParser, +}; use crate::{asset::color_map::Color, utils::nom::*}; -use itertools::Itertools; const COLOR_COUNT: usize = 256; -#[derive(Debug)] pub struct Skybox { pub palette: Vec, - pub texture: Vec>, + pub texture: Texture, } impl AssetParser for Skybox { @@ -24,13 +26,10 @@ impl AssetParser for Skybox { let (input, palette) = multi::count!(number::le_u16, 256)(input)?; let palette: Vec<_> = palette.into_iter().map(Color::from_12_bit).collect(); - let (input, texture) = multi::count!(number::le_u8, (width * height) as usize)(input)?; - let texture = texture - .into_iter() - .chunks(width as usize) - .into_iter() - .map(Iterator::collect) - .collect(); + let (_, texture) = Texture::parser(TextureSize { + width: width as usize, + height: height as usize, + })(input)?; Ok((&[], Self { palette, texture })) } diff --git a/engine/src/asset/texture/dat/size.rs b/engine/src/asset/texture/dat/size.rs index 9325d3c..bb26a3c 100644 --- a/engine/src/asset/texture/dat/size.rs +++ b/engine/src/asset/texture/dat/size.rs @@ -2,22 +2,14 @@ use std::ops::Div; #[derive(Clone, Copy)] pub struct TextureSize { - pub width: u16, - pub height: u16, + pub width: usize, + pub height: usize, } -impl Div for TextureSize { +impl Div for TextureSize { type Output = Self; - fn div(self, rhs: u16) -> Self::Output { - &self / rhs - } -} - -impl Div for &TextureSize { - type Output = TextureSize; - - fn div(self, rhs: u16) -> Self::Output { + fn div(self, rhs: usize) -> Self::Output { TextureSize { width: self.width / rhs, height: self.height / rhs, diff --git a/engine/src/asset/texture/dat/texture.rs b/engine/src/asset/texture/dat/texture.rs index 068236b..0aaa782 100644 --- a/engine/src/asset/texture/dat/texture.rs +++ b/engine/src/asset/texture/dat/texture.rs @@ -1,10 +1,15 @@ use super::size::TextureSize; use crate::{ - asset::{extension::*, AssetParser}, + asset::{ + color_map::{Color, PaletteTexture}, + extension::*, + AssetParser, + }, utils::nom::*, }; use itertools::Itertools; +// TODO(nenikitov): Move this to a separate public module later #[derive(Clone)] pub struct Texture { pub colors: Vec>, @@ -13,18 +18,15 @@ pub struct Texture { impl AssetParser for Texture { type Output = Self; - type Context<'ctx> = &'ctx TextureSize; + type Context<'ctx> = TextureSize; fn parser(size: Self::Context<'_>) -> impl Fn(Input) -> Result { - let width = size.width as usize; - let height = size.height as usize; - move |input| { - let (input, colors) = multi::count!(number::le_u8, width * height)(input)?; + let (input, colors) = multi::count!(number::le_u8, size.width * size.height)(input)?; let colors = colors .into_iter() - .chunks(width) + .chunks(size.width) .into_iter() .map(Iterator::collect) .collect(); @@ -34,6 +36,16 @@ impl AssetParser for Texture { } } +impl Texture { + pub fn width(&self) -> usize { + self.colors[0].len() + } + + pub fn height(&self) -> usize { + self.colors.len() + } +} + #[derive(Clone)] pub struct MippedTexture { pub mips: [Texture; 4], @@ -46,10 +58,10 @@ impl AssetParser for MippedTexture { fn parser(size: Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { - let (input, mip_1) = Texture::parser(&size)(input)?; - let (input, mip_2) = Texture::parser(&(size / 2))(input)?; - let (input, mip_3) = Texture::parser(&(size / 4))(input)?; - let (input, mip_4) = Texture::parser(&(size / 8))(input)?; + let (input, mip_1) = Texture::parser(size)(input)?; + let (input, mip_2) = Texture::parser(size / 2)(input)?; + let (input, mip_3) = Texture::parser(size / 4)(input)?; + let (input, mip_4) = Texture::parser(size / 8)(input)?; Ok(( &[], @@ -60,3 +72,9 @@ impl AssetParser for MippedTexture { } } } + +impl PaletteTexture for Texture { + fn with_palette(&self, palette: &[Color]) -> Vec> { + self.colors.with_palette(palette) + } +} diff --git a/engine/src/asset/texture/mod.rs b/engine/src/asset/texture/mod.rs index afc2621..bbcc4c1 100644 --- a/engine/src/asset/texture/mod.rs +++ b/engine/src/asset/texture/mod.rs @@ -1,7 +1,8 @@ -mod dat; +// TODO(nenikitov): When textures are moved to a separate public module later, +// this `pub(crate)` could be deleted +pub(crate) mod dat; use self::dat::texture::Texture; - use super::{extension::*, AssetParser}; use crate::utils::{compression::decompress, nom::*}; use dat::{offset::TextureOffset, size::TextureSize, texture::MippedTexture}; @@ -48,8 +49,8 @@ impl AssetParser for MippedTextureCollection { let input = decompress(input); MippedTexture::parser(TextureSize { - width: o.width, - height: o.height, + width: o.width as usize, + height: o.height as usize, })(&input) .map(|(_, d)| (d, o)) })