diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 90214605..4a7c5e49 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -371,7 +371,6 @@ jobs: with: sparse-checkout: | port/android - port/third_party/extract-xiso tools - uses: actions/download-artifact@v4 diff --git a/AGENTS.md b/AGENTS.md index 20a6e0fb..5e5a277b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -190,10 +190,18 @@ otherwise. ### Third-party code and licences -The repository is CC0. Vendored code goes in `port/third_party/`, -copied unchanged, with a README naming the upstream, the version or commit, -the checksum and what uses it. GPL code (OpenSauce, Reclaimer) may be read -as documentation of file formats, never copied. +The repository is `AGPL-3.0-or-later` (`LICENSE`); the commits before the +change, and the releases built from them, stay CC0 (`LICENSES/CC0-1.0.txt`). +The README's "Licence" section says what each part is under. Vendored code +goes in `port/third_party/`, copied unchanged, with a README naming +the upstream, the version or commit, the licence, the checksum and what uses +it. Its licence must let it be combined with the AGPL 3.0: MIT, BSD (without +an advertising clause), ISC, zlib, Apache 2.0, MPL 2.0, LGPL or GPL 3.0 or +later, public domain. GPL-2.0-only code and the 4-clause BSD licence cannot +be. Code taken from another project, GPL 3.0 code included, keeps its +notices and its licence, and says where it came from. +The decompilation's upstream, punpckhdq/halo, is AGPL 3.0 from 17 October +2026; bnunu/halo-1 is CC0. ### Commits and pull requests diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..be3f7b28 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 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 Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + + 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + 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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + 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 AGPL, see +. diff --git a/LICENSE.md b/LICENSES/CC0-1.0.txt similarity index 100% rename from LICENSE.md rename to LICENSES/CC0-1.0.txt diff --git a/README.md b/README.md index 9e2b3d3f..d7cb976d 100644 --- a/README.md +++ b/README.md @@ -170,8 +170,9 @@ Ringfall: Mjolnir VR stands on the work of others: - **[HaloCE-VR](https://github.com/astromaddie/HaloCE-VR)** by astromaddie (CC0): the Steam Frame work, whose findings (such as quieting Mesa's trace markers) are used here. -- **[extract-xiso](https://github.com/XboxDev/extract-xiso)**: the disc - image readers follow its format handling. +- **[xboxdevwiki](https://xboxdevwiki.net/XDVDFS)**: its description of + the Xbox's disc file system, which the disc image readers are written + from. - The libraries and fonts in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). Thank you all. @@ -198,6 +199,26 @@ distributed. You need your own, legally purchased copy of the game. If you enjoy this mod, please support the creators of the original by buying their games. -This project's own code is released under [CC0 1.0](LICENSE.md), as the -decompilation it builds on. Third-party code and fonts keep their own -licenses: see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). +### Licence + +Ringfall: Mjolnir VR is free software: you can give it to others and change +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, version 3 of the License or (at your option) +any later version (`AGPL-3.0-or-later`), as OpenCE, which it builds on, is. +It comes with no warranty. The full text is in [LICENSE](LICENSE). + +- **From build 312 on.** The AGPL applies from the commit that took in + OpenCE's change of licence (19 October 2026), and to each release built + from it or a later one. Earlier releases, and the source at every earlier + commit, were released under [CC0 1.0](LICENSES/CC0-1.0.txt) and stay + that way. +- **Hosts and servers.** If you change the game and let other persons play + with your version over a network (for example, as the host of an + internet game), the AGPL asks you to offer them the source of your + version. The game as released has its source here, and each release's + source is its tag (`build-`). +- **Third-party code and fonts** keep their own licences: see + [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). +- **Halo is not covered.** The licence covers the work of this project's + and OpenCE's authors. It gives no rights in Halo, its maps or anything + else that Microsoft owns. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index d319ab2c..715c063d 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -1,14 +1,16 @@ # Third-party notices -Ringfall: Mjolnir VR's own code is released under [CC0 1.0](LICENSE.md). -It includes or builds on the following, under their own licenses; each -license's full text is in the file named. +Ringfall: Mjolnir VR is released under the GNU Affero General Public +License, version 3 or later ([LICENSE](LICENSE)); releases before build 312 +were released under [CC0 1.0](LICENSES/CC0-1.0.txt). It includes or builds +on the following, under their own licenses; each license's full text is in +the file named. ## Code | Component | Used for | License | Text | | --- | --- | --- | --- | -| [OpenCE](https://github.com/OpenCommunityEdition/OpenCE) and the decompilation ([bnunu/halo-1](https://github.com/bnunu/halo-1), [punpckhdq/halo](https://github.com/punpckhdq/halo)) | the game's code, the port | CC0 1.0 | [LICENSE.md](LICENSE.md) | +| [OpenCE](https://github.com/OpenCommunityEdition/OpenCE) and the decompilation ([bnunu/halo-1](https://github.com/bnunu/halo-1), [punpckhdq/halo](https://github.com/punpckhdq/halo)) | the game's code, the port | AGPL 3.0 or later (OpenCE from 19 October 2026, punpckhdq/halo from 17 October 2026; CC0 1.0 before); bnunu/halo-1: CC0 1.0 | [LICENSE](LICENSE), [LICENSES/CC0-1.0.txt](LICENSES/CC0-1.0.txt) | | [HaloCE-Quest-VR](https://github.com/moistman42069/HaloCE-Quest-VR) | the arm IK, forearm twist and finger poses (`port/linux/game/vr_arms.c`) | CC0 1.0 | (its repository) | | [HaloCE-VR](https://github.com/astromaddie/HaloCE-VR) | Steam Frame findings | CC0 1.0 | (its repository) | | [SDL 3](https://libsdl.org) | windows, input, sound | zlib | (in SDL's source, `LICENSE.txt`) | @@ -22,11 +24,11 @@ license's full text is in the file named. | [tomlc17](https://github.com/cktan/tomlc17) | reading `config.toml` | MIT | [port/third_party/tomlc17/LICENSE](port/third_party/tomlc17/LICENSE) | | [stb](https://github.com/nothings/stb) | drawing the game's text sharply (stb_truetype) | MIT or public domain | [port/third_party/stb/LICENSE](port/third_party/stb/LICENSE) | | [SMAA](https://www.iryoku.com/smaa/) | anti-aliasing (flat screens; off in VR) | MIT | [port/third_party/smaa/LICENSE](port/third_party/smaa/LICENSE) | -| [extract-xiso](https://github.com/XboxDev/extract-xiso) | reading disc images (followed by `XisoExtractor.java`, `steam_frame_xiso.py`, `xiso.c`) | BSD-style (in@fishtank.com) | [port/third_party/extract-xiso/LICENSE.TXT](port/third_party/extract-xiso/LICENSE.TXT) | -The disc image readers include this acknowledgement, as extract-xiso's -license asks: *This product includes software developed by in -<in@fishtank.com>.* +The disc image readers (`XisoExtractor.java`, `steam_frame_xiso.py`, +`xiso.c`) are written from a description of the Xbox's disc file system +([xboxdevwiki's XDVDFS page](https://xboxdevwiki.net/XDVDFS)), not from +extract-xiso, which earlier releases followed. ## Fonts diff --git a/docs/OPENCE_README.md b/docs/OPENCE_README.md index cf4756bc..725ea8f4 100644 --- a/docs/OPENCE_README.md +++ b/docs/OPENCE_README.md @@ -207,3 +207,34 @@ Enter `adb pull` to copy the recording parts to a computer. A profiling build plays with normal builds. It does not use `--pgo=train`. + +## Licence + +OpenCE is free software: you can give it to others and change it under the +terms of the GNU Affero General Public License as published by the Free +Software Foundation, version 3 of the License or (at your option) any later +version (`AGPL-3.0-or-later`). It comes with no warranty. The full text is in +[LICENSE](LICENSE). + +- **From the change.** The AGPL applies from the commit that made the change, + and to each release built from that commit or a later one. Earlier + releases, and the source at every earlier commit, were released under + [CC0 1.0](LICENSES/CC0-1.0.txt) and stay that way. +- **What OpenCE builds on.** The decompilation of + [punpckhdq/halo](https://github.com/punpckhdq/halo) is under the AGPL 3.0 + from 17 October 2026, and under CC0 1.0 before that date. + [bnunu/halo-1](https://github.com/bnunu/halo-1) is under CC0 1.0. +- **Contributions** are under `AGPL-3.0-or-later`. A change taken from a + project under GPL 3.0 keeps that licence and its notices; the GPL 3.0 and + the AGPL 3.0 let the two be combined (section 13 of each). +- **Hosts and servers.** If you change the game and let other persons play + with your version over a network (for example, as the host of an internet + game, or on a web site), the AGPL asks you to offer them the source of + your version (section 13). The game as released has its source here, and + each release's source is its tag (`build-`). +- **Third-party code, fonts and art** keep their own licences, which are + next to them (`port/third_party`, `port/assets/fonts`, + `port/assets/icons`, `port/assets/text_filter`). The release downloads and + the web site's `licenses.txt` carry their notices. +- **Halo is not covered.** The licence covers the work of OpenCE's authors. + It gives no rights in Halo, its maps or anything else that Microsoft owns. diff --git a/docs/custom_edition_caches.md b/docs/custom_edition_caches.md index 070a5efb..76a91cea 100644 --- a/docs/custom_edition_caches.md +++ b/docs/custom_edition_caches.md @@ -493,9 +493,9 @@ file the same as the host's; with `network.map_download` off, or a host with but their model parts have its 0x68-byte layout and their shader types its numbering, which makes them a check on what the conversion produces. -OpenSauce and Reclaimer are GPL-3.0 and this repository is CC0: no code from -either was copied. They were read as documentation of the file formats, and -every layout used was then checked against the sample maps. +No code from OpenSauce or Reclaimer (both GPL-3.0) was copied: they were +read as documentation of the file formats, and every layout used was then +checked against the sample maps. ### Documented facts (source, checked against the sample maps) diff --git a/port/android/app/src/main/java/com/halo/decomp/XisoExtractor.java b/port/android/app/src/main/java/com/halo/decomp/XisoExtractor.java index 5bbf7260..cb53557a 100644 --- a/port/android/app/src/main/java/com/halo/decomp/XisoExtractor.java +++ b/port/android/app/src/main/java/com/halo/decomp/XisoExtractor.java @@ -4,101 +4,88 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; -import java.nio.ByteOrder; import java.nio.channels.FileChannel; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.BitSet; import java.util.List; import java.util.Locale; /** - * Copies the maps folder out of an Xbox disc image (an "xiso", or a whole - * disc's ".iso"), as the desktop games do (port/linux/src/xiso.c, which this - * follows). + * Copies the maps folder out of the player's Xbox disc image of Halo, for + * LauncherActivity, as the desktop games do (port/linux/src/xiso.c; the web + * page's is port/web/site/xiso-worker.js). The image is a document the + * player picked, read through its FileChannel at absolute positions. * - * The image's file system is XDVDFS, read as extract-xiso does - * (https://github.com/XboxDev/extract-xiso, extract-xiso.c, whose format - * handling this follows; its license is below and in - * port/third_party/extract-xiso/LICENSE.TXT): 2048-byte sectors; a volume - * descriptor at 0x10000 that starts and ends with "MICROSOFT*XBOX*MEDIA" and - * gives the root directory's sector and size; and directories whose entries - * form a binary tree (each entry: left and right subtree offsets in 4-byte - * units, the start sector, the size, attributes, the name's length and the - * name, 4-byte aligned). Images made from a whole disc put the game partition - * further in, at one of the offsets below. + * Written from a description of the Xbox's disc file system, XDVDFS + * (xboxdevwiki's XDVDFS page, https://xboxdevwiki.net/XDVDFS, and the format + * facts OpenCE's readers rely on). What this class relies on: * - * The files are written to maps.partial first and moved into maps once they - * all are, so an interrupted extraction never leaves maps/ui.map behind. + * - Sectors are 2048 bytes and integers little-endian. Sector numbers count + * from the start of the game partition: an image of the partition alone + * has it at 0, an image of a whole disc further in (0x0FD90000, 0x02080000 + * or 0x18300000 for XGD2, XGD3 and XGD1 discs). They are tried in that + * order. + * - The volume descriptor is the partition's sector 32 (at 0x10000). It + * holds "MICROSOFT*XBOX*MEDIA" at its start and again at 0x7EC, and the + * root directory table's start sector (u32 at 0x14) and size in bytes (u32 + * at 0x18). + * - A directory table is a binary tree of entries whose root is the entry at + * offset 0. An entry is a left and a right subtree link (u16 each, in + * 4-byte units from the table's start, 0 for none), the start sector and + * the size of the file or of the subdirectory's table (u32 each), the + * attributes (u8, 0x10 for a directory), the name's length (u8) and the + * name. Unused bytes of a table are 0xFF, so a left link of 0xFFFF is + * padding, not an entry. + * - A file is one contiguous run of bytes from the start of its sector. * - * A European disc in two languages (Spanish and Italian) has no maps folder - * but one for each, maps_es and maps_it: the device's language's is taken, - * else English's, else the first, and its language written to - * maps/language.txt, which the game asks the Xbox for (game.language unset, - * port/linux/src/xbox_xapi.c): the menus in it too. + * The image is the player's file and nothing read from it is trusted. Only + * the root's table and the maps folder's are read, each whole and at most + * 4 MB; the walk of a table visits each offset once, goes at most 64 links + * deep and looks at most 4096 entries; and a file is written only under a + * plain name that no file before it had (ignoring case). * - * Some parts of this code are copyright in@fishtank.com. This product - * includes software developed by in <in@fishtank.com>. - * - * Copyright (c) 2003 in <in@fishtank.com> - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * 3. All advertising materials mentioning features or use of this software - * must display the following acknowledgement: - * - * This product includes software developed by in <in@fishtank.com>. - * - * 4. Neither the name of "in" nor the email address "in@fishtank.com" - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED `AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * AUTHOR OR ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; - * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR - * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * The app starts the game once maps/ui.map exists, and makes the maps folder + * itself, so the copy is written to maps.partial (emptied first of what an + * earlier attempt left) and its files moved into maps once all are + * complete, ui.map last. */ final class XisoExtractor { private static final int SECTOR_SIZE = 2048; - private static final long VOLUME_DESCRIPTOR_OFFSET = 0x10000; - private static final int ENTRY_HEADER_SIZE = 14; + private static final int DESCRIPTOR_OFFSET = 0x10000; + private static final byte[] MAGIC = "MICROSOFT*XBOX*MEDIA".getBytes(StandardCharsets.ISO_8859_1); + private static final int SECOND_MAGIC_OFFSET = 0x7EC; + private static final int ENTRY_FIXED_SIZE = 14; private static final int ATTRIBUTE_DIRECTORY = 0x10; - /* directory tables are a few sectors; anything much larger is not one */ - private static final long MAXIMUM_DIRECTORY_SIZE = 4 << 20; - private static final int MAXIMUM_FILES = 256; - private static final int COPY_BUFFER_SIZE = 1 << 20; - private static final byte[] VOLUME_MAGIC = "MICROSOFT*XBOX*MEDIA".getBytes(StandardCharsets.US_ASCII); - - /* where the game partition starts: a plain image, and whole-disc images - (extract-xiso's GLOBAL, XGD3 and XGD1 offsets) */ - private static final long[] PARTITION_OFFSETS = { 0, 0x0FD90000L, 0x02080000L, 0x18300000L }; + private static final int PADDING_LINK = 0xFFFF; + /** where the game partition may start, in the order they are tried */ + private static final long[] PARTITION_STARTS = { + 0x00000000L, // an image of the game partition alone + 0x0FD90000L, // a whole XGD2 disc + 0x02080000L, // a whole XGD3 disc + 0x18300000L, // a whole XGD1 disc (the original Xbox's) + }; + /** directory tables are a few sectors; anything much larger is not one */ + private static final int MAXIMUM_TABLE_SIZE = 4 * 1024 * 1024; + private static final int MAXIMUM_DEPTH = 64; + private static final int MAXIMUM_VISITS = 4096; + private static final int MAXIMUM_ENTRIES = 256; + private static final int COPY_CHUNK = 1024 * 1024; + private static final String PARTIAL_DIRECTORY = "maps.partial"; + /** Told as the copy goes: the file being copied, and the bytes copied of all the files'. */ interface Progress { void report(String file, long done, long total); } - /** a reason the player can act on */ + /** A reason the player can act on: its message is shown as it is. */ static final class ExtractException extends IOException { ExtractException(String message) { super(message); } } + /** a file or folder found in a directory table */ private static final class Entry { final String name; final long sector; @@ -111,140 +98,60 @@ final class XisoExtractor { } } - private final FileChannel image; - private long partition; - - private XisoExtractor(FileChannel image) { - this.image = image; + private XisoExtractor() { } - /** copies the image's maps folder to destination/maps */ + /** Copies the image's maps folder to destination/maps. */ static void extractMaps(FileChannel image, File destination, Progress progress) throws IOException { - new XisoExtractor(image).extract(destination, progress); - } + long imageSize = image.size(); + long partition = -1; + long rootSector = 0, rootSize = 0; + ByteBuffer descriptor = ByteBuffer.allocate(SECTOR_SIZE); - private void readAt(long offset, ByteBuffer buffer) throws IOException { - while (buffer.hasRemaining()) { - int count = image.read(buffer, offset); - - if (count <= 0) - throw new ExtractException("Could not read the disc image (is it complete?)."); - offset += count; - } - buffer.flip(); - } - - private static boolean magicAt(ByteBuffer buffer, int offset) { - for (int index = 0; index < VOLUME_MAGIC.length; index++) { - if (buffer.get(offset + index) != VOLUME_MAGIC[index]) - return false; - } - return true; - } - - /** whether a name is one to write in the maps folder: no path, no "." or - * "..", and only printable ASCII that every file system takes as a plain - * name (a disc's maps folder holds .map files and loading.tga) */ - static boolean isPlainName(String name) { - if (name.isEmpty() || name.equals(".") || name.equals("..")) - return false; - for (int index = 0; index < name.length(); index++) { - char c = name.charAt(index); - if (c < 0x20 || c > 0x7E || "/\\:*?\"<>|".indexOf(c) >= 0) - return false; - } - return true; - } - - /** the partition's volume descriptor: the root directory's sector and size */ - private long[] findVolume() throws IOException { - for (long offset : PARTITION_OFFSETS) { - ByteBuffer descriptor = ByteBuffer.allocate(SECTOR_SIZE).order(ByteOrder.LITTLE_ENDIAN); - - try { - readAt(offset + VOLUME_DESCRIPTOR_OFFSET, descriptor); - } catch (IOException e) { + for (long start : PARTITION_STARTS) { + if (start + DESCRIPTOR_OFFSET + SECTOR_SIZE > imageSize) continue; - } - if (!magicAt(descriptor, 0) || !magicAt(descriptor, 0x7EC)) + descriptor.clear(); + if (!readFully(image, descriptor, start + DESCRIPTOR_OFFSET)) continue; - partition = offset; - return new long[] { - descriptor.getInt(20) & 0xFFFFFFFFL, descriptor.getInt(24) & 0xFFFFFFFFL, - }; + byte[] bytes = descriptor.array(); + if (!hasMagic(bytes, 0) || !hasMagic(bytes, SECOND_MAGIC_OFFSET)) + continue; + partition = start; + rootSector = u32(bytes, 0x14); + rootSize = u32(bytes, 0x18); + break; } - throw new ExtractException("This is not an Xbox disc image."); - } + if (partition < 0) + throw new ExtractException("This is not an Xbox disc image."); - /** a directory's table, read whole */ - private ByteBuffer readDirectory(long sector, long size, String what) throws IOException { - if (size <= 0 || size > MAXIMUM_DIRECTORY_SIZE) - throw new ExtractException(what); - ByteBuffer table = ByteBuffer.allocate((int) size).order(ByteOrder.LITTLE_ENDIAN); - readAt(partition + sector * SECTOR_SIZE, table); - return table; - } - - /** - * collects the entries of the subtree at offset (4-byte units), files or - * directories as asked - */ - private static void walk(ByteBuffer table, long offset, int depth, boolean directories, List out, - int[] visited) { - offset *= 4; - /* a malformed table must not loop or run off its end */ - if (depth > 64 || ++visited[0] > 4096 || offset + ENTRY_HEADER_SIZE > table.limit()) - return; - int at = (int) offset; - int left = table.getShort(at) & 0xFFFF; - int right = table.getShort(at + 2) & 0xFFFF; - /* 0xFFFF: padding, an empty directory */ - if (left == 0xFFFF) - return; - int nameLength = table.get(at + 13) & 0xFF; - if (left != 0) - walk(table, left, depth + 1, directories, out, visited); - boolean isDirectory = (table.get(at + 12) & ATTRIBUTE_DIRECTORY) != 0; - if (at + ENTRY_HEADER_SIZE + nameLength <= table.limit() && nameLength > 0 && isDirectory == directories - && out.size() < MAXIMUM_FILES) { - byte[] bytes = new byte[nameLength]; - for (int index = 0; index < nameLength; index++) - bytes[index] = table.get(at + ENTRY_HEADER_SIZE + index); - String name = new String(bytes, StandardCharsets.ISO_8859_1); - /* (as extract-xiso refuses them: no name may leave the folder, and - none but a plain file name is written) */ - if (isPlainName(name)) - out.add(new Entry(name, table.getInt(at + 4) & 0xFFFFFFFFL, table.getInt(at + 8) & 0xFFFFFFFFL)); - } - if (right != 0) - walk(table, right, depth + 1, directories, out, visited); - } - - private void extract(File destination, Progress progress) throws IOException { - long[] root = findVolume(); - - /* the root's maps folder */ - ByteBuffer table = readDirectory(root[0], root[1], "The disc image's file system is damaged."); - List directories = new ArrayList<>(); - walk(table, 0, 0, true, directories, new int[1]); + // the root's maps folder + byte[] root = readTable(image, partition, rootSector, rootSize, "The disc image's file system is damaged."); Entry maps = null; - for (Entry entry : directories) { - if (entry.name.equalsIgnoreCase("maps")) + for (Entry entry : walkTable(root, true)) { + if (entry.name.equalsIgnoreCase("maps")) { maps = entry; + break; + } } + // (Ringfall: a European disc in two languages, Spanish and Italian, + // has no maps folder but one for each, maps_es and maps_it: the + // device's language's is taken, else English's, else the first, and + // its language written to maps/language.txt, which the game asks the + // Xbox for while game.language is unset: port/linux/src/xbox_xapi.c) String language = null; if (maps == null) { String wanted = Locale.getDefault().getLanguage(); Entry english = null, first = null; - for (Entry entry : directories) { + for (Entry entry : walkTable(root, true)) { String code = mapsLanguage(entry.name); if (code == null) continue; - if (code.equals(wanted)) + if (code.equals(wanted) && maps == null) maps = entry; - if (code.equals("en")) + if (code.equals("en") && english == null) english = entry; if (first == null) first = entry; @@ -257,73 +164,68 @@ final class XisoExtractor { if (maps == null) throw new ExtractException("The disc image has no maps folder: it is not a Halo disc."); - /* its files */ - table = readDirectory(maps.sector, maps.size, "The disc image's maps folder is damaged."); - List files = new ArrayList<>(); - walk(table, 0, 0, false, files, new int[1]); - long total = 0; - boolean hasUi = false; + // its files + byte[] table = readTable(image, partition, maps.sector, maps.size, "The disc image's maps folder is damaged."); + List files = walkTable(table, false); + Entry userInterface = null; for (Entry file : files) { - total += file.size; - hasUi |= file.name.equalsIgnoreCase("ui.map"); + if (file.name.equalsIgnoreCase("ui.map")) + userInterface = file; } - if (!hasUi) + if (userInterface == null) throw new ExtractException("The disc image's maps folder has no ui.map: it is not a Halo disc."); - - File partial = new File(destination, "maps.partial"); - File finished = new File(destination, "maps"); - if (!partial.isDirectory() && !partial.mkdirs()) - throw new ExtractException("Could not create " + partial + "."); - ByteBuffer buffer = ByteBuffer.allocateDirect(COPY_BUFFER_SIZE); - long done = 0; + // (an image that ends before a file does is refused before anything + // is written; an empty file's sector need not be anywhere) + long total = 0; for (Entry file : files) { - File path = new File(partial, file.name); - long offset = partition + file.sector * SECTOR_SIZE; - long remaining = file.size; + if (file.size > 0 && partition + file.sector * SECTOR_SIZE + file.size > imageSize) + throw new ExtractException(readFailure(file)); + total += file.size; + } - try (FileOutputStream out = new FileOutputStream(path)) { - FileChannel output = out.getChannel(); - - while (remaining > 0) { - buffer.clear(); - buffer.limit((int) Math.min(remaining, COPY_BUFFER_SIZE)); - readAt(offset, buffer); - int count = buffer.remaining(); - while (buffer.hasRemaining()) - output.write(buffer); - offset += count; - remaining -= count; - done += count; - progress.report(file.name, done, total); - } - } catch (ExtractException e) { - throw new ExtractException("Could not read " + file.name + " from the disc image (is it complete?)."); - } catch (IOException e) { - throw new ExtractException("Could not write " + path + " (is the storage full?)."); + // the copy, to maps.partial + File partial = new File(destination, PARTIAL_DIRECTORY); + if (partial.isDirectory()) { + File[] leftovers = partial.listFiles(); + if (leftovers != null) { + for (File leftover : leftovers) + leftover.delete(); } + } else { + partial.delete(); + partial.mkdirs(); } + if (!partial.isDirectory()) + throw new ExtractException("Could not create " + partial.getPath() + "."); + ByteBuffer buffer = ByteBuffer.allocate(COPY_CHUNK); + long done = 0; + for (Entry file : files) + done = copyFile(image, partition, file, new File(partial, file.name), buffer, done, total, progress); - /* (the maps folder may be there, empty: the app makes it for adb) */ - if (!finished.isDirectory() && !finished.mkdirs()) - throw new ExtractException("Could not create " + finished + "."); - for (Entry file : files) { - File from = new File(partial, file.name); - File to = new File(finished, file.name); - - to.delete(); - if (!from.renameTo(to)) - throw new ExtractException("Could not move " + file.name + " into " + finished + "."); - } - partial.delete(); - File languageFile = new File(finished, "language.txt"); + // then into maps, ui.map (the app's sign that the data is there) last + File mapsFolder = new File(destination, "maps"); + if (!mapsFolder.isDirectory() && !mapsFolder.mkdirs() && !mapsFolder.isDirectory()) + throw new ExtractException("Could not create " + mapsFolder.getPath() + "."); + // (the language of a language's maps folder: before ui.map is there) + File languageFile = new File(mapsFolder, "language.txt"); languageFile.delete(); if (language != null) { try (FileOutputStream out = new FileOutputStream(languageFile)) { out.write((language + "\n").getBytes(StandardCharsets.US_ASCII)); } catch (IOException e) { - throw new ExtractException("Could not write " + languageFile + " (is the storage full?)."); + throw new ExtractException("Could not write " + languageFile.getPath() + " (is the storage full?)."); } } + List order = new ArrayList<>(files); + order.remove(userInterface); + order.add(userInterface); + for (Entry file : order) { + File target = new File(mapsFolder, file.name); + target.delete(); + if (!new File(partial, file.name).renameTo(target)) + throw new ExtractException("Could not move " + file.name + " into " + mapsFolder.getPath() + "."); + } + partial.delete(); } /** a language's maps folder's language ("maps_it": "it"), else null */ @@ -333,4 +235,190 @@ final class XisoExtractor { String code = name.substring(5).toLowerCase(Locale.ROOT); return Character.isLetter(code.charAt(0)) && Character.isLetter(code.charAt(1)) ? code : null; } + + /** No path, no . or .., and only printable ASCII that every file system takes as a plain name. */ + static boolean isPlainName(String name) { + if (name == null || name.isEmpty() || name.equals(".") || name.equals("..")) + return false; + for (int index = 0; index < name.length(); index++) { + char character = name.charAt(index); + + if (character < 0x20 || character > 0x7E || "/\\:*?\"<>|".indexOf(character) >= 0) + return false; + } + return true; + } + + /* ---------- the image */ + + private static long u16(byte[] bytes, int offset) { + return (bytes[offset] & 0xFF) | (bytes[offset + 1] & 0xFF) << 8; + } + + private static long u32(byte[] bytes, int offset) { + return u16(bytes, offset) | u16(bytes, offset + 2) << 16; + } + + private static boolean hasMagic(byte[] bytes, int offset) { + for (int index = 0; index < MAGIC.length; index++) { + if (bytes[offset + index] != MAGIC[index]) + return false; + } + return true; + } + + /** Fills buffer from the image at position; false if the image ends first. */ + private static boolean readFully(FileChannel image, ByteBuffer buffer, long position) throws IOException { + while (buffer.hasRemaining()) { + int count = image.read(buffer, position); + + if (count <= 0) + return false; + position += count; + } + return true; + } + + private static byte[] readTable(FileChannel image, long partition, long sector, long size, String damaged) + throws IOException { + if (size == 0 || size > MAXIMUM_TABLE_SIZE) + throw new ExtractException(damaged); + ByteBuffer table = ByteBuffer.allocate((int) size); + if (!readFully(image, table, partition + sector * SECTOR_SIZE)) + throw new ExtractException("Could not read the disc image (is it complete?)."); + return table.array(); + } + + /* ---------- directory tables */ + + private static final class Walk { + final byte[] table; + final boolean directories; + final BitSet visited = new BitSet(); + final List entries = new ArrayList<>(); + int visits; + boolean stopped; + + Walk(byte[] table, boolean directories) { + this.table = table; + this.directories = directories; + } + } + + /** The folders (or the files) of a table with plain names, in the tree's order. */ + private static List walkTable(byte[] table, boolean directories) { + Walk walk = new Walk(table, directories); + + walkEntry(walk, 0, 0); + return walk.entries; + } + + /** + * The subtree whose root entry is at offset: its left subtree, the + * entry, its right subtree. A link to padding, past the table or back to + * an entry already visited ends that branch. + */ + private static void walkEntry(Walk walk, int offset, int depth) { + byte[] table = walk.table; + + if (walk.stopped || depth > MAXIMUM_DEPTH) + return; + if (offset >= table.length || table.length - offset < ENTRY_FIXED_SIZE) + return; + int left = (int) u16(table, offset); + int right = (int) u16(table, offset + 2); + if (left == PADDING_LINK || walk.visited.get(offset / 4)) + return; + walk.visited.set(offset / 4); + if (walk.visits >= MAXIMUM_VISITS) { + walk.stopped = true; + return; + } + walk.visits++; + + if (left != 0) + walkEntry(walk, left * 4, depth + 1); + int nameLength = table[offset + 13] & 0xFF; + if (nameLength != 0 && nameLength <= table.length - offset - ENTRY_FIXED_SIZE) + collect(walk, offset, nameLength); + if (right != 0) + walkEntry(walk, right * 4, depth + 1); + } + + private static void collect(Walk walk, int offset, int nameLength) { + byte[] table = walk.table; + boolean directory = (table[offset + 12] & ATTRIBUTE_DIRECTORY) != 0; + + if (directory != walk.directories || walk.entries.size() >= MAXIMUM_ENTRIES) + return; + String name = new String(table, offset + ENTRY_FIXED_SIZE, nameLength, StandardCharsets.ISO_8859_1); + if (!isPlainName(name)) + return; + // (the destination's file system may ignore case) + for (Entry entry : walk.entries) { + if (entry.name.equalsIgnoreCase(name)) + return; + } + walk.entries.add(new Entry(name, u32(table, offset + 4), u32(table, offset + 8))); + } + + /* ---------- the copy */ + + private static String readFailure(Entry file) { + return "Could not read " + file.name + " from the disc image (is it complete?)."; + } + + /** Copies one file of the image to output; returns the bytes copied of all the files so far. */ + private static long copyFile(FileChannel image, long partition, Entry file, File output, ByteBuffer buffer, + long done, long total, Progress progress) throws IOException { + String writeFailure = "Could not write " + output.getPath() + " (is the storage full?)."; + long position = partition + file.sector * SECTOR_SIZE; + long remaining = file.size; + FileOutputStream stream = null; + + try { + stream = new FileOutputStream(output); + FileChannel channel = stream.getChannel(); + while (remaining > 0) { + int count = (int) Math.min(remaining, COPY_CHUNK); + boolean read; + + buffer.clear(); + buffer.limit(count); + try { + read = readFully(image, buffer, position); + } catch (IOException exception) { + read = false; + } + if (!read) + throw new ExtractException(readFailure(file)); + buffer.flip(); + while (buffer.hasRemaining()) { + if (channel.write(buffer) <= 0) + throw new ExtractException(writeFailure); + } + position += count; + remaining -= count; + done += count; + if (progress != null) + progress.report(file.name, done, total); + } + FileOutputStream closing = stream; + stream = null; + closing.close(); + } catch (ExtractException exception) { + throw exception; + } catch (IOException exception) { + throw new ExtractException(writeFailure); + } finally { + if (stream != null) { + try { + stream.close(); + } catch (IOException ignored) { + // (the copy failed already) + } + } + } + return done; + } } diff --git a/port/android/steam_frame_xiso.py b/port/android/steam_frame_xiso.py index 401ad4d3..5a57f1aa 100644 --- a/port/android/steam_frame_xiso.py +++ b/port/android/steam_frame_xiso.py @@ -1,66 +1,50 @@ #!/usr/bin/env python3 """ -Copies the maps folder out of an Xbox disc image (an "xiso", or a whole -disc's ".iso") on the Steam Frame's SteamOS side, for its installer -(steam_frame_install.sh). It reads the image as the app's own import does +Copies the maps folder out of the player's Xbox disc image of Halo (an +"xiso", or a whole disc's ".iso") on the Steam Frame's SteamOS side, for +its installer (steam_frame_install.sh), as the app's own import does (app/src/main/java/com/halo/decomp/XisoExtractor.java, which this follows; the desktop games' is port/linux/src/xiso.c). steam_frame_xiso.py -writes /maps (through maps.partial, so an interrupted copy -never leaves maps/ui.map behind) and prints its progress for zenity ---progress: a percentage on a line of its own, and "# " lines. On -failure it prints "error: " to stderr and -exits 1. +writes /maps (through maps.partial, ui.map moved in last, so +an interrupted copy never leaves maps/ui.map behind) and prints its +progress for zenity --progress: a percentage on a line of its own, and +"# " lines. On failure it prints "error: " to stderr and exits 1. -The image's file system is XDVDFS, read as extract-xiso does -(https://github.com/XboxDev/extract-xiso, extract-xiso.c, whose format -handling this follows; its license is below and in -port/third_party/extract-xiso/LICENSE.TXT): 2048-byte sectors; a volume -descriptor at 0x10000 that starts and ends with "MICROSOFT*XBOX*MEDIA" and -gives the root directory's sector and size; and directories whose entries -form a binary tree (each entry: left and right subtree offsets in 4-byte -units, the start sector, the size, attributes, the name's length and the -name). Images made from a whole disc put the game partition further in, at -one of the offsets below. +Written from a description of the Xbox's disc file system, XDVDFS +(xboxdevwiki's XDVDFS page, https://xboxdevwiki.net/XDVDFS), as +XisoExtractor.java is: -Some parts of this code are copyright in@fishtank.com. This product -includes software developed by in . +- Sectors are 2048 bytes and integers little-endian. Sector numbers count + from the start of the game partition: an image of the partition alone has + it at 0, an image of a whole disc further in (PARTITION_STARTS, tried in + that order). +- The volume descriptor is the partition's sector 32 (at 0x10000). It holds + "MICROSOFT*XBOX*MEDIA" at its start and again at 0x7EC, and the root + directory table's start sector (u32 at 0x14) and size in bytes (u32 at + 0x18). +- A directory table is a binary tree of entries whose root is the entry at + offset 0. An entry is a left and a right subtree link (u16 each, in 4-byte + units from the table's start, 0 for none), the start sector and the size + of the file or of the subdirectory's table (u32 each), the attributes (u8, + 0x10 for a directory), the name's length (u8) and the name. Unused bytes + are 0xFF, so a left link of 0xFFFF is padding, not an entry. +- A file is one contiguous run of bytes from the start of its sector. -Copyright (c) 2003 in -All rights reserved. +The image is the player's file and nothing read from it is trusted: only +the root's table and the maps folder's are read, each whole and at most +4 MB; a table's walk visits each offset once, goes at most 64 links deep +and looks at most 4096 entries; and a file is written only under a plain +name that no file before it had (ignoring case). -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -3. All advertising materials mentioning features or use of this software - must display the following acknowledgement: - - This product includes software developed by in . - -4. Neither the name of "in" nor the email address "in@fishtank.com" - may be used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED `AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES -INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -AUTHOR OR ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; -OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF -ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +A European disc in two languages (Spanish and Italian) has no maps folder +but one for each, maps_es and maps_it: the one for the language asked for +(RINGFALL_LANGUAGE, else the system's) is taken, else English's, else the +first, and its language written to maps/language.txt, which the game takes +as the Xbox's language while game.language is unset. """ import os @@ -68,74 +52,100 @@ import struct import sys SECTOR_SIZE = 2048 -VOLUME_DESCRIPTOR_OFFSET = 0x10000 -ENTRY_HEADER_SIZE = 14 +DESCRIPTOR_OFFSET = 0x10000 +MAGIC = b"MICROSOFT*XBOX*MEDIA" +SECOND_MAGIC_OFFSET = 0x7EC +ENTRY_FIXED_SIZE = 14 ATTRIBUTE_DIRECTORY = 0x10 +PADDING_LINK = 0xFFFF +# where the game partition may start: an image of the partition alone, a +# whole XGD2, XGD3 and XGD1 (the original Xbox's) disc +PARTITION_STARTS = (0x00000000, 0x0FD90000, 0x02080000, 0x18300000) # directory tables are a few sectors; anything much larger is not one -MAXIMUM_DIRECTORY_SIZE = 4 << 20 -MAXIMUM_FILES = 256 -COPY_BUFFER_SIZE = 1 << 20 -VOLUME_MAGIC = b"MICROSOFT*XBOX*MEDIA" -# where the game partition starts: a plain image, and whole-disc images -# (extract-xiso's GLOBAL, XGD3 and XGD1 offsets) -PARTITION_OFFSETS = (0, 0x0FD90000, 0x02080000, 0x18300000) +MAXIMUM_TABLE_SIZE = 4 << 20 +MAXIMUM_DEPTH = 64 +MAXIMUM_VISITS = 4096 +MAXIMUM_ENTRIES = 256 +COPY_CHUNK = 1 << 20 +PARTIAL_DIRECTORY = "maps.partial" class ExtractError(Exception): """a reason the player can act on""" -def read_at(image, offset, size): - image.seek(offset) +def read_exactly(image, position, size): + """size bytes of the image from position; None if it ends first""" + image.seek(position) data = image.read(size) - if len(data) != size: - raise ExtractError("Could not read the disc image (is it complete?).") - return data + return data if len(data) == size else None -def find_volume(image): - """the game partition's offset, and the root directory's sector and size""" - for offset in PARTITION_OFFSETS: - try: - descriptor = read_at(image, offset + VOLUME_DESCRIPTOR_OFFSET, SECTOR_SIZE) - except ExtractError: +def find_partition(image, image_size): + """the game partition's start, and the root table's sector and size""" + for start in PARTITION_STARTS: + if start + DESCRIPTOR_OFFSET + SECTOR_SIZE > image_size: continue - if descriptor[:20] != VOLUME_MAGIC or descriptor[0x7EC:0x7EC + 20] != VOLUME_MAGIC: + descriptor = read_exactly(image, start + DESCRIPTOR_OFFSET, SECTOR_SIZE) + if (descriptor is None or descriptor[:len(MAGIC)] != MAGIC or + descriptor[SECOND_MAGIC_OFFSET:SECOND_MAGIC_OFFSET + len(MAGIC)] != MAGIC): continue - sector, size = struct.unpack_from(" MAXIMUM_DIRECTORY_SIZE: - raise ExtractError(what) - return read_at(image, partition + sector * SECTOR_SIZE, size) +def read_table(image, partition, sector, size, damaged): + if size == 0 or size > MAXIMUM_TABLE_SIZE: + raise ExtractError(damaged) + table = read_exactly(image, partition + sector * SECTOR_SIZE, size) + if table is None: + raise ExtractError("Could not read the disc image (is it complete?).") + return table -def walk(table, offset, depth, directories, out, visited): - """collects the entries (name, sector, size) of the subtree at offset - (4-byte units), files or directories as asked""" - at = offset * 4 - # a malformed table must not loop or run off its end - visited[0] += 1 - if depth > 64 or visited[0] > 4096 or at + ENTRY_HEADER_SIZE > len(table): - return - left, right, sector, size, attributes, name_length = struct.unpack_from(" 0 - and is_directory == directories and len(out) < MAXIMUM_FILES): - name = table[at + ENTRY_HEADER_SIZE:at + ENTRY_HEADER_SIZE + name_length].decode("latin-1") - # (as extract-xiso refuses them: no name may leave the folder) - if name not in (".", "..") and "/" not in name and "\\" not in name: - out.append((name, sector, size)) - if right: - walk(table, right, depth + 1, directories, out, visited) +def is_plain_name(name): + """no path, no . or .., and only printable ASCII that every file system + takes as a plain name""" + if not name or name in (".", ".."): + return False + return all(0x20 <= ord(character) <= 0x7E and character not in '/\\:*?"<>|' for character in name) + + +def walk_table(table, directories): + """the folders (or the files) of a table with plain names, as (name, + sector, size), in the tree's order: each entry's left subtree, the entry, + its right subtree; a link to padding, past the table or back to an entry + already visited ends that branch""" + entries, visited = [], set() + visits = [0] + + def walk(offset, depth): + if depth > MAXIMUM_DEPTH or visits[0] >= MAXIMUM_VISITS: + return + if offset >= len(table) or len(table) - offset < ENTRY_FIXED_SIZE: + return + left, right = struct.unpack_from(" image_size: + raise ExtractError("Could not read %s from the disc image (is it complete?)." % name) total = sum(size for _, _, size in files) or 1 - partial = os.path.join(destination, "maps.partial") - finished = os.path.join(destination, "maps") + # the copy, to maps.partial (emptied of what an earlier attempt left) + partial = os.path.join(destination, PARTIAL_DIRECTORY) try: os.makedirs(partial, exist_ok=True) + for leftover in os.listdir(partial): + os.remove(os.path.join(partial, leftover)) except OSError: raise ExtractError("Could not create %s." % partial) - done = 0 - shown = -1 + done, shown = 0, -1 for name, sector, size in files: path = os.path.join(partial, name) - offset = partition + sector * SECTOR_SIZE - remaining = size + position, remaining = partition + sector * SECTOR_SIZE, size print("# Copying maps/%s" % name, flush=True) try: with open(path, "wb") as out: while remaining > 0: - count = min(remaining, COPY_BUFFER_SIZE) - out.write(read_at(image, offset, count)) - offset += count + count = min(remaining, COPY_CHUNK) + data = read_exactly(image, position, count) + if data is None: + raise ExtractError("Could not read %s from the disc image (is it complete?)." % name) + out.write(data) + position += count remaining -= count done += count # (zenity closes at 100: that is left to the caller) @@ -211,23 +223,24 @@ def extract(image_path, destination): if percent != shown: print(percent, flush=True) shown = percent - except ExtractError: - raise ExtractError("Could not read %s from the disc image (is it complete?)." % name) except OSError: raise ExtractError("Could not write %s (is the storage full?)." % path) - # (the maps folder may be there, empty) + # then into maps (which may be there, empty), the language before and + # ui.map (the sign that the data is there) last + finished = os.path.join(destination, "maps") try: os.makedirs(finished, exist_ok=True) - for name, _, _ in files: - os.replace(os.path.join(partial, name), os.path.join(finished, name)) - os.rmdir(partial) language_file = os.path.join(finished, "language.txt") if os.path.exists(language_file): os.remove(language_file) if language: with open(language_file, "w") as out: out.write(language + "\n") + order = [file for file in files if file is not user_interface] + [user_interface] + for name, _, _ in order: + os.replace(os.path.join(partial, name), os.path.join(finished, name)) + os.rmdir(partial) except OSError as error: raise ExtractError("Could not move the maps into %s (%s)." % (finished, error.strerror)) diff --git a/port/linux/game/cache_file_formats.c b/port/linux/game/cache_file_formats.c index 9f856c6e..8a691c7c 100644 --- a/port/linux/game/cache_file_formats.c +++ b/port/linux/game/cache_file_formats.c @@ -14,11 +14,11 @@ Sources for the layouts, cited per structure below: - BlamLib, in the same archive: the font and unicode string list layouts. - Reclaimer (GPL-3.0, Gravemind2401; the Reclaimer-master archive): how bitmap tags and pixel data are found in bitmaps.map. -No code from either project is used: they are documentation here, and this -repository is CC0. Facts that neither source states were established by -reading the sample Custom Edition maps; the comments below say so where they -are used ("observed", "every map examined"), and -docs/custom_edition_caches.md lists each with its evidence. +No code from either project is used: they are documentation here. Facts +that neither source states were established by reading the sample Custom +Edition maps; the comments below say so where they are used ("observed", +"every map examined"), and docs/custom_edition_caches.md lists each with its +evidence. */ /* ---------- headers */ diff --git a/port/linux/src/p2p_webrtc.c b/port/linux/src/p2p_webrtc.c index 914a694c..d0cc6f03 100644 --- a/port/linux/src/p2p_webrtc.c +++ b/port/linux/src/p2p_webrtc.c @@ -24,7 +24,15 @@ play's, sealed as to any peer; WebRTC is only their way through. the tunnel's datagrams, whose streams KCP makes reliable, as on UDP). Messages larger than a packet are split and joined again. What this end sends and the peer has not acknowledged in ABANDON_TIME is given up - (FORWARD-TSN, RFC 3758); what it receives is acknowledged at once. + (FORWARD-TSN, RFC 3758); what it receives is acknowledged at once, every + run past a gap (a SACK of only some has the peer count the others in + flight, and fill its window), and again each SACK_INTERVAL while a gap + stays. What is missing before a TSN that came GAP_TIME ago is given up as + the peer's FORWARD-TSN would give it up (the channel's messages are + never sent again, and the peer sent it before that TSN): Firefox's SCTP, + after one loss, gives up others only once its cumulative acknowledgement + has passed the first, which the gaps after it stop, and the channel would + stall for seconds, then fall out of TSN_WINDOW. Everything runs on the p2p thread, under p2p_lock. */ @@ -54,7 +62,9 @@ enum TSN_WINDOW = 1024, /* fragments of messages kept for joining */ MAXIMUM_FRAGMENTS = 16, - MAXIMUM_GAP_BLOCKS = 16, + /* the runs past the cumulative TSN a SACK reports: all of them, at a + tenth of the datagrams lost (a SACK of 12 + 4 * this bytes) */ + MAXIMUM_GAP_BLOCKS = 128, SCTP_PORT = 5000, /* the window this end says it has: messages are passed on at once */ RECEIVE_WINDOW = 1 << 20, @@ -65,6 +75,10 @@ enum PUNCH_TIME = 30000, ABANDON_TIME = 300, FORWARD_INTERVAL = 200, + SACK_INTERVAL = 200, + /* a gap given up by this end (longer than ABANDON_TIME: what is only + late still comes, and the peer may give it up first) */ + GAP_TIME = 1000, /* a path not checked in this long is not one the browser uses */ PATH_TIME = 30000, }; @@ -178,7 +192,11 @@ struct connection unsigned int cumulative; unsigned int highest; unsigned char received[TSN_WINDOW]; + /* (system time) when each TSN past the cumulative one came */ + unsigned long arrived[TSN_WINDOW]; int sack_due; + /* when the last SACK went */ + unsigned long sack_time; struct fragment fragments[MAXIMUM_FRAGMENTS]; /* sending: the next TSN, the peer's cumulative acknowledgement, when each TSN past it was sent (by TSN modulo TSN_WINDOW), and the last @@ -528,6 +546,7 @@ static void sctp_send_sack(struct connection *connection) put_short(value + 10, 0); sctp_send_chunk(connection, _chunk_sack, 0, value, 12 + blocks * 4); connection->sack_due = 0; + connection->sack_time = p2p_now(); } /* a message on the channel (stream 0, unordered), split as needed */ @@ -663,6 +682,7 @@ static void data_received(struct connection *connection, int flags, const unsign return; } connection->received[tsn % TSN_WINDOW] = 1; + connection->arrived[tsn % TSN_WINDOW] = p2p_now(); if (tsn_after(tsn, connection->highest)) connection->highest = tsn; while (connection->received[(connection->cumulative + 1) % TSN_WINDOW] && @@ -712,14 +732,13 @@ static void data_received(struct connection *connection, int flags, const unsign } } -static void forward_tsn_received(struct connection *connection, const unsigned char *value, int length) +/* what the peer gave up (FORWARD-TSN), or this end did: the cumulative TSN +moved to forward and past what came after it, and the fragments of messages +given up let go */ +static void skip_to(struct connection *connection, unsigned int forward) { - unsigned int forward; int index; - if (length < 4) - return; - forward = get_long(value); connection->sack_due = 1; if (!tsn_after(forward, connection->cumulative)) return; @@ -750,6 +769,35 @@ static void forward_tsn_received(struct connection *connection, const unsigned c } } +static void forward_tsn_received(struct connection *connection, const unsigned char *value, int length) +{ + if (length >= 4) + skip_to(connection, get_long(value)); +} + +/* the gaps past the cumulative TSN: what is missing before a TSN that came +GAP_TIME ago given up, and the rest told again each SACK_INTERVAL */ +static void gap_check(struct connection *connection) +{ + unsigned int tsn; + + if (!tsn_after(connection->highest, connection->cumulative)) + return; + for (tsn = connection->highest; tsn_after(tsn, connection->cumulative); tsn--) + { + if (connection->received[tsn % TSN_WINDOW] && elapsed(connection->arrived[tsn % TSN_WINDOW], GAP_TIME)) + { + skip_to(connection, tsn); + break; + } + } + if (connection->sack_due || (tsn_after(connection->highest, connection->cumulative) && + elapsed(connection->sack_time, SACK_INTERVAL))) + { + sctp_send_sack(connection); + } +} + static void sack_received(struct connection *connection, const unsigned char *value, int length) { unsigned int cumulative; @@ -1138,7 +1186,10 @@ int p2p_webrtc_update(void) /* (the handshake's retransmissions) */ connection_receive(connection); if (connection->state == _sctp_established) + { sctp_forward(connection); + gap_check(connection); + } else if (!elapsed(connection->created_time, PUNCH_TIME) && elapsed(connection->punch_time, PUNCH_INTERVAL)) punch(connection); } diff --git a/port/linux/src/updater.c b/port/linux/src/updater.c index fb466496..c99488c5 100644 --- a/port/linux/src/updater.c +++ b/port/linux/src/updater.c @@ -522,13 +522,15 @@ static void updater_update(void) } } -/* the files a previous update left behind */ +/* the files a previous update left behind, and the notice of extract-xiso, +which releases carried until the disc image readers were written again */ static void updater_clean_up(void) { static const char *const names[] = { "halo.old", "halo.exe.old", "SDL3.dll.old", "libSDL3.so.0.old", "SDL3-LICENSE.txt.old", - "extract-xiso-LICENSE.txt.old", "mbedtls-LICENSE.txt.old", + "extract-xiso-LICENSE.txt.old", "mbedtls-LICENSE.txt.old", "LICENSE.txt.old", + "extract-xiso-LICENSE.txt", }; char path[1200]; size_t index; diff --git a/port/linux/src/xiso.c b/port/linux/src/xiso.c index 84fae910..e2097950 100644 --- a/port/linux/src/xiso.c +++ b/port/linux/src/xiso.c @@ -1,307 +1,408 @@ /* XISO.C -Copies the maps folder out of an Xbox disc image (an "xiso"), for the -desktop ports' first start without game data (sdl_platform.c). +The maps folder out of an Xbox disc image, for the desktop games (Linux and +Windows). When no data root has a maps folder, sdl_platform.c asks the player +for their disc image of Halo and this copies the image's maps folder next to +the executable. (The Android app's copy of this is XisoExtractor.java, the +web page's xiso-worker.js.) -The image's file system is XDVDFS, read as extract-xiso does -(https://github.com/XboxDev/extract-xiso, extract-xiso.c, whose format -handling this follows; its license is below and in -port/third_party/extract-xiso/LICENSE.TXT): 2048-byte sectors; a -volume descriptor at 0x10000 that starts and ends with -"MICROSOFT*XBOX*MEDIA" and gives the root directory's sector and size; and -directories whose entries form a binary tree (each entry: left and right -subtree offsets in 4-byte units, the start sector, the size, attributes, the -name's length and the name, 4-byte aligned). Images made from a whole disc -put the game partition further in, at one of the offsets below. +Written from a description of the Xbox's disc file system, XDVDFS +(xboxdevwiki's XDVDFS page, https://xboxdevwiki.net/XDVDFS, and the format +facts OpenCE's readers rely on). What this file relies on: -The files are written to /maps.partial first, which becomes -/maps once they all are, so an interrupted extraction never -leaves a maps folder that looks complete. +- Sectors are 2048 bytes and integers little-endian. Sector numbers count + from the start of the game partition: an image of the partition alone has + it at 0, an image of a whole disc further in (0x0FD90000, 0x02080000 or + 0x18300000 for XGD2, XGD3 and XGD1 discs). They are tried in that order. +- The volume descriptor is the partition's sector 32 (at 0x10000). It holds + "MICROSOFT*XBOX*MEDIA" at its start and again at 0x7EC, and the root + directory table's start sector (u32 at 0x14) and size in bytes (u32 at + 0x18). +- A directory table is a binary tree of entries whose root is the entry at + offset 0. An entry is a left and a right subtree link (u16 each, in 4-byte + units from the table's start, 0 for none), the start sector and the size + of the file or of the subdirectory's table (u32 each), the attributes (u8, + 0x10 for a directory), the name's length (u8) and the name. Unused bytes of + a table are 0xFF, so a left link of 0xFFFF is padding, not an entry. +- A file is one contiguous run of bytes from the start of its sector. -Some parts of this code are copyright in@fishtank.com. This product -includes software developed by in . +The image is the player's file and nothing read from it is trusted. Only the +root's table and the maps folder's are read, each whole and at most 4 MB; +the walk of a table visits each offset once, goes at most 64 links deep and +looks at most 4096 entries; and a file is written only under a plain name +that no file before it had (ignoring case). - * Copyright (c) 2003 in - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * 3. All advertising materials mentioning features or use of this software - * must display the following acknowledgement: - * - * This product includes software developed by in . - * - * 4. Neither the name of "in" nor the email address "in@fishtank.com" - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED `AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * AUTHOR OR ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; - * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR - * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +The game takes a data root as having data once it has a maps folder, so the +copy is written to maps.partial (emptied first of what an earlier attempt +left) and given the name maps only when every file is complete. */ -/* (the desktop ports only: the Android app and the web page import the game -data themselves) */ -#if !defined(HALO_ANDROID) && !defined(HALO_WEB) - #include "platform.h" -#include "posix.h" #include "xiso.h" +#if !defined(HALO_ANDROID) && !defined(HALO_WEB) + +#include "posix.h" + +#include #include +#include #include #include #include #include +/* (images are larger than 4 GB; Windows has neither flag) */ #ifndef O_LARGEFILE #define O_LARGEFILE 0 #endif -#ifndef O_CLOEXEC -#define O_CLOEXEC 0 -#endif #ifndef O_NOFOLLOW #define O_NOFOLLOW 0 #endif -enum +#define XISO_SECTOR_SIZE 2048 +#define XISO_DESCRIPTOR_OFFSET 0x10000 +#define XISO_MAGIC "MICROSOFT*XBOX*MEDIA" +#define XISO_MAGIC_LENGTH 20 +#define XISO_SECOND_MAGIC_OFFSET 0x7EC +#define XISO_ENTRY_FIXED_SIZE 14 +#define XISO_ATTRIBUTE_DIRECTORY 0x10 +#define XISO_PADDING_LINK 0xFFFF + +/* directory tables are a few sectors; anything much larger is not one */ +#define XISO_MAXIMUM_TABLE_SIZE (4 * 1024 * 1024) +#define XISO_MAXIMUM_DEPTH 64 +#define XISO_MAXIMUM_VISITS 4096 +#define XISO_MAXIMUM_ENTRIES 256 +#define XISO_MAXIMUM_PATH 4096 +#define XISO_COPY_CHUNK (1024 * 1024) + +#define XISO_PARTIAL_DIRECTORY "maps.partial" + +/* where the game partition may start, in the order they are tried */ +static const unsigned long xiso_partition_starts[] = { - SECTOR_SIZE = 2048, - VOLUME_DESCRIPTOR_OFFSET = 0x10000, - ENTRY_HEADER_SIZE = 14, - ATTRIBUTE_DIRECTORY = 0x10, - /* directory tables are a few sectors; anything much larger is not one */ - MAXIMUM_DIRECTORY_SIZE = 4 << 20, - MAXIMUM_FILES = 256, - COPY_BUFFER_SIZE = 1 << 20, -}; - -static const char volume_magic[] = "MICROSOFT*XBOX*MEDIA"; - -/* where the game partition starts: a plain image, and whole-disc images -(extract-xiso's GLOBAL, XGD3 and XGD1 offsets) */ -static const unsigned long long partition_offsets[] = { 0, 0x0FD90000ull, 0x02080000ull, 0x18300000ull }; - -struct xiso_file -{ - char name[256]; - unsigned long sector; - unsigned long size; + 0x00000000, /* an image of the game partition alone */ + 0x0FD90000, /* a whole XGD2 disc */ + 0x02080000, /* a whole XGD3 disc */ + 0x18300000, /* a whole XGD1 disc (the original Xbox's) */ }; struct xiso_image { int descriptor; unsigned long long partition; - char *error; - int error_size; + /* the image's size in bytes, or ~0 when it could not be found */ + unsigned long long size; }; -static int name_is_plain(const char *name); - -static unsigned long read_u32(const unsigned char *bytes) +/* a file or folder found in a directory table */ +struct xiso_entry { - return (unsigned long)bytes[0] | (unsigned long)bytes[1] << 8 | (unsigned long)bytes[2] << 16 | - (unsigned long)bytes[3] << 24; + char name[256]; + unsigned long sector; + unsigned long size; +}; + +struct xiso_walk +{ + const unsigned char *table; + unsigned long size; + /* a bit for each 4-byte offset of the table, set once it is visited */ + unsigned char *visited; + int visits; + int stopped; + /* whether folders are collected (or else files) */ + int directories; + struct xiso_entry *entries; + int entry_count; +}; + +/* ---------- errors */ + +static void xiso_error(char *error, int error_size, const char *format, ...) +{ + va_list arguments; + + if (!error || error_size <= 0) + return; + va_start(arguments, format); + vsnprintf(error, (size_t)error_size, format, arguments); + va_end(arguments); + error[error_size - 1] = 0; } -static int fail(struct xiso_image *image, const char *format, const char *detail) +/* ---------- names */ + +static int xiso_lower(int character) { - snprintf(image->error, (size_t)image->error_size, format, detail ? detail : ""); - return 0; + return character >= 'A' && character <= 'Z' ? character - 'A' + 'a' : character; } -/* reads size bytes at offset (from the start of the file) */ -static int read_at(struct xiso_image *image, unsigned long long offset, void *buffer, unsigned long size) +/* whether a name of length bytes is name (a C string), ignoring ASCII case */ +static int xiso_name_is(const char *bytes, int length, const char *name) { - posix_ulong low, high; - unsigned char *cursor = buffer; + int index; - if (posix_seek(image->descriptor, (posix_long)(offset & 0xFFFFFFFFu), (posix_long)(offset >> 32), SEEK_SET, - &low, &high) != 0) - { + if ((int)strlen(name) != length) return 0; - } - while (size > 0) + for (index = 0; index < length; index++) { - long count = (long)read(image->descriptor, cursor, size > COPY_BUFFER_SIZE ? COPY_BUFFER_SIZE : size); - - if (count <= 0) + if (xiso_lower((unsigned char)bytes[index]) != xiso_lower((unsigned char)name[index])) return 0; - cursor += count; - size -= (unsigned long)count; } return 1; } -/* the partition's volume descriptor: the root directory's sector and size */ -static int find_volume(struct xiso_image *image, unsigned long *root_sector, unsigned long *root_size) +/* no path, no . or .., and only printable ASCII that every desktop file +system takes as a plain name */ +static int xiso_plain_name(const unsigned char *name, int length) { int index; - for (index = 0; index < (int)(sizeof(partition_offsets) / sizeof(*partition_offsets)); index++) + if (length <= 0 || (length == 1 && name[0] == '.') || (length == 2 && name[0] == '.' && name[1] == '.')) + return 0; + for (index = 0; index < length; index++) { - unsigned char descriptor[SECTOR_SIZE]; - unsigned long long offset = partition_offsets[index] + VOLUME_DESCRIPTOR_OFFSET; - - if (!read_at(image, offset, descriptor, sizeof(descriptor))) - continue; - if (memcmp(descriptor, volume_magic, 20) || memcmp(descriptor + 0x7EC, volume_magic, 20)) - continue; - image->partition = partition_offsets[index]; - *root_sector = read_u32(descriptor + 20); - *root_size = read_u32(descriptor + 24); - return 1; + if (name[index] < 0x20 || name[index] > 0x7E || strchr("/\\:*?\"<>|", name[index])) + return 0; } - return fail(image, "This is not an Xbox disc image.%s", NULL); + return 1; } -/* a directory's table, read whole */ -static unsigned char *read_directory(struct xiso_image *image, unsigned long sector, unsigned long size) +/* ---------- the image */ + +static unsigned long xiso_u16(const unsigned char *bytes) +{ + return (unsigned long)bytes[0] | (unsigned long)bytes[1] << 8; +} + +static unsigned long xiso_u32(const unsigned char *bytes) +{ + return xiso_u16(bytes) | xiso_u16(bytes + 2) << 16; +} + +/* reads size bytes at offset (from the start of the image file); returns +nonzero if all of them were read */ +static int xiso_read(struct xiso_image *image, unsigned long long offset, void *buffer, unsigned long size) +{ + posix_ulong position_low, position_high; + unsigned long count = 0; + + if (posix_seek(image->descriptor, (posix_long)(offset & 0xFFFFFFFFUL), (posix_long)(offset >> 32), SEEK_SET, + &position_low, &position_high) != 0) + { + return 0; + } + while (count < size) + { + int result = (int)read(image->descriptor, (char *)buffer + count, (unsigned int)(size - count)); + + if (result < 0 && errno == EINTR) + continue; + if (result <= 0) + return 0; + count += (unsigned long)result; + } + return 1; +} + +/* the byte offset of a sector of the game partition */ +static unsigned long long xiso_sector_offset(struct xiso_image *image, unsigned long sector) +{ + return image->partition + (unsigned long long)sector * XISO_SECTOR_SIZE; +} + +/* finds the game partition: the first start whose volume descriptor has +both magics. Returns the root table's sector and size */ +static int xiso_find_volume(struct xiso_image *image, unsigned long *root_sector, unsigned long *root_size) +{ + unsigned char descriptor[XISO_SECTOR_SIZE]; + int index; + + for (index = 0; index < (int)(sizeof(xiso_partition_starts) / sizeof(xiso_partition_starts[0])); index++) + { + unsigned long long start = xiso_partition_starts[index]; + + if (!xiso_read(image, start + XISO_DESCRIPTOR_OFFSET, descriptor, sizeof(descriptor)) || + memcmp(descriptor, XISO_MAGIC, XISO_MAGIC_LENGTH) != 0 || + memcmp(descriptor + XISO_SECOND_MAGIC_OFFSET, XISO_MAGIC, XISO_MAGIC_LENGTH) != 0) + { + continue; + } + image->partition = start; + *root_sector = xiso_u32(descriptor + 0x14); + *root_size = xiso_u32(descriptor + 0x18); + return 1; + } + return 0; +} + +/* reads a directory table whole into a new allocation; returns NULL if it +is empty, too large or cannot be read, setting *out_of_memory when that is +the reason */ +static unsigned char *xiso_read_table(struct xiso_image *image, unsigned long sector, unsigned long size, + int *out_of_memory) { unsigned char *table; - if (!size || size > MAXIMUM_DIRECTORY_SIZE) + *out_of_memory = 0; + if (size == 0 || size > XISO_MAXIMUM_TABLE_SIZE) return NULL; table = malloc(size); - if (table && !read_at(image, image->partition + (unsigned long long)sector * SECTOR_SIZE, table, size)) + if (!table) + { + *out_of_memory = 1; + return NULL; + } + if (!xiso_read(image, xiso_sector_offset(image, sector), table, size)) { free(table); - table = NULL; + return NULL; } return table; } -struct directory_walk -{ - const unsigned char *table; - unsigned long size; - struct xiso_file *entries; - int entry_count; - int maximum_count; - int visited; - int directories; -}; +/* ---------- directory tables */ -/* collects the entries of the subtree at offset (4-byte units), files or -directories as asked */ -static void walk_directory(struct directory_walk *walk, unsigned long offset, int depth) +static void xiso_walk_collect(struct xiso_walk *walk, const unsigned char *entry) +{ + struct xiso_entry *collected; + int name_length = entry[13]; + int index; + + if (((entry[12] & XISO_ATTRIBUTE_DIRECTORY) != 0) != (walk->directories != 0)) + return; + if (walk->entry_count >= XISO_MAXIMUM_ENTRIES || !xiso_plain_name(entry + XISO_ENTRY_FIXED_SIZE, name_length)) + return; + /* (the destination's file system may ignore case) */ + for (index = 0; index < walk->entry_count; index++) + { + if (xiso_name_is((const char *)entry + XISO_ENTRY_FIXED_SIZE, name_length, walk->entries[index].name)) + return; + } + collected = &walk->entries[walk->entry_count++]; + memcpy(collected->name, entry + XISO_ENTRY_FIXED_SIZE, (size_t)name_length); + collected->name[name_length] = 0; + collected->sector = xiso_u32(entry + 4); + collected->size = xiso_u32(entry + 8); +} + +/* the subtree whose root entry is at offset: its left subtree, the entry, +its right subtree. A link to padding, past the table or back to an entry +already visited ends that branch */ +static void xiso_walk_entry(struct xiso_walk *walk, unsigned long offset, int depth) { const unsigned char *entry; - unsigned long left, right; - int name_length; + unsigned long left, right, name_length; - offset *= 4; - /* a malformed table must not loop or run off its end */ - if (depth > 64 || ++walk->visited > 4096 || offset + ENTRY_HEADER_SIZE > walk->size) + if (walk->stopped || depth > XISO_MAXIMUM_DEPTH) + return; + if (offset >= walk->size || walk->size - offset < XISO_ENTRY_FIXED_SIZE) return; entry = walk->table + offset; - left = (unsigned long)entry[0] | (unsigned long)entry[1] << 8; - right = (unsigned long)entry[2] | (unsigned long)entry[3] << 8; - /* 0xFFFF: padding, an empty directory */ - if (left == 0xFFFF) + left = xiso_u16(entry); + right = xiso_u16(entry + 2); + if (left == XISO_PADDING_LINK) return; - name_length = entry[13]; + /* (links are in 4-byte units, so every offset is a multiple of 4) */ + if (walk->visited[offset >> 5] & (1 << ((offset >> 2) & 7))) + return; + walk->visited[offset >> 5] |= (unsigned char)(1 << ((offset >> 2) & 7)); + if (walk->visits >= XISO_MAXIMUM_VISITS) + { + walk->stopped = 1; + return; + } + walk->visits++; + if (left) - walk_directory(walk, left, depth + 1); - if (offset + ENTRY_HEADER_SIZE + (unsigned long)name_length <= walk->size && name_length > 0 && - !(entry[12] & ATTRIBUTE_DIRECTORY) == !walk->directories && walk->entry_count < walk->maximum_count) - { - struct xiso_file *file = &walk->entries[walk->entry_count]; - - memcpy(file->name, entry + ENTRY_HEADER_SIZE, (size_t)name_length); - file->name[name_length] = 0; - file->sector = read_u32(entry + 4); - file->size = read_u32(entry + 8); - /* (as extract-xiso refuses them: no name may leave the folder, and - none but a plain file name is written) */ - if (name_is_plain(file->name)) - { - walk->entry_count++; - } - } + xiso_walk_entry(walk, left * 4, depth + 1); + name_length = entry[13]; + if (name_length && name_length <= walk->size - offset - XISO_ENTRY_FIXED_SIZE) + xiso_walk_collect(walk, entry); if (right) - walk_directory(walk, right, depth + 1); + xiso_walk_entry(walk, right * 4, depth + 1); } -/* whether a name is one to write in the maps folder: no path, no "." or -"..", and only printable ASCII that every desktop file system takes as a -plain name (a disc's maps folder holds .map files and loading.tga) */ -static int name_is_plain(const char *name) +/* collects the folders (or the files) of a table, with plain names, into +entries (XISO_MAXIMUM_ENTRIES of them); returns how many, or -1 when out of +memory */ +static int xiso_walk_table(const unsigned char *table, unsigned long size, int directories, + struct xiso_entry *entries) { - const char *at; + struct xiso_walk walk; - if (!*name || !strcmp(name, ".") || !strcmp(name, "..")) - return 0; - for (at = name; *at; at++) + memset(&walk, 0, sizeof(walk)); + walk.table = table; + walk.size = size; + walk.directories = directories; + walk.entries = entries; + walk.visited = calloc(size / 32 + 1, 1); + if (!walk.visited) + return -1; + xiso_walk_entry(&walk, 0, 0); + free(walk.visited); + return walk.entry_count; +} + +/* ---------- the copy */ + +/* removes what an earlier, interrupted copy left in the folder at path */ +static void xiso_empty_directory(const char *path, char *file_path, size_t file_path_size) +{ + void *directory = posix_directory_open(path); + char name[260]; + + if (!directory) + return; + while (posix_directory_next(directory, name, sizeof(name))) { - unsigned char c = (unsigned char)*at; + int length = snprintf(file_path, file_path_size, "%s/%s", path, name); - if (c < 0x20 || c > 0x7e || strchr("/\\:*?\"<>|", c)) - return 0; + if (length > 0 && (size_t)length < file_path_size) + remove(file_path); } - return 1; + posix_directory_close(directory); } -static int names_match(const char *a, const char *b) +/* copies one file of the image to path, adding to *done as it goes */ +static int xiso_copy_file(struct xiso_image *image, const struct xiso_entry *entry, const char *path, + unsigned char *buffer, unsigned long long *done, unsigned long long total, xiso_progress_proc progress, + void *context, char *error, int error_size) { - for (; *a && *b; a++, b++) - { - char x = *a >= 'A' && *a <= 'Z' ? (char)(*a - 'A' + 'a') : *a; - char y = *b >= 'A' && *b <= 'Z' ? (char)(*b - 'A' + 'a') : *b; - - if (x != y) - return 0; - } - return !*a && !*b; -} - -static int copy_file(struct xiso_image *image, const struct xiso_file *file, const char *path, - unsigned char *buffer, unsigned long long *done, unsigned long long total, - xiso_progress_proc progress, void *context) -{ - unsigned long long offset = image->partition + (unsigned long long)file->sector * SECTOR_SIZE; - unsigned long remaining = file->size; - /* (not through a link left in the folder) */ + unsigned long long offset = xiso_sector_offset(image, entry->sector); + unsigned long remaining = entry->size; int output = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_NOFOLLOW, 0644); if (output < 0) - return fail(image, "Could not write %s.", path); - while (remaining > 0) { - unsigned long count = remaining > COPY_BUFFER_SIZE ? COPY_BUFFER_SIZE : remaining; + xiso_error(error, error_size, "Could not write %s.", path); + return 0; + } + while (remaining) + { + unsigned long count = remaining < XISO_COPY_CHUNK ? remaining : XISO_COPY_CHUNK; unsigned long written = 0; - if (!read_at(image, offset, buffer, count)) + if (!xiso_read(image, offset, buffer, count)) { close(output); - return fail(image, "Could not read %s from the disc image (is it complete?).", file->name); + xiso_error(error, error_size, "Could not read %s from the disc image (is it complete?).", entry->name); + return 0; } while (written < count) { - long result = (long)write(output, buffer + written, count - written); + int result = (int)write(output, buffer + written, (unsigned int)(count - written)); + if (result < 0 && errno == EINTR) + continue; if (result <= 0) { close(output); - return fail(image, "Could not write %s (is the disk full?).", path); + xiso_error(error, error_size, "Could not write %s (is the disk full?).", path); + return 0; } written += (unsigned long)result; } @@ -309,109 +410,166 @@ static int copy_file(struct xiso_image *image, const struct xiso_file *file, con remaining -= count; *done += count; if (progress) - progress(context, file->name, *done, total); + progress(context, entry->name, *done, total); } if (close(output) != 0) - return fail(image, "Could not write %s (is the disk full?).", path); + { + xiso_error(error, error_size, "Could not write %s (is the disk full?).", path); + return 0; + } return 1; } +/* ---------- public code */ + int xiso_extract_maps(const char *image_path, const char *destination, xiso_progress_proc progress, void *context, char *error, int error_size) { struct xiso_image image; - struct xiso_file *entries = NULL; + struct xiso_entry *entries = NULL; unsigned char *table = NULL; unsigned char *buffer = NULL; - unsigned long root_sector, root_size; - unsigned long long total = 0, done = 0; - char partial[1024], final[1024], path[1300]; - int result = 0; - int index; + char *partial_path = NULL, *file_path = NULL, *maps_path = NULL; + unsigned long root_sector, root_size, maps_sector = 0, maps_size = 0; + unsigned long long done = 0, total = 0; + posix_ulong size_low, size_high; + int count, index, found, out_of_memory, succeeded = 0; - image.error = error; - image.error_size = error_size; + memset(&image, 0, sizeof(image)); image.descriptor = open(image_path, O_RDONLY | O_LARGEFILE | O_CLOEXEC); if (image.descriptor < 0) - return fail(&image, "Could not open %s.", image_path); - entries = calloc(MAXIMUM_FILES, sizeof(*entries)); - buffer = malloc(COPY_BUFFER_SIZE); - if (!entries || !buffer) { - fail(&image, "Out of memory.%s", NULL); + xiso_error(error, error_size, "Could not open %s.", image_path); + return 0; + } + image.size = ~0ULL; + if (posix_seek(image.descriptor, 0, 0, SEEK_END, &size_low, &size_high) == 0) + image.size = (unsigned long long)size_high << 32 | size_low; + + entries = malloc(sizeof(*entries) * XISO_MAXIMUM_ENTRIES); + buffer = malloc(XISO_COPY_CHUNK); + partial_path = malloc(XISO_MAXIMUM_PATH); + file_path = malloc(XISO_MAXIMUM_PATH); + maps_path = malloc(XISO_MAXIMUM_PATH); + if (!entries || !buffer || !partial_path || !file_path || !maps_path) + { + xiso_error(error, error_size, "Out of memory."); goto done; } - if (!find_volume(&image, &root_sector, &root_size)) + + if (!xiso_find_volume(&image, &root_sector, &root_size)) + { + xiso_error(error, error_size, "This is not an Xbox disc image."); goto done; + } /* the root's maps folder */ - table = read_directory(&image, root_sector, root_size); + table = xiso_read_table(&image, root_sector, root_size, &out_of_memory); if (!table) { - fail(&image, "The disc image's file system is damaged.%s", NULL); + xiso_error(error, error_size, out_of_memory ? "Out of memory." : "The disc image's file system is damaged."); goto done; } + count = xiso_walk_table(table, root_size, 1, entries); + free(table); + table = NULL; + if (count < 0) { - struct directory_walk walk = { table, root_size, entries, 0, MAXIMUM_FILES, 0, 1 }; - - walk_directory(&walk, 0, 0); - for (index = 0; index < walk.entry_count && !names_match(entries[index].name, "maps"); index++) - ; - if (index == walk.entry_count) + xiso_error(error, error_size, "Out of memory."); + goto done; + } + for (found = 0, index = 0; index < count && !found; index++) + { + if (xiso_name_is(entries[index].name, (int)strlen(entries[index].name), "maps")) { - fail(&image, "The disc image has no maps folder: it is not a Halo disc.%s", NULL); - goto done; + maps_sector = entries[index].sector; + maps_size = entries[index].size; + found = 1; } - free(table); - table = read_directory(&image, entries[index].sector, entries[index].size); - if (!table) - { - fail(&image, "The disc image's maps folder is damaged.%s", NULL); - goto done; - } - root_size = entries[index].size; + } + if (!found) + { + xiso_error(error, error_size, "The disc image has no maps folder: it is not a Halo disc."); + goto done; } /* its files */ + table = xiso_read_table(&image, maps_sector, maps_size, &out_of_memory); + if (!table) { - struct directory_walk walk = { table, root_size, entries, 0, MAXIMUM_FILES, 0, 0 }; - int has_ui = 0; - - walk_directory(&walk, 0, 0); - for (index = 0; index < walk.entry_count; index++) + xiso_error(error, error_size, out_of_memory ? "Out of memory." : "The disc image's maps folder is damaged."); + goto done; + } + count = xiso_walk_table(table, maps_size, 0, entries); + free(table); + table = NULL; + if (count < 0) + { + xiso_error(error, error_size, "Out of memory."); + goto done; + } + for (found = 0, index = 0; index < count && !found; index++) + found = xiso_name_is(entries[index].name, (int)strlen(entries[index].name), "ui.map"); + if (!found) + { + xiso_error(error, error_size, "The disc image's maps folder has no ui.map: it is not a Halo disc."); + goto done; + } + /* (an image that ends before a file does is refused before anything is + written; an empty file's sector need not be anywhere) */ + for (index = 0; index < count; index++) + { + if (entries[index].size && + xiso_sector_offset(&image, entries[index].sector) + entries[index].size > image.size) { - total += entries[index].size; - has_ui |= names_match(entries[index].name, "ui.map"); - } - if (!has_ui) - { - fail(&image, "The disc image's maps folder has no ui.map: it is not a Halo disc.%s", NULL); + xiso_error(error, error_size, "Could not read %s from the disc image (is it complete?).", + entries[index].name); goto done; } - snprintf(partial, sizeof(partial), "%s/maps.partial", destination); - snprintf(final, sizeof(final), "%s/maps", destination); - posix_make_directory(partial); - for (index = 0; index < walk.entry_count; index++) + total += entries[index].size; + } + + /* the copy, to maps.partial */ + if (snprintf(partial_path, XISO_MAXIMUM_PATH, "%s/" XISO_PARTIAL_DIRECTORY, destination) >= XISO_MAXIMUM_PATH || + snprintf(maps_path, XISO_MAXIMUM_PATH, "%s/maps", destination) >= XISO_MAXIMUM_PATH) + { + xiso_error(error, error_size, "Could not create %s/maps.", destination); + goto done; + } + /* (an existing one is reused, emptied; a failure to make it shows as a + failure to write the first file) */ + posix_make_directory(partial_path); + xiso_empty_directory(partial_path, file_path, XISO_MAXIMUM_PATH); + for (index = 0; index < count; index++) + { + platform_log("extracting maps/%s (%lu bytes)", entries[index].name, entries[index].size); + if (snprintf(file_path, XISO_MAXIMUM_PATH, "%s/%s", partial_path, entries[index].name) >= XISO_MAXIMUM_PATH) { - snprintf(path, sizeof(path), "%s/%s", partial, entries[index].name); - platform_log("extracting maps/%s (%lu bytes)", entries[index].name, entries[index].size); - if (!copy_file(&image, &entries[index], path, buffer, &done, total, progress, context)) - goto done; + xiso_error(error, error_size, "Could not write %s/%s.", partial_path, entries[index].name); + goto done; } - if (rename(partial, final) != 0) + if (!xiso_copy_file(&image, &entries[index], file_path, buffer, &done, total, progress, context, + error, error_size)) { - fail(&image, "Could not create %s.", final); goto done; } } - result = 1; + if (rename(partial_path, maps_path) != 0) + { + xiso_error(error, error_size, "Could not create %s.", maps_path); + goto done; + } + succeeded = 1; done: - close(image.descriptor); free(table); - free(entries); + free(maps_path); + free(file_path); + free(partial_path); free(buffer); - return result; + free(entries); + close(image.descriptor); + return succeeded; } #endif diff --git a/port/third_party/extract-xiso/LICENSE.TXT b/port/third_party/extract-xiso/LICENSE.TXT deleted file mode 100644 index b89be76a..00000000 --- a/port/third_party/extract-xiso/LICENSE.TXT +++ /dev/null @@ -1,75 +0,0 @@ -This is an xdvdfs (xbox iso) file creation/extraction utility for linux/darwin/ -freebsd. - -If you want to port it to your OS it shouldn't be too hard, just make sure your -fseek/ftell takes 64-bit file offsets. - -I wrote this code completely from scratch today because xbiso (the one on -sourceforge) keeps seg-faulting and the code is buggy. I got useful -documentation on the xdvdfs file system from http://xbox-linux.sourceforge.net. - -I will say thanks though to the author of xbiso, it was a good start and got me -thinking! - - -Regarding licensing: - -I think the GPL sucks! (it stands for Generosity Poor License) - -My open-source code is really *FREE* so you can do whatever you want with it, as -long as 1) you don't claim that you wrote my code and 2) you retain a notice -that some parts of the code are copyright in@fishtank.com and 3) you understand -there are no warranties. I only guarantee that it will take up disk space! - -If you want to help out with this project it would be welcome, just email me at -in@fishtank.com. - -This code is copyright in@fishtank.com and is licensed under a slightly -modifified version of the Berkeley Software License, which follows: - -/* - * Copyright (c) 2003 in - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * 3. All advertising materials mentioning features or use of this software - * must display the following acknowledgement: - * - * This product includes software developed by in . - * - * 4. Neither the name of "in" nor the email address "in@fishtank.com" - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED `AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * AUTHOR OR ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; - * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR - * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -Please send bug reports to me at in@fishtank.com - -To compile, run 'make' in the source directory. - -NOTE: You need to be running GNU make and /bin/bash must be executable - -Enjoy! - -in version 1.0 -March 10, 2003 diff --git a/port/third_party/extract-xiso/README.md b/port/third_party/extract-xiso/README.md deleted file mode 100644 index 8b61fcd5..00000000 --- a/port/third_party/extract-xiso/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# extract-xiso - -An Xbox disc image (xdvdfs) extraction and creation tool by in -, under a modified BSD license (see `LICENSE.TXT`, copied -unchanged from upstream). - -Upstream: https://github.com/XboxDev/extract-xiso, commit -3f5b62cfe68f000b0e3c8a30104973f3a297948e. - -None of its code is built as it is: `port/linux/src/xiso.c`, which copies -the maps folder out of a disc image when a desktop port starts without game -data, reads the format as `extract-xiso.c` does and carries its notice. The -desktop builds' artifacts include `LICENSE.TXT` as -`extract-xiso-LICENSE.txt`. - -This product includes software developed by in . diff --git a/port/web/licenses.py b/port/web/licenses.py index e8ef775c..98c01d33 100644 --- a/port/web/licenses.py +++ b/port/web/licenses.py @@ -1,7 +1,7 @@ -"""Writes the web site's licenses.txt (tools/web_build.py): the notices of -the code and fonts that halo.wasm and the page carry, which their licences -ask copies to include, as the other builds' releases carry them -(tools/ci_build.py). +"""Writes the web site's licenses.txt (tools/web_build.py): OpenCE's licence, +and the notices of the code and fonts that halo.wasm and the page carry, +which their licences ask copies to include, as the other builds' releases +carry them (tools/ci_build.py). python port/web/licenses.py OUTPUT SDL3-LICENSE """ @@ -21,7 +21,6 @@ NOTICES = [ ("tomlc17 (config.toml, mod.toml, load_order.toml)", "port/third_party/tomlc17/LICENSE"), ("musl (the maths functions)", "port/third_party/musl-math/COPYRIGHT"), ("stb_vorbis (Custom Edition maps' and mods' sounds)", "port/third_party/stb_vorbis/LICENSE"), - ("extract-xiso (how the disc image is read)", "port/third_party/extract-xiso/LICENSE.TXT"), ("Overpass (the menus' text and the page's)", "port/assets/fonts/Overpass-OFL.txt"), ("OpenCE (the title font, from Newtown)", "port/assets/fonts/OpenCE-OFL.txt"), ("Newtown", "port/assets/fonts/Newtown-LICENSE.txt"), @@ -33,12 +32,17 @@ def main() -> None: output, sdl_license = Path(sys.argv[1]), Path(sys.argv[2]) parts = [ "OpenCE for the web\n\n" - "OpenCE's own code is CC0 (https://github.com/OpenCommunityEdition/OpenCE). The game data\n" - "comes from the player's own disc. It carries the following, under their licences:\n", + "OpenCE is free software under the GNU Affero General Public License, version 3 or (at\n" + "your option) any later version; its text is at the end of this file. Its source is at\n" + "https://github.com/OpenCommunityEdition/OpenCE, and each release's at its tag. The game\n" + "data comes from the player's own disc. OpenCE carries the following, under their\n" + "licences:\n", f"\n==================== SDL 3 (windows, input and audio)\n\n{sdl_license.read_text(encoding='utf-8')}", ] for title, path in NOTICES: parts.append(f"\n==================== {title}\n\n{(ROOT / path).read_text(encoding='utf-8')}") + parts.append(f"\n==================== OpenCE (GNU Affero General Public License)\n\n" + f"{(ROOT / 'LICENSE').read_text(encoding='utf-8')}") output.write_text("".join(parts), encoding="utf-8") diff --git a/port/web/site/xiso-worker.js b/port/web/site/xiso-worker.js index 4aaaa49f..49e973fe 100644 --- a/port/web/site/xiso-worker.js +++ b/port/web/site/xiso-worker.js @@ -1,211 +1,245 @@ /* XISO-WORKER.JS -Copies the game's maps folder into the site's Origin Private File System, -where the game finds it (/data/maps: port/web/src/web_main.c), from the -player's own Xbox disc image of Halo: Combat Evolved (an "xiso", .iso), as -port/linux/src/xiso.c does for the desktop ports, or from a maps folder -already copied out of one. It runs in a Worker, which can write with -synchronous OPFS access handles (Safari has no OPFS writable streams), and -reads the image a slice at a time: an image is 4 to 7 GB, and nothing of it -is kept but the maps. +Copies the game data into the site's Origin Private File System, where the +game reads it (/data/maps: port/web/src/web_main.c): the maps folder of the +player's Xbox disc image of Halo: Combat Evolved, or the files of a maps +folder the player chose. It runs in a Worker, which can write with +synchronous OPFS access handles (Safari has no OPFS writable streams). Only +the maps and loading.tga are kept, and each map's header is checked as the +game checks it, so that a disc the game cannot play is refused here. -The image's file system is XDVDFS, read as extract-xiso does -(port/third_party/extract-xiso): 2048-byte sectors; a volume descriptor at -0x10000 that starts and ends with "MICROSOFT*XBOX*MEDIA" and gives the root -directory's sector and size; directories whose entries form a binary tree -(left and right subtree offsets in 4-byte units, the start sector, the size, -attributes, the name's length and the name). Images of a whole disc put the -game partition further in, at one of PARTITION_OFFSETS. The image is the -player's file, so it is untrusted: every offset and size is checked against -the image and the directory it is read from, and a directory's walk is -bounded. +Written from a description of the Xbox's disc file system, XDVDFS +(xboxdevwiki's XDVDFS page, https://xboxdevwiki.net/XDVDFS, and the format +facts OpenCE's readers rely on). What this file relies on: -Each map is checked as the game checks it when it loads it (cache_files.c): -"head" and "foot" signatures, the Xbox's cache version, and the build of a -retail disc (01.10.12.2276) or of this decompilation's (01.01.14.2342). +- Sectors are 2048 bytes and integers little-endian. Sector numbers count + from the start of the game partition: an image of the partition alone has + it at 0, an image of a whole disc further in (0x0FD90000, 0x02080000 or + 0x18300000 for XGD2, XGD3 and XGD1 discs). They are tried in that order. +- The volume descriptor is the partition's sector 32 (at 0x10000). It holds + "MICROSOFT*XBOX*MEDIA" at its start and again at 0x7EC, and the root + directory table's start sector (u32 at 0x14) and size in bytes (u32 at + 0x18). +- A directory table is a binary tree of entries whose root is the entry at + offset 0. An entry is a left and a right subtree link (u16 each, in 4-byte + units from the table's start, 0 for none), the start sector and the size + of the file or of the subdirectory's table (u32 each), the attributes (u8, + 0x10 for a directory), the name's length (u8) and the name. Unused bytes of + a table are 0xFF, so a left link of 0xFFFF is padding, not an entry. +- A file is one contiguous run of bytes from the start of its sector. -Messages from the page: { type: 'image', file } or { type: 'folder', files } -(a maps folder's File objects). To the page: { type: 'progress', file, done, -total }, then { type: 'done', files, bytes } or { type: 'error', message }. +The image is the player's file (4 to 7 GB) and nothing read from it is +trusted: it is read a slice at a time, only the root's table and the maps +folder's (each at most 1 MB), and the walk of a table visits each offset +once, goes at most 64 links deep and looks at most 4096 entries. + +Messages from the page: { type: 'image', file } (the disc image) or +{ type: 'folder', files } (a chosen folder's File objects). To the page: +{ type: 'progress', file, done, total }, then { type: 'done', files, bytes } +or { type: 'error', message }. A new copy first removes the old maps folder +and writes maps/.complete ({"files":[...],"bytes":n}) last: the page takes +the data as ready only once that marker is there. */ 'use strict'; const SECTOR_SIZE = 2048; -const VOLUME_DESCRIPTOR_OFFSET = 0x10000; -const PARTITION_OFFSETS = [0, 0x0FD90000, 0x02080000, 0x18300000]; +const DESCRIPTOR_OFFSET = 0x10000; const MAGIC = 'MICROSOFT*XBOX*MEDIA'; -const ENTRY_HEADER_SIZE = 14; +const SECOND_MAGIC_OFFSET = 0x7EC; +const ENTRY_FIXED_SIZE = 14; const ATTRIBUTE_DIRECTORY = 0x10; -const MAXIMUM_DIRECTORY_SIZE = 1024 * 1024; -const MAXIMUM_ENTRIES = 4096; +const PADDING_LINK = 0xFFFF; +// where the game partition may start, in the order they are tried: an image +// of the partition alone, then whole XGD2, XGD3 and XGD1 discs +const PARTITION_STARTS = [0x00000000, 0x0FD90000, 0x02080000, 0x18300000]; +// directory tables are a few sectors; anything much larger is not one +const MAXIMUM_TABLE_SIZE = 1024 * 1024; const MAXIMUM_DEPTH = 64; -const COPY_CHUNK = 4 * 1024 * 1024; +const MAXIMUM_ENTRIES = 4096; const MAP_HEADER_SIZE = 0x800; -const CACHE_VERSION = 5; -const BUILDS = ['01.10.12.2276', '01.01.14.2342']; -// written last: the page starts the game only when it is there -const COMPLETE_MARKER = '.complete'; +const MAP_BUILDS = ['01.10.12.2276', '01.01.14.2342']; +const COPY_CHUNK = 4 * 1024 * 1024; -async function readAt(file, offset, size) { - const end = Math.min(offset + size, file.size); - if (offset < 0 || offset >= end) return new Uint8Array(0); - return new Uint8Array(await file.slice(offset, end).arrayBuffer()); +// ---------- names + +function asciiLower(text) { + return text.replace(/[A-Z]/g, (letter) => String.fromCharCode(letter.charCodeAt(0) + 32)); } -function text(bytes, start, length) { - let result = ''; - for (let i = 0; i < length; i++) result += String.fromCharCode(bytes[start + i]); - return result; +function sameName(a, b) { + return asciiLower(a) === asciiLower(b); } -function u16(bytes, at) { - return bytes[at] | bytes[at + 1] << 8; +// A name a file of the maps folder may have: no path, nothing hidden. +function plainName(name) { + return typeof name === 'string' && /^[A-Za-z0-9_. -]{1,64}$/.test(name) && !name.startsWith('.'); } -function u32(bytes, at) { - return (bytes[at] | bytes[at + 1] << 8 | bytes[at + 2] << 16 | bytes[at + 3] << 24) >>> 0; +function latin1(bytes) { + let text = ''; + for (const byte of bytes) text += String.fromCharCode(byte); + return text; } +// ---------- the image + +async function readBytes(file, start, end) { + return new Uint8Array(await file.slice(start, end).arrayBuffer()); +} + +// The game partition: { partition, rootSector, rootSize }, from the first +// start whose volume descriptor has both magics; null if none has. async function findVolume(file) { - for (const partition of PARTITION_OFFSETS) { - const descriptor = await readAt(file, partition + VOLUME_DESCRIPTOR_OFFSET, SECTOR_SIZE); - if (descriptor.length < SECTOR_SIZE) continue; - if (text(descriptor, 0, 20) === MAGIC && text(descriptor, 0x7EC, 20) === MAGIC) { - return { partition, rootSector: u32(descriptor, 20), rootSize: u32(descriptor, 24) }; + for (const partition of PARTITION_STARTS) { + const start = partition + DESCRIPTOR_OFFSET; + if (start + SECTOR_SIZE > file.size) continue; + const descriptor = await readBytes(file, start, start + SECTOR_SIZE); + if (descriptor.length !== SECTOR_SIZE) continue; + if (latin1(descriptor.subarray(0, MAGIC.length)) !== MAGIC || + latin1(descriptor.subarray(SECOND_MAGIC_OFFSET, SECOND_MAGIC_OFFSET + MAGIC.length)) !== MAGIC) { + continue; } + const view = new DataView(descriptor.buffer, descriptor.byteOffset, descriptor.byteLength); + return { partition, rootSector: view.getUint32(0x14, true), rootSize: view.getUint32(0x18, true) }; } return null; } -// a name a file of the maps folder may have: no path, nothing hidden -function plainName(name) { - return name.length > 0 && name.length <= 64 && /^[A-Za-z0-9_.\- ]+$/.test(name) && !name.startsWith('.'); -} - -// the entries of a directory table: { name, sector, size, directory } +// A directory table's entries, { name, sector, size, directory }, for every +// entry that has a name, in the tree's order (left subtree, the entry, right +// subtree). A link to padding, past the table or to an offset already +// visited ends that branch. function walkDirectory(table) { + const view = new DataView(table.buffer, table.byteOffset, table.byteLength); + const visited = new Set(); const entries = []; - const seen = new Set(); const walk = (offset, depth) => { - offset *= 4; - // (a cycle, a tree too deep or too wide, or an entry past the table: - // what is left of it is ignored) - if (depth > MAXIMUM_DEPTH || seen.has(offset) || seen.size >= MAXIMUM_ENTRIES || - offset + ENTRY_HEADER_SIZE > table.length) { - return; - } - seen.add(offset); - const left = u16(table, offset); - const right = u16(table, offset + 2); - // (padding at the end of a sector) - if (left === 0xFFFF) return; + if (depth > MAXIMUM_DEPTH || offset + ENTRY_FIXED_SIZE > table.length) return; + const left = view.getUint16(offset, true); + const right = view.getUint16(offset + 2, true); + if (left === PADDING_LINK || visited.has(offset) || visited.size >= MAXIMUM_ENTRIES) return; + visited.add(offset); + if (left) walk(left * 4, depth + 1); const nameLength = table[offset + 13]; - if (left) walk(left, depth + 1); - if (nameLength > 0 && offset + ENTRY_HEADER_SIZE + nameLength <= table.length) { + const nameStart = offset + ENTRY_FIXED_SIZE; + if (nameLength && nameStart + nameLength <= table.length) { entries.push({ - name: text(table, offset + ENTRY_HEADER_SIZE, nameLength), - sector: u32(table, offset + 4), - size: u32(table, offset + 8), + name: latin1(table.subarray(nameStart, nameStart + nameLength)), + sector: view.getUint32(offset + 4, true), + size: view.getUint32(offset + 8, true), directory: (table[offset + 12] & ATTRIBUTE_DIRECTORY) !== 0, }); } - if (right) walk(right, depth + 1); + if (right) walk(right * 4, depth + 1); }; walk(0, 0); return entries; } -async function readDirectory(file, volume, sector, size) { - if (!size || size > MAXIMUM_DIRECTORY_SIZE) return null; - const offset = volume.partition + sector * SECTOR_SIZE; - if (offset + size > file.size) return null; - const table = await readAt(file, offset, size); - return table.length === size ? table : null; +async function readTable(file, partition, sector, size, message) { + const start = partition + sector * SECTOR_SIZE; + if (size === 0 || size > MAXIMUM_TABLE_SIZE || start + size > file.size) throw new Error(message); + const table = await readBytes(file, start, start + size); + if (table.length !== size) throw new Error(message); + return table; } -// why a map's header is not one the game loads, or null +// ---------- the maps + +// Why the game would not load a map whose first bytes are header, or null. function mapProblem(header) { if (header.length < MAP_HEADER_SIZE) return 'it is too short'; - if (text(header, 0, 4) !== 'daeh' || text(header, 0x7FC, 4) !== 'toof') return 'it is not a map'; - if (u32(header, 4) !== CACHE_VERSION) return 'it is not an Xbox map (its cache version is ' + u32(header, 4) + ')'; - const build = text(header, 0x40, 32).replace(/\0.*$/s, ''); - if (!BUILDS.includes(build)) return 'its build, ' + JSON.stringify(build) + ', is not one the game loads'; + if (latin1(header.subarray(0, 4)) !== 'daeh' || latin1(header.subarray(0x7FC, 0x800)) !== 'toof') { + return 'it is not a map'; + } + const view = new DataView(header.buffer, header.byteOffset, header.byteLength); + const version = view.getUint32(4, true); + if (version !== 5) return `it is not an Xbox map (its cache version is ${version})`; + const field = header.subarray(0x40, 0x60); + const end = field.indexOf(0); + const build = latin1(end >= 0 ? field.subarray(0, end) : field); + if (!MAP_BUILDS.includes(build)) return `its build, ${JSON.stringify(build)}, is not one the game loads`; return null; } -// the files to copy, each { name, size, read(offset, size) }; the maps are -// checked, other files (loading.tga) copied as they are +// The files to copy, of those given ({ name, size, read }): the maps (each +// checked) and loading.tga, under plain names, the first of names that +// differ only in case; throws why they cannot be used. async function checkedFiles(candidates) { - const files = []; + const kept = []; for (const candidate of candidates) { - const lower = candidate.name.toLowerCase(); if (!plainName(candidate.name)) continue; + if (kept.some((file) => sameName(file.name, candidate.name))) continue; + const lower = asciiLower(candidate.name); if (lower.endsWith('.map')) { const problem = mapProblem(await candidate.read(0, MAP_HEADER_SIZE)); if (problem) throw new Error(`${candidate.name} cannot be used: ${problem}.`); - files.push(candidate); + kept.push(candidate); } else if (lower === 'loading.tga') { - files.push(candidate); + kept.push(candidate); } } - if (!files.some((file) => file.name.toLowerCase() === 'ui.map')) { + if (!kept.some((file) => sameName(file.name, 'ui.map'))) { throw new Error('There is no ui.map among the maps: this is not Halo: Combat Evolved for the Xbox.'); } - return files; + return kept; } +// The maps folder's files in a disc image, as { name, size, read(offset, +// size) }; throws why it cannot be used. async function imageFiles(file) { const volume = await findVolume(file); if (!volume) throw new Error('This file is not an Xbox disc image (no XDVDFS volume was found).'); - const root = await readDirectory(file, volume, volume.rootSector, volume.rootSize); - if (!root) throw new Error("The disc image's file system is damaged."); - const maps = walkDirectory(root).find((entry) => entry.directory && entry.name.toLowerCase() === 'maps'); + const { partition } = volume; + const root = await readTable(file, partition, volume.rootSector, volume.rootSize, + 'The disc image\'s file system is damaged.'); + const maps = walkDirectory(root).find((entry) => entry.directory && sameName(entry.name, 'maps')); if (!maps) throw new Error('The disc image has no maps folder: it is not a Halo disc.'); - const table = await readDirectory(file, volume, maps.sector, maps.size); - if (!table) throw new Error("The disc image's maps folder is damaged."); - return checkedFiles(walkDirectory(table).filter((entry) => !entry.directory).map((entry) => { - const offset = volume.partition + entry.sector * SECTOR_SIZE; - if (offset + entry.size > file.size) { + const table = await readTable(file, partition, maps.sector, maps.size, 'The disc image\'s maps folder is damaged.'); + const candidates = []; + for (const entry of walkDirectory(table)) { + if (entry.directory) continue; + const start = partition + entry.sector * SECTOR_SIZE; + // (an empty file's sector need not be anywhere) + if (entry.size && start + entry.size > file.size) { throw new Error(`The disc image ends before ${entry.name} does: is it complete?`); } - return { name: entry.name, size: entry.size, read: (at, size) => readAt(file, offset + at, Math.min(size, entry.size - at)) }; - })); + candidates.push({ + name: entry.name, + size: entry.size, + read: (offset, size) => { + const count = Math.max(0, Math.min(size, entry.size - offset)); + return readBytes(file, Math.min(start + offset, file.size), Math.min(start + offset + count, file.size)); + }, + }); + } + return checkedFiles(candidates); } +// The same, from the files of a chosen folder. async function folderFiles(list) { - return checkedFiles(list.map((file) => ({ name: file.name, size: file.size, read: (at, size) => readAt(file, at, size) }))); + return checkedFiles(list.map((file) => ({ + name: file.name, + size: file.size, + read: (offset, size) => readBytes(file, offset, offset + size), + }))); } -async function writeFile(directory, source, onChunk) { - const handle = await directory.getFileHandle(source.name, { create: true }); - const access = await handle.createSyncAccessHandle(); - try { - access.truncate(0); - let position = 0; - while (position < source.size) { - const count = Math.min(COPY_CHUNK, source.size - position); - const bytes = await source.read(position, count); - if (bytes.length !== count) throw new Error(`Could not read ${source.name} (is the file complete?).`); - let written = 0; - while (written < count) { - const result = access.write(bytes.subarray(written), { at: position + written }); - if (!result) throw new Error(`Could not write ${source.name}: is the device full?`); - written += result; - } - position += count; - onChunk(count); - } - access.flush(); - } finally { - access.close(); +// ---------- the copy + +function writeAll(access, bytes, position, name) { + let written = 0; + while (written < bytes.length) { + const result = access.write(bytes.subarray(written), { at: position + written }); + if (!result) throw new Error(`Could not write ${name}: is the device full?`); + written += result; } } async function copy(files) { - const total = files.reduce((sum, entry) => sum + entry.size, 0); - const storage = await navigator.storage.getDirectory(); + const total = files.reduce((sum, file) => sum + file.size, 0); if (navigator.storage.estimate) { const { quota, usage } = await navigator.storage.estimate(); if (quota && quota - usage < total) { @@ -213,34 +247,54 @@ async function copy(files) { `and the maps take ${Math.ceil(total / 1e6)} MB.`); } } - // a new copy replaces whatever an earlier, perhaps interrupted, one left + const storage = await navigator.storage.getDirectory(); + // (a new copy replaces whatever an earlier, perhaps interrupted, one left) await storage.removeEntry('maps', { recursive: true }).catch(() => {}); - const directory = await storage.getDirectoryHandle('maps', { create: true }); + const maps = await storage.getDirectoryHandle('maps', { create: true }); let done = 0; let lastReport = 0; - for (const entry of files) { - await writeFile(directory, entry, (count) => { - done += count; - const now = Date.now(); - if (now - lastReport > 100 || done === total) { - lastReport = now; - postMessage({ type: 'progress', file: entry.name, done, total }); + for (const file of files) { + const handle = await maps.getFileHandle(file.name, { create: true }); + const access = await handle.createSyncAccessHandle(); + try { + access.truncate(0); + let position = 0; + while (position < file.size) { + const count = Math.min(COPY_CHUNK, file.size - position); + const bytes = await file.read(position, count); + if (bytes.length < count) throw new Error(`Could not read ${file.name} (is the file complete?).`); + writeAll(access, bytes.subarray(0, count), position, file.name); + position += count; + done += count; + const now = Date.now(); + if (now - lastReport > 100 || done === total) { + lastReport = now; + postMessage({ type: 'progress', file: file.name, done, total }); + } } - }); + access.flush(); + } finally { + access.close(); + } + } + // last: the sign that the copy is complete + const marker = await (await maps.getFileHandle('.complete', { create: true })).createSyncAccessHandle(); + try { + marker.truncate(0); + const text = new TextEncoder().encode(JSON.stringify({ files: files.map((file) => file.name), bytes: total })); + writeAll(marker, text, 0, '.complete'); + marker.flush(); + } finally { + marker.close(); } - const marker = await directory.getFileHandle(COMPLETE_MARKER, { create: true }); - const access = await marker.createSyncAccessHandle(); - access.truncate(0); - access.write(new TextEncoder().encode(JSON.stringify({ files: files.map((f) => f.name), bytes: total })), { at: 0 }); - access.flush(); - access.close(); return { files: files.length, bytes: total }; } if (typeof onmessage !== 'undefined' || typeof WorkerGlobalScope !== 'undefined') { onmessage = async (event) => { try { - const files = event.data.type === 'folder' ? await folderFiles(event.data.files) : await imageFiles(event.data.file); + const message = event.data; + const files = message.type === 'folder' ? await folderFiles(message.files) : await imageFiles(message.file); postMessage({ type: 'done', ...(await copy(files)) }); } catch (error) { postMessage({ type: 'error', message: error && error.message ? error.message : String(error) }); diff --git a/port/web/tests/webrtc_native.mjs b/port/web/tests/webrtc_native.mjs index b3405a3d..46470e7e 100644 --- a/port/web/tests/webrtc_native.mjs +++ b/port/web/tests/webrtc_native.mjs @@ -11,7 +11,10 @@ // messages of every size, split across SCTP's packets and not, which it // sends back, and the browser checks them; then again with a tenth of the // datagrams each way lost, when the channel must stay open (what was lost is -// given up, FORWARD-TSN) and carry the rest. No game data is needed. +// given up, FORWARD-TSN) and carry the rest. The channel is unreliable (no +// retransmissions, as the tunnel's), and a busy computer's loopback can drop +// a burst, so even without losses a few messages may not come back: every +// size must, nearly every time. No game data is needed. import { spawn, spawnSync } from 'node:child_process'; import { mkdtempSync, readdirSync, rmSync } from 'node:fs'; @@ -122,6 +125,7 @@ async function run(program, kind, loss, rounds) { // (as large as a tunnel packet: p2p.c's MAXIMUM_PACKET_SIZE) const sizes = [1, 10, 100, 1000, 1100, 1101, 1200, 1431]; const sent = new Map(); + const echoedBySize = {}; let echoed = 0; let wrong = 0; let lastEchoed = -1; @@ -132,6 +136,7 @@ async function run(program, kind, loss, rounds) { if (!expected || expected.length !== bytes.length || expected.some((b, i) => b !== bytes[i])) wrong++; else { echoed++; + echoedBySize[bytes.length] = (echoedBySize[bytes.length] || 0) + 1; lastEchoed = Math.max(lastEchoed, number); } }; @@ -148,7 +153,7 @@ async function run(program, kind, loss, rounds) { await new Promise((resolve) => setTimeout(resolve, 25)); } await new Promise((resolve) => setTimeout(resolve, 2000)); - return { sent: number, echoed, wrong, lastEchoed, open: dc.readyState === 'open' }; + return { sent: number, echoed, wrong, lastEchoed, open: dc.readyState === 'open', rounds, echoedBySize }; }, { sdp: answer(await page.evaluate(() => window.mid)), rounds }); return result; } finally { @@ -163,7 +168,12 @@ try { for (const kind of browsers) { const clean = await run(program, kind, 0, 40); console.log(`${kind}: ${clean.echoed} of ${clean.sent} messages back`); - if (clean.wrong || clean.echoed !== clean.sent) fail(`${kind}: ${JSON.stringify(clean)}`); + // (all but a dropped burst, every size nearly every time, and to the end) + const sizes = Object.values(clean.echoedBySize); + if (clean.wrong || clean.echoed < clean.sent * 0.97 || clean.lastEchoed !== clean.sent - 1 || sizes.length !== 8 || + sizes.some((count) => count < clean.rounds - 2)) { + fail(`${kind}: ${JSON.stringify(clean)}`); + } const lossy = await run(program, kind, 10, 120); console.log(`${kind}, a tenth of the datagrams lost: ${lossy.echoed} of ${lossy.sent} messages back`); // (a message comes back if its every datagram passed each way: about diff --git a/source/ai/actor_moving.c b/source/ai/actor_moving.c index a052aa6c..60db1b1a 100644 --- a/source/ai/actor_moving.c +++ b/source/ai/actor_moving.c @@ -204,7 +204,6 @@ symbols in this file: /* ---------- headers */ -#define REAL_MATH_EXTERNAL_POINT_FROM_LINE3D #include "cseries/cseries.h" #include "cseries/errors.h" #define PATH_EXTERNAL_FLEE_ROUTINES @@ -235,7 +234,6 @@ symbols in this file: #include "objects/objects.h" #include "physics/collision_model_definitions.h" #undef PATH_EXTERNAL_FLEE_ROUTINES -#undef REAL_MATH_EXTERNAL_POINT_FROM_LINE3D /* ---------- constants */ @@ -655,24 +653,10 @@ void actor_move_transform_avoidance_vector( real_vector3d const *avoidance_vector, real_vector3d *direction_vector) { - real component; - *direction_vector = *global_zero_vector3d; - - component = avoidance_vector->i; - direction_vector->i += component * avoidance_data->forward.i; - direction_vector->j += component * avoidance_data->forward.j; - direction_vector->k += component * avoidance_data->forward.k; - - component = avoidance_vector->j; - direction_vector->i += component * avoidance_data->left.i; - direction_vector->j += component * avoidance_data->left.j; - direction_vector->k += component * avoidance_data->left.k; - - component = avoidance_vector->k; - direction_vector->i += component * avoidance_data->up.i; - direction_vector->j += component * avoidance_data->up.j; - direction_vector->k += component * avoidance_data->up.k; + point_from_line3d((real_point3d *)direction_vector, &avoidance_data->forward, (avoidance_vector->i), (real_point3d *)direction_vector); + point_from_line3d((real_point3d *)direction_vector, &avoidance_data->left, (avoidance_vector->j), (real_point3d *)direction_vector); + point_from_line3d((real_point3d *)direction_vector, &avoidance_data->up, (avoidance_vector->k), (real_point3d *)direction_vector); return; } @@ -832,10 +816,8 @@ static short actor_move_test_avoidance_vector( real *collision_t, byte *collision_timer) { - struct collision_bsp_test_vector_result collision_result; - real_vector3d offset; - real_vector3d divergence; - real scale; + real_vector3d divergence_direction; + real_vector3d origin_offset; short result = _actor_vector_avoidance_clear; short object_index; @@ -854,46 +836,51 @@ static short actor_move_test_avoidance_vector( *collision_t = REAL_MAX; - actor_move_transform_avoidance_vector(avoidance_data, &avoidance_ray->offset, &offset); - actor_move_transform_avoidance_vector(avoidance_data, &avoidance_ray->divergence, &divergence); + origin_offset = *global_zero_vector3d; + point_from_line3d((real_point3d *)&origin_offset, &avoidance_data->forward, (avoidance_ray->offset.i), (real_point3d *)&origin_offset); + point_from_line3d((real_point3d *)&origin_offset, &avoidance_data->left, (avoidance_ray->offset.j), (real_point3d *)&origin_offset); + point_from_line3d((real_point3d *)&origin_offset, &avoidance_data->up, (avoidance_ray->offset.k), (real_point3d *)&origin_offset); + divergence_direction = *global_zero_vector3d; + point_from_line3d((real_point3d *)&divergence_direction, &avoidance_data->forward, (avoidance_ray->divergence.i), (real_point3d *)&divergence_direction); + point_from_line3d((real_point3d *)&divergence_direction, &avoidance_data->left, (avoidance_ray->divergence.j), (real_point3d *)&divergence_direction); + point_from_line3d((real_point3d *)&divergence_direction, &avoidance_data->up, (avoidance_ray->divergence.k), (real_point3d *)&divergence_direction); - ray_origin->x = offset.i*avoidance_data->avoid_width + avoidance_data->origin.x; - ray_origin->y = offset.j*avoidance_data->avoid_width + avoidance_data->origin.y; - ray_origin->z = offset.k*avoidance_data->avoid_width + avoidance_data->origin.z; + point_from_line3d(&avoidance_data->origin, &origin_offset, (avoidance_data->avoid_width), ray_origin); + scale_vector3d(&divergence_direction, avoidance_ray->length*avoidance_data->avoid_distance, ray_direction); - scale = avoidance_data->avoid_distance*avoidance_ray->length; - ray_direction->i = divergence.i*scale; - ray_direction->j = divergence.j*scale; - ray_direction->k = divergence.k*scale; - - vector_from_points3d(&avoidance_data->origin, ray_origin, &offset); - if (collision_bsp_test_vector( - FLAG(_collision_test_front_facing_surfaces_bit) | - FLAG(_collision_test_back_facing_surfaces_bit), - avoidance_data->bsp, - 0, - NULL, - &avoidance_data->origin, - &offset, - 1.f, - &collision_result)) { - result = _actor_vector_avoidance_obstructed_structure; - *collision_t = 0.f; - } - else if (collision_bsp_test_vector( - FLAG(_collision_test_front_facing_surfaces_bit) | - FLAG(_collision_test_back_facing_surfaces_bit), - avoidance_data->bsp, - 0, - NULL, - ray_origin, - ray_direction, - 1.f, - &collision_result)) - { - result = _actor_vector_avoidance_obstructed_structure; - *collision_t = collision_result.t; + real_vector3d vector_to_origin; + struct collision_bsp_test_vector_result collision_result; + + vector_from_points3d(&avoidance_data->origin, ray_origin, &vector_to_origin); + if (collision_bsp_test_vector( + FLAG(_collision_test_front_facing_surfaces_bit) | + FLAG(_collision_test_back_facing_surfaces_bit), + avoidance_data->bsp, + 0, + NULL, + &avoidance_data->origin, + &vector_to_origin, + 1.f, + &collision_result)) + { + result = _actor_vector_avoidance_obstructed_structure; + *collision_t = 0.f; + } + else if (collision_bsp_test_vector( + FLAG(_collision_test_front_facing_surfaces_bit) | + FLAG(_collision_test_back_facing_surfaces_bit), + avoidance_data->bsp, + 0, + NULL, + ray_origin, + ray_direction, + 1.f, + &collision_result)) + { + result = _actor_vector_avoidance_obstructed_structure; + *collision_t = collision_result.t; + } } for (object_index = 0; @@ -902,7 +889,8 @@ static short actor_move_test_avoidance_vector( { struct vehicle_avoidance_cylinder *cylinder = &avoidance_data->avoidance_objects[object_index]; - real object_t; + real_vector3d dummy_normal; + real intersect_t; if (pill_test_vector3d( &cylinder->base, @@ -910,12 +898,12 @@ static short actor_move_test_avoidance_vector( cylinder->width, ray_origin, ray_direction, - &object_t, - &offset) && - object_t < *collision_t) + &intersect_t, + &dummy_normal) && + intersect_t < *collision_t) { result = _actor_vector_avoidance_obstructed_object; - *collision_t = object_t; + *collision_t = intersect_t; } } diff --git a/source/ai/actor_type_definitions.h b/source/ai/actor_type_definitions.h deleted file mode 100644 index 04b25b63..00000000 --- a/source/ai/actor_type_definitions.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -ACTOR_TYPE_DEFINITIONS.H - -file has inline function assertions. -*/ - -#ifndef __ACTOR_TYPE_DEFINITIONS_H -#define __ACTOR_TYPE_DEFINITIONS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __ACTOR_TYPE_DEFINITIONS_H diff --git a/source/ai/ai_constants.h b/source/ai/ai_constants.h deleted file mode 100644 index 21cd8c91..00000000 --- a/source/ai/ai_constants.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -AI_CONSTANTS.H - -header included in hcex build. -*/ - -#ifndef __AI_CONSTANTS_H -#define __AI_CONSTANTS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __AI_CONSTANTS_H diff --git a/source/bitmaps/bitmap_macros.h b/source/bitmaps/bitmap_macros.h deleted file mode 100644 index bd547c9f..00000000 --- a/source/bitmaps/bitmap_macros.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -BITMAP_MACROS.H - -header included in hcex build. -*/ - -#ifndef __BITMAP_MACROS_H -#define __BITMAP_MACROS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __BITMAP_MACROS_H diff --git a/source/bitmaps/s3tc/s3tc.c b/source/bitmaps/s3tc/s3tc.c index ac22649f..2d62daf9 100644 --- a/source/bitmaps/s3tc/s3tc.c +++ b/source/bitmaps/s3tc/s3tc.c @@ -655,7 +655,19 @@ void DecodeBlockRGB__single_pixel( if (pblockSrc == NULL) { - memset(colorDst, 0, sizeof(*colorDst)); + /* BUG (original, preserved for exact matching): January clears S3TC_BLOCK_PIXELS colors here, + * 0x40 bytes (0x45fc30 +0x11 push 0x40, +0x15 call _csmemset), as the full-block decoder + * DecodeBlockRGB does, but colorDst addresses ONE color. In bitmap_2d_get_pixel, the only caller + * chain, that color is the four-byte local pixel, so the clear would overwrite 60 further bytes of + * that function's frame. + * The clear is not executed in January: bitmap_2d_get_pixel decodes only when + * bitmap->base_address is not NULL and halts (display_assert, then an unconditional system_exit) + * unless block_address >= bitmap->base_address; it passes block_address here for dxt1, and + * DecodeBlockAlpha4__single_pixel and DecodeBlockAlpha3__single_pixel pass &source->rgb + * (block_address + 8) for dxt3 and dxt5. No other function references this one. + * January's bytes give the length (0x40) only; its spelling here is inferred from DecodeBlockRGB, + * not recovered source. A corrected build clears sizeof(*colorDst). */ + memset(colorDst, 0, S3TC_BLOCK_PIXELS * sizeof(struct s3tc_color)); return; } @@ -692,8 +704,13 @@ void DecodeBlockRGB__single_pixel( colors[3].rgba[S3TC_ALPHA] = 0; } - match_assert("c:\\halo\\SOURCE\\bitmaps\\s3tc\\s3tc.c", 773, u >= 0 && u < 4); - match_assert("c:\\halo\\SOURCE\\bitmaps\\s3tc\\s3tc.c", 774, v >= 0 && v < 4); + /* BUG (original, preserved for exact matching): both assertions admit 4 (0x45fc30 +0x17c cmp si,4 / + * +0x180 jle; +0x1aa cmp bx,4 / +0x1ae jle; the assertion strings are January's "u>=0 && u<=4" and + * "v>=0 && v<=4"), although a block has four columns and four rows, 0..3. With u or v equal to 4 the + * selector shift below can reach 32 or more. bitmap_2d_get_pixel masks both coordinates with 3 before + * every call, so neither value exceeds 3 in January. A corrected build asserts u<4 and v<4. */ + match_assert("c:\\halo\\SOURCE\\bitmaps\\s3tc\\s3tc.c", 773, u>=0 && u<=4); + match_assert("c:\\halo\\SOURCE\\bitmaps\\s3tc\\s3tc.c", 774, v>=0 && v<=4); *colorDst = colors[(pblockSrc->bitmap >> (2 * (4 * v + u))) & 3]; diff --git a/source/bungie_net/common/memory_manager.h b/source/bungie_net/common/memory_manager.h deleted file mode 100644 index b110f063..00000000 --- a/source/bungie_net/common/memory_manager.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -MEMORY_MANAGER.H - -header included in hcex build. -*/ - -#ifndef __MEMORY_MANAGER_H -#define __MEMORY_MANAGER_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __MEMORY_MANAGER_H diff --git a/source/bungie_net/network/transport.h b/source/bungie_net/network/transport.h index 6b385cd9..82835ae6 100644 --- a/source/bungie_net/network/transport.h +++ b/source/bungie_net/network/transport.h @@ -70,7 +70,7 @@ struct transport_address /* ---------- prototypes */ -short transport_initialize( +long transport_initialize( void); short transport_dispose( void); diff --git a/source/bungie_net/network/transport_endpoint_set_winsock.c b/source/bungie_net/network/transport_endpoint_set_winsock.c index 4e26f467..5d3aa9f8 100644 --- a/source/bungie_net/network/transport_endpoint_set_winsock.c +++ b/source/bungie_net/network/transport_endpoint_set_winsock.c @@ -249,25 +249,27 @@ void transport_pop_key( return; } -short transport_initialize( +long transport_initialize( void) { + long result = _transport_error_none; + if (!transport_initialized) { WSADATA info = { 0 }; XNetStartupParams startup_params = { 0 }; DWORD link_status; - DWORD address_status; - unsigned long deadline; FILE *bypass_file; - short wsa_error; startup_params.cfgSizeOfStruct = sizeof(startup_params); + startup_params.cfgFlags = 0; startup_params.cfgPrivatePoolSizeInPages = 24; startup_params.cfgEnetReceiveQueueLength = 8; startup_params.cfgIpFragMaxSimultaneous = 4; startup_params.cfgIpFragMaxPacketDiv256 = 8; startup_params.cfgSockMaxSockets = 128; + startup_params.cfgSockDefaultRecvBufsizeInK = 0; + startup_params.cfgSockDefaultSendBufsizeInK = 0; startup_params.cfgKeyRegMax = 1; startup_params.cfgSecRegMax = 32; @@ -281,6 +283,9 @@ short transport_initialize( (link_status & XNET_ETHERNET_LINK_FULL_DUPLEX) ? " in full-duplex mode" : "", (link_status & XNET_ETHERNET_LINK_HALF_DUPLEX) ? " in half-duplex mode" : ""); + /* January stores these two fields a second time here. */ + startup_params.cfgSizeOfStruct = sizeof(startup_params); + startup_params.cfgFlags = 0; bypass_file = fopen("d:\\bypass_security.txt", "r"); if (bypass_file) { @@ -290,35 +295,53 @@ short transport_initialize( } if (XNetStartup(&startup_params) != 0) - return _transport_error_not_initialized; - - wsa_error = WSAStartup(MAKEWORD(2, 0), &info); - if (wsa_error != 0) { - XNetCleanup(); - winsock_error_to_string(wsa_error); - return _transport_error_not_initialized; + result = _transport_error_not_initialized; } - - deadline = system_milliseconds() + 10000; - do + else { - address_status = XNetGetTitleXnAddr(&global_address); - if (system_milliseconds() > deadline || - address_status == XNET_GET_XNADDR_NONE) + short wsa_error = WSAStartup(MAKEWORD(2, 0), &info); + + if (wsa_error != 0) { - WSACleanup(); XNetCleanup(); + winsock_error_to_string(wsa_error); + return _transport_error_not_initialized; } - } - while (address_status == XNET_GET_XNADDR_PENDING); + else + { + unsigned long deadline = system_milliseconds() + 10000; + DWORD address_status; - XNetRandom(global_nonce, sizeof(global_nonce)); - transport_initialized = TRUE; + do + { + address_status = XNetGetTitleXnAddr(&global_address); + if (system_milliseconds() > deadline) + { + address_status = XNET_GET_XNADDR_NONE; + break; + } + } + while (address_status == XNET_GET_XNADDR_PENDING); + + if (address_status == XNET_GET_XNADDR_NONE) + { + WSACleanup(); + XNetCleanup(); + result = _transport_error_not_initialized; + } + else + { + XNetRandom(global_nonce, sizeof(global_nonce)); + result = _transport_error_none; + transport_initialized = TRUE; + } + } + } } - return _transport_error_none; + return result; } void transport_client_stop( @@ -468,18 +491,25 @@ static int __cdecl poll_ep_array_compare_proc( void const *a, void const *b) { - struct transport_endpoint *endpoint_a = *(struct transport_endpoint *const *)a; - struct transport_endpoint *endpoint_b = *(struct transport_endpoint *const *)b; + int result; - if (!endpoint_a && endpoint_b) + a = *(struct transport_endpoint const *const *)a; + b = *(struct transport_endpoint const *const *)b; + + if (!a && b) { - return 1; + result = 1; } - else if (endpoint_a && !endpoint_b) + else if (a && !b) { - return -1; + result = -1; } - return 0; + else + { + result = 0; + } + + return result; } /* ---------- public code */ diff --git a/source/camera/camera_track_definitions.h b/source/camera/camera_track_definitions.h deleted file mode 100644 index 00d97956..00000000 --- a/source/camera/camera_track_definitions.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -CAMERA_TRACK_DEFINITIONS.H - -header included in hcex build. -*/ - -#ifndef __CAMERA_TRACK_DEFINITIONS_H -#define __CAMERA_TRACK_DEFINITIONS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __CAMERA_TRACK_DEFINITIONS_H diff --git a/source/cseries/build_number.h b/source/cseries/build_number.h deleted file mode 100644 index 5d59c92f..00000000 --- a/source/cseries/build_number.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -BUILD_NUMBER.H - -header included in hcex build. -*/ - -#ifndef __BUILD_NUMBER_H -#define __BUILD_NUMBER_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __BUILD_NUMBER_H diff --git a/source/cseries/headers.h b/source/cseries/headers.h deleted file mode 100644 index b870c2cc..00000000 --- a/source/cseries/headers.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -HEADERS.H - -header included in hcex build. -*/ - -#ifndef __HEADERS_H -#define __HEADERS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __HEADERS_H diff --git a/source/cseries/platform.h b/source/cseries/platform.h deleted file mode 100644 index 178b0844..00000000 --- a/source/cseries/platform.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -PLATFORM.H - -header included in hcex build. -*/ - -#ifndef __PLATFORM_H -#define __PLATFORM_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __PLATFORM_H diff --git a/source/cutscene/cinematics.c b/source/cutscene/cinematics.c index ab5c78ae..eeddaf6f 100644 --- a/source/cutscene/cinematics.c +++ b/source/cutscene/cinematics.c @@ -431,7 +431,6 @@ void cinematic_render( if (!ui_widgets_active()) { real_argb_color text_color; - long shadow_alpha; pixel32_to_real_argb_color( title->foreground_color, @@ -454,15 +453,13 @@ void cinematic_render( title->text_flags, &text_color); - shadow_alpha = PIN( - fast_ftol( - (real)(long)(byte)(title->shadow_color >> 24) * - fade_amount), - 0, - 255); - rasterizer_text_set_shadow_color( - ((pixel32)shadow_alpha << 24) | + ((pixel32)PIN( + fast_ftol( + (real)(long)(byte)(title->shadow_color >> 24) * + fade_amount), + 0, + 255) << 24) | (title->shadow_color & 0x00FFFFFF)); { diff --git a/source/dialogs/dialogs.h b/source/dialogs/dialogs.h deleted file mode 100644 index 1bbe187c..00000000 --- a/source/dialogs/dialogs.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -DIALOGS.H - -header included in hcex build. -*/ - -#ifndef __DIALOGS_H -#define __DIALOGS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __DIALOGS_H diff --git a/source/effects/particle_definitions.h b/source/effects/particle_definitions.h deleted file mode 100644 index aee56088..00000000 --- a/source/effects/particle_definitions.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -PARTICLE_DEFINITIONS.H - -header included in hcex build. -*/ - -#ifndef __PARTICLE_DEFINITIONS_H -#define __PARTICLE_DEFINITIONS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __PARTICLE_DEFINITIONS_H diff --git a/source/hs/hs_compile.c b/source/hs/hs_compile.c index e31047e0..5f67b1fa 100644 --- a/source/hs/hs_compile.c +++ b/source/hs/hs_compile.c @@ -413,7 +413,7 @@ symbols in this file: #include "ai/ai_script.h" #include "hs/hs.h" #include "hs/hs_library_internal.h" -#include "hs/hs_library_internal_compile.h" +/* Empty reconstructed header removed; no declarations to include. */ #include "hs/hs_scenario_definitions.h" #include "interface/hud_definitions.h" #include "interface/interface.h" diff --git a/source/hs/hs_enum.h b/source/hs/hs_enum.h deleted file mode 100644 index 32c750f1..00000000 --- a/source/hs/hs_enum.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -HS_ENUM.H - -header included in hcex build. -*/ - -#ifndef __HS_ENUM_H -#define __HS_ENUM_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __HS_ENUM_H diff --git a/source/hs/hs_function.h b/source/hs/hs_function.h deleted file mode 100644 index 06d0cf0a..00000000 --- a/source/hs/hs_function.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -HS_FUNCTION.H - -header included in hcex build. -*/ - -#ifndef __HS_FUNCTION_H -#define __HS_FUNCTION_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __HS_FUNCTION_H diff --git a/source/hs/hs_glue.h b/source/hs/hs_glue.h deleted file mode 100644 index d2b7381f..00000000 --- a/source/hs/hs_glue.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -HS_GLUE.H - -header included in hcex build. -*/ - -#ifndef __HS_GLUE_H -#define __HS_GLUE_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __HS_GLUE_H diff --git a/source/hs/hs_library_internal_compile.h b/source/hs/hs_library_internal_compile.h deleted file mode 100644 index 80ebfedf..00000000 --- a/source/hs/hs_library_internal_compile.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -HS_LIBRARY_INTERNAL_COMPILE.H - -file has inline function assertions. -*/ - -#ifndef __HS_LIBRARY_INTERNAL_COMPILE_H -#define __HS_LIBRARY_INTERNAL_COMPILE_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/HS_COMPILE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __HS_LIBRARY_INTERNAL_COMPILE_H diff --git a/source/hs/hs_library_internal_runtime.h b/source/hs/hs_library_internal_runtime.h deleted file mode 100644 index 6b51fde9..00000000 --- a/source/hs/hs_library_internal_runtime.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -HS_LIBRARY_INTERNAL_RUNTIME.H - -file has inline function assertions. -*/ - -#ifndef __HS_LIBRARY_INTERNAL_RUNTIME_H -#define __HS_LIBRARY_INTERNAL_RUNTIME_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/HS_RUNTIME.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __HS_LIBRARY_INTERNAL_RUNTIME_H diff --git a/source/hs/hs_runtime.c b/source/hs/hs_runtime.c index f9f27a75..6a4a0f3d 100644 --- a/source/hs/hs_runtime.c +++ b/source/hs/hs_runtime.c @@ -266,7 +266,7 @@ symbols in this file: #include "ai/ai_script.h" #include "hs/hs.h" #include "hs/hs_library_internal.h" -#include "hs/hs_library_internal_runtime.h" +/* Empty reconstructed header removed; no declarations to include. */ #include "hs/object_lists.h" #include "hs/hs_scenario_definitions.h" #include "math/real_math.h" diff --git a/source/input/input_device_defaults.h b/source/input/input_device_defaults.h deleted file mode 100644 index ca9989e7..00000000 --- a/source/input/input_device_defaults.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -INPUT_DEVICE_DEFAULTS.H - -header included in hcex build. -*/ - -#ifndef __INPUT_DEVICE_DEFAULTS_H -#define __INPUT_DEVICE_DEFAULTS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __INPUT_DEVICE_DEFAULTS_H diff --git a/source/input/input_windows.h b/source/input/input_windows.h deleted file mode 100644 index c18cd27c..00000000 --- a/source/input/input_windows.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -INPUT_WINDOWS.H - -header included in hcex build. -*/ - -#ifndef __INPUT_WINDOWS_H -#define __INPUT_WINDOWS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __INPUT_WINDOWS_H diff --git a/source/interface/hud_messaging_definitions.h b/source/interface/hud_messaging_definitions.h deleted file mode 100644 index 3d766085..00000000 --- a/source/interface/hud_messaging_definitions.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -HUD_MESSAGING_DEFINITIONS.H - -header included in hcex build. -*/ - -#ifndef __HUD_MESSAGING_DEFINITIONS_H -#define __HUD_MESSAGING_DEFINITIONS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __HUD_MESSAGING_DEFINITIONS_H diff --git a/source/interface/hud_nav_points.c b/source/interface/hud_nav_points.c index f30b2705..f1b6eca1 100644 --- a/source/interface/hud_nav_points.c +++ b/source/interface/hud_nav_points.c @@ -677,109 +677,67 @@ short hud_get_nav_point_render_type( void custom_render_nav_point( short local_player_index, - real_point3d const *position, + real_point3d const *position_pointer, short nav_index, short waypoint_type) { long return_eip = get_return_eip(); long stack_buffer[STACK_BUFFER_LENGTH]; struct hud_waypoint_arrow *arrow; - real_point3d view_point; + real_point3d position; real distance; - real arrow_scale; + real scale; real_point2d screen_position; - real horizontal_radius; - real vertical_radius; - real vertical_component; - real horizontal_component; - real radius_product; + real a, b, ab, bx, ay; real theta; - csmemset(stack_buffer, 0x62, sizeof(stack_buffer)); - - arrow = TAG_BLOCK_GET_ELEMENT( - &hud_globals->waypoint.arrows, - nav_index, - struct hud_waypoint_arrow); - view_point = *position; + memset(stack_buffer, 0x62, sizeof(stack_buffer)); + arrow = TAG_BLOCK_GET_ELEMENT(&hud_globals->waypoint.arrows, nav_index, struct hud_waypoint_arrow); + position = *position_pointer; { - long unit_index = local_player_get_player_index(local_player_index)==NONE ? - NONE : - player_get(local_player_get_player_index(local_player_index))->unit_index; real_point3d cam_pos; - real delta_x; - real delta_y; - real delta_z; - unit_get_camera_position(unit_index, &cam_pos); - - delta_x = position->x-cam_pos.x; - delta_y = position->y-cam_pos.y; - delta_z = position->z-cam_pos.z; - distance = square_root( - delta_x*delta_x + (delta_y*delta_y + delta_z*delta_z)); + unit_get_camera_position(local_player_get_player_index(local_player_index) == NONE ? NONE : player_get(local_player_get_player_index(local_player_index))->unit_index, &cam_pos); + distance = distance3d(&cam_pos, position_pointer); } - if (distance>15.0f) + if (distance > 15.f) { - arrow_scale = 0.5f; + scale = 0.5f; } else { - arrow_scale = (real)pow( - 1.0f-distance*(1.0f/15.0f), - 0.7) + 0.5f; + scale = (real)pow(1.f - distance / 15.f, 0.7) + 0.5f; } - matrix4x3_transform_point( - &render.frustum.world_to_view, - &view_point, - &view_point); - - if (waypoint_type==_waypoint_off_screen || - !render_camera_view_to_screen( - &render.camera, - &render.frustum, - &view_point, - &screen_position)) + matrix4x3_transform_point(&render.frustum.world_to_view, &position, &position); + if (waypoint_type == _waypoint_off_screen || !render_camera_view_to_screen(&render.camera, &render.frustum, &position, &screen_position)) { - screen_position.x = view_point.x; - screen_position.y = -view_point.y; + screen_position.x = position.x; + screen_position.y = -position.y; waypoint_type = _waypoint_off_screen; } else { - screen_position.x -= (real)( - ((render.camera.viewport_bounds.x1-render.camera.viewport_bounds.x0)/2) + - render.camera.viewport_bounds.x0); - screen_position.y -= (real)( - ((render.camera.viewport_bounds.y1-render.camera.viewport_bounds.y0)/2) + - render.camera.viewport_bounds.y0); + screen_position.x -= render.camera.viewport_bounds.x0 + (render.camera.viewport_bounds.x1 - render.camera.viewport_bounds.x0) / 2; + screen_position.y -= render.camera.viewport_bounds.y0 + (render.camera.viewport_bounds.y1 - render.camera.viewport_bounds.y0) / 2; } - horizontal_radius = - ((real)(render.camera.window_bounds.x1-render.camera.window_bounds.x0) - - (hud_globals->waypoint.right_offset+hud_globals->waypoint.left_offset))*0.5f; - vertical_radius = - ((real)(render.camera.window_bounds.y1-render.camera.window_bounds.y0) - - (hud_globals->waypoint.bottom_offset+hud_globals->waypoint.top_offset))*0.5f; - radius_product = vertical_radius*horizontal_radius; - vertical_component = vertical_radius*screen_position.x; - horizontal_component = horizontal_radius*screen_position.y; - theta = 0.0f; + a = ((render.camera.window_bounds.x1 - render.camera.window_bounds.x0) - (hud_globals->waypoint.right_offset + hud_globals->waypoint.left_offset)) / 2.f; + b = ((render.camera.window_bounds.y1 - render.camera.window_bounds.y0) - (hud_globals->waypoint.bottom_offset + hud_globals->waypoint.top_offset)) / 2.f; + ab = a * b; + bx = b * screen_position.x; + ay = a * screen_position.y; + theta = 0.f; - if (waypoint_type==_waypoint_off_screen || - radius_product*radius_product <= - vertical_component*vertical_component + horizontal_component*horizontal_component) + if (waypoint_type == _waypoint_off_screen || ab * ab <= bx * bx + ay * ay) { - real scale = square_root( - (radius_product*radius_product) / - (vertical_component*vertical_component + horizontal_component*horizontal_component)); + real factor = square_root(ab * ab / (bx * bx + ay * ay)); + screen_position.x *= factor; + screen_position.y *= factor; waypoint_type = _waypoint_off_screen; - screen_position.x *= scale; - screen_position.y *= scale; if (!TEST_FLAG(arrow->flags, _hud_waypoint_dont_rotate_offscreen_bit)) { @@ -787,112 +745,64 @@ void custom_render_nav_point( } } - screen_position.x += (real)( - (render.camera.viewport_bounds.x1-render.camera.viewport_bounds.x0)/2); - screen_position.y += (real)( - (render.camera.viewport_bounds.y1-render.camera.viewport_bounds.y0)/2); - - match_assert( - "c:\\halo\\SOURCE\\interface\\hud_nav_points.c", - 615, - waypoint_type!=NONE); - + screen_position.x += (render.camera.viewport_bounds.x1 - render.camera.viewport_bounds.x0) / 2; + screen_position.y += (render.camera.viewport_bounds.y1 - render.camera.viewport_bounds.y0) / 2; + match_assert("c:\\halo\\SOURCE\\interface\\hud_nav_points.c", 615, waypoint_type!=NONE); { - long bitmap_group_index = hud_globals->waypoint.arrow_bitmap.index; + long bitmap_index = hud_globals->waypoint.arrow_bitmap.index; struct bitmap_data const *bitmap = NULL; real_rectangle2d const *clip = NULL; - hud_retrieve_bitmap_and_bounding_rect( - bitmap_group_index, - arrow->sequence_indices[waypoint_type], - 0, - &bitmap, - &clip); - - if (bitmap && _texture_cache_bitmap_get_hardware_format( - (struct bitmap_data *)bitmap, FALSE, TRUE)) + hud_retrieve_bitmap_and_bounding_rect(bitmap_index, arrow->sequence_indices[waypoint_type], 0, &bitmap, &clip); + if (bitmap && _texture_cache_bitmap_get_hardware_format((struct bitmap_data *)bitmap, FALSE, TRUE)) { - point2d point; - real_rgb_color rgb_temp; + point2d corner; byte alpha; - real fade; + real_rgb_color rgb_temp; - point.x = (short)(long)screen_position.x; - point.y = (short)(long)screen_position.y; - alpha = (byte)PIN(fast_ftol_C(arrow->opacity)*255, 0, 255); + corner.x = (short)screen_position.x; + corner.y = (short)screen_position.y; + alpha = PIN(255 * fast_ftol_C(arrow->opacity), 0, 255); pixel32_to_real_rgb_color(arrow->color, &rgb_temp); - fade = 1.0f-arrow->fade; - rgb_temp.red *= PIN(fade, 0.0f, 1.0f); - rgb_temp.green *= PIN(fade, 0.0f, 1.0f); - rgb_temp.blue *= PIN(fade, 0.0f, 1.0f); + rgb_temp.red *= PIN(1.f - arrow->fade, 0.f, 1.f); + rgb_temp.green *= PIN(1.f - arrow->fade, 0.f, 1.f); + rgb_temp.blue *= PIN(1.f - arrow->fade, 0.f, 1.f); + hud_draw_bitmap_direct(bitmap, _hud_anchor_center, &corner, clip, scale, theta, ((pixel32)alpha << 24) | real_rgb_color_to_pixel32(&rgb_temp), FALSE); - hud_draw_bitmap_direct( - bitmap, - _hud_anchor_center, - &point, - clip, - arrow_scale, - theta, - ((pixel32)alpha<<24) | real_rgb_color_to_pixel32(&rgb_temp), - FALSE); - - if (waypoint_type!=_waypoint_off_screen) + if (waypoint_type != _waypoint_off_screen) { - struct number_hud_element_definition numbers; struct hud_absolute_placement_definition placement; - real decimal_modulus; - real bitmap_extent; - - distance *= 3.0480001f; - csmemset(&placement, 0, sizeof(placement)); - csmemset(&numbers, 0, sizeof(numbers)); + struct number_hud_element_definition numbers; + real fractional_value; + distance *= 3.048f; + memset(&placement, 0, sizeof(struct hud_absolute_placement_definition)); + memset(&numbers, 0, sizeof(struct number_hud_element_definition)); placement.corner = _hud_anchor_top_left; - numbers.colors.color = - ((pixel32)alpha<<24) | real_rgb_color_to_pixel32(&rgb_temp); - numbers.colors.flash_color = - ((pixel32)alpha<<24) | real_rgb_color_to_pixel32(&rgb_temp); + numbers.colors.color = ((pixel32)alpha << 24) | real_rgb_color_to_pixel32(&rgb_temp); + numbers.colors.flash_color = ((pixel32)alpha << 24) | real_rgb_color_to_pixel32(&rgb_temp); numbers.digits = 3; numbers.fractional_digits = 1; - numbers.number_flags = - FLAG(_hud_number_show_all_leading_zeros_bit) | - FLAG(_hud_number_show_trailing_m_bit); + numbers.number_flags = FLAG(_hud_number_show_all_leading_zeros_bit) | FLAG(_hud_number_show_trailing_m_bit); - bitmap_extent = - ((clip->x1-clip->x0)*(real)bitmap->width)*0.5f; - numbers.placement.offset.x = (short)(long)( - bitmap_extent*arrow_scale*0.33000001f + (real)point.x); - bitmap_extent = - ((clip->y1-clip->y0)*(real)bitmap->height)*0.5f; - numbers.placement.offset.y = (short)(long)( - bitmap_extent*arrow_scale*0.66000003f + (real)point.y); - numbers.placement.offset.x -= render.camera.window_bounds.x0; - numbers.placement.offset.x += render.camera.viewport_bounds.x0; - numbers.placement.offset.y -= render.camera.window_bounds.y0; - numbers.placement.offset.y += render.camera.viewport_bounds.y0; + numbers.placement.offset.x = corner.x + (bitmap->width * (clip->x1 - clip->x0) / 2.f) * scale * 0.33f; + numbers.placement.offset.y = corner.y + (bitmap->height * (clip->y1 - clip->y0) / 2.f) * scale * 0.66f; + numbers.placement.offset.x -= render.camera.window_bounds.x0 - render.camera.viewport_bounds.x0; + numbers.placement.offset.y -= render.camera.window_bounds.y0 - render.camera.viewport_bounds.y0; - decimal_modulus = (real)pow(10.0, 4.0); - { - short decimal_value = (short)fast_ftol( - (real)fmod( - fabs(decimal_modulus*distance), - decimal_modulus)); - - hud_draw_numbers( - local_player_index, - &placement, - &numbers, - (short)fast_ftol_C(distance), - decimal_value, - 0, - 0, - 0.0f); - } + fractional_value = power(10.f, 4.f); + hud_draw_numbers(local_player_index, &placement, &numbers, + fast_ftol_C(distance), fast_ftol(fmod(fabs((real)(fractional_value * distance)), fractional_value)), + 0, 0, 0.f); } } } - match_assert_stack_frame("c:\\halo\\SOURCE\\interface\\hud_nav_points.c", 675); + { + short corrupt_index = check_stack_buffer(stack_buffer); + match_vassert("c:\\halo\\SOURCE\\interface\\hud_nav_points.c", 675, return_eip == get_return_eip(), "corrupt return address!"); + match_vassert("c:\\halo\\SOURCE\\interface\\hud_nav_points.c", 675, corrupt_index == NONE, csprintf(temporary, "corrupt stack at %d!", corrupt_index)); + } return; } diff --git a/source/interface/interface_panels.h b/source/interface/interface_panels.h deleted file mode 100644 index dee43d2f..00000000 --- a/source/interface/interface_panels.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -INTERFACE_PANELS.H - -header included in hcex build. -*/ - -#ifndef __INTERFACE_PANELS_H -#define __INTERFACE_PANELS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __INTERFACE_PANELS_H diff --git a/source/interface/inventory_displays.h b/source/interface/inventory_displays.h deleted file mode 100644 index de198854..00000000 --- a/source/interface/inventory_displays.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -INVENTORY_DISPLAYS.H - -header included in hcex build. -*/ - -#ifndef __INVENTORY_DISPLAYS_H -#define __INVENTORY_DISPLAYS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __INVENTORY_DISPLAYS_H diff --git a/source/interface/meter_definitions.h b/source/interface/meter_definitions.h deleted file mode 100644 index 8dbd1846..00000000 --- a/source/interface/meter_definitions.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -METER_DEFINITIONS.H - -header included in hcex build. -*/ - -#ifndef __METER_DEFINITIONS_H -#define __METER_DEFINITIONS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __METER_DEFINITIONS_H diff --git a/source/interface/ui_widget_group.h b/source/interface/ui_widget_group.h deleted file mode 100644 index 6d2b511f..00000000 --- a/source/interface/ui_widget_group.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -UI_WIDGET_GROUP.H - -header included in hcex build. -*/ - -#ifndef __UI_WIDGET_GROUP_H -#define __UI_WIDGET_GROUP_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __UI_WIDGET_GROUP_H diff --git a/source/items/garbage_definitions.h b/source/items/garbage_definitions.h deleted file mode 100644 index a8530c5c..00000000 --- a/source/items/garbage_definitions.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -GARBAGE_DEFINITIONS.H - -header included in hcex build. -*/ - -#ifndef __GARBAGE_DEFINITIONS_H -#define __GARBAGE_DEFINITIONS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __GARBAGE_DEFINITIONS_H diff --git a/source/items/items.c b/source/items/items.c index 942008ac..7e756066 100644 --- a/source/items/items.c +++ b/source/items/items.c @@ -461,7 +461,6 @@ void item_accelerate( { real acceleration_magnitude = magnitude3d(acceleration); real_vector3d rotation_axis; - double rotation_magnitude; if (acceleration_magnitude < 0.0001f) { @@ -477,10 +476,10 @@ void item_accelerate( &rotation_axis); } - rotation_magnitude = - real_seed_random(get_global_random_seed_address()) * - acceleration_magnitude * 1.5707964f; - scale_vector3d(&rotation_axis, (real)rotation_magnitude, &rotation_axis); + scale_vector3d( + &rotation_axis, + real_seed_random(get_global_random_seed_address()) * 1.5707964f * acceleration_magnitude, + &rotation_axis); add_vectors3d( &item->object.angular_velocity, &rotation_axis, diff --git a/source/items/weapon_interface_definitions.h b/source/items/weapon_interface_definitions.h deleted file mode 100644 index 37e38014..00000000 --- a/source/items/weapon_interface_definitions.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -WEAPON_INTERFACE_DEFINITIONS.H - -header included in hcex build. -*/ - -#ifndef __WEAPON_INTERFACE_DEFINITIONS_H -#define __WEAPON_INTERFACE_DEFINITIONS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __WEAPON_INTERFACE_DEFINITIONS_H diff --git a/source/math/real_math.h b/source/math/real_math.h index 219604e4..d69d9c97 100644 --- a/source/math/real_math.h +++ b/source/math/real_math.h @@ -1556,7 +1556,7 @@ __inline real plane3d_distance_to_point( real_plane3d const *plane, real_point3d const *point) { - return (dot_product3d((real_vector3d *)point, &plane->n) - plane->d); + return dot_product3d((real_vector3d *)point, &plane->n) - plane->d; } #endif diff --git a/source/math/vector_tree.h b/source/math/vector_tree.h deleted file mode 100644 index 856b4cbf..00000000 --- a/source/math/vector_tree.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -VECTOR_TREE.H - -header included in hcex build. -*/ - -#ifndef __VECTOR_TREE_H -#define __VECTOR_TREE_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __VECTOR_TREE_H diff --git a/source/memory/sl_list.h b/source/memory/sl_list.h deleted file mode 100644 index fd854e2e..00000000 --- a/source/memory/sl_list.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -SL_LIST.H - -header included in hcex build. -*/ - -#ifndef __SL_LIST_H -#define __SL_LIST_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __SL_LIST_H diff --git a/source/objects/index_resolution.h b/source/objects/index_resolution.h deleted file mode 100644 index 6c360e2f..00000000 --- a/source/objects/index_resolution.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -INDEX_RESOLUTION.H - -header included in hcex build. -*/ - -#ifndef __INDEX_RESOLUTION_H -#define __INDEX_RESOLUTION_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __INDEX_RESOLUTION_H diff --git a/source/objects/widgets/aleph.h b/source/objects/widgets/aleph.h deleted file mode 100644 index d32d1360..00000000 --- a/source/objects/widgets/aleph.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -ALEPH.H - -header included in hcex build. -*/ - -#ifndef __ALEPH_H -#define __ALEPH_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __ALEPH_H diff --git a/source/objects/widgets/antenna_definitions.h b/source/objects/widgets/antenna_definitions.h deleted file mode 100644 index 5788f773..00000000 --- a/source/objects/widgets/antenna_definitions.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -ANTENNA_DEFINITIONS.H - -header included in hcex build. -*/ - -#ifndef __ANTENNA_DEFINITIONS_H -#define __ANTENNA_DEFINITIONS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __ANTENNA_DEFINITIONS_H diff --git a/source/objects/widgets/cellular_automata.h b/source/objects/widgets/cellular_automata.h deleted file mode 100644 index 2c0bdfdf..00000000 --- a/source/objects/widgets/cellular_automata.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -CELLULAR_AUTOMATA.H - -header included in hcex build. -*/ - -#ifndef __CELLULAR_AUTOMATA_H -#define __CELLULAR_AUTOMATA_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __CELLULAR_AUTOMATA_H diff --git a/source/objects/widgets/cellular_automata2d.h b/source/objects/widgets/cellular_automata2d.h deleted file mode 100644 index 0445d870..00000000 --- a/source/objects/widgets/cellular_automata2d.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -CELLULAR_AUTOMATA2D.H - -header included in hcex build. -*/ - -#ifndef __CELLULAR_AUTOMATA2D_H -#define __CELLULAR_AUTOMATA2D_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __CELLULAR_AUTOMATA2D_H diff --git a/source/objects/widgets/flag_definitions.h b/source/objects/widgets/flag_definitions.h deleted file mode 100644 index be3c394e..00000000 --- a/source/objects/widgets/flag_definitions.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -FLAG_DEFINITIONS.H - -header included in hcex build. -*/ - -#ifndef __FLAG_DEFINITIONS_H -#define __FLAG_DEFINITIONS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __FLAG_DEFINITIONS_H diff --git a/source/objects/widgets/glow_definitions.h b/source/objects/widgets/glow_definitions.h deleted file mode 100644 index 5e73eba3..00000000 --- a/source/objects/widgets/glow_definitions.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -GLOW_DEFINITIONS.H - -header included in hcex build. -*/ - -#ifndef __GLOW_DEFINITIONS_H -#define __GLOW_DEFINITIONS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __GLOW_DEFINITIONS_H diff --git a/source/objects/widgets/light_volume_definitions.h b/source/objects/widgets/light_volume_definitions.h deleted file mode 100644 index 4737f509..00000000 --- a/source/objects/widgets/light_volume_definitions.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -LIGHT_VOLUME_DEFINITIONS.H - -header included in hcex build. -*/ - -#ifndef __LIGHT_VOLUME_DEFINITIONS_H -#define __LIGHT_VOLUME_DEFINITIONS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __LIGHT_VOLUME_DEFINITIONS_H diff --git a/source/objects/widgets/lightning_definitions.h b/source/objects/widgets/lightning_definitions.h deleted file mode 100644 index 19c16522..00000000 --- a/source/objects/widgets/lightning_definitions.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -LIGHTNING_DEFINITIONS.H - -header included in hcex build. -*/ - -#ifndef __LIGHTNING_DEFINITIONS_H -#define __LIGHTNING_DEFINITIONS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __LIGHTNING_DEFINITIONS_H diff --git a/source/physics/physics_constants.h b/source/physics/physics_constants.h deleted file mode 100644 index fff8c8b2..00000000 --- a/source/physics/physics_constants.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -PHYSICS_CONSTANTS.H - -header included in hcex build. -*/ - -#ifndef __PHYSICS_CONSTANTS_H -#define __PHYSICS_CONSTANTS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __PHYSICS_CONSTANTS_H diff --git a/source/physics/terrain_definitions.h b/source/physics/terrain_definitions.h deleted file mode 100644 index 73ba3f17..00000000 --- a/source/physics/terrain_definitions.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -TERRAIN_DEFINITIONS.H - -header included in hcex build. -*/ - -#ifndef __TERRAIN_DEFINITIONS_H -#define __TERRAIN_DEFINITIONS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __TERRAIN_DEFINITIONS_H diff --git a/source/rasterizer/rasterizer_hardware_format_utilities.h b/source/rasterizer/rasterizer_hardware_format_utilities.h deleted file mode 100644 index 6797ecde..00000000 --- a/source/rasterizer/rasterizer_hardware_format_utilities.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -RASTERIZER_HARDWARE_FORMAT_UTILITIES.H - -header included in hcex build. -*/ - -#ifndef __RASTERIZER_HARDWARE_FORMAT_UTILITIES_H -#define __RASTERIZER_HARDWARE_FORMAT_UTILITIES_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __RASTERIZER_HARDWARE_FORMAT_UTILITIES_H diff --git a/source/rasterizer/rasterizer_memory_pool.c b/source/rasterizer/rasterizer_memory_pool.c index 30c4b28e..1b8e37fa 100644 --- a/source/rasterizer/rasterizer_memory_pool.c +++ b/source/rasterizer/rasterizer_memory_pool.c @@ -8,7 +8,7 @@ RASTERIZER_MEMORY_POOL.C #include "errors.h" #include "real_math.h" #include "rasterizer.h" -#include "rasterizer_memory_pool.h" +/* Empty reconstructed header removed; no declarations to include. */ /* ---------- globals */ diff --git a/source/rasterizer/rasterizer_memory_pool.h b/source/rasterizer/rasterizer_memory_pool.h deleted file mode 100644 index 29ec4acf..00000000 --- a/source/rasterizer/rasterizer_memory_pool.h +++ /dev/null @@ -1,11 +0,0 @@ -/* -RASTERIZER_MEMORY_POOL.H - -Narrow cross-translation-unit interface owned by RASTERIZER_MEMORY_POOL.C. -*/ - -#ifndef __RASTERIZER_MEMORY_POOL_H -#define __RASTERIZER_MEMORY_POOL_H -#pragma once - -#endif /* __RASTERIZER_MEMORY_POOL_H */ diff --git a/source/rasterizer/xbox/rasterizer_xbox_environment.c b/source/rasterizer/xbox/rasterizer_xbox_environment.c index 4df20253..95710f24 100644 --- a/source/rasterizer/xbox/rasterizer_xbox_environment.c +++ b/source/rasterizer/xbox/rasterizer_xbox_environment.c @@ -155,7 +155,7 @@ symbols in this file: #include "rasterizer/common/rasterizer_common.h" #include "rasterizer/rasterizer_geometry.h" #include "rasterizer/rasterizer_lights.h" -#include "rasterizer/rasterizer_memory_pool.h" +/* Empty reconstructed header removed; no declarations to include. */ #include "rasterizer/rasterizer_transparent_geometry.h" #include "shaders/shader_definitions.h" #include "shaders/shaders.h" diff --git a/source/rasterizer/xbox/rasterizer_xbox_environment_fog.c b/source/rasterizer/xbox/rasterizer_xbox_environment_fog.c index 5e29c5b9..0ed768a0 100644 --- a/source/rasterizer/xbox/rasterizer_xbox_environment_fog.c +++ b/source/rasterizer/xbox/rasterizer_xbox_environment_fog.c @@ -180,11 +180,6 @@ enum MAXIMUM_ATMOSPHERE_DOMINANT_WARNINGS = 20, }; -enum -{ - NUMBER_OF_PIXEL_SHADER_STAGES = 8, -}; - enum { _fog_definition_atmosphere_dominant_bit = 1, diff --git a/source/saved films/saved_films.h b/source/saved films/saved_films.h deleted file mode 100644 index f2b88ed7..00000000 --- a/source/saved films/saved_films.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -SAVED_FILMS.H - -header included in hcex build. -*/ - -#ifndef __SAVED_FILMS_H -#define __SAVED_FILMS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __SAVED_FILMS_H diff --git a/source/saved games/game_state_procs.h b/source/saved games/game_state_procs.h deleted file mode 100644 index 5463311c..00000000 --- a/source/saved games/game_state_procs.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -GAME_STATE_PROCS.H - -header included in hcex build. -*/ - -#ifndef __GAME_STATE_PROCS_H -#define __GAME_STATE_PROCS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __GAME_STATE_PROCS_H diff --git a/source/scenario/scenario_placement.h b/source/scenario/scenario_placement.h deleted file mode 100644 index bfb6c9da..00000000 --- a/source/scenario/scenario_placement.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -SCENARIO_PLACEMENT.H - -header included in hcex build. -*/ - -#ifndef __SCENARIO_PLACEMENT_H -#define __SCENARIO_PLACEMENT_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __SCENARIO_PLACEMENT_H diff --git a/source/scenario/structure_bsp_procs.h b/source/scenario/structure_bsp_procs.h deleted file mode 100644 index 9ed9bc80..00000000 --- a/source/scenario/structure_bsp_procs.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -STRUCTURE_BSP_PROCS.H - -header included in hcex build. -*/ - -#ifndef __STRUCTURE_BSP_PROCS_H -#define __STRUCTURE_BSP_PROCS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __STRUCTURE_BSP_PROCS_H diff --git a/source/shell/shell_windows.h b/source/shell/shell_windows.h deleted file mode 100644 index 7375799b..00000000 --- a/source/shell/shell_windows.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -SHELL_WINDOWS.H - -header included in hcex build. -*/ - -#ifndef __SHELL_WINDOWS_H -#define __SHELL_WINDOWS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __SHELL_WINDOWS_H diff --git a/source/sound/platform_sound.h b/source/sound/platform_sound.h deleted file mode 100644 index f0ad55ef..00000000 --- a/source/sound/platform_sound.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -PLATFORM_SOUND.H - -header included in hcex build. -*/ - -#ifndef __PLATFORM_SOUND_H -#define __PLATFORM_SOUND_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __PLATFORM_SOUND_H diff --git a/source/strings/resource.h b/source/strings/resource.h deleted file mode 100644 index f7a4e678..00000000 --- a/source/strings/resource.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -RESOURCE.H - -header included in hcex build. -*/ - -#ifndef __RESOURCE_H -#define __RESOURCE_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __RESOURCE_H diff --git a/source/structures/detail_object_definitions.h b/source/structures/detail_object_definitions.h deleted file mode 100644 index 89c61bb9..00000000 --- a/source/structures/detail_object_definitions.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -DETAIL_OBJECT_DEFINITIONS.H - -header included in hcex build. -*/ - -#ifndef __DETAIL_OBJECT_DEFINITIONS_H -#define __DETAIL_OBJECT_DEFINITIONS_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __DETAIL_OBJECT_DEFINITIONS_H diff --git a/source/structures/radiosity/radiosity.h b/source/structures/radiosity/radiosity.h deleted file mode 100644 index 37535934..00000000 --- a/source/structures/radiosity/radiosity.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -RADIOSITY.H - -header included in hcex build. -*/ - -#ifndef __RADIOSITY_H -#define __RADIOSITY_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __RADIOSITY_H diff --git a/source/structures/radiosity/radiosity_io.h b/source/structures/radiosity/radiosity_io.h deleted file mode 100644 index bc21a6b5..00000000 --- a/source/structures/radiosity/radiosity_io.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -RADIOSITY_IO.H - -header included in hcex build. -*/ - -#ifndef __RADIOSITY_IO_H -#define __RADIOSITY_IO_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __RADIOSITY_IO_H diff --git a/source/text/draw_string_types.h b/source/text/draw_string_types.h deleted file mode 100644 index 0fc8fbf3..00000000 --- a/source/text/draw_string_types.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -DRAW_STRING_TYPES.H - -header included in hcex build. -*/ - -#ifndef __DRAW_STRING_TYPES_H -#define __DRAW_STRING_TYPES_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __DRAW_STRING_TYPES_H diff --git a/source/tool/connected_geometry.c b/source/tool/connected_geometry.c index 1d19c83c..9e6b19a6 100644 --- a/source/tool/connected_geometry.c +++ b/source/tool/connected_geometry.c @@ -46,6 +46,8 @@ enum /* ---------- macros */ +#define CONNECTED_GEOMETRY_COPLANAR_EPSILON 0.01f + /* ---------- structures */ struct intermediate_geometry_triangle @@ -446,13 +448,12 @@ long connected_geometry_add_triangle( } static boolean triangle_coplanar( - void *predicate_data, + void *data, struct connected_geometry *geometry, struct connected_geometry_triangle *triangle, long group_index) { - real_plane3d *plane = predicate_data; - real_point3d *point0 = dynamic_array_get_element( + real_point3d const *point0= dynamic_array_get_element( &geometry->points, ((struct connected_geometry_edge *)dynamic_array_get_element( &geometry->edges, @@ -460,7 +461,7 @@ static boolean triangle_coplanar( sizeof(struct connected_geometry_edge)))->point_indices[ (triangle->edge_designators[0] & LONG_MIN) != 0], sizeof(real_point3d)); - real_point3d *point1 = dynamic_array_get_element( + real_point3d const *point1= dynamic_array_get_element( &geometry->points, ((struct connected_geometry_edge *)dynamic_array_get_element( &geometry->edges, @@ -468,48 +469,31 @@ static boolean triangle_coplanar( sizeof(struct connected_geometry_edge)))->point_indices[ (triangle->edge_designators[1] & LONG_MIN) != 0], sizeof(real_point3d)); - real_point3d *point2 = dynamic_array_get_element( + real_point3d const *point2= dynamic_array_get_element( &geometry->points, ((struct connected_geometry_edge *)dynamic_array_get_element( &geometry->edges, triangle->edge_designators[2] & LONG_MAX, sizeof(struct connected_geometry_edge)))->point_indices[ (triangle->edge_designators[2] & LONG_MIN) != 0], - sizeof(real_point3d)); + sizeof(real_point3d)); real_plane3d triangle_plane; - real point_distance; - real point_product; - real facing; + boolean coplanar; - if (fabs( - point0->y*plane->n.j + - point0->z*plane->n.k + - point0->x*plane->n.i - - plane->d) < 0.01f && - fabs( - point1->y*plane->n.j + - point1->z*plane->n.k + - point1->x*plane->n.i - - plane->d) < 0.01f) + if (fabs(plane3d_distance_to_point((real_plane3d const *)data, point0))n)>0.f) { - point_distance = point2->y*plane->n.j; - point_distance += point2->z*plane->n.k; - point_product = point2->x*plane->n.i; - point_distance += point_product; - if (fabs(point_distance - plane->d) < 0.01f && - plane3d_from_points(&triangle_plane, point0, point2, point1) != NULL) - { - facing = triangle_plane.n.i*plane->n.i; - facing += triangle_plane.n.k*plane->n.k; - facing += triangle_plane.n.j*plane->n.j; - if (facing > 0.0f) - { - return TRUE; - } - } + coplanar= TRUE; + } + else + { + coplanar= FALSE; } - return FALSE; + return coplanar; } void connected_geometry_add_intermediate_triangle( diff --git a/source/tool/intermediate_geometry.h b/source/tool/intermediate_geometry.h deleted file mode 100644 index 22cd0756..00000000 --- a/source/tool/intermediate_geometry.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -INTERMEDIATE_GEOMETRY.H - -header included in hcex build. -*/ - -#ifndef __INTERMEDIATE_GEOMETRY_H -#define __INTERMEDIATE_GEOMETRY_H -#pragma once - -/* ---------- constants */ - -/* ---------- macros */ - -/* ---------- structures */ - -/* ---------- prototypes/EXAMPLE.C */ - -/* ---------- globals */ - -/* ---------- public code */ - -#endif // __INTERMEDIATE_GEOMETRY_H diff --git a/source/units/bipeds.c b/source/units/bipeds.c index f94b9f99..2410c760 100644 --- a/source/units/bipeds.c +++ b/source/units/bipeds.c @@ -233,7 +233,6 @@ symbols in this file: /* ---------- headers */ -#define REAL_MATH_EXTERNAL_POINT_FROM_LINE3D #define REAL_MATH_EXTERNAL_REAL_RANDOM_RANGE #include "cseries.h" #include "ai/ai_communication.h" @@ -952,11 +951,7 @@ void biped_adjust_placement( if (TEST_FLAG(flags, _biped_pill_centered_at_origin_bit) && !TEST_FLAG(flags, _biped_flying_bit)) { - real height_offset = definition->biped.collision_radius; - - data->position.x += data->up.i*height_offset; - data->position.y += data->up.j*height_offset; - data->position.z += data->up.k*height_offset; + point_from_line3d(&data->position, &data->up, (definition->biped.collision_radius), &data->position); } return; @@ -1025,20 +1020,8 @@ void biped_get_sight_position( left.j = desired_facing->i; left.k = 0.f; - { - real forward_distance = desired_gun_offset->i; - - sight_position->x += desired_facing->i*forward_distance; - sight_position->y += desired_facing->j*forward_distance; - sight_position->z += desired_facing->k*forward_distance; - { - real sideways_distance = desired_gun_offset->j; - - sight_position->x += left.i*sideways_distance; - sight_position->y += left.j*sideways_distance; - sight_position->z += left.k*sideways_distance; - } - } + point_from_line3d(sight_position, desired_facing, (desired_gun_offset->i), sight_position); + point_from_line3d(sight_position, &left, (desired_gun_offset->j), sight_position); sight_position->z += desired_gun_offset->k; } else @@ -1487,10 +1470,7 @@ static long biped_find_ground_surface( global_current_collision_users[global_current_collision_user_depth++] = _collision_user_bipeds; object_get_origin(object_index, &origin); - /* Preserve January's inline schedule without owning point_from_line3d here. */ - origin.x = global_up3d->i*0.4f + origin.x; - origin.y = global_up3d->j*0.4f + origin.y; - origin.z = global_up3d->k*0.4f + origin.z; + point_from_line3d(&origin, global_up3d, 0.4f, &origin); scale_vector3d(direction, distance, &vector); if (collision_bsp_test_vector( @@ -1505,15 +1485,7 @@ static long biped_find_ground_surface( { surface_index = result.surface_index; if (point) - { - real_point3d const *line_point = &origin; - real_vector3d const *line_vector = &vector; - real line_t = result.t; - - point->x = line_vector->i*line_t + line_point->x; - point->y = line_vector->j*line_t + line_point->y; - point->z = line_vector->k*line_t + line_point->z; - } + point_from_line3d(&origin, &vector, result.t, point); if (normal) *normal = result.plane->n; } @@ -1900,11 +1872,11 @@ static boolean biped_jump( if (upward_velocityobject.up.i*velocity_delta; - jump_velocity.j += biped->object.up.j*velocity_delta; - jump_velocity.k += biped->object.up.k*velocity_delta; + point_from_line3d( + (real_point3d *)&jump_velocity, + &biped->object.up, + jump_magnitude-upward_velocity, + (real_point3d *)&jump_velocity); } } @@ -2247,6 +2219,11 @@ static void biped_update_jumping( if (cheat.jetpack && biped->unit.player_index != NONE) { struct player_datum *player = player_get(biped->unit.player_index); + /* inferred, descriptive names (not recovered Bungie identifiers): no PDB records the locals of this block */ + const real minimum_acceleration = 0.01f; + const real maximum_acceleration = 0.05f; + const real maximum_velocity = 1.4f; + const real damping = 0.8f; boolean impulse = FALSE; if (TEST_FLAG(biped->unit.control_flags, _unit_control_weapon_primary_trigger_bit) && @@ -2257,7 +2234,7 @@ static void biped_update_jumping( dot_product3d( &biped->object.translational_velocity, &biped->unit.aiming_vector)); - real acceleration_scale = PIN(forward_velocity * 0.71428573f, 0.f, 1.f); + real acceleration_scale = PIN(forward_velocity / maximum_velocity, 0.f, 1.f); real_vector3d lateral_velocity; point_from_line3d( @@ -2268,25 +2245,24 @@ static void biped_update_jumping( point_from_line3d( (real_point3d *)&biped->object.translational_velocity, &lateral_velocity, - 0.8f - 1.f, + damping - 1.f, (real_point3d *)&biped->object.translational_velocity); point_from_line3d( (real_point3d *)&biped->object.translational_velocity, &biped->unit.aiming_vector, - acceleration_scale * 0.04f - - acceleration_scale * acceleration_scale * 0.05f + 0.01f, + acceleration_scale * (maximum_acceleration - minimum_acceleration) - + acceleration_scale * acceleration_scale * maximum_acceleration + minimum_acceleration, (real_point3d *)&biped->object.translational_velocity); SET_FLAG(biped->biped.flags, _biped_airborne_bit, TRUE); impulse = TRUE; } - - if (!impulse && - TEST_FLAG(biped->unit.control_flags, _unit_control_crouch_modifier_bit) && + else if (TEST_FLAG(biped->unit.control_flags, _unit_control_crouch_modifier_bit) && TEST_FLAG(biped->biped.flags, _biped_airborne_bit)) { - real_vector3d *velocity = &biped->object.translational_velocity; - - scale_vector3d(velocity, 0.8f - 1.f, velocity); + scale_vector3d( + &biped->object.translational_velocity, + damping - 1.f, + &biped->object.translational_velocity); } if (player->local_player_index != NONE && impulse) diff --git a/tools/ci_build.py b/tools/ci_build.py index a0f6335f..76f92cdd 100644 --- a/tools/ci_build.py +++ b/tools/ci_build.py @@ -117,10 +117,8 @@ def main() -> int: for pdb in [ROOT / "build/windows/halo.pdb", *sorted((ROOT / "build/windows/third_party").glob("SDL3-*/lib/x86/SDL3.pdb"))]: shutil.copy2(pdb, symbols) print(f"{pdb.relative_to(ROOT)} -> {symbols.relative_to(ROOT)}", flush=True) - # the disc image readers (port/linux/src/xiso.c, and the Android app's - # XisoExtractor.java) follow extract-xiso, whose license asks binaries - # to carry its notice - shutil.copy2(ROOT / "port/third_party/extract-xiso/LICENSE.TXT", dist / "extract-xiso-LICENSE.txt") + # OpenCE's own licence, the AGPL, which asks each copy to carry its text + shutil.copy2(ROOT / "LICENSE", dist / "LICENSE.txt") if args.platform != "web": # internet play's DTLS with browsers, and the Linux self-updater's TLS # (port/third_party/mbedtls), whose Apache license asks the same diff --git a/tools/steam_frame_bundle.py b/tools/steam_frame_bundle.py index 13cda574..488b9199 100644 --- a/tools/steam_frame_bundle.py +++ b/tools/steam_frame_bundle.py @@ -10,8 +10,8 @@ writes into the output folder (steam_frame_launch.sh, as ringfall-mjolnir-vr.sh, which updates the game from the repository named), the build's number (build.txt: the workflow's run number, HALO_BUILD_NUMBER or GITHUB_RUN_NUMBER; 0, which never - updates, without one), the disc image reader (steam_frame_xiso.py, with - extract-xiso's license) and the library pictures (grid/); + updates, without one), the disc image reader (steam_frame_xiso.py) and + the library pictures (grid/); - Install-Ringfall-Mjolnir-VR.desktop (port/android/steam_frame_installer.desktop): what the player downloads and opens in Desktop Mode; it fetches the latest release's zip from the repository named. @@ -37,7 +37,6 @@ def main(): (ANDROID / "steam_frame_install.sh", "install.sh", 0o755), (ANDROID / "steam_frame_launch.sh", "ringfall-mjolnir-vr.sh", 0o755), (ANDROID / "steam_frame_xiso.py", "steam_frame_xiso.py", 0o755), - (ROOT / "port/third_party/extract-xiso/LICENSE.TXT", "extract-xiso-LICENSE.txt", 0o644), (apk, "RingfallMjolnirVR.apk", 0o644), ] files += [(picture, "grid/" + picture.name, 0o644) diff --git a/tools/test_web_build.py b/tools/test_web_build.py index 2045c97d..b9708e77 100644 --- a/tools/test_web_build.py +++ b/tools/test_web_build.py @@ -108,6 +108,22 @@ def test_the_link_makes_the_memory_the_window_needs(): assert "-sASSERTIONS=0" in link_flags(release).split() +def test_the_link_exports_what_the_page_calls(): + """every function the page's scripts call on the game's module (game._x, + module._x) is exported: KEEPALIVE alone leaves it off the module, and the + page then waits for frames it cannot count (the loading notice stays)""" + exported = re.search(r"-sEXPORTED_FUNCTIONS=(\S+)", link_flags(graph())).group(1).split(",") + called = set() + for script in sorted(Path("port/web/site").glob("*.js")): + called |= set(re.findall(r"\b(?:game|module)\.(_[A-Za-z0-9_]+)", script.read_text())) + assert called, "no calls found: the pattern is stale" + assert not called - set(exported), f"called on the module, not exported: {sorted(called - set(exported))}" + for name in exported: + if name.startswith("_web_"): + assert re.search(r"EMSCRIPTEN_KEEPALIVE[^;{]*\b" + name[1:] + r"\(", + "".join(path.read_text() for path in Path("port/web/src").glob("*.c"))), name + + def test_the_memory_layout_agrees_with_the_sources(): platform = (ROOT / "port/linux/src/platform.h").read_text() base = int(re.search(r"#define PLATFORM_CONTIGUOUS_BASE (0x[0-9A-Fa-f]+)", platform).group(1), 16) diff --git a/tools/web_build.py b/tools/web_build.py index c208b7d0..677fdeea 100644 --- a/tools/web_build.py +++ b/tools/web_build.py @@ -56,6 +56,10 @@ PORT_CONFIG = LINUX_DIR / "port.json" SDL_PORT = PORT_DIR / "halo_sdl3.py" # (the release port/web/halo_sdl3.py fetches, as the other builds') SDL_TAG = "release-3.4.16" +# the game's functions the page (port/web/site) calls on its module, besides +# main: tools/test_web_build.py checks that it names them all +PAGE_EXPORTS = ["_main", "_malloc", "_free", "_web_state", "_web_net_receive", "_web_net_peer_lost", + "_web_p2p_fingerprint", "_web_p2p_described", "_web_p2p_receive"] # the page's fonts, the menus' own (port/assets/fonts) SITE_FONTS = ["OpenCE-Regular.ttf", "Overpass-750.ttf", "Overpass-900.ttf"] # the memory watch by page contents, shared with the Android host @@ -323,7 +327,11 @@ def generate_web_build(n: Writer, sln: Any) -> None: # (the memory, which the page reads web_state's words from, and what # the page hands the room's frames to the game with: net.js) "-sEXPORTED_RUNTIME_METHODS=HEAPU8,HEAPU32", - "-sEXPORTED_FUNCTIONS=_main,_malloc,_free", + # (and what the page calls on the module: the frames shown, which hide + # its loading notice, the room's frames and internet play's + # connections; EMSCRIPTEN_KEEPALIVE alone keeps a function in the wasm, + # not on the module, since Emscripten 4) + "-sEXPORTED_FUNCTIONS=" + ",".join(PAGE_EXPORTS), f"-sASSERTIONS={0 if release else 1}", # function names in stack traces (the name section only) "--profiling-funcs", @@ -358,7 +366,7 @@ def generate_web_build(n: Writer, sln: Any) -> None: command=f"$python {PORT_DIR}/licenses.py $out {sdl_license}", description="WEB LICENSES $out", ) - n.build(outputs=licenses, rule="web_licenses", implicit=[PORT_DIR / "licenses.py", sdl_license]) + n.build(outputs=licenses, rule="web_licenses", implicit=[PORT_DIR / "licenses.py", Path("LICENSE"), sdl_license]) site_files.append(licenses) n.rule( name="web_version",