From 8eef1da5086009e96b9a97d27f95c83ba9f4e01f Mon Sep 17 00:00:00 2001 From: HSZemi Date: Thu, 18 Jan 2024 23:29:17 +0100 Subject: [PATCH] Initial commit --- .gitignore | 1 + LICENSE | 657 +++++-------------- README.md | 55 +- pyproject.toml | 28 + src/genieutils/__init__.py | 0 src/genieutils/civ.py | 57 ++ src/genieutils/common.py | 172 +++++ src/genieutils/datatypes.py | 35 + src/genieutils/datfile.py | 167 +++++ src/genieutils/effect.py | 56 ++ src/genieutils/graphic.py | 190 ++++++ src/genieutils/playercolour.py | 43 ++ src/genieutils/randommaps.py | 305 +++++++++ src/genieutils/scripts.py | 27 + src/genieutils/sound.py | 68 ++ src/genieutils/task.py | 109 ++++ src/genieutils/tech.py | 92 +++ src/genieutils/techtree.py | 334 ++++++++++ src/genieutils/terrainblock.py | 289 +++++++++ src/genieutils/terrainrestriction.py | 47 ++ src/genieutils/unit.py | 932 +++++++++++++++++++++++++++ src/genieutils/unitheaders.py | 33 + 22 files changed, 3198 insertions(+), 499 deletions(-) create mode 100644 pyproject.toml create mode 100644 src/genieutils/__init__.py create mode 100644 src/genieutils/civ.py create mode 100644 src/genieutils/common.py create mode 100644 src/genieutils/datatypes.py create mode 100644 src/genieutils/datfile.py create mode 100644 src/genieutils/effect.py create mode 100644 src/genieutils/graphic.py create mode 100644 src/genieutils/playercolour.py create mode 100644 src/genieutils/randommaps.py create mode 100644 src/genieutils/scripts.py create mode 100644 src/genieutils/sound.py create mode 100644 src/genieutils/task.py create mode 100644 src/genieutils/tech.py create mode 100644 src/genieutils/techtree.py create mode 100644 src/genieutils/terrainblock.py create mode 100644 src/genieutils/terrainrestriction.py create mode 100644 src/genieutils/unit.py create mode 100644 src/genieutils/unitheaders.py diff --git a/.gitignore b/.gitignore index 68bc17f..1f0fc7a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.idea # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/LICENSE b/LICENSE index 8000a6f..351c651 100644 --- a/LICENSE +++ b/LICENSE @@ -1,504 +1,165 @@ GNU LESSER GENERAL PUBLIC LICENSE - Version 2.1, February 1999 + Version 3, 29 June 2007 - Copyright (C) 1991, 1999 Free Software Foundation, Inc. - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + 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. -[This is the first released version of the Lesser GPL. It also counts - as the successor of the GNU Library Public License, version 2, hence - the version number 2.1.] - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -Licenses are intended to guarantee your freedom to share and change -free software--to make sure the software is free for all its users. - - This license, the Lesser General Public License, applies to some -specially designated software packages--typically libraries--of the -Free Software Foundation and other authors who decide to use it. You -can use it too, but we suggest you first think carefully about whether -this license or the ordinary General Public License is the better -strategy to use in any particular case, based on the explanations below. - - When we speak of free software, we are referring to freedom of use, -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 this service if you wish); that you receive source code or can get -it if you want it; that you can change the software and use pieces of -it in new free programs; and that you are informed that you can do -these things. - - To protect your rights, we need to make restrictions that forbid -distributors to deny you these rights or to ask you to surrender these -rights. These restrictions translate to certain responsibilities for -you if you distribute copies of the library or if you modify it. - - For example, if you distribute copies of the library, whether gratis -or for a fee, you must give the recipients all the rights that we gave -you. You must make sure that they, too, receive or can get the source -code. If you link other code with the library, you must provide -complete object files to the recipients, so that they can relink them -with the library after making changes to the library and recompiling -it. And you must show them these terms so they know their rights. - - We protect your rights with a two-step method: (1) we copyright the -library, and (2) we offer you this license, which gives you legal -permission to copy, distribute and/or modify the library. - - To protect each distributor, we want to make it very clear that -there is no warranty for the free library. Also, if the library is -modified by someone else and passed on, the recipients should know -that what they have is not the original version, so that the original -author's reputation will not be affected by problems that might be -introduced by others. - - Finally, software patents pose a constant threat to the existence of -any free program. We wish to make sure that a company cannot -effectively restrict the users of a free program by obtaining a -restrictive license from a patent holder. Therefore, we insist that -any patent license obtained for a version of the library must be -consistent with the full freedom of use specified in this license. - - Most GNU software, including some libraries, is covered by the -ordinary GNU General Public License. This license, the GNU Lesser -General Public License, applies to certain designated libraries, and -is quite different from the ordinary General Public License. We use -this license for certain libraries in order to permit linking those -libraries into non-free programs. - - When a program is linked with a library, whether statically or using -a shared library, the combination of the two is legally speaking a -combined work, a derivative of the original library. The ordinary -General Public License therefore permits such linking only if the -entire combination fits its criteria of freedom. The Lesser General -Public License permits more lax criteria for linking other code with -the library. - - We call this license the "Lesser" General Public License because it -does Less to protect the user's freedom than the ordinary General -Public License. It also provides other free software developers Less -of an advantage over competing non-free programs. These disadvantages -are the reason we use the ordinary General Public License for many -libraries. However, the Lesser license provides advantages in certain -special circumstances. - - For example, on rare occasions, there may be a special need to -encourage the widest possible use of a certain library, so that it becomes -a de-facto standard. To achieve this, non-free programs must be -allowed to use the library. A more frequent case is that a free -library does the same job as widely used non-free libraries. In this -case, there is little to gain by limiting the free library to free -software only, so we use the Lesser General Public License. - - In other cases, permission to use a particular library in non-free -programs enables a greater number of people to use a large body of -free software. For example, permission to use the GNU C Library in -non-free programs enables many more people to use the whole GNU -operating system, as well as its variant, the GNU/Linux operating -system. - - Although the Lesser General Public License is Less protective of the -users' freedom, it does ensure that the user of a program that is -linked with the Library has the freedom and the wherewithal to run -that program using a modified version of the Library. - - The precise terms and conditions for copying, distribution and -modification follow. Pay close attention to the difference between a -"work based on the library" and a "work that uses the library". The -former contains code derived from the library, whereas the latter must -be combined with the library in order to run. - GNU LESSER GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License Agreement applies to any software library or other -program which contains a notice placed by the copyright holder or -other authorized party saying it may be distributed under the terms of -this Lesser General Public License (also called "this License"). -Each licensee is addressed as "you". - - A "library" means a collection of software functions and/or data -prepared so as to be conveniently linked with application programs -(which use some of those functions and data) to form executables. - - The "Library", below, refers to any such software library or work -which has been distributed under these terms. A "work based on the -Library" means either the Library or any derivative work under -copyright law: that is to say, a work containing the Library or a -portion of it, either verbatim or with modifications and/or translated -straightforwardly into another language. (Hereinafter, translation is -included without limitation in the term "modification".) - - "Source code" for a work means the preferred form of the work for -making modifications to it. For a library, complete source code means -all the source code for all modules it contains, plus any associated -interface definition files, plus the scripts used to control compilation -and installation of the library. - - Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running a program using the Library is not restricted, and output from -such a program is covered only if its contents constitute a work based -on the Library (independent of the use of the Library in a tool for -writing it). Whether that is true depends on what the Library does -and what the program that uses the Library does. - - 1. You may copy and distribute verbatim copies of the Library's -complete source code as you receive it, in any medium, provided that -you conspicuously and appropriately publish on each copy an -appropriate copyright notice and disclaimer of warranty; keep intact -all the notices that refer to this License and to the absence of any -warranty; and distribute a copy of this License along with the -Library. - - You may charge a fee for the physical act of transferring a copy, -and you may at your option offer warranty protection in exchange for a -fee. - - 2. You may modify your copy or copies of the Library or any portion -of it, thus forming a work based on the Library, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) The modified work must itself be a software library. - - b) You must cause the files modified to carry prominent notices - stating that you changed the files and the date of any change. - - c) You must cause the whole of the work to be licensed at no - charge to all third parties under the terms of this License. - - d) If a facility in the modified Library refers to a function or a - table of data to be supplied by an application program that uses - the facility, other than as an argument passed when the facility - is invoked, then you must make a good faith effort to ensure that, - in the event an application does not supply such function or - table, the facility still operates, and performs whatever part of - its purpose remains meaningful. - - (For example, a function in a library to compute square roots has - a purpose that is entirely well-defined independent of the - application. Therefore, Subsection 2d requires that any - application-supplied function or table used by this function must - be optional: if the application does not supply it, the square - root function must still compute square roots.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Library, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Library, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote -it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Library. - -In addition, mere aggregation of another work not based on the Library -with the Library (or with a work based on the Library) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may opt to apply the terms of the ordinary GNU General Public -License instead of this License to a given copy of the Library. To do -this, you must alter all the notices that refer to this License, so -that they refer to the ordinary GNU General Public License, version 2, -instead of to this License. (If a newer version than version 2 of the -ordinary GNU General Public License has appeared, then you can specify -that version instead if you wish.) Do not make any other change in -these notices. - - Once this change is made in a given copy, it is irreversible for -that copy, so the ordinary GNU General Public License applies to all -subsequent copies and derivative works made from that copy. - - This option is useful when you wish to copy part of the code of -the Library into a program that is not a library. - - 4. You may copy and distribute the Library (or a portion or -derivative of it, under Section 2) in object code or executable form -under the terms of Sections 1 and 2 above provided that you accompany -it with the complete corresponding machine-readable source code, which -must be distributed under the terms of Sections 1 and 2 above on a -medium customarily used for software interchange. - - If distribution of object code is made by offering access to copy -from a designated place, then offering equivalent access to copy the -source code from the same place satisfies the requirement to -distribute the source code, even though third parties are not -compelled to copy the source along with the object code. - - 5. A program that contains no derivative of any portion of the -Library, but is designed to work with the Library by being compiled or -linked with it, is called a "work that uses the Library". Such a -work, in isolation, is not a derivative work of the Library, and -therefore falls outside the scope of this License. - - However, linking a "work that uses the Library" with the Library -creates an executable that is a derivative of the Library (because it -contains portions of the Library), rather than a "work that uses the -library". The executable is therefore covered by this License. -Section 6 states terms for distribution of such executables. - - When a "work that uses the Library" uses material from a header file -that is part of the Library, the object code for the work may be a -derivative work of the Library even though the source code is not. -Whether this is true is especially significant if the work can be -linked without the Library, or if the work is itself a library. The -threshold for this to be true is not precisely defined by law. - - If such an object file uses only numerical parameters, data -structure layouts and accessors, and small macros and small inline -functions (ten lines or less in length), then the use of the object -file is unrestricted, regardless of whether it is legally a derivative -work. (Executables containing this object code plus portions of the -Library will still fall under Section 6.) - - Otherwise, if the work is a derivative of the Library, you may -distribute the object code for the work under the terms of Section 6. -Any executables containing that work also fall under Section 6, -whether or not they are linked directly with the Library itself. - - 6. As an exception to the Sections above, you may also combine or -link a "work that uses the Library" with the Library to produce a -work containing portions of the Library, and distribute that work -under terms of your choice, provided that the terms permit -modification of the work for the customer's own use and reverse -engineering for debugging such modifications. - - You must give prominent notice with each copy of the work that the -Library is used in it and that the Library and its use are covered by -this License. You must supply a copy of this License. If the work -during execution displays copyright notices, you must include the -copyright notice for the Library among them, as well as a reference -directing the user to the copy of this License. Also, you must do one -of these things: - - a) Accompany the work with the complete corresponding - machine-readable source code for the Library including whatever - changes were used in the work (which must be distributed under - Sections 1 and 2 above); and, if the work is an executable linked - with the Library, with the complete machine-readable "work that - uses the Library", as object code and/or source code, so that the - user can modify the Library and then relink to produce a modified - executable containing the modified Library. (It is understood - that the user who changes the contents of definitions files in the - Library will not necessarily be able to recompile the application - to use the modified definitions.) - - b) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (1) uses at run time a - copy of the library already present on the user's computer system, - rather than copying library functions into the executable, and (2) - will operate properly with a modified version of the library, if - the user installs one, as long as the modified version is - interface-compatible with the version that the work was made with. - - c) Accompany the work with a written offer, valid for at - least three years, to give the same user the materials - specified in Subsection 6a, above, for a charge no more - than the cost of performing this distribution. - - d) If distribution of the work is made by offering access to copy - from a designated place, offer equivalent access to copy the above - specified materials from the same place. - - e) Verify that the user has already received a copy of these - materials or that you have already sent this user a copy. - - For an executable, the required form of the "work that uses the -Library" must include any data and utility programs needed for -reproducing the executable from it. However, as a special exception, -the materials to be distributed need not include anything that is -normally distributed (in either source or binary form) with the major -components (compiler, kernel, and so on) of the operating system on -which the executable runs, unless that component itself accompanies -the executable. - - It may happen that this requirement contradicts the license -restrictions of other proprietary libraries that do not normally -accompany the operating system. Such a contradiction means you cannot -use both them and the Library together in an executable that you -distribute. - - 7. You may place library facilities that are a work based on the -Library side-by-side in a single library together with other library -facilities not covered by this License, and distribute such a combined -library, provided that the separate distribution of the work based on -the Library and of the other library facilities is otherwise -permitted, and provided that you do these two things: - - a) Accompany the combined library with a copy of the same work - based on the Library, uncombined with any other library - facilities. This must be distributed under the terms of the - Sections above. - - b) Give prominent notice with the combined library of the fact - that part of it is a work based on the Library, and explaining - where to find the accompanying uncombined form of the same work. - - 8. You may not copy, modify, sublicense, link with, or distribute -the Library except as expressly provided under this License. Any -attempt otherwise to copy, modify, sublicense, link with, or -distribute the Library is void, and will automatically terminate your -rights under this License. However, parties who have received copies, -or rights, from you under this License will not have their licenses -terminated so long as such parties remain in full compliance. - - 9. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Library or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Library (or any work based on the -Library), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Library or works based on it. - - 10. Each time you redistribute the Library (or any work based on the -Library), the recipient automatically receives a license from the -original licensor to copy, distribute, link with or modify the Library -subject to these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties with -this License. - - 11. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -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 -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Library at all. For example, if a patent -license would not permit royalty-free redistribution of the Library by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Library. - -If any portion of this section is held invalid or unenforceable under any -particular circumstance, the balance of the section is intended to apply, -and the section as a whole is intended to apply in other circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 12. If the distribution and/or use of the Library is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Library under this License may add -an explicit geographical distribution limitation excluding those countries, -so that distribution is permitted only in or among countries not thus -excluded. In such case, this License incorporates the limitation as if -written in the body of this License. - - 13. The Free Software Foundation may publish revised and/or new -versions of the Lesser 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 Library -specifies a version number of this License which applies to it and -"any later version", you have the option of following the terms and -conditions either of that version or of any later version published by -the Free Software Foundation. If the Library does not specify a -license version number, you may choose any version ever published by -the Free Software Foundation. - - 14. If you wish to incorporate parts of the Library into other free -programs whose distribution conditions are incompatible with these, -write to the author to ask for permission. For software which is -copyrighted by the Free Software Foundation, write to the Free -Software Foundation; we sometimes make exceptions for this. Our -decision will be guided by the two goals of preserving the free status -of all derivatives of our free software and of promoting the sharing -and reuse of software generally. - - NO WARRANTY - - 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO -WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. -EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR -OTHER PARTIES PROVIDE THE LIBRARY "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 -LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME -THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN -WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY -AND/OR REDISTRIBUTE THE LIBRARY 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 -LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF -SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Libraries - - If you develop a new library, and you want it to be of the greatest -possible use to the public, we recommend making it free software that -everyone can redistribute and change. You can do so by permitting -redistribution under these terms (or, alternatively, under the terms of the -ordinary General Public License). - - To apply these terms, attach the following notices to the library. It is -safest to attach them to the start of each source file to most effectively -convey 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 library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library 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 - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 - USA - -Also add information on how to contact you by electronic and paper mail. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the library, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the - library `Frob' (a library for tweaking knobs) written by James Random - Hacker. - - , 1 April 1990 - Ty Coon, President of Vice - -That's all there is to it! + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser 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 +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. \ No newline at end of file diff --git a/README.md b/README.md index 19fc964..d884248 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,55 @@ # genieutils-py -Python implementation of genieutils + +Python implementation of [genieutils](https://github.com/Tapsa/genieutils). + +This library can be used to read and write `empires2_x2_p1.dat` files for Age of Empires II Definitive Edition. + + +## Supported dat versions + +Currently, only the latest version used in Age of Empires II Definitive Edition is supported (`GV_LatestDE2`/`GV_C20`). + + +## Installation + +```shell +pip install genieutils-py +``` + +## Usage examples + +### Dump the whole dat file as json + +The package comes with a handy command line tool that does that for you. + +```shell +dat-to-json path/to/empires2_x2_p1.dat +``` + + +### Change cost of Loom to 69 Gold + +```python +from genieutils.datfile import DatFile + +data = DatFile.parse('path/to/empires2_x2_p1.dat') +data.techs[22].resource_costs[0].amount = 69 +data.save('path/to/modded/empires2_x2_p1.dat') +``` + +### Prevent Kings from garrisoning + +```python +from genieutils.datfile import DatFile + +data = DatFile.parse('path/to/empires2_x2_p1.dat') +for civ in data.civs: + civ.units[434].bird.task_size -= 1 + civ.units[434].bird.tasks.pop() +data.save('path/to/modded/empires2_x2_p1.dat') +``` + + +## Authors + +[HSZemi](https://github.com/hszemi) - Original Author diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8982b18 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,28 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/genieutils"] + +[project] +name = "genieutils-py" +version = "0.0.1" +authors = [ + { name = "SiegeEngineers", email = "genieutils@siegeengineers.org" }, +] +description = "Re-implementation of genieutils in Python" +readme = "README.md" +requires-python = ">=3.11" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", + "Operating System :: OS Independent", +] + +[project.urls] +Homepage = "https://github.com/SiegeEngineers/genieutils-py" +Issues = "https://github.com/SiegeEngineers/genieutils-py/issues" + +[project.scripts] +dat-to-json = "genieutils.scripts:dat_to_json" diff --git a/src/genieutils/__init__.py b/src/genieutils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/genieutils/civ.py b/src/genieutils/civ.py new file mode 100644 index 0000000..deba462 --- /dev/null +++ b/src/genieutils/civ.py @@ -0,0 +1,57 @@ +from dataclasses import dataclass + +from genieutils.common import ByteHandler, GenieClass +from genieutils.unit import Unit + + +@dataclass +class Civ(GenieClass): + player_type: int + name: str + resources_size: int + tech_tree_id: int + team_bonus_id: int + resources: list[float] + icon_set: int + units_size: int + unit_pointers: list[int] + units: list[Unit | None] + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'Civ': + player_type = content.read_int_8() + name = content.read_debug_string() + resources_size = content.read_int_16() + tech_tree_id = content.read_int_16() + team_bonus_id = content.read_int_16() + resources = content.read_float_array(resources_size) + icon_set = content.read_int_8() + units_size = content.read_int_16() + unit_pointers = content.read_int_32_array(units_size) + units = content.read_class_array_with_pointers(Unit, units_size, unit_pointers) + return cls( + player_type=player_type, + name=name, + resources_size=resources_size, + tech_tree_id=tech_tree_id, + team_bonus_id=team_bonus_id, + resources=resources, + icon_set=icon_set, + units_size=units_size, + unit_pointers=unit_pointers, + units=units, + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_8(self.player_type), + self.write_debug_string(self.name), + self.write_int_16(self.resources_size), + self.write_int_16(self.tech_tree_id), + self.write_int_16(self.team_bonus_id), + self.write_float_array(self.resources), + self.write_int_8(self.icon_set), + self.write_int_16(self.units_size), + self.write_int_32_array(self.unit_pointers), + self.write_class_array_with_pointers(self.unit_pointers, self.units), + ]) diff --git a/src/genieutils/common.py b/src/genieutils/common.py new file mode 100644 index 0000000..bd4200e --- /dev/null +++ b/src/genieutils/common.py @@ -0,0 +1,172 @@ +from abc import ABC +from enum import IntEnum +from typing import TypeVar + +from genieutils.datatypes import Int, Float, String + +TILE_TYPE_COUNT = 19 +TERRAIN_COUNT = 200 +TERRAIN_UNITS_SIZE = 30 + + +class UnitType(IntEnum): + EyeCandy = 10 + Trees = 15 + Flag = 20 + DeadFish = 30 + Bird = 40 + Combatant = 50 + Projectile = 60 + Creatable = 70 + Building = 80 + AoeTrees = 90 + + +class GenieClass(ABC): + @classmethod + def from_bytes(cls, data: 'ByteHandler'): + raise NotImplementedError + + @classmethod + def from_bytes_with_count(cls, data: 'ByteHandler', terrains_used_1: int): + raise NotImplementedError + + def to_bytes(self) -> bytes: + raise NotImplementedError + + def write_debug_string(self, value: str) -> bytes: + return (self.write_int_16(0x0A60, signed=False) + + self.write_int_16(len(value), signed=False) + + value.encode('utf-8')) + + def write_string(self, length: int, value: str) -> bytes: + return String.to_bytes(value, length) + + def write_int_8(self, value: int) -> bytes: + return Int.to_bytes(value, length=1, signed=False) + + def write_int_8_array(self, value: list[int]) -> bytes: + return b''.join(self.write_int_8(v) for v in value) + + def write_int_16(self, value: int, signed=True) -> bytes: + return Int.to_bytes(value, length=2, signed=signed) + + def write_int_16_array(self, value: list[int]) -> bytes: + return b''.join(self.write_int_16(v) for v in value) + + def write_int_32(self, value: int, signed=True) -> bytes: + return Int.to_bytes(value, length=4, signed=signed) + + def write_int_32_array(self, value: list[int]) -> bytes: + return b''.join(self.write_int_32(v) for v in value) + + def write_float(self, value: float) -> bytes: + return Float.to_bytes(value) + + def write_float_array(self, value: list[float]) -> bytes: + return b''.join(self.write_float(v) for v in value) + + def write_class(self, value: 'GenieClass') -> bytes: + retval = value.to_bytes() + if retval: + return retval + return b'' + + def write_class_array(self, value: list['GenieClass']) -> bytes: + retval = b''.join(self.write_class(v) for v in value) + if retval: + return retval + return b'' + + def write_class_array_with_pointers(self, pointers: list[int], value: list['GenieClass']) -> bytes: + retval = b''.join(self.write_class(v) for i, v in enumerate(value) if pointers[i]) + if retval: + return retval + return b'' + + +C = TypeVar('C', bound=GenieClass) + + +class ByteHandler: + def __init__(self, content: memoryview): + self.content = content + self.offset = 0 + + def consume_range(self, length: int) -> memoryview: + start = self.offset + end = start + length + self.offset = end + return self.content[start:end] + + def read_debug_string(self) -> str: + tmp_size = self.read_int_16(signed=False) + assert tmp_size == 0x0A60 + size = self.read_int_16(signed=False) + return String.from_bytes(self.consume_range(size)) + + def read_string(self, length: int) -> str: + return String.from_bytes(self.consume_range(length)) + + def read_int_8(self) -> int: + return Int.from_bytes(self.consume_range(1), signed=False) + + def read_int_8_array(self, size: int) -> list[int]: + elements = [] + for i in range(size): + elements.append(self.read_int_8()) + return elements + + def read_int_16(self, signed=True) -> int: + return Int.from_bytes(self.consume_range(2), signed=signed) + + def read_int_16_array(self, size: int) -> list[int]: + elements = [] + for i in range(size): + elements.append(self.read_int_16()) + return elements + + def read_int_32(self, signed=True) -> int: + return Int.from_bytes(self.consume_range(4), signed=signed) + + def read_int_32_array(self, size: int) -> list[int]: + elements = [] + for i in range(size): + elements.append(self.read_int_32()) + return elements + + def read_float(self) -> float: + return Float.from_bytes(self.consume_range(4)) + + def read_float_array(self, size: int) -> list[float]: + elements = [] + for i in range(size): + elements.append(self.read_float()) + return elements + + def read_class(self, class_: type[C]) -> C: + element = class_.from_bytes(self) + return element + + def read_class_array(self, class_: type[C], size: int) -> list[C]: + elements = [] + for i in range(size): + element = class_.from_bytes(self) + elements.append(element) + return elements + + def read_class_array_with_pointers(self, class_: type[C], size: int, pointers: list[int]) -> list[C | None]: + elements = [] + for i in range(size): + element = None + if pointers[i]: + element = class_.from_bytes(self) + elements.append(element) + return elements + + def read_class_array_with_param(self, class_: type[C], size: int, terrains_used_1: int) -> list[C]: + elements = [] + for i in range(size): + terrain_restriction = class_.from_bytes_with_count(self, terrains_used_1) + elements.append(terrain_restriction) + return elements diff --git a/src/genieutils/datatypes.py b/src/genieutils/datatypes.py new file mode 100644 index 0000000..085b734 --- /dev/null +++ b/src/genieutils/datatypes.py @@ -0,0 +1,35 @@ +import struct + + +class String: + @staticmethod + def from_bytes(content: memoryview) -> str: + return bytes(content).rstrip(b'\0').decode() + + @staticmethod + def to_bytes(content: str, length=None) -> bytes: + encoded = content.encode() + if not length: + length = len(encoded) + 1 + zfill = length - len(encoded) + return encoded + (b'\0' * zfill) + + +class Int: + @staticmethod + def from_bytes(content: memoryview, signed=True) -> int: + return int.from_bytes(content, byteorder='little', signed=signed) + + @staticmethod + def to_bytes(content: int, length=2, signed=True) -> bytes: + return content.to_bytes(length, 'little', signed=signed) + + +class Float: + @staticmethod + def from_bytes(content: memoryview) -> float: + return struct.unpack('f', content)[0] + + @staticmethod + def to_bytes(content: float) -> bytes: + return struct.pack('f', content) diff --git a/src/genieutils/datfile.py b/src/genieutils/datfile.py new file mode 100644 index 0000000..cac0a8e --- /dev/null +++ b/src/genieutils/datfile.py @@ -0,0 +1,167 @@ +import zlib +from dataclasses import dataclass +from os import PathLike +from pathlib import Path + +from genieutils.civ import Civ +from genieutils.common import ByteHandler, GenieClass +from genieutils.effect import Effect +from genieutils.graphic import Graphic +from genieutils.playercolour import PlayerColour +from genieutils.randommaps import RandomMaps +from genieutils.sound import Sound +from genieutils.tech import Tech +from genieutils.techtree import TechTree +from genieutils.terrainblock import TerrainBlock +from genieutils.terrainrestriction import TerrainRestriction +from genieutils.unitheaders import UnitHeaders + + +@dataclass +class DatFile(GenieClass): + version: str + terrain_restrictions_size: int + terrains_used_1: int + float_ptr_terrain_tables: list[int] + terrain_pass_graphic_pointers: list[int] + terrain_restrictions: list[TerrainRestriction] + player_colours_size: int + player_colours: list[PlayerColour] + sounds_size: int + sounds: list[Sound] + graphics_size: int + graphic_pointers: list[int] + graphics: list[Graphic | None] + terrain_block: TerrainBlock + random_maps: RandomMaps + effects_size: int + effects: list[Effect] + unit_headers_size: int + unit_headers: list[UnitHeaders] + civs_size: int + civs: list[Civ] + techs_size: int + techs: list[Tech] + time_slice: int + unit_kill_rate: int + unit_kill_total: int + unit_hit_point_rate: int + unit_hit_point_total: int + razing_kill_rate: int + razing_kill_total: int + tech_tree: TechTree + + @classmethod + def parse(cls, input_file: Path | PathLike | str) -> 'DatFile': + content = Path(input_file).read_bytes() + data = zlib.decompress(content, wbits=-15) + byte_handler = ByteHandler(memoryview(data)) + return cls.from_bytes(byte_handler) + + def save(self, target_file: Path | PathLike | str): + uncompressed = self.to_bytes() + compressed = zlib.compress(uncompressed, level=-1, wbits=-15) + Path(target_file).write_bytes(compressed) + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'DatFile': + version = content.read_string(8) + terrain_restrictions_size = content.read_int_16() + terrains_used_1 = content.read_int_16() + float_ptr_terrain_tables = content.read_int_32_array(terrain_restrictions_size) + terrain_pass_graphic_pointers = content.read_int_32_array(terrain_restrictions_size) + terrain_restrictions = content.read_class_array_with_param(TerrainRestriction, terrain_restrictions_size, + terrains_used_1) + player_colours_size = content.read_int_16() + player_colours = content.read_class_array(PlayerColour, player_colours_size) + sounds_size = content.read_int_16() + sounds = content.read_class_array(Sound, sounds_size) + graphics_size = content.read_int_16() + graphic_pointers = content.read_int_32_array(graphics_size) + graphics = content.read_class_array_with_pointers(Graphic, graphics_size, graphic_pointers) + terrain_block = content.read_class(TerrainBlock) + random_maps = content.read_class(RandomMaps) + effects_size = content.read_int_32() + effects = content.read_class_array(Effect, effects_size) + unit_headers_size = content.read_int_32() + unit_headers = content.read_class_array(UnitHeaders, unit_headers_size) + civs_size = content.read_int_16() + civs = content.read_class_array(Civ, civs_size) + techs_size = content.read_int_16() + techs = content.read_class_array(Tech, techs_size) + time_slice = content.read_int_32() + unit_kill_rate = content.read_int_32() + unit_kill_total = content.read_int_32() + unit_hit_point_rate = content.read_int_32() + unit_hit_point_total = content.read_int_32() + razing_kill_rate = content.read_int_32() + razing_kill_total = content.read_int_32() + tech_tree = content.read_class(TechTree) + return cls( + version, + terrain_restrictions_size, + terrains_used_1, + float_ptr_terrain_tables, + terrain_pass_graphic_pointers, + terrain_restrictions, + player_colours_size, + player_colours, + sounds_size, + sounds, + graphics_size, + graphic_pointers, + graphics, + terrain_block, + random_maps, + effects_size, + effects, + unit_headers_size, + unit_headers, + civs_size, + civs, + techs_size, + techs, + time_slice, + unit_kill_rate, + unit_kill_total, + unit_hit_point_rate, + unit_hit_point_total, + razing_kill_rate, + razing_kill_total, + tech_tree, + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_string(8, self.version), + self.write_int_16(self.terrain_restrictions_size), + self.write_int_16(self.terrains_used_1), + self.write_int_32_array(self.float_ptr_terrain_tables), + self.write_int_32_array(self.terrain_pass_graphic_pointers), + self.write_class_array(self.terrain_restrictions), + self.write_int_16(self.player_colours_size), + self.write_class_array(self.player_colours), + self.write_int_16(self.sounds_size), + self.write_class_array(self.sounds), + self.write_int_16(self.graphics_size), + self.write_int_32_array(self.graphic_pointers), + self.write_class_array_with_pointers(self.graphic_pointers, self.graphics), + self.write_class(self.terrain_block), + self.write_class(self.random_maps), + self.write_int_32(self.effects_size), + self.write_class_array(self.effects), + self.write_int_32(self.unit_headers_size), + self.write_class_array(self.unit_headers), + self.write_int_16(self.civs_size), + self.write_class_array(self.civs), + self.write_int_16(self.techs_size), + self.write_class_array(self.techs), + self.write_int_32(self.time_slice), + self.write_int_32(self.unit_kill_rate), + self.write_int_32(self.unit_kill_total), + self.write_int_32(self.unit_hit_point_rate), + self.write_int_32(self.unit_hit_point_total), + self.write_int_32(self.razing_kill_rate), + self.write_int_32(self.razing_kill_total), + self.write_class(self.tech_tree), + ]) diff --git a/src/genieutils/effect.py b/src/genieutils/effect.py new file mode 100644 index 0000000..d18b1ab --- /dev/null +++ b/src/genieutils/effect.py @@ -0,0 +1,56 @@ +from dataclasses import dataclass + +from genieutils.common import ByteHandler, GenieClass + + +@dataclass +class EffectCommand(GenieClass): + type: int + a: int + b: int + c: int + d: float + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'EffectCommand': + return cls( + type=content.read_int_8(), + a=content.read_int_16(), + b=content.read_int_16(), + c=content.read_int_16(), + d=content.read_float(), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_8(self.type), + self.write_int_16(self.a), + self.write_int_16(self.b), + self.write_int_16(self.c), + self.write_float(self.d), + ]) + + +@dataclass +class Effect(GenieClass): + name: str + effect_command_count: int + effect_commands: list[EffectCommand] + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'Effect': + name = content.read_debug_string() + effect_command_count = content.read_int_16() + effect_commands = content.read_class_array(EffectCommand, effect_command_count) + return cls( + name=name, + effect_command_count=effect_command_count, + effect_commands=effect_commands, + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_debug_string(self.name), + self.write_int_16(self.effect_command_count), + self.write_class_array(self.effect_commands), + ]) diff --git a/src/genieutils/graphic.py b/src/genieutils/graphic.py new file mode 100644 index 0000000..afe6528 --- /dev/null +++ b/src/genieutils/graphic.py @@ -0,0 +1,190 @@ +from dataclasses import dataclass + +from genieutils.common import ByteHandler, GenieClass + + +@dataclass +class GraphicDelta(GenieClass): + graphic_id: int + padding_1: int + sprite_ptr: int + offset_x: int + offset_y: int + display_angle: int + padding_2: int + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'GraphicDelta': + return cls( + graphic_id=content.read_int_16(), + padding_1=content.read_int_16(), + sprite_ptr=content.read_int_32(), + offset_x=content.read_int_16(), + offset_y=content.read_int_16(), + display_angle=content.read_int_16(), + padding_2=content.read_int_16(), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_16(self.graphic_id), + self.write_int_16(self.padding_1), + self.write_int_32(self.sprite_ptr), + self.write_int_16(self.offset_x), + self.write_int_16(self.offset_y), + self.write_int_16(self.display_angle), + self.write_int_16(self.padding_2), + ]) + + +@dataclass +class GraphicAngleSound(GenieClass): + frame_num: int + sound_id: int + wwise_sound_id: int + frame_num_2: int + sound_id_2: int + wwise_sound_id_2: int + frame_num_3: int + sound_id_3: int + wwise_sound_id_3: int + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'GraphicAngleSound': + return cls( + frame_num=content.read_int_16(), + sound_id=content.read_int_16(), + wwise_sound_id=content.read_int_32(), + frame_num_2=content.read_int_16(), + sound_id_2=content.read_int_16(), + wwise_sound_id_2=content.read_int_32(), + frame_num_3=content.read_int_16(), + sound_id_3=content.read_int_16(), + wwise_sound_id_3=content.read_int_32(), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_16(self.frame_num), + self.write_int_16(self.sound_id), + self.write_int_32(self.wwise_sound_id), + self.write_int_16(self.frame_num_2), + self.write_int_16(self.sound_id_2), + self.write_int_32(self.wwise_sound_id_2), + self.write_int_16(self.frame_num_3), + self.write_int_16(self.sound_id_3), + self.write_int_32(self.wwise_sound_id_3), + ]) + + +@dataclass +class Graphic(GenieClass): + name: str + file_name: str + particle_effect_name: str + slp: int + is_loaded: int + old_color_flag: int + layer: int + player_color: int + transparent_selection: int + coordinates: list[int] + delta_count: int + sound_id: int + wwise_sound_id: int + angle_sounds_used: int + frame_count: int + angle_count: int + speed_multiplier: float + frame_duration: float + replay_delay: float + sequence_type: int + id: int + mirroring_mode: int + editor_flag: int + deltas: list[GraphicDelta] + angle_sounds: list[GraphicAngleSound] + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'Graphic': + name = content.read_debug_string() + file_name = content.read_debug_string() + particle_effect_name = content.read_debug_string() + slp = content.read_int_32() + is_loaded = content.read_int_8() + old_color_flag = content.read_int_8() + layer = content.read_int_8() + player_color = content.read_int_16() + transparent_selection = content.read_int_8() + coordinates = content.read_int_16_array(4) + delta_count = content.read_int_16() + sound_id = content.read_int_16() + wwise_sound_id = content.read_int_32() + angle_sounds_used = content.read_int_8() + frame_count = content.read_int_16() + angle_count = content.read_int_16() + speed_multiplier = content.read_float() + frame_duration = content.read_float() + replay_delay = content.read_float() + sequence_type = content.read_int_8() + id_ = content.read_int_16() + mirroring_mode = content.read_int_8() + editor_flag = content.read_int_8() + deltas = content.read_class_array(GraphicDelta, delta_count) + angle_sounds = content.read_class_array(GraphicAngleSound, angle_count) if angle_sounds_used else [] + return cls( + name=name, + file_name=file_name, + particle_effect_name=particle_effect_name, + slp=slp, + is_loaded=is_loaded, + old_color_flag=old_color_flag, + layer=layer, + player_color=player_color, + transparent_selection=transparent_selection, + coordinates=coordinates, + delta_count=delta_count, + sound_id=sound_id, + wwise_sound_id=wwise_sound_id, + angle_sounds_used=angle_sounds_used, + frame_count=frame_count, + angle_count=angle_count, + speed_multiplier=speed_multiplier, + frame_duration=frame_duration, + replay_delay=replay_delay, + sequence_type=sequence_type, + id=id_, + mirroring_mode=mirroring_mode, + editor_flag=editor_flag, + deltas=deltas, + angle_sounds=angle_sounds, + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_debug_string(self.name), + self.write_debug_string(self.file_name), + self.write_debug_string(self.particle_effect_name), + self.write_int_32(self.slp), + self.write_int_8(self.is_loaded), + self.write_int_8(self.old_color_flag), + self.write_int_8(self.layer), + self.write_int_16(self.player_color), + self.write_int_8(self.transparent_selection), + self.write_int_16_array(self.coordinates), + self.write_int_16(self.delta_count), + self.write_int_16(self.sound_id), + self.write_int_32(self.wwise_sound_id), + self.write_int_8(self.angle_sounds_used), + self.write_int_16(self.frame_count), + self.write_int_16(self.angle_count), + self.write_float(self.speed_multiplier), + self.write_float(self.frame_duration), + self.write_float(self.replay_delay), + self.write_int_8(self.sequence_type), + self.write_int_16(self.id), + self.write_int_8(self.mirroring_mode), + self.write_int_8(self.editor_flag), + self.write_class_array(self.deltas), + self.write_class_array(self.angle_sounds), + ]) diff --git a/src/genieutils/playercolour.py b/src/genieutils/playercolour.py new file mode 100644 index 0000000..66fdaa0 --- /dev/null +++ b/src/genieutils/playercolour.py @@ -0,0 +1,43 @@ +from dataclasses import dataclass + +from genieutils.common import ByteHandler, GenieClass + + +@dataclass +class PlayerColour(GenieClass): + id: int + player_color_base: int + unit_outline_color: int + unit_selection_color_1: int + unit_selection_color_2: int + minimap_color: int + minimap_color_2: int + minimap_color_3: int + statistics_text: int + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'PlayerColour': + return cls( + id=content.read_int_32(), + player_color_base=content.read_int_32(), + unit_outline_color=content.read_int_32(), + unit_selection_color_1=content.read_int_32(), + unit_selection_color_2=content.read_int_32(), + minimap_color=content.read_int_32(), + minimap_color_2=content.read_int_32(), + minimap_color_3=content.read_int_32(), + statistics_text=content.read_int_32(), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_32(self.id), + self.write_int_32(self.player_color_base), + self.write_int_32(self.unit_outline_color), + self.write_int_32(self.unit_selection_color_1), + self.write_int_32(self.unit_selection_color_2), + self.write_int_32(self.minimap_color), + self.write_int_32(self.minimap_color_2), + self.write_int_32(self.minimap_color_3), + self.write_int_32(self.statistics_text), + ]) diff --git a/src/genieutils/randommaps.py b/src/genieutils/randommaps.py new file mode 100644 index 0000000..d367c95 --- /dev/null +++ b/src/genieutils/randommaps.py @@ -0,0 +1,305 @@ +from dataclasses import dataclass + +from genieutils.common import ByteHandler, GenieClass + + +@dataclass +class MapUnit(GenieClass): + unit: int + host_terrain: int + group_placing: int + scale_flag: int + padding_1: int + objects_per_group: int + fluctuation: int + groups_per_player: int + group_arena: int + player_id: int + set_place_for_all_players: int + min_distance_to_players: int + max_distance_to_players: int + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'MapUnit': + return cls( + unit=content.read_int_32(), + host_terrain=content.read_int_32(), + group_placing=content.read_int_8(), + scale_flag=content.read_int_8(), + padding_1=content.read_int_16(), + objects_per_group=content.read_int_32(), + fluctuation=content.read_int_32(), + groups_per_player=content.read_int_32(), + group_arena=content.read_int_32(), + player_id=content.read_int_32(), + set_place_for_all_players=content.read_int_32(), + min_distance_to_players=content.read_int_32(), + max_distance_to_players=content.read_int_32(), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_32(self.unit), + self.write_int_32(self.host_terrain), + self.write_int_8(self.group_placing), + self.write_int_8(self.scale_flag), + self.write_int_16(self.padding_1), + self.write_int_32(self.objects_per_group), + self.write_int_32(self.fluctuation), + self.write_int_32(self.groups_per_player), + self.write_int_32(self.group_arena), + self.write_int_32(self.player_id), + self.write_int_32(self.set_place_for_all_players), + self.write_int_32(self.min_distance_to_players), + self.write_int_32(self.max_distance_to_players), + ]) + + +@dataclass +class MapTerrain(GenieClass): + proportion: int + terrain: int + clump_count: int + edge_spacing: int + placement_terrain: int + clumpiness: int + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'MapTerrain': + return cls( + proportion=content.read_int_32(), + terrain=content.read_int_32(), + clump_count=content.read_int_32(), + edge_spacing=content.read_int_32(), + placement_terrain=content.read_int_32(), + clumpiness=content.read_int_32(), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_32(self.proportion), + self.write_int_32(self.terrain), + self.write_int_32(self.clump_count), + self.write_int_32(self.edge_spacing), + self.write_int_32(self.placement_terrain), + self.write_int_32(self.clumpiness), + ]) + + +@dataclass +class MapLand(GenieClass): + land_id: int + terrain: int + land_spacing: int + base_size: int + zone: int + placement_type: int + padding_1: int + base_x: int + base_y: int + land_proportion: int + by_player_flag: int + padding_2: int + start_area_radius: int + terrain_edge_fade: int + clumpiness: int + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'MapLand': + return cls( + land_id=content.read_int_32(), + terrain=content.read_int_32(signed=False), + land_spacing=content.read_int_32(), + base_size=content.read_int_32(), + zone=content.read_int_8(), + placement_type=content.read_int_8(), + padding_1=content.read_int_16(), + base_x=content.read_int_32(), + base_y=content.read_int_32(), + land_proportion=content.read_int_8(), + by_player_flag=content.read_int_8(), + padding_2=content.read_int_16(), + start_area_radius=content.read_int_32(), + terrain_edge_fade=content.read_int_32(), + clumpiness=content.read_int_32(), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_32(self.land_id), + self.write_int_32(self.terrain, signed=False), + self.write_int_32(self.land_spacing), + self.write_int_32(self.base_size), + self.write_int_8(self.zone), + self.write_int_8(self.placement_type), + self.write_int_16(self.padding_1), + self.write_int_32(self.base_x), + self.write_int_32(self.base_y), + self.write_int_8(self.land_proportion), + self.write_int_8(self.by_player_flag), + self.write_int_16(self.padding_2), + self.write_int_32(self.start_area_radius), + self.write_int_32(self.terrain_edge_fade), + self.write_int_32(self.clumpiness), + ]) + + +@dataclass +class MapElevation(GenieClass): + proportion: int + terrain: int + clump_count: int + base_terrain: int + base_elevation: int + tile_spacing: int + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'MapElevation': + return cls( + proportion=content.read_int_32(), + terrain=content.read_int_32(), + clump_count=content.read_int_32(), + base_terrain=content.read_int_32(), + base_elevation=content.read_int_32(), + tile_spacing=content.read_int_32(), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_32(self.proportion), + self.write_int_32(self.terrain), + self.write_int_32(self.clump_count), + self.write_int_32(self.base_terrain), + self.write_int_32(self.base_elevation), + self.write_int_32(self.tile_spacing), + ]) + + +@dataclass +class MapInfo(GenieClass): + map_id: int + border_south_west: int + border_north_west: int + border_north_east: int + border_south_east: int + border_usage: int + water_shape: int + base_terrain: int + land_coverage: int + unused_id: int + map_lands_size: int + map_lands_ptr: int + map_lands: list[MapLand] + map_terrains_size: int + map_terrains_ptr: int + map_terrains: list[MapTerrain] + map_units_size: int + map_units_ptr: int + map_units: list[MapUnit] + map_elevations_size: int + map_elevations_ptr: int + map_elevations: list[MapElevation] + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'MapInfo': + map_id = content.read_int_32() + border_south_west = content.read_int_32() + border_north_west = content.read_int_32() + border_north_east = content.read_int_32() + border_south_east = content.read_int_32() + border_usage = content.read_int_32() + water_shape = content.read_int_32() + base_terrain = content.read_int_32() + land_coverage = content.read_int_32() + unused_id = content.read_int_32() + map_lands_size = content.read_int_32(signed=False) + map_lands_ptr = content.read_int_32() + map_lands = content.read_class_array(MapLand, map_lands_size) + map_terrains_size = content.read_int_32(signed=False) + map_terrains_ptr = content.read_int_32() + map_terrains = content.read_class_array(MapTerrain, map_terrains_size) + map_units_size = content.read_int_32(signed=False) + map_units_ptr = content.read_int_32() + map_units = content.read_class_array(MapUnit, map_units_size) + map_elevations_size = content.read_int_32(signed=False) + map_elevations_ptr = content.read_int_32() + map_elevations = content.read_class_array(MapElevation, map_elevations_size) + return cls( + map_id=map_id, + border_south_west=border_south_west, + border_north_west=border_north_west, + border_north_east=border_north_east, + border_south_east=border_south_east, + border_usage=border_usage, + water_shape=water_shape, + base_terrain=base_terrain, + land_coverage=land_coverage, + unused_id=unused_id, + map_lands_size=map_lands_size, + map_lands_ptr=map_lands_ptr, + map_lands=map_lands, + map_terrains_size=map_terrains_size, + map_terrains_ptr=map_terrains_ptr, + map_terrains=map_terrains, + map_units_size=map_units_size, + map_units_ptr=map_units_ptr, + map_units=map_units, + map_elevations_size=map_elevations_size, + map_elevations_ptr=map_elevations_ptr, + map_elevations=map_elevations, + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_32(self.map_id), + self.write_int_32(self.border_south_west), + self.write_int_32(self.border_north_west), + self.write_int_32(self.border_north_east), + self.write_int_32(self.border_south_east), + self.write_int_32(self.border_usage), + self.write_int_32(self.water_shape), + self.write_int_32(self.base_terrain), + self.write_int_32(self.land_coverage), + self.write_int_32(self.unused_id), + self.write_int_32(self.map_lands_size, signed=False), + self.write_int_32(self.map_lands_ptr), + self.write_class_array(self.map_lands), + self.write_int_32(self.map_terrains_size, signed=False), + self.write_int_32(self.map_terrains_ptr), + self.write_class_array(self.map_terrains), + self.write_int_32(self.map_units_size, signed=False), + self.write_int_32(self.map_units_ptr), + self.write_class_array(self.map_units), + self.write_int_32(self.map_elevations_size, signed=False), + self.write_int_32(self.map_elevations_ptr), + self.write_class_array(self.map_elevations), + ]) + + +@dataclass +class RandomMaps(GenieClass): + random_map_count: int + random_maps_ptr: int + map_info_1: list[MapInfo] + map_info_2: list[MapInfo] + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'RandomMaps': + random_map_count = content.read_int_32(signed=False) + random_maps_ptr = content.read_int_32() + map_info_1 = content.read_class_array(MapInfo, random_map_count) + map_info_2 = content.read_class_array(MapInfo, random_map_count) + return cls( + random_map_count=random_map_count, + random_maps_ptr=random_maps_ptr, + map_info_1=map_info_1, + map_info_2=map_info_2, + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_32(self.random_map_count, signed=False), + self.write_int_32(self.random_maps_ptr), + self.write_class_array(self.map_info_1), + self.write_class_array(self.map_info_2), + ]) diff --git a/src/genieutils/scripts.py b/src/genieutils/scripts.py new file mode 100644 index 0000000..6cea7ad --- /dev/null +++ b/src/genieutils/scripts.py @@ -0,0 +1,27 @@ +#! /usr/bin/env python3 +import argparse +import dataclasses +import json +from pathlib import Path + +from genieutils.datfile import DatFile + + +def dat_to_json(): + parser = argparse.ArgumentParser( + prog='dat-to-json', + description='Read a genie engine dat file and print the json representation to stdout', + ) + parser.add_argument('filename', type=Path, help='The dat file to read') + args = parser.parse_args() + + dat_file = DatFile.parse(args.filename) + print(json.dumps(dataclasses.asdict(dat_file), indent=2)) + + +def main(): + dat_to_json() + + +if __name__ == '__main__': + main() diff --git a/src/genieutils/sound.py b/src/genieutils/sound.py new file mode 100644 index 0000000..1f7771c --- /dev/null +++ b/src/genieutils/sound.py @@ -0,0 +1,68 @@ +from dataclasses import dataclass + +from genieutils.common import ByteHandler, GenieClass + + +@dataclass +class SoundItem(GenieClass): + filename: str + resource_id: int + probability: int + civilization: int + icon_set: int + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'SoundItem': + return cls( + filename=content.read_debug_string(), + resource_id=content.read_int_32(), + probability=content.read_int_16(), + civilization=content.read_int_16(), + icon_set=content.read_int_16(), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_debug_string(self.filename), + self.write_int_32(self.resource_id), + self.write_int_16(self.probability), + self.write_int_16(self.civilization), + self.write_int_16(self.icon_set), + ]) + + +@dataclass +class Sound(GenieClass): + id: int + play_delay: int + items_size: int + cache_time: int + total_probability: int + items: list[SoundItem] + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'Sound': + id_ = content.read_int_16() + play_delay = content.read_int_16() + items_size = content.read_int_16() + cache_time = content.read_int_32() + total_probability = content.read_int_16() + items = content.read_class_array(SoundItem, items_size) + return cls( + id=id_, + play_delay=play_delay, + items_size=items_size, + cache_time=cache_time, + total_probability=total_probability, + items=items, + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_16(self.id), + self.write_int_16(self.play_delay), + self.write_int_16(self.items_size), + self.write_int_32(self.cache_time), + self.write_int_16(self.total_probability), + self.write_class_array(self.items), + ]) diff --git a/src/genieutils/task.py b/src/genieutils/task.py new file mode 100644 index 0000000..93a1979 --- /dev/null +++ b/src/genieutils/task.py @@ -0,0 +1,109 @@ +from dataclasses import dataclass + +from genieutils.common import ByteHandler, GenieClass + + +@dataclass +class Task(GenieClass): + task_type: int + id: int + is_default: int + action_type: int + class_id: int + unit_id: int + terrain_id: int + resource_in: int + resource_multiplier: int + resource_out: int + unused_resource: int + work_value_1: float + work_value_2: float + work_range: float + auto_search_targets: int + search_wait_time: float + enable_targeting: int + combat_level_flag: int + gather_type: int + work_flag_2: int + target_diplomacy: int + carry_check: int + pick_for_construction: int + moving_graphic_id: int + proceeding_graphic_id: int + working_graphic_id: int + carrying_graphic_id: int + resource_gathering_sound_id: int + resource_deposit_sound_id: int + wwise_resource_gathering_sound_id: int + wwise_resource_deposit_sound_id: int + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'Task': + return cls( + task_type=content.read_int_16(), + id=content.read_int_16(), + is_default=content.read_int_8(), + action_type=content.read_int_16(), + class_id=content.read_int_16(), + unit_id=content.read_int_16(), + terrain_id=content.read_int_16(), + resource_in=content.read_int_16(), + resource_multiplier=content.read_int_16(), + resource_out=content.read_int_16(), + unused_resource=content.read_int_16(), + work_value_1=content.read_float(), + work_value_2=content.read_float(), + work_range=content.read_float(), + auto_search_targets=content.read_int_8(), + search_wait_time=content.read_float(), + enable_targeting=content.read_int_8(), + combat_level_flag=content.read_int_8(), + gather_type=content.read_int_16(), + work_flag_2=content.read_int_16(), + target_diplomacy=content.read_int_8(), + carry_check=content.read_int_8(), + pick_for_construction=content.read_int_8(), + moving_graphic_id=content.read_int_16(), + proceeding_graphic_id=content.read_int_16(), + working_graphic_id=content.read_int_16(), + carrying_graphic_id=content.read_int_16(), + resource_gathering_sound_id=content.read_int_16(), + resource_deposit_sound_id=content.read_int_16(), + wwise_resource_gathering_sound_id=content.read_int_32(), + wwise_resource_deposit_sound_id=content.read_int_32(), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_16(self.task_type), + self.write_int_16(self.id), + self.write_int_8(self.is_default), + self.write_int_16(self.action_type), + self.write_int_16(self.class_id), + self.write_int_16(self.unit_id), + self.write_int_16(self.terrain_id), + self.write_int_16(self.resource_in), + self.write_int_16(self.resource_multiplier), + self.write_int_16(self.resource_out), + self.write_int_16(self.unused_resource), + self.write_float(self.work_value_1), + self.write_float(self.work_value_2), + self.write_float(self.work_range), + self.write_int_8(self.auto_search_targets), + self.write_float(self.search_wait_time), + self.write_int_8(self.enable_targeting), + self.write_int_8(self.combat_level_flag), + self.write_int_16(self.gather_type), + self.write_int_16(self.work_flag_2), + self.write_int_8(self.target_diplomacy), + self.write_int_8(self.carry_check), + self.write_int_8(self.pick_for_construction), + self.write_int_16(self.moving_graphic_id), + self.write_int_16(self.proceeding_graphic_id), + self.write_int_16(self.working_graphic_id), + self.write_int_16(self.carrying_graphic_id), + self.write_int_16(self.resource_gathering_sound_id), + self.write_int_16(self.resource_deposit_sound_id), + self.write_int_32(self.wwise_resource_gathering_sound_id), + self.write_int_32(self.wwise_resource_deposit_sound_id), + ]) diff --git a/src/genieutils/tech.py b/src/genieutils/tech.py new file mode 100644 index 0000000..7ad7cef --- /dev/null +++ b/src/genieutils/tech.py @@ -0,0 +1,92 @@ +from dataclasses import dataclass + +from genieutils.common import ByteHandler, GenieClass + + +@dataclass +class ResearchResourceCost(GenieClass): + type: int + amount: int + flag: int + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'ResearchResourceCost': + return cls( + type=content.read_int_16(), + amount=content.read_int_16(), + flag=content.read_int_8(), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_16(self.type), + self.write_int_16(self.amount), + self.write_int_8(self.flag), + ]) + + +@dataclass +class Tech(GenieClass): + required_techs: list[int] + resource_costs: list[ResearchResourceCost] + required_tech_count: int + civ: int + full_tech_mode: int + research_location: int + language_dll_name: int + language_dll_description: int + research_time: int + effect_id: int + type: int + icon_id: int + button_id: int + language_dll_help: int + language_dll_tech_tree: int + hot_key: int + name: str + repeatable: int + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'Tech': + return cls( + required_techs=content.read_int_16_array(6), + resource_costs=content.read_class_array(ResearchResourceCost, 3), + required_tech_count=content.read_int_16(), + civ=content.read_int_16(), + full_tech_mode=content.read_int_16(), + research_location=content.read_int_16(), + language_dll_name=content.read_int_32(), + language_dll_description=content.read_int_32(), + research_time=content.read_int_16(), + effect_id=content.read_int_16(), + type=content.read_int_16(), + icon_id=content.read_int_16(), + button_id=content.read_int_8(), + language_dll_help=content.read_int_32(), + language_dll_tech_tree=content.read_int_32(), + hot_key=content.read_int_32(), + name=content.read_debug_string(), + repeatable=content.read_int_8(), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_16_array(self.required_techs), + self.write_class_array(self.resource_costs), + self.write_int_16(self.required_tech_count), + self.write_int_16(self.civ), + self.write_int_16(self.full_tech_mode), + self.write_int_16(self.research_location), + self.write_int_32(self.language_dll_name), + self.write_int_32(self.language_dll_description), + self.write_int_16(self.research_time), + self.write_int_16(self.effect_id), + self.write_int_16(self.type), + self.write_int_16(self.icon_id), + self.write_int_8(self.button_id), + self.write_int_32(self.language_dll_help), + self.write_int_32(self.language_dll_tech_tree), + self.write_int_32(self.hot_key), + self.write_debug_string(self.name), + self.write_int_8(self.repeatable), + ]) diff --git a/src/genieutils/techtree.py b/src/genieutils/techtree.py new file mode 100644 index 0000000..901c016 --- /dev/null +++ b/src/genieutils/techtree.py @@ -0,0 +1,334 @@ +from dataclasses import dataclass + +from genieutils.common import ByteHandler, GenieClass + + +@dataclass +class Common(GenieClass): + slots_used: int + unit_research: list[int] + mode: list[int] + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'Common': + return cls( + slots_used=content.read_int_32(), + unit_research=content.read_int_32_array(10), + mode=content.read_int_32_array(10), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_32(self.slots_used), + self.write_int_32_array(self.unit_research), + self.write_int_32_array(self.mode), + ]) + + +@dataclass +class TechTreeAge(GenieClass): + id: int + status: int + buildings_count: int + buildings: list[int] + units_count: int + units: list[int] + techs_count: int + techs: list[int] + common: Common + num_building_levels: int + buildings_per_zone: list[int] + group_length_per_zone: list[int] + max_age_length: int + line_mode: int + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'TechTreeAge': + id_ = content.read_int_32() + status = content.read_int_8() + buildings_count = content.read_int_8() + buildings = content.read_int_32_array(buildings_count) + units_count = content.read_int_8() + units = content.read_int_32_array(units_count) + techs_count = content.read_int_8() + techs = content.read_int_32_array(techs_count) + common = content.read_class(Common) + num_building_levels = content.read_int_8() + buildings_per_zone = content.read_int_8_array(10) + group_length_per_zone = content.read_int_8_array(10) + max_age_length = content.read_int_8() + line_mode = content.read_int_32() + return cls( + id=id_, + status=status, + buildings_count=buildings_count, + buildings=buildings, + units_count=units_count, + units=units, + techs_count=techs_count, + techs=techs, + common=common, + num_building_levels=num_building_levels, + buildings_per_zone=buildings_per_zone, + group_length_per_zone=group_length_per_zone, + max_age_length=max_age_length, + line_mode=line_mode, + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_32(self.id), + self.write_int_8(self.status), + self.write_int_8(self.buildings_count), + self.write_int_32_array(self.buildings), + self.write_int_8(self.units_count), + self.write_int_32_array(self.units), + self.write_int_8(self.techs_count), + self.write_int_32_array(self.techs), + self.write_class(self.common), + self.write_int_8(self.num_building_levels), + self.write_int_8_array(self.buildings_per_zone), + self.write_int_8_array(self.group_length_per_zone), + self.write_int_8(self.max_age_length), + self.write_int_32(self.line_mode), + ]) + + +@dataclass +class BuildingConnection(GenieClass): + id: int + status: int + buildings_count: int + buildings: list[int] + units_count: int + units: list[int] + techs_count: int + techs: list[int] + common: Common + location_in_age: int + units_techs_total: list[int] + units_techs_first: list[int] + line_mode: int + enabling_research: int + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'BuildingConnection': + id_ = content.read_int_32() + status = content.read_int_8() + buildings_count = content.read_int_8() + buildings = content.read_int_32_array(buildings_count) + units_count = content.read_int_8() + units = content.read_int_32_array(units_count) + techs_count = content.read_int_8() + techs = content.read_int_32_array(techs_count) + common = content.read_class(Common) + location_in_age = content.read_int_8() + units_techs_total = content.read_int_8_array(5) + units_techs_first = content.read_int_8_array(5) + line_mode = content.read_int_32() + enabling_research = content.read_int_32() + return cls( + id=id_, + status=status, + buildings_count=buildings_count, + buildings=buildings, + units_count=units_count, + units=units, + techs_count=techs_count, + techs=techs, + common=common, + location_in_age=location_in_age, + units_techs_total=units_techs_total, + units_techs_first=units_techs_first, + line_mode=line_mode, + enabling_research=enabling_research, + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_32(self.id), + self.write_int_8(self.status), + self.write_int_8(self.buildings_count), + self.write_int_32_array(self.buildings), + self.write_int_8(self.units_count), + self.write_int_32_array(self.units), + self.write_int_8(self.techs_count), + self.write_int_32_array(self.techs), + self.write_class(self.common), + self.write_int_8(self.location_in_age), + self.write_int_8_array(self.units_techs_total), + self.write_int_8_array(self.units_techs_first), + self.write_int_32(self.line_mode), + self.write_int_32(self.enabling_research), + ]) + + +@dataclass +class UnitConnection(GenieClass): + id: int + status: int + upper_building: int + common: Common + vertical_line: int + units_count: int + units: list[int] + location_in_age: int + required_research: int + line_mode: int + enabling_research: int + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'UnitConnection': + id_ = content.read_int_32() + status = content.read_int_8() + upper_building = content.read_int_32() + common = content.read_class(Common) + vertical_line = content.read_int_32() + units_count = content.read_int_8() + units = content.read_int_32_array(units_count) + location_in_age = content.read_int_32() + required_research = content.read_int_32() + line_mode = content.read_int_32() + enabling_research = content.read_int_32() + return cls( + id=id_, + status=status, + upper_building=upper_building, + common=common, + vertical_line=vertical_line, + units_count=units_count, + units=units, + location_in_age=location_in_age, + required_research=required_research, + line_mode=line_mode, + enabling_research=enabling_research, + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_32(self.id), + self.write_int_8(self.status), + self.write_int_32(self.upper_building), + self.write_class(self.common), + self.write_int_32(self.vertical_line), + self.write_int_8(self.units_count), + self.write_int_32_array(self.units), + self.write_int_32(self.location_in_age), + self.write_int_32(self.required_research), + self.write_int_32(self.line_mode), + self.write_int_32(self.enabling_research), + ]) + + +@dataclass +class ResearchConnection(GenieClass): + id: int + status: int + upper_building: int + buildings_count: int + buildings: list[int] + units_count: int + units: list[int] + techs_count: int + techs: list[int] + common: Common + vertical_line: int + location_in_age: int + line_mode: int + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'ResearchConnection': + id_ = content.read_int_32() + status = content.read_int_8() + upper_building = content.read_int_32() + buildings_count = content.read_int_8() + buildings = content.read_int_32_array(buildings_count) + units_count = content.read_int_8() + units = content.read_int_32_array(units_count) + techs_count = content.read_int_8() + techs = content.read_int_32_array(techs_count) + common = content.read_class(Common) + vertical_line = content.read_int_32() + location_in_age = content.read_int_32() + line_mode = content.read_int_32() + return cls( + id=id_, + status=status, + upper_building=upper_building, + buildings_count=buildings_count, + buildings=buildings, + units_count=units_count, + units=units, + techs_count=techs_count, + techs=techs, + common=common, + vertical_line=vertical_line, + location_in_age=location_in_age, + line_mode=line_mode, + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_32(self.id), + self.write_int_8(self.status), + self.write_int_32(self.upper_building), + self.write_int_8(self.buildings_count), + self.write_int_32_array(self.buildings), + self.write_int_8(self.units_count), + self.write_int_32_array(self.units), + self.write_int_8(self.techs_count), + self.write_int_32_array(self.techs), + self.write_class(self.common), + self.write_int_32(self.vertical_line), + self.write_int_32(self.location_in_age), + self.write_int_32(self.line_mode), + ]) + + +@dataclass +class TechTree(GenieClass): + age_count: int + building_count: int + unit_count: int + research_count: int + total_unit_tech_groups: int + tech_tree_ages: list[TechTreeAge] + building_connections: list[BuildingConnection] + unit_connections: list[UnitConnection] + research_connections: list[ResearchConnection] + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'TechTree': + age_count = content.read_int_8() + building_count = content.read_int_8() + unit_count = content.read_int_8() + research_count = content.read_int_8() + total_unit_tech_groups = content.read_int_32() + tech_tree_ages = content.read_class_array(TechTreeAge, age_count) + building_connections = content.read_class_array(BuildingConnection, building_count) + unit_connections = content.read_class_array(UnitConnection, unit_count) + research_connections = content.read_class_array(ResearchConnection, research_count) + return cls( + age_count=age_count, + building_count=building_count, + unit_count=unit_count, + research_count=research_count, + total_unit_tech_groups=total_unit_tech_groups, + tech_tree_ages=tech_tree_ages, + building_connections=building_connections, + unit_connections=unit_connections, + research_connections=research_connections, + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_8(self.age_count), + self.write_int_8(self.building_count), + self.write_int_8(self.unit_count), + self.write_int_8(self.research_count), + self.write_int_32(self.total_unit_tech_groups), + self.write_class_array(self.tech_tree_ages), + self.write_class_array(self.building_connections), + self.write_class_array(self.unit_connections), + self.write_class_array(self.research_connections), + ]) diff --git a/src/genieutils/terrainblock.py b/src/genieutils/terrainblock.py new file mode 100644 index 0000000..d48852d --- /dev/null +++ b/src/genieutils/terrainblock.py @@ -0,0 +1,289 @@ +from dataclasses import dataclass + +from genieutils.common import ByteHandler, TILE_TYPE_COUNT, TERRAIN_COUNT, GenieClass, TERRAIN_UNITS_SIZE + + +@dataclass +class FrameData(GenieClass): + frame_count: int + angle_count: int + shape_id: int + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'FrameData': + return cls( + frame_count=content.read_int_16(), + angle_count=content.read_int_16(), + shape_id=content.read_int_16(), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_16(self.frame_count), + self.write_int_16(self.angle_count), + self.write_int_16(self.shape_id), + ]) + + +@dataclass +class Terrain(GenieClass): + enabled: int + random: int + is_water: int + hide_in_editor: int + string_id: int + name: str + name_2: str + slp: int + shape_ptr: int + sound_id: int + wwise_sound_id: int + wwise_sound_stop_id: int + blend_priority: int + blend_type: int + overlay_mask_name: str + colors: list[int] + cliff_colors: list[int] + passable_terrain: int + impassable_terrain: int + is_animated: int + animation_frames: int + pause_frames: int + interval: float + pause_between_loops: float + frame: int + draw_frame: int + animate_last: float + frame_changed: int + drawn: int + frame_data: list[FrameData] + terrain_to_draw: int + terrain_dimensions: list[int] + terrain_unit_masked_density: list[int] + terrain_unit_id: list[int] + terrain_unit_density: list[int] + terrain_unit_centering: list[int] + number_of_terrain_units_used: int + phantom: int + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'Terrain': + return cls( + enabled=content.read_int_8(), + random=content.read_int_8(), + is_water=content.read_int_8(), + hide_in_editor=content.read_int_8(), + string_id=content.read_int_32(), + name=content.read_debug_string(), + name_2=content.read_debug_string(), + slp=content.read_int_32(), + shape_ptr=content.read_int_32(), + sound_id=content.read_int_32(), + wwise_sound_id=content.read_int_32(signed=False), + wwise_sound_stop_id=content.read_int_32(signed=False), + blend_priority=content.read_int_32(), + blend_type=content.read_int_32(), + overlay_mask_name=content.read_debug_string(), + colors=content.read_int_8_array(3), + cliff_colors=content.read_int_8_array(2), + passable_terrain=content.read_int_8(), + impassable_terrain=content.read_int_8(), + is_animated=content.read_int_8(), + animation_frames=content.read_int_16(), + pause_frames=content.read_int_16(), + interval=content.read_float(), + pause_between_loops=content.read_float(), + frame=content.read_int_16(), + draw_frame=content.read_int_16(), + animate_last=content.read_float(), + frame_changed=content.read_int_8(), + drawn=content.read_int_8(), + frame_data=content.read_class_array(FrameData, TILE_TYPE_COUNT), + terrain_to_draw=content.read_int_16(), + terrain_dimensions=content.read_int_16_array(2), + terrain_unit_masked_density=content.read_int_16_array(TERRAIN_UNITS_SIZE), + terrain_unit_id=content.read_int_16_array(TERRAIN_UNITS_SIZE), + terrain_unit_density=content.read_int_16_array(TERRAIN_UNITS_SIZE), + terrain_unit_centering=content.read_int_8_array(TERRAIN_UNITS_SIZE), + number_of_terrain_units_used=content.read_int_16(), + phantom=content.read_int_16(), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_8(self.enabled), + self.write_int_8(self.random), + self.write_int_8(self.is_water), + self.write_int_8(self.hide_in_editor), + self.write_int_32(self.string_id), + self.write_debug_string(self.name), + self.write_debug_string(self.name_2), + self.write_int_32(self.slp), + self.write_int_32(self.shape_ptr), + self.write_int_32(self.sound_id), + self.write_int_32(self.wwise_sound_id, signed=False), + self.write_int_32(self.wwise_sound_stop_id, signed=False), + self.write_int_32(self.blend_priority), + self.write_int_32(self.blend_type), + self.write_debug_string(self.overlay_mask_name), + self.write_int_8_array(self.colors), + self.write_int_8_array(self.cliff_colors), + self.write_int_8(self.passable_terrain), + self.write_int_8(self.impassable_terrain), + self.write_int_8(self.is_animated), + self.write_int_16(self.animation_frames), + self.write_int_16(self.pause_frames), + self.write_float(self.interval), + self.write_float(self.pause_between_loops), + self.write_int_16(self.frame), + self.write_int_16(self.draw_frame), + self.write_float(self.animate_last), + self.write_int_8(self.frame_changed), + self.write_int_8(self.drawn), + self.write_class_array(self.frame_data), + self.write_int_16(self.terrain_to_draw), + self.write_int_16_array(self.terrain_dimensions), + self.write_int_16_array(self.terrain_unit_masked_density), + self.write_int_16_array(self.terrain_unit_id), + self.write_int_16_array(self.terrain_unit_density), + self.write_int_8_array(self.terrain_unit_centering), + self.write_int_16(self.number_of_terrain_units_used), + self.write_int_16(self.phantom), + ]) + + +@dataclass +class TileSize(GenieClass): + width: int + height: int + delta_y: int + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'TileSize': + return cls( + width=content.read_int_16(), + height=content.read_int_16(), + delta_y=content.read_int_16(), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_16(self.width), + self.write_int_16(self.height), + self.write_int_16(self.delta_y), + ]) + + +@dataclass +class TerrainBlock(GenieClass): + virtual_function_ptr: int + map_pointer: int + map_width: int + map_height: int + world_width: int + world_height: int + tile_sizes: list[TileSize] + padding_ts: int + terrains: list[Terrain] + map_min_x: float + map_min_y: float + map_max_x: float + map_max_y: float + map_max_x_plus_1: float + map_max_y_plus_1: float + terrains_used_2: int + borders_used: int + max_terrain: int + tile_width: int + tile_height: int + tile_half_height: int + tile_half_width: int + elev_height: int + cur_row: int + cur_col: int + block_beg_row: int + block_end_row: int + block_beg_col: int + block_end_col: int + search_map_ptr: int + search_map_rows_ptr: int + any_frame_change: int + map_visible_flag: int + fog_flag: int + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'TerrainBlock': + return cls( + virtual_function_ptr=content.read_int_32(signed=False), + map_pointer=content.read_int_32(signed=False), + map_width=content.read_int_32(), + map_height=content.read_int_32(), + world_width=content.read_int_32(), + world_height=content.read_int_32(), + tile_sizes=content.read_class_array(TileSize, TILE_TYPE_COUNT), + padding_ts=content.read_int_16(), + terrains=content.read_class_array(Terrain, TERRAIN_COUNT), + map_min_x=content.read_float(), + map_min_y=content.read_float(), + map_max_x=content.read_float(), + map_max_y=content.read_float(), + map_max_x_plus_1=content.read_float(), + map_max_y_plus_1=content.read_float(), + terrains_used_2=content.read_int_16(), + borders_used=content.read_int_16(), + max_terrain=content.read_int_16(), + tile_width=content.read_int_16(), + tile_height=content.read_int_16(), + tile_half_height=content.read_int_16(), + tile_half_width=content.read_int_16(), + elev_height=content.read_int_16(), + cur_row=content.read_int_16(), + cur_col=content.read_int_16(), + block_beg_row=content.read_int_16(), + block_end_row=content.read_int_16(), + block_beg_col=content.read_int_16(), + block_end_col=content.read_int_16(), + search_map_ptr=content.read_int_32(signed=False), + search_map_rows_ptr=content.read_int_32(signed=False), + any_frame_change=content.read_int_8(), + map_visible_flag=content.read_int_8(), + fog_flag=content.read_int_8(), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_32(self.virtual_function_ptr, signed=False), + self.write_int_32(self.map_pointer, signed=False), + self.write_int_32(self.map_width), + self.write_int_32(self.map_height), + self.write_int_32(self.world_width), + self.write_int_32(self.world_height), + self.write_class_array(self.tile_sizes), + self.write_int_16(self.padding_ts), + self.write_class_array(self.terrains), + self.write_float(self.map_min_x), + self.write_float(self.map_min_y), + self.write_float(self.map_max_x), + self.write_float(self.map_max_y), + self.write_float(self.map_max_x_plus_1), + self.write_float(self.map_max_y_plus_1), + self.write_int_16(self.terrains_used_2), + self.write_int_16(self.borders_used), + self.write_int_16(self.max_terrain), + self.write_int_16(self.tile_width), + self.write_int_16(self.tile_height), + self.write_int_16(self.tile_half_height), + self.write_int_16(self.tile_half_width), + self.write_int_16(self.elev_height), + self.write_int_16(self.cur_row), + self.write_int_16(self.cur_col), + self.write_int_16(self.block_beg_row), + self.write_int_16(self.block_end_row), + self.write_int_16(self.block_beg_col), + self.write_int_16(self.block_end_col), + self.write_int_32(self.search_map_ptr, signed=False), + self.write_int_32(self.search_map_rows_ptr, signed=False), + self.write_int_8(self.any_frame_change), + self.write_int_8(self.map_visible_flag), + self.write_int_8(self.fog_flag), + ]) diff --git a/src/genieutils/terrainrestriction.py b/src/genieutils/terrainrestriction.py new file mode 100644 index 0000000..2ae83e0 --- /dev/null +++ b/src/genieutils/terrainrestriction.py @@ -0,0 +1,47 @@ +from dataclasses import dataclass + +from genieutils.common import ByteHandler, GenieClass + + +@dataclass +class TerrainPassGraphic(GenieClass): + exit_tile_sprite_id: int + enter_tile_sprite_id: int + walk_tile_sprite_id: int + walk_sprite_rate: int + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'TerrainPassGraphic': + return cls( + exit_tile_sprite_id=content.read_int_32(), + enter_tile_sprite_id=content.read_int_32(), + walk_tile_sprite_id=content.read_int_32(), + walk_sprite_rate=content.read_int_32(), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_32(self.exit_tile_sprite_id), + self.write_int_32(self.enter_tile_sprite_id), + self.write_int_32(self.walk_tile_sprite_id), + self.write_int_32(self.walk_sprite_rate), + ]) + + +@dataclass +class TerrainRestriction(GenieClass): + passable_buildable_dmg_multiplier: list[float] + terrain_pass_graphics: list[TerrainPassGraphic] + + @classmethod + def from_bytes_with_count(cls, content: ByteHandler, terrain_count: int) -> 'TerrainRestriction': + return cls( + passable_buildable_dmg_multiplier=content.read_float_array(terrain_count), + terrain_pass_graphics=content.read_class_array(TerrainPassGraphic, terrain_count), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_float_array(self.passable_buildable_dmg_multiplier), + self.write_class_array(self.terrain_pass_graphics), + ]) diff --git a/src/genieutils/unit.py b/src/genieutils/unit.py new file mode 100644 index 0000000..3cdd8de --- /dev/null +++ b/src/genieutils/unit.py @@ -0,0 +1,932 @@ +from dataclasses import dataclass + +from genieutils.common import ByteHandler, UnitType, GenieClass +from genieutils.task import Task + + +@dataclass +class ResourceStorage(GenieClass): + type: int + amount: float + flag: int + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'ResourceStorage': + return cls( + type=content.read_int_16(), + amount=content.read_float(), + flag=content.read_int_8(), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_16(self.type), + self.write_float(self.amount), + self.write_int_8(self.flag), + ]) + + +@dataclass +class DamageGraphic(GenieClass): + graphic_id: int + damage_percent: int + apply_mode: int + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'DamageGraphic': + return cls( + graphic_id=content.read_int_16(), + damage_percent=content.read_int_16(), + apply_mode=content.read_int_8(), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_16(self.graphic_id), + self.write_int_16(self.damage_percent), + self.write_int_8(self.apply_mode), + ]) + + +@dataclass +class DeadFish(GenieClass): + walking_graphic: int + running_graphic: int + rotation_speed: float + old_size_class: int + tracking_unit: int + tracking_unit_mode: int + tracking_unit_density: float + old_move_algorithm: int + turn_radius: float + turn_radius_speed: float + max_yaw_per_second_moving: float + stationary_yaw_revolution_time: float + max_yaw_per_second_stationary: float + min_collision_size_multiplier: float + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'DeadFish': + return cls( + walking_graphic=content.read_int_16(), + running_graphic=content.read_int_16(), + rotation_speed=content.read_float(), + old_size_class=content.read_int_8(), + tracking_unit=content.read_int_16(), + tracking_unit_mode=content.read_int_8(), + tracking_unit_density=content.read_float(), + old_move_algorithm=content.read_int_8(), + turn_radius=content.read_float(), + turn_radius_speed=content.read_float(), + max_yaw_per_second_moving=content.read_float(), + stationary_yaw_revolution_time=content.read_float(), + max_yaw_per_second_stationary=content.read_float(), + min_collision_size_multiplier=content.read_float(), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_16(self.walking_graphic), + self.write_int_16(self.running_graphic), + self.write_float(self.rotation_speed), + self.write_int_8(self.old_size_class), + self.write_int_16(self.tracking_unit), + self.write_int_8(self.tracking_unit_mode), + self.write_float(self.tracking_unit_density), + self.write_int_8(self.old_move_algorithm), + self.write_float(self.turn_radius), + self.write_float(self.turn_radius_speed), + self.write_float(self.max_yaw_per_second_moving), + self.write_float(self.stationary_yaw_revolution_time), + self.write_float(self.max_yaw_per_second_stationary), + self.write_float(self.min_collision_size_multiplier), + ]) + + +@dataclass +class Bird(GenieClass): + default_task_id: int + search_radius: float + work_rate: float + drop_sites: list[int] + task_swap_group: int + attack_sound: int + move_sound: int + wwise_attack_sound_id: int + wwise_move_sound_id: int + run_pattern: int + task_size: int + tasks: list[Task] + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'Bird': + default_task_id = content.read_int_16() + search_radius = content.read_float() + work_rate = content.read_float() + drop_sites = content.read_int_16_array(3) + task_swap_group = content.read_int_8() + attack_sound = content.read_int_16() + move_sound = content.read_int_16() + wwise_attack_sound_id = content.read_int_32() + wwise_move_sound_id = content.read_int_32() + run_pattern = content.read_int_8() + task_size = content.read_int_16() + tasks = content.read_class_array(Task, task_size) + return cls( + default_task_id=default_task_id, + search_radius=search_radius, + work_rate=work_rate, + drop_sites=drop_sites, + task_swap_group=task_swap_group, + attack_sound=attack_sound, + move_sound=move_sound, + wwise_attack_sound_id=wwise_attack_sound_id, + wwise_move_sound_id=wwise_move_sound_id, + run_pattern=run_pattern, + task_size=task_size, + tasks=tasks, + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_16(self.default_task_id), + self.write_float(self.search_radius), + self.write_float(self.work_rate), + self.write_int_16_array(self.drop_sites), + self.write_int_8(self.task_swap_group), + self.write_int_16(self.attack_sound), + self.write_int_16(self.move_sound), + self.write_int_32(self.wwise_attack_sound_id), + self.write_int_32(self.wwise_move_sound_id), + self.write_int_8(self.run_pattern), + self.write_int_16(self.task_size), + self.write_class_array(self.tasks), + ]) + + +@dataclass +class AttackOrArmor(GenieClass): + class_: int + amount: int + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'AttackOrArmor': + return cls( + class_=content.read_int_16(), + amount=content.read_int_16(), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_16(self.class_), + self.write_int_16(self.amount), + ]) + + +@dataclass +class Type50(GenieClass): + base_armor: int + attack_count: int + attacks: list[AttackOrArmor] + armour_count: int + armours: list[AttackOrArmor] + defense_terrain_bonus: int + bonus_damage_resistance: float + max_range: float + blast_width: float + reload_time: float + projectile_unit_id: int + accuracy_percent: int + break_off_combat: int + frame_delay: int + graphic_displacement: list[float] + blast_attack_level: int + min_range: float + accuracy_dispersion: float + attack_graphic: int + displayed_melee_armour: int + displayed_attack: int + displayed_range: float + displayed_reload_time: float + blast_damage: float + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'Type50': + base_armor = content.read_int_16() + attack_count = content.read_int_16() + attacks = content.read_class_array(AttackOrArmor, attack_count) + armour_count = content.read_int_16() + armours = content.read_class_array(AttackOrArmor, armour_count) + defense_terrain_bonus = content.read_int_16() + bonus_damage_resistance = content.read_float() + max_range = content.read_float() + blast_width = content.read_float() + reload_time = content.read_float() + projectile_unit_id = content.read_int_16() + accuracy_percent = content.read_int_16() + break_off_combat = content.read_int_8() + frame_delay = content.read_int_16() + graphic_displacement = content.read_float_array(3) + blast_attack_level = content.read_int_8() + min_range = content.read_float() + accuracy_dispersion = content.read_float() + attack_graphic = content.read_int_16() + displayed_melee_armour = content.read_int_16() + displayed_attack = content.read_int_16() + displayed_range = content.read_float() + displayed_reload_time = content.read_float() + blast_damage = content.read_float() + return cls( + base_armor=base_armor, + attack_count=attack_count, + attacks=attacks, + armour_count=armour_count, + armours=armours, + defense_terrain_bonus=defense_terrain_bonus, + bonus_damage_resistance=bonus_damage_resistance, + max_range=max_range, + blast_width=blast_width, + reload_time=reload_time, + projectile_unit_id=projectile_unit_id, + accuracy_percent=accuracy_percent, + break_off_combat=break_off_combat, + frame_delay=frame_delay, + graphic_displacement=graphic_displacement, + blast_attack_level=blast_attack_level, + min_range=min_range, + accuracy_dispersion=accuracy_dispersion, + attack_graphic=attack_graphic, + displayed_melee_armour=displayed_melee_armour, + displayed_attack=displayed_attack, + displayed_range=displayed_range, + displayed_reload_time=displayed_reload_time, + blast_damage=blast_damage, + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_16(self.base_armor), + self.write_int_16(self.attack_count), + self.write_class_array(self.attacks), + self.write_int_16(self.armour_count), + self.write_class_array(self.armours), + self.write_int_16(self.defense_terrain_bonus), + self.write_float(self.bonus_damage_resistance), + self.write_float(self.max_range), + self.write_float(self.blast_width), + self.write_float(self.reload_time), + self.write_int_16(self.projectile_unit_id), + self.write_int_16(self.accuracy_percent), + self.write_int_8(self.break_off_combat), + self.write_int_16(self.frame_delay), + self.write_float_array(self.graphic_displacement), + self.write_int_8(self.blast_attack_level), + self.write_float(self.min_range), + self.write_float(self.accuracy_dispersion), + self.write_int_16(self.attack_graphic), + self.write_int_16(self.displayed_melee_armour), + self.write_int_16(self.displayed_attack), + self.write_float(self.displayed_range), + self.write_float(self.displayed_reload_time), + self.write_float(self.blast_damage), + ]) + + +@dataclass +class Projectile(GenieClass): + projectile_type: int + smart_mode: int + hit_mode: int + vanish_mode: int + area_effect_specials: int + projectile_arc: float + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'Projectile': + return cls( + projectile_type=content.read_int_8(), + smart_mode=content.read_int_8(), + hit_mode=content.read_int_8(), + vanish_mode=content.read_int_8(), + area_effect_specials=content.read_int_8(), + projectile_arc=content.read_float(), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_8(self.projectile_type), + self.write_int_8(self.smart_mode), + self.write_int_8(self.hit_mode), + self.write_int_8(self.vanish_mode), + self.write_int_8(self.area_effect_specials), + self.write_float(self.projectile_arc), + ]) + + +@dataclass +class ResourceCost(GenieClass): + type: int + amount: int + flag: int + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'ResourceCost': + return cls( + type=content.read_int_16(), + amount=content.read_int_16(), + flag=content.read_int_16(), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_16(self.type), + self.write_int_16(self.amount), + self.write_int_16(self.flag), + ]) + + +@dataclass +class Creatable(GenieClass): + resource_costs: list[ResourceCost] + train_time: int + train_location_id: int + button_id: int + rear_attack_modifier: float + flank_attack_modifier: float + creatable_type: int + hero_mode: int + garrison_graphic: int + spawning_graphic: int + upgrade_graphic: int + hero_glow_graphic: int + max_charge: float + recharge_rate: float + charge_event: int + charge_type: int + min_conversion_time_mod: float + max_conversion_time_mod: float + conversion_chance_mod: float + total_projectiles: float + max_total_projectiles: int + projectile_spawning_area: list[float] + secondary_projectile_unit: int + special_graphic: int + special_ability: int + displayed_pierce_armor: int + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'Creatable': + return cls( + resource_costs=content.read_class_array(ResourceCost, 3), + train_time=content.read_int_16(), + train_location_id=content.read_int_16(), + button_id=content.read_int_8(), + rear_attack_modifier=content.read_float(), + flank_attack_modifier=content.read_float(), + creatable_type=content.read_int_8(), + hero_mode=content.read_int_8(), + garrison_graphic=content.read_int_32(), + spawning_graphic=content.read_int_16(), + upgrade_graphic=content.read_int_16(), + hero_glow_graphic=content.read_int_16(), + max_charge=content.read_float(), + recharge_rate=content.read_float(), + charge_event=content.read_int_16(), + charge_type=content.read_int_16(), + min_conversion_time_mod=content.read_float(), + max_conversion_time_mod=content.read_float(), + conversion_chance_mod=content.read_float(), + total_projectiles=content.read_float(), + max_total_projectiles=content.read_int_8(), + projectile_spawning_area=content.read_float_array(3), + secondary_projectile_unit=content.read_int_32(), + special_graphic=content.read_int_32(), + special_ability=content.read_int_8(), + displayed_pierce_armor=content.read_int_16(), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_class_array(self.resource_costs), + self.write_int_16(self.train_time), + self.write_int_16(self.train_location_id), + self.write_int_8(self.button_id), + self.write_float(self.rear_attack_modifier), + self.write_float(self.flank_attack_modifier), + self.write_int_8(self.creatable_type), + self.write_int_8(self.hero_mode), + self.write_int_32(self.garrison_graphic), + self.write_int_16(self.spawning_graphic), + self.write_int_16(self.upgrade_graphic), + self.write_int_16(self.hero_glow_graphic), + self.write_float(self.max_charge), + self.write_float(self.recharge_rate), + self.write_int_16(self.charge_event), + self.write_int_16(self.charge_type), + self.write_float(self.min_conversion_time_mod), + self.write_float(self.max_conversion_time_mod), + self.write_float(self.conversion_chance_mod), + self.write_float(self.total_projectiles), + self.write_int_8(self.max_total_projectiles), + self.write_float_array(self.projectile_spawning_area), + self.write_int_32(self.secondary_projectile_unit), + self.write_int_32(self.special_graphic), + self.write_int_8(self.special_ability), + self.write_int_16(self.displayed_pierce_armor), + ]) + + +@dataclass +class BuildingAnnex(GenieClass): + unit_id: int + misplacement_x: float + misplacement_y: float + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'BuildingAnnex': + return cls( + unit_id=content.read_int_16(), + misplacement_x=content.read_float(), + misplacement_y=content.read_float(), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_16(self.unit_id), + self.write_float(self.misplacement_x), + self.write_float(self.misplacement_y), + ]) + + +@dataclass +class Building(GenieClass): + construction_graphic_id: int + snow_graphic_id: int + destruction_graphic_id: int + destruction_rubble_graphic_id: int + researching_graphic: int + research_completed_graphic: int + adjacent_mode: int + graphics_angle: int + disappears_when_built: int + stack_unit_id: int + foundation_terrain_id: int + old_overlap_id: int + tech_id: int + can_burn: int + annexes: list[BuildingAnnex] + head_unit: int + transform_unit: int + transform_sound: int + construction_sound: int + wwise_transform_sound_id: int + wwise_construction_sound_id: int + garrison_type: int + garrison_heal_rate: float + garrison_repair_rate: float + pile_unit: int + looting_table: list[int] + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'Building': + return cls( + construction_graphic_id=content.read_int_16(), + snow_graphic_id=content.read_int_16(), + destruction_graphic_id=content.read_int_16(), + destruction_rubble_graphic_id=content.read_int_16(), + researching_graphic=content.read_int_16(), + research_completed_graphic=content.read_int_16(), + adjacent_mode=content.read_int_8(), + graphics_angle=content.read_int_16(), + disappears_when_built=content.read_int_8(), + stack_unit_id=content.read_int_16(), + foundation_terrain_id=content.read_int_16(), + old_overlap_id=content.read_int_16(), + tech_id=content.read_int_16(), + can_burn=content.read_int_8(), + annexes=content.read_class_array(BuildingAnnex, 4), + head_unit=content.read_int_16(), + transform_unit=content.read_int_16(), + transform_sound=content.read_int_16(), + construction_sound=content.read_int_16(), + wwise_transform_sound_id=content.read_int_32(), + wwise_construction_sound_id=content.read_int_32(), + garrison_type=content.read_int_8(), + garrison_heal_rate=content.read_float(), + garrison_repair_rate=content.read_float(), + pile_unit=content.read_int_16(), + looting_table=content.read_int_8_array(6), + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_16(self.construction_graphic_id), + self.write_int_16(self.snow_graphic_id), + self.write_int_16(self.destruction_graphic_id), + self.write_int_16(self.destruction_rubble_graphic_id), + self.write_int_16(self.researching_graphic), + self.write_int_16(self.research_completed_graphic), + self.write_int_8(self.adjacent_mode), + self.write_int_16(self.graphics_angle), + self.write_int_8(self.disappears_when_built), + self.write_int_16(self.stack_unit_id), + self.write_int_16(self.foundation_terrain_id), + self.write_int_16(self.old_overlap_id), + self.write_int_16(self.tech_id), + self.write_int_8(self.can_burn), + self.write_class_array(self.annexes), + self.write_int_16(self.head_unit), + self.write_int_16(self.transform_unit), + self.write_int_16(self.transform_sound), + self.write_int_16(self.construction_sound), + self.write_int_32(self.wwise_transform_sound_id), + self.write_int_32(self.wwise_construction_sound_id), + self.write_int_8(self.garrison_type), + self.write_float(self.garrison_heal_rate), + self.write_float(self.garrison_repair_rate), + self.write_int_16(self.pile_unit), + self.write_int_8_array(self.looting_table), + ]) + + +@dataclass +class Unit(GenieClass): + type: int + id: int + language_dll_name: int + language_dll_creation: int + class_: int + standing_graphic: list[int] + dying_graphic: int + undead_graphic: int + undead_mode: int + hit_points: int + line_of_sight: float + garrison_capacity: int + collision_size_x: float + collision_size_y: float + collision_size_z: float + train_sound: int + damage_sound: int + dead_unit_id: int + blood_unit_id: int + sort_number: int + can_be_built_on: int + icon_id: int + hide_in_editor: int + old_portrait_pict: int + enabled: int + disabled: int + placement_side_terrain: list[int] + placement_terrain: list[int] + clearance_size: list[float] + hill_mode: int + fog_visibility: int + terrain_restriction: int + fly_mode: int + resource_capacity: int + resource_decay: float + blast_defense_level: int + combat_level: int + interation_mode: int + minimap_mode: int + interface_kind: int + multiple_attribute_mode: float + minimap_color: int + language_dll_help: int + language_dll_hotkey_text: int + hot_key: int + recyclable: int + enable_auto_gather: int + create_doppelganger_on_death: int + resource_gather_group: int + occlusion_mode: int + obstruction_type: int + obstruction_class: int + trait: int + civilization: int + nothing: int + selection_effect: int + editor_selection_colour: int + outline_size_x: float + outline_size_y: float + outline_size_z: float + scenario_triggers_1: int + scenario_triggers_2: int + resource_storages: list[ResourceStorage] + damage_graphic_size: int + damage_graphics: list[DamageGraphic] + selection_sound: int + dying_sound: int + wwise_train_sound_id: int + wwise_damage_sound_id: int + wwise_selection_sound_id: int + wwise_dying_sound_id: int + old_attack_reaction: int + convert_terrain: int + name: str + copy_id: int + base_id: int + speed: float | None = None + dead_fish: DeadFish | None = None + bird: Bird | None = None + type_50: Type50 | None = None + projectile: Projectile | None = None + creatable: Creatable | None = None + building: Building | None = None + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'Unit': + type_ = content.read_int_8() + id_ = content.read_int_16() + language_dll_name = content.read_int_32() + language_dll_creation = content.read_int_32() + class_ = content.read_int_16() + standing_graphic = content.read_int_16_array(2) + dying_graphic = content.read_int_16() + undead_graphic = content.read_int_16() + undead_mode = content.read_int_8() + hit_points = content.read_int_16() + line_of_sight = content.read_float() + garrison_capacity = content.read_int_8() + collision_size_x = content.read_float() + collision_size_y = content.read_float() + collision_size_z = content.read_float() + train_sound = content.read_int_16() + damage_sound = content.read_int_16() + dead_unit_id = content.read_int_16() + blood_unit_id = content.read_int_16() + sort_number = content.read_int_8() + can_be_built_on = content.read_int_8() + icon_id = content.read_int_16() + hide_in_editor = content.read_int_8() + old_portrait_pict = content.read_int_16() + enabled = content.read_int_8() + disabled = content.read_int_8() + placement_side_terrain = content.read_int_16_array(2) + placement_terrain = content.read_int_16_array(2) + clearance_size = content.read_float_array(2) + hill_mode = content.read_int_8() + fog_visibility = content.read_int_8() + terrain_restriction = content.read_int_16() + fly_mode = content.read_int_8() + resource_capacity = content.read_int_16() + resource_decay = content.read_float() + blast_defense_level = content.read_int_8() + combat_level = content.read_int_8() + interation_mode = content.read_int_8() + minimap_mode = content.read_int_8() + interface_kind = content.read_int_8() + multiple_attribute_mode = content.read_float() + minimap_color = content.read_int_8() + language_dll_help = content.read_int_32() + language_dll_hotkey_text = content.read_int_32() + hot_key = content.read_int_32() + recyclable = content.read_int_8() + enable_auto_gather = content.read_int_8() + create_doppelganger_on_death = content.read_int_8() + resource_gather_group = content.read_int_8() + occlusion_mode = content.read_int_8() + obstruction_type = content.read_int_8() + obstruction_class = content.read_int_8() + trait = content.read_int_8() + civilization = content.read_int_8() + nothing = content.read_int_16() + selection_effect = content.read_int_8() + editor_selection_colour = content.read_int_8() + outline_size_x = content.read_float() + outline_size_y = content.read_float() + outline_size_z = content.read_float() + scenario_triggers_1 = content.read_int_32() + scenario_triggers_2 = content.read_int_32() + resource_storages = content.read_class_array(ResourceStorage, 3) + damage_graphic_size = content.read_int_8() + damage_graphics = content.read_class_array(DamageGraphic, damage_graphic_size) + selection_sound = content.read_int_16() + dying_sound = content.read_int_16() + wwise_train_sound_id = content.read_int_32() + wwise_damage_sound_id = content.read_int_32() + wwise_selection_sound_id = content.read_int_32() + wwise_dying_sound_id = content.read_int_32() + old_attack_reaction = content.read_int_8() + convert_terrain = content.read_int_8() + name = content.read_debug_string() + copy_id = content.read_int_16() + base_id = content.read_int_16() + speed = None + dead_fish = None + bird = None + type_50 = None + projectile = None + creatable = None + building = None + if type_ != UnitType.AoeTrees: + if type_ >= UnitType.Flag: + speed = content.read_float() + if type_ >= UnitType.DeadFish: + dead_fish = content.read_class(DeadFish) + if type_ >= UnitType.Bird: + bird = content.read_class(Bird) + if type_ >= UnitType.Combatant: + type_50 = content.read_class(Type50) + if type_ == UnitType.Projectile: + projectile = content.read_class(Projectile) + if type_ >= UnitType.Creatable: + creatable = content.read_class(Creatable) + if type_ == UnitType.Building: + building = content.read_class(Building) + + return cls( + type=type_, + id=id_, + language_dll_name=language_dll_name, + language_dll_creation=language_dll_creation, + class_=class_, + standing_graphic=standing_graphic, + dying_graphic=dying_graphic, + undead_graphic=undead_graphic, + undead_mode=undead_mode, + hit_points=hit_points, + line_of_sight=line_of_sight, + garrison_capacity=garrison_capacity, + collision_size_x=collision_size_x, + collision_size_y=collision_size_y, + collision_size_z=collision_size_z, + train_sound=train_sound, + damage_sound=damage_sound, + dead_unit_id=dead_unit_id, + blood_unit_id=blood_unit_id, + sort_number=sort_number, + can_be_built_on=can_be_built_on, + icon_id=icon_id, + hide_in_editor=hide_in_editor, + old_portrait_pict=old_portrait_pict, + enabled=enabled, + disabled=disabled, + placement_side_terrain=placement_side_terrain, + placement_terrain=placement_terrain, + clearance_size=clearance_size, + hill_mode=hill_mode, + fog_visibility=fog_visibility, + terrain_restriction=terrain_restriction, + fly_mode=fly_mode, + resource_capacity=resource_capacity, + resource_decay=resource_decay, + blast_defense_level=blast_defense_level, + combat_level=combat_level, + interation_mode=interation_mode, + minimap_mode=minimap_mode, + interface_kind=interface_kind, + multiple_attribute_mode=multiple_attribute_mode, + minimap_color=minimap_color, + language_dll_help=language_dll_help, + language_dll_hotkey_text=language_dll_hotkey_text, + hot_key=hot_key, + recyclable=recyclable, + enable_auto_gather=enable_auto_gather, + create_doppelganger_on_death=create_doppelganger_on_death, + resource_gather_group=resource_gather_group, + occlusion_mode=occlusion_mode, + obstruction_type=obstruction_type, + obstruction_class=obstruction_class, + trait=trait, + civilization=civilization, + nothing=nothing, + selection_effect=selection_effect, + editor_selection_colour=editor_selection_colour, + outline_size_x=outline_size_x, + outline_size_y=outline_size_y, + outline_size_z=outline_size_z, + scenario_triggers_1=scenario_triggers_1, + scenario_triggers_2=scenario_triggers_2, + resource_storages=resource_storages, + damage_graphic_size=damage_graphic_size, + damage_graphics=damage_graphics, + selection_sound=selection_sound, + dying_sound=dying_sound, + wwise_train_sound_id=wwise_train_sound_id, + wwise_damage_sound_id=wwise_damage_sound_id, + wwise_selection_sound_id=wwise_selection_sound_id, + wwise_dying_sound_id=wwise_dying_sound_id, + old_attack_reaction=old_attack_reaction, + convert_terrain=convert_terrain, + name=name, + copy_id=copy_id, + base_id=base_id, + speed=speed, + dead_fish=dead_fish, + bird=bird, + type_50=type_50, + projectile=projectile, + creatable=creatable, + building=building, + ) + + def to_bytes(self) -> bytes: + speed = b'' + dead_fish = b'' + bird = b'' + type_50 = b'' + projectile = b'' + creatable = b'' + building = b'' + if self.type != UnitType.AoeTrees: + if self.type >= UnitType.Flag: + speed = self.write_float(self.speed) + if self.type >= UnitType.DeadFish: + dead_fish = self.write_class(self.dead_fish) + if self.type >= UnitType.Bird: + bird = self.write_class(self.bird) + if self.type >= UnitType.Combatant: + type_50 = self.write_class(self.type_50) + if self.type == UnitType.Projectile: + projectile = self.write_class(self.projectile) + if self.type >= UnitType.Creatable: + creatable = self.write_class(self.creatable) + if self.type == UnitType.Building: + building = self.write_class(self.building) + return b''.join([ + self.write_int_8(self.type), + self.write_int_16(self.id), + self.write_int_32(self.language_dll_name), + self.write_int_32(self.language_dll_creation), + self.write_int_16(self.class_), + self.write_int_16_array(self.standing_graphic), + self.write_int_16(self.dying_graphic), + self.write_int_16(self.undead_graphic), + self.write_int_8(self.undead_mode), + self.write_int_16(self.hit_points), + self.write_float(self.line_of_sight), + self.write_int_8(self.garrison_capacity), + self.write_float(self.collision_size_x), + self.write_float(self.collision_size_y), + self.write_float(self.collision_size_z), + self.write_int_16(self.train_sound), + self.write_int_16(self.damage_sound), + self.write_int_16(self.dead_unit_id), + self.write_int_16(self.blood_unit_id), + self.write_int_8(self.sort_number), + self.write_int_8(self.can_be_built_on), + self.write_int_16(self.icon_id), + self.write_int_8(self.hide_in_editor), + self.write_int_16(self.old_portrait_pict), + self.write_int_8(self.enabled), + self.write_int_8(self.disabled), + self.write_int_16_array(self.placement_side_terrain), + self.write_int_16_array(self.placement_terrain), + self.write_float_array(self.clearance_size), + self.write_int_8(self.hill_mode), + self.write_int_8(self.fog_visibility), + self.write_int_16(self.terrain_restriction), + self.write_int_8(self.fly_mode), + self.write_int_16(self.resource_capacity), + self.write_float(self.resource_decay), + self.write_int_8(self.blast_defense_level), + self.write_int_8(self.combat_level), + self.write_int_8(self.interation_mode), + self.write_int_8(self.minimap_mode), + self.write_int_8(self.interface_kind), + self.write_float(self.multiple_attribute_mode), + self.write_int_8(self.minimap_color), + self.write_int_32(self.language_dll_help), + self.write_int_32(self.language_dll_hotkey_text), + self.write_int_32(self.hot_key), + self.write_int_8(self.recyclable), + self.write_int_8(self.enable_auto_gather), + self.write_int_8(self.create_doppelganger_on_death), + self.write_int_8(self.resource_gather_group), + self.write_int_8(self.occlusion_mode), + self.write_int_8(self.obstruction_type), + self.write_int_8(self.obstruction_class), + self.write_int_8(self.trait), + self.write_int_8(self.civilization), + self.write_int_16(self.nothing), + self.write_int_8(self.selection_effect), + self.write_int_8(self.editor_selection_colour), + self.write_float(self.outline_size_x), + self.write_float(self.outline_size_y), + self.write_float(self.outline_size_z), + self.write_int_32(self.scenario_triggers_1), + self.write_int_32(self.scenario_triggers_2), + self.write_class_array(self.resource_storages), + self.write_int_8(self.damage_graphic_size), + self.write_class_array(self.damage_graphics), + self.write_int_16(self.selection_sound), + self.write_int_16(self.dying_sound), + self.write_int_32(self.wwise_train_sound_id), + self.write_int_32(self.wwise_damage_sound_id), + self.write_int_32(self.wwise_selection_sound_id), + self.write_int_32(self.wwise_dying_sound_id), + self.write_int_8(self.old_attack_reaction), + self.write_int_8(self.convert_terrain), + self.write_debug_string(self.name), + self.write_int_16(self.copy_id), + self.write_int_16(self.base_id), + speed, + dead_fish, + bird, + type_50, + projectile, + creatable, + building, + ]) diff --git a/src/genieutils/unitheaders.py b/src/genieutils/unitheaders.py new file mode 100644 index 0000000..ced6dd1 --- /dev/null +++ b/src/genieutils/unitheaders.py @@ -0,0 +1,33 @@ +from dataclasses import dataclass + +from genieutils.common import ByteHandler, GenieClass +from genieutils.task import Task + + +@dataclass +class UnitHeaders(GenieClass): + exists: int + task_count: int | None = None + task_list: list[Task] | None = None + + @classmethod + def from_bytes(cls, content: ByteHandler) -> 'UnitHeaders': + super().__init__(content) + exists = content.read_int_8() + task_count = None + task_list = None + if exists: + task_count = content.read_int_16() + task_list = content.read_class_array(Task, task_count) + return cls( + exists=exists, + task_count=task_count, + task_list=task_list, + ) + + def to_bytes(self) -> bytes: + return b''.join([ + self.write_int_8(self.exists), + self.write_int_16(self.task_count) if self.exists else b'', + self.write_class_array(self.task_list) if self.exists else b'', + ])