aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXavier Del Campo Romero <xavi92@disroot.org>2025-12-04 13:58:02 +0100
committerXavier Del Campo Romero <xavi92@disroot.org>2025-12-15 23:04:39 +0100
commit600ff28dd73f2cf17725382b68a4b1b2573f2e34 (patch)
treea105c686458d8998438652aeca6299cf9000edcd
First commitHEADabcmain
-rw-r--r--.gitignore1
-rw-r--r--CMakeLists.txt10
-rw-r--r--LICENSE338
-rw-r--r--README.md40
-rw-r--r--doc/abc.md22
-rw-r--r--src/CMakeLists.txt4
-rw-r--r--src/abc/CMakeLists.txt4
-rw-r--r--src/abc/abc.cpp912
-rw-r--r--src/abc/abc.h177
-rw-r--r--src/dtx/CMakeLists.txt4
-rw-r--r--src/dtx/dtx.cpp207
-rw-r--r--src/dtx/dtx.h37
-rw-r--r--src/main.cpp99
-rw-r--r--src/rez/CMakeLists.txt9
-rw-r--r--src/rez/dir.cpp117
-rw-r--r--src/rez/entry.cpp10
-rw-r--r--src/rez/file.cpp100
-rw-r--r--src/rez/io.cpp174
-rw-r--r--src/rez/resource.cpp46
-rw-r--r--src/rez/rez.cpp124
-rw-r--r--src/rez/rez.h46
-rw-r--r--src/rez/rez/dir.h34
-rw-r--r--src/rez/rez/entry.h31
-rw-r--r--src/rez/rez/file.h34
-rw-r--r--src/rez/rez/io.h34
-rw-r--r--src/rez/rez/resource.h30
26 files changed, 2644 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a0155cb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/build*/
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..87b0a1f
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,10 @@
+cmake_minimum_required(VERSION 3.13)
+project(globalops CXX)
+find_package(SDL2 REQUIRED)
+find_package(OpenGL REQUIRED)
+add_executable(${PROJECT_NAME})
+add_subdirectory(src)
+target_compile_options(${PROJECT_NAME} PRIVATE -pedantic -Wall)
+target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17)
+target_link_libraries(${PROJECT_NAME} PRIVATE SDL2::SDL2 OpenGL::GL)
+set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 17 CXX_EXTENSIONS OFF)
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..9efa6fb
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,338 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+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 Program or any portion
+of it, thus forming a work based on the Program, 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) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+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 Program, 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 Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) 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; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, 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 executable. However, as a
+special exception, the source code 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.
+
+If distribution of executable or 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 counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program 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.
+
+ 5. 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 Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program 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 to
+this License.
+
+ 7. 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 Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program 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 Program.
+
+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.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program 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.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies 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 Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, 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
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. 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 PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+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.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Moe Ghoul>, 1 April 1989
+ Moe Ghoul, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..23443cd
--- /dev/null
+++ b/README.md
@@ -0,0 +1,40 @@
+# GlobalOps
+
+GlobalOps is a free and open source re-implementation of the [LithTech]-based
+engine used by [Global Operations], a first-person shooter developed by
+Barking Dog Studios and co-published by Crave Entertainment and Electronic
+Arts in 2002.
+
+## License
+
+Unless stated otherwise, all source files are licensed under the
+following license:
+
+```
+GlobalOps, a first-person shooter engine.
+Copyright (C) 2025 Xavier Del Campo Romero
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License along
+with this program; if not, see <https://www.gnu.org/licenses/>.
+```
+
+Also, see [`LICENSE`](./LICENSE).
+
+## Trademark disclaimer
+
+LithTech, Global Operations, Barking Dog Studios, Crave Entertainment
+and Electronic Arts are either trademarks or registered trademarks of their
+respective holders.
+
+[LithTech]: https://en.wikipedia.org/wiki/Lithtech
+[Global Operations]: https://en.wikipedia.org/wiki/Global_Operations
diff --git a/doc/abc.md b/doc/abc.md
new file mode 100644
index 0000000..f085e5d
--- /dev/null
+++ b/doc/abc.md
@@ -0,0 +1,22 @@
+# Differences in the ABC format
+
+## `Header` section
+
+- Versions range from `0x6a` to `0x6c` instead of `0xa` to `0xc`.
+- There are four unknown 4-byte words after the command string.
+
+## `Nodes` section
+
+- The section starts with an unknown 4-byte word.
+
+## `Animation` section
+
+- A 4-byte word, always assigned to `0xffffffff`, appears before every
+`nodeanim`.
+- Aside from its position and rotation vecots, every animation item contains
+two extra 4-byte words, apparently always assigned to zero.
+
+## `HitGroups` section
+
+- This sections seems to be an extension to the standard ABC format.
+It is defined by two 4-byte words with unknown purpose.
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
new file mode 100644
index 0000000..e8e3894
--- /dev/null
+++ b/src/CMakeLists.txt
@@ -0,0 +1,4 @@
+target_sources(${PROJECT_NAME} PRIVATE main.cpp)
+add_subdirectory(abc)
+add_subdirectory(dtx)
+add_subdirectory(rez)
diff --git a/src/abc/CMakeLists.txt b/src/abc/CMakeLists.txt
new file mode 100644
index 0000000..52460e3
--- /dev/null
+++ b/src/abc/CMakeLists.txt
@@ -0,0 +1,4 @@
+target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_LIST_DIR})
+target_sources(${PROJECT_NAME} PRIVATE
+ abc.cpp
+)
diff --git a/src/abc/abc.cpp b/src/abc/abc.cpp
new file mode 100644
index 0000000..2ad45f1
--- /dev/null
+++ b/src/abc/abc.cpp
@@ -0,0 +1,912 @@
+#include <abc.h>
+#include <cstdint>
+#include <cstring>
+#include <functional>
+#include <iostream>
+#include <string>
+
+int abc::parse(rez::file &f, std::string &name) const
+{
+ uint16_t len;
+
+ if (f.read_le(len))
+ {
+ std::cerr << f.path() << ": failed to read name length\n";
+ return -1;
+ }
+ else if (len && f.read(name, len))
+ {
+ std::cerr << f.path() << ": failed to read name\n";
+ return -1;
+ }
+
+ return 0;
+}
+
+int abc::parse_header(rez::file &f)
+{
+ struct header &h = this->header;
+
+ if (f.read_le(h.version) ||
+ f.read_le(h.n_keyframe) ||
+ f.read_le(h.n_anim) ||
+ f.read_le(h.n_nodes) ||
+ f.read_le(h.n_pieces) ||
+ f.read_le(h.n_children) ||
+ f.read_le(h.n_triangles) ||
+ f.read_le(h.n_vertices) ||
+ f.read_le(h.n_vertweights) ||
+ f.read_le(h.n_lod) ||
+ f.read_le(h.n_socket) ||
+ f.read_le(h.n_weightsets) ||
+ f.read_le(h.n_strings) ||
+ f.read_le(h.n_strlen))
+ {
+ std::cerr << f.path() << ": failed to read header\n";
+ return -1;
+ }
+
+ // Apparently, versions are numbered from 0x6a to 0x6c, as opposed
+ // to other titles with the same engine, that used 0xa to 0xc instead.
+ const uint32_t min_version = 0x6a, max_version = 0x6c;
+
+ if (h.version < min_version || h.version > max_version)
+ {
+ std::cerr << f.path() << ": unsupported version " << h.version
+ << ", expected {" << min_version << ", " << max_version << "}\n";
+ return -1;
+ }
+
+ uint16_t cmdlen;
+ std::string cmd;
+ uint32_t n_loddist;
+
+ if (f.read_le(cmdlen))
+ {
+ std::cerr << f.path() << ": failed to read command length\n";
+ return -1;
+ }
+ else if (cmdlen && f.read(cmd, cmdlen))
+ {
+ std::cerr << f.path() << ": failed to read command\n";
+ return -1;
+ }
+
+ // These do not appear on the ABC v13 specification.
+ // Maybe they are game-specific?
+ uint32_t unknown[3], uint_radius;
+
+ for (auto &w : unknown)
+ if (f.read_le(w))
+ return -1;
+
+ if (f.read_le(uint_radius))
+ {
+ std::cerr << f.path() << ": failed to read internal radius\n";
+ return -1;
+ }
+ else if (f.read_le(n_loddist))
+ {
+ std::cerr << f.path() << ": failed to read LoD distance count\n";
+ return -1;
+ }
+
+ long cur = f.tell();
+
+ if (cur < 0)
+ {
+ std::cerr << f.path() << ": failed to tell offset\n";
+ return -1;
+ }
+ else if (f.seek(cur + 60))
+ {
+ std::cerr << f.path() << ": failed to skip padding bytes\n";
+ return -1;
+ }
+
+ std::vector<float> lod_distances;
+
+ for (uint32_t i = 0; i < n_loddist; i++)
+ {
+ uint32_t v;
+
+ if (f.read_le(v))
+ {
+ std::cerr << f.path() << ": failed to read LoD number "
+ << i << std::endl;
+ return -1;
+ }
+
+ lod_distances.push_back(v);
+ }
+
+ memcpy(&int_radius, &uint_radius, sizeof uint_radius);
+ return 0;
+}
+
+int abc::parse(rez::file &f, triangle &triangle) const
+{
+ uint16_t trifs;
+ uint32_t a, b;
+
+ if (f.read_le(a) || f.read_le(b) || f.read_le(trifs))
+ {
+ std::cerr << f.path() << ": failed to read triangle coords\n";
+ return -1;
+ }
+
+ memcpy(&triangle.a, &a, sizeof a);
+ memcpy(&triangle.b, &b, sizeof b);
+ triangle.trifs = trifs;
+ return 0;
+}
+
+int abc::parse(rez::file &f, vector &vector) const
+{
+ uint32_t x, y, z;
+
+ if (f.read_le(x) || f.read_le(y) || f.read_le(z))
+ return -1;
+
+ memcpy(&vector.x, &x, sizeof x);
+ memcpy(&vector.y, &y, sizeof y);
+ memcpy(&vector.z, &z, sizeof z);
+ return 0;
+}
+
+int abc::parse(rez::file &f, weight &weight) const
+{
+ uint32_t bias;
+
+ if (f.read_le(weight.node_index))
+ {
+ std::cerr << f.path() << ": failed to read node index\n";
+ return -1;
+ }
+ else if (parse(f, weight.location))
+ {
+ std::cerr << f.path() << ": failed to read location\n";
+ return -1;
+ }
+ else if (f.read_le(bias))
+ {
+ std::cerr << f.path() << ": failed to read bias\n";
+ return -1;
+ }
+
+ memcpy(&weight.bias, &bias, sizeof bias);
+ return 0;
+}
+
+int abc::parse(rez::file &f, vertex &vertex) const
+{
+ uint16_t n_weights, unknown;
+
+ if (f.read_le(n_weights))
+ {
+ std::cerr << f.path() << ": failed to read number of weights\n";
+ return -1;
+ }
+ else if (f.read_le(unknown))
+ {
+ std::cerr << f.path() << ": failed to read reserved halfword\n";
+ return -1;
+ }
+
+ for (uint16_t i = 0; i < n_weights; i++)
+ {
+ weight weight;
+
+ if (parse(f, weight))
+ return -1;
+
+ vertex.weights.push_back(weight);
+ }
+
+ if (parse(f, vertex.vertex))
+ {
+ std::cerr << f.path() << ": failed to read vertex\n";
+ return -1;
+ }
+ else if (parse(f, vertex.normal))
+ {
+ std::cerr << f.path() << ": failed to read normal\n";
+ return -1;
+ }
+
+ return 0;
+}
+
+int abc::parse(rez::file &f, piece &piece) const
+{
+ long offset;
+
+ if (f.read_le(piece.tex_index))
+ {
+ std::cerr << f.path() << ": failed to read texture index\n";
+ return -1;
+ }
+ else if (f.read_le(piece.specular_power))
+ {
+ std::cerr << f.path() << ": failed to read specular power\n";
+ return -1;
+ }
+ else if (f.read_le(piece.specular_scale))
+ {
+ std::cerr << f.path() << ": failed to read specular scale\n";
+ return -1;
+ }
+ else if (f.read_le(piece.lod_weight))
+ {
+ std::cerr << f.path() << ": failed to read LoD weight\n";
+ return -1;
+ }
+ else if ((offset = f.tell()) < 0)
+ {
+ std::cerr << f.path() << ": failed to get offset\n";
+ return -1;
+ }
+ else if (f.seek(offset + sizeof (uint64_t)))
+ {
+ std::cerr << f.path() << ": failed to seek\n";
+ return -1;
+ }
+ else if (parse(f, piece.name))
+ {
+ std::cerr << f.path() << ": failed to read piece name\n";
+ return -1;
+ }
+ else if (f.read_le(piece.n_tris))
+ {
+ std::cerr << f.path() << ": failed to read n. trianges\n";
+ return -1;
+ }
+
+ piece.triangles.reserve(piece.n_tris * 3);
+
+ for (uint32_t i = 0; i < piece.n_tris * 3; i++)
+ {
+ struct triangle triangle;
+
+ if (parse(f, triangle))
+ return -1;
+
+ piece.triangles.push_back(triangle);
+ }
+
+ uint32_t n_vertices;
+
+ if (f.read_le(n_vertices))
+ {
+ std::cerr << f.path() << ": failed to read number of vertices\n";
+ return -1;
+ }
+
+ piece.vertices.reserve(n_vertices);
+
+ for (uint32_t i = 0; i < n_vertices; i++)
+ {
+ struct vertex vertex;
+
+ if (parse(f, vertex))
+ return -1;
+
+ piece.vertices.push_back(vertex);
+ }
+
+ return 0;
+}
+
+int abc::parse_pieces(rez::file &f)
+{
+ uint32_t n_weights, n_pieces;
+
+ if (f.read_le(n_weights))
+ {
+ std::cerr << f.path() << ": failed to get weight count\n";
+ return -1;
+ }
+ else if (f.read_le(n_pieces))
+ {
+ std::cerr << f.path() << ": failed to get piece count\n";
+ return -1;
+ }
+
+ for (uint32_t i = 0; i < n_pieces; i++)
+ {
+ piece piece;
+
+ if (parse(f, piece))
+ {
+ std::cerr << f.path() << ": failed to read piece " << i
+ << std::endl;
+ return -1;
+ }
+
+ pieces.push_back(piece);
+ }
+
+ return 0;
+}
+
+int abc::parse(rez::file &f, set &set) const
+{
+ uint32_t n_weights;
+
+ if (parse(f, set.name))
+ {
+ std::cerr << f.path() << ": failed to read set name\n";
+ return -1;
+ }
+ else if (f.read_le(n_weights))
+ {
+ std::cerr << f.path() << ": failed to read number of weights\n";
+ return -1;
+ }
+
+ for (uint32_t i = 0; i < n_weights; i++)
+ {
+ uint32_t weight;
+ float fweight;
+
+ if (f.read_le(weight))
+ {
+ std::cerr << f.path() << ": failed to read weight " << i
+ << std::endl;
+ return -1;
+ }
+
+ memcpy(&fweight, &weight, sizeof weight);
+ set.weights.push_back(fweight);
+ }
+
+ return 0;
+}
+
+int abc::parse_sets(rez::file &f)
+{
+ uint32_t n_sets;
+
+ if (f.read_le(n_sets))
+ {
+ std::cerr << f.path() << ": failed to read number of sets\n";
+ return -1;
+ }
+
+ std::cerr << "n_sets: " << n_sets << std::endl;
+
+ for (uint32_t i = 0; i < n_sets; i++)
+ {
+ set set;
+
+ if (parse(f, set))
+ return -1;
+
+ sets.push_back(set);
+ }
+
+ return 0;
+}
+
+int abc::parse(rez::file &f, rotation &rot) const
+{
+ uint32_t w;
+
+ if (parse(f, rot.v) || f.read_le(w))
+ return -1;
+
+ memcpy(&rot.w, &w, sizeof w);
+ return 0;
+}
+
+int abc::parse(rez::file &f, node &parent)
+{
+ uint32_t n_children;
+ uint32_t unknown2;
+ node node;
+
+ if (parse(f, node.name))
+ {
+ std::cerr << f.path() << ": failed to read node name\n";
+ return -1;
+ }
+ else if (f.read_le(node.transform_index))
+ {
+ std::cerr << f.path() << ": failed to read transform index\n";
+ return -1;
+ }
+ else if (f.read(node.flags))
+ {
+ std::cerr << f.path() << ": failed to read node flags\n";
+ return -1;
+ }
+ else if (f.read_le(unknown2))
+ {
+ std::cerr << f.path() << ": failed to read unknown word\n";
+ return -1;
+ }
+
+ for (auto &rot : node.matrix)
+ if (parse(f, rot))
+ {
+ std::cerr << f.path() << ": failed to read rotation\n";
+ return -1;
+ }
+
+ if (f.read_le(n_children))
+ {
+ std::cerr << f.path() << ": failed to read number of children\n";
+ return -1;
+ }
+
+ for (uint32_t i = 0; i < n_children; i++)
+ if (parse(f, node))
+ return -1;
+
+ parent.children.push_back(node);
+ return 0;
+}
+
+int abc::print(const node &node, unsigned level) const
+{
+ for (unsigned i = 0; i < level; i++)
+ std::cout << "|\t";
+
+ std::cout << node.name << '\n';
+
+ for (const auto &child : node.children)
+ print(child, level + 1);
+
+ return 0;
+}
+
+size_t abc::count(const node &node) const
+{
+ size_t ret = 0;
+
+ for (const auto &child : node.children)
+ ret += count(child);
+
+ return ret + node.children.size();
+}
+
+int abc::parse_nodes(rez::file &f)
+{
+ uint32_t unknown, n_nodes;
+
+ if (f.read_le(unknown))
+ {
+ std::cerr << f.path() << ": failed to read unknown word\n";
+ return -1;
+ }
+ else if (parse(f, root))
+ {
+ std::cerr << f.path() << ": failed to parse node\n";
+ return -1;
+ }
+ else if ((n_nodes = count(root)) != header.n_nodes)
+ {
+ std::cerr << f.path() << ": expected to read " << header.n_nodes
+ << " nodes, got " << n_nodes << '\n';
+ return -1;
+ }
+
+ return 0;
+}
+
+int abc::parse(rez::file &f, item &item) const
+{
+ if (parse(f, item.pos))
+ {
+ std::cerr << f.path() << ": failed to read position\n";
+ return -1;
+ }
+ else if (parse(f, item.rot))
+ {
+ std::cerr << f.path() << ": failed to read rotation\n";
+ return -1;
+ }
+
+ return 0;
+}
+
+int abc::parse(rez::file &f, childmodel &cm, bool self) const
+{
+ uint32_t unknown;
+
+ if (self)
+ cm.name = "SELF";
+ else if (parse(f, cm.name))
+ return -1;
+
+ if (f.read_le(unknown))
+ {
+ std::cerr << f.path() << ": failed to read unknown word\n";
+ return -1;
+ }
+
+ cm.items.reserve(header.n_nodes);
+
+ for (uint32_t i = 0; i < header.n_nodes; i++)
+ {
+ item item;
+
+ if (parse(f, item))
+ return -1;
+
+ cm.items.push_back(item);
+ }
+
+ return 0;
+}
+
+int abc::parse_childmodels(rez::file &f)
+{
+ childmodel self;
+ uint32_t n;
+
+ if (f.read_le(n))
+ {
+ std::cerr << f.path() << ": failed to read number of child models\n";
+ return -1;
+ }
+ else if (parse(f, self, true))
+ {
+ std::cerr << f.path() << ": failed to read self child model\n";
+ return -1;
+ }
+
+ childmodels.reserve(n);
+ childmodels.push_back(self);
+
+ for (uint32_t i = 1; i < n; i++)
+ {
+ childmodel cm;
+
+ if (parse(f, cm))
+ return -1;
+
+ childmodels.push_back(cm);
+ }
+
+ return 0;
+}
+
+int abc::parse(rez::file &f, keyframe &keyframe) const
+{
+ if (f.read_le(keyframe.time))
+ {
+ std::cerr << f.path() << ": failed to read keyframe time\n";
+ return -1;
+ }
+ else if (parse(f, keyframe.cmd))
+ {
+ std::cerr << f.path() << ": failed to read keyframe cmd\n";
+ return -1;
+ }
+
+ return 0;
+}
+
+int abc::parse(rez::file &f, animitem &animitem) const
+{
+ if (parse(f, animitem.item))
+ return -1;
+ else if (f.read_le(animitem.unknown))
+ {
+ std::cerr << f.path() << ": failed to read unknown word 1\n";
+ return -1;
+ }
+ else if (f.read_le(animitem.unknown2))
+ {
+ std::cerr << f.path() << ": failed to read unknown word 2\n";
+ return -1;
+ }
+
+ return 0;
+}
+
+int abc::parse(rez::file &f, nodeanim &nodeanim, uint32_t n_keyframes) const
+{
+ nodeanim.animitems.reserve(n_keyframes);
+
+ for (uint32_t i = 0; i < n_keyframes; i++)
+ {
+ animitem animitem;
+
+ if (parse(f, animitem))
+ return -1;
+
+ nodeanim.animitems.push_back(animitem);
+ }
+
+ return 0;
+}
+
+int abc::parse(rez::file &f, animation &animation) const
+{
+ uint32_t n_keyframes;
+
+ if (parse(f, animation.dimensions))
+ {
+ std::cerr << f.path() << ": failed to read dimensions\n";
+ return -1;
+ }
+ else if (parse(f, animation.name))
+ {
+ std::cerr << f.path() << ": failed to read animation name\n";
+ return -1;
+ }
+ else if (f.read_le(animation.compression_type))
+ {
+ std::cerr << f.path() << ": failed to read compression type\n";
+ return -1;
+ }
+ else if (f.read_le(animation.interp_time))
+ {
+ std::cerr << f.path() << ": failed to read interp time\n";
+ return -1;
+ }
+ else if (f.read_le(n_keyframes))
+ {
+ std::cerr << f.path() << ": failed to read number of keyframes\n";
+ return -1;
+ }
+
+ animation.keyframes.reserve(n_keyframes);
+
+ for (uint32_t i = 0; i < n_keyframes; i++)
+ {
+ keyframe keyframe;
+
+ if (parse(f, keyframe))
+ return -1;
+
+ animation.keyframes.push_back(keyframe);
+ }
+
+ animation.nodeanims.reserve(header.n_nodes);
+
+ for (uint32_t i = 0; i < header.n_nodes; i++)
+ {
+ nodeanim nodeanim;
+ uint32_t unknown;
+
+ if (f.read_le(unknown))
+ {
+ std::cerr << f.path() << ": failed to read unknown word\n";
+ return -1;
+ }
+ else if (parse(f, nodeanim, n_keyframes))
+ return -1;
+
+ animation.nodeanims.push_back(nodeanim);
+ }
+
+ return 0;
+}
+
+int abc::parse_animation(rez::file &f)
+{
+ uint32_t n;
+
+ if (f.read_le(n))
+ {
+ std::cerr << f.path() << ": failed to read number of animations\n";
+ return -1;
+ }
+
+ animations.reserve(n);
+
+ for (uint32_t i = 0; i < n; i++)
+ {
+ animation animation;
+
+ if (parse(f, animation))
+ return -1;
+
+ animations.push_back(animation);
+ }
+
+ return 0;
+}
+
+int abc::parse(rez::file &f, socket &socket) const
+{
+ if (f.read_le(socket.node_index))
+ {
+ std::cerr << f.path() << ": failed to read node index\n";
+ return -1;
+ }
+ else if (parse(f, socket.name))
+ {
+ std::cerr << f.path() << ": failed to read socket name\n";
+ return -1;
+ }
+ else if (parse(f, socket.quat))
+ {
+ std::cerr << f.path() << ": failed to read socket quat\n";
+ return -1;
+ }
+ else if (parse(f, socket.pos))
+ {
+ std::cerr << f.path() << ": failed to read socket position\n";
+ return -1;
+ }
+
+ return 0;
+}
+
+int abc::parse_sockets(rez::file &f)
+{
+ uint32_t n;
+
+ if (f.read_le(n))
+ {
+ std::cerr << f.path() << ": failed to read number of sockets\n";
+ return -1;
+ }
+
+ for (uint32_t i = 0; i < n; i++)
+ {
+ socket socket;
+
+ if (parse(f, socket))
+ return -1;
+ }
+
+ return 0;
+}
+
+int abc::parse(rez::file &f, childanim &childanim) const
+{
+ if (parse(f, childanim.name))
+ {
+ std::cerr << f.path() << ": failed to read childanim name\n";
+ return -1;
+ }
+ else if (parse(f, childanim.dimensions))
+ {
+ std::cerr << f.path() << ": failed to read childanim dimensions\n";
+ return -1;
+ }
+ else if (parse(f, childanim.translation))
+ {
+ std::cerr << f.path() << ": failed to read childanim translation\n";
+ return -1;
+ }
+
+ return 0;
+}
+
+int abc::parse(rez::file &f, animbinding &animbinding) const
+{
+ uint32_t n;
+
+ if (f.read_le(n))
+ {
+ std::cerr << f.path() << ": failed to read number of animbindings\n";
+ return -1;
+ }
+
+ animbinding.childanims.reserve(n);
+
+ for (uint32_t i = 0; i < n; i++)
+ {
+ childanim childanim;
+
+ if (parse(f, childanim))
+ return -1;
+
+ animbinding.childanims.push_back(childanim);
+ }
+
+ return 0;
+}
+
+int abc::parse_animbindings(rez::file &f)
+{
+ animbindings.reserve(childmodels.size());
+
+ for (size_t i = 0; i < childmodels.size(); i++)
+ {
+ animbinding animbinding;
+
+ if (parse(f, animbinding))
+ return -1;
+
+ animbindings.push_back(animbinding);
+ }
+
+ return 0;
+}
+
+int abc::parse_hitgroups(rez::file &f)
+{
+ uint32_t unknown, unknown2;
+
+ if (f.read_le(unknown) || f.read_le(unknown2))
+ {
+ std::cerr << f.path() << ": failed to read unknown words\n";
+ return -1;
+ }
+
+ return 0;
+}
+
+int abc::parse(rez::file &f)
+{
+ int32_t offset;
+
+ do
+ {
+ uint32_t uoffset;
+ std::string chunk;
+
+ if (parse(f, chunk) || f.read_le(uoffset))
+ return -1;
+
+ offset = uoffset;
+
+ static const struct
+ {
+ const char *chunk;
+ int (abc::*fn)(rez::file &);
+ } fns[] =
+ {
+ {"Header", &abc::parse_header},
+ {"Pieces", &abc::parse_pieces},
+ {"Nodes", &abc::parse_nodes},
+ {"ChildModels", &abc::parse_childmodels},
+ {"Animation", &abc::parse_animation},
+ {"Sockets", &abc::parse_sockets},
+ {"AnimBindings", &abc::parse_animbindings},
+ {"HitGroups", &abc::parse_hitgroups}
+ };
+
+ bool found = false;
+
+ for (const auto &fn : fns)
+ {
+ long before, after;
+
+ if (chunk != fn.chunk)
+ continue;
+ else if ((before = f.tell()) < 0)
+ {
+ std::cerr << f.path() << ": failed to get \"before\" offset\n";
+ return -1;
+ }
+ else if (std::invoke(fn.fn, this, f))
+ return -1;
+ else if ((after = f.tell()) < 0)
+ {
+ std::cerr << f.path() << ": failed to get offset\n";
+ return -1;
+ }
+ else if (offset > 0 && after != offset)
+ {
+ std::cerr << f.path() << ": expected file offset "
+ << offset << " after reading section " << chunk
+ << ", but current offset is " << after << '\n';
+ return -1;
+ }
+
+ found = true;
+ break;
+ }
+
+ if (!found)
+ {
+ std::cerr << f.path() << ": unsupported section: " << chunk << '\n';
+ return -1;
+ }
+ } while (offset != -1);
+
+ return 0;
+}
+
+abc::abc() :
+ int_radius(0.0f)
+{
+}
diff --git a/src/abc/abc.h b/src/abc/abc.h
new file mode 100644
index 0000000..b86f13c
--- /dev/null
+++ b/src/abc/abc.h
@@ -0,0 +1,177 @@
+#ifndef ABC_H
+#define ABC_H
+
+#include <rez/file.h>
+#include <cstdint>
+#include <list>
+#include <memory>
+#include <vector>
+
+class abc
+{
+public:
+ abc();
+ int parse(rez::file &f);
+
+private:
+
+ struct vector
+ {
+ float x, y, z;
+ };
+
+ struct rotation
+ {
+ vector v;
+ float w;
+ };
+
+ struct weight
+ {
+ uint32_t node_index;
+ vector location;
+ float bias;
+ };
+
+ struct vertex
+ {
+ std::vector<weight> weights;
+ vector vertex, normal;
+ };
+
+ struct triangle
+ {
+ float a, b;
+ uint16_t trifs;
+ };
+
+ struct piece
+ {
+ uint16_t tex_index;
+ uint32_t specular_power, specular_scale, lod_weight, n_tris;
+ std::string name;
+ std::vector<triangle> triangles;
+ std::vector<vertex> vertices;
+ };
+
+ struct set
+ {
+ std::string name;
+ std::vector<float> weights;
+ };
+
+ struct node
+ {
+ std::string name;
+ uint16_t transform_index;
+ uint8_t flags;
+ rotation matrix[4];
+ std::list<node> children;
+ };
+
+ struct header
+ {
+ uint32_t version, n_keyframe, n_anim, n_nodes, n_pieces, n_children,
+ n_triangles, n_vertices, n_vertweights, n_lod, n_socket,
+ n_weightsets, n_strings, n_strlen;
+ };
+
+ struct item
+ {
+ vector pos;
+ rotation rot;
+ };
+
+ struct childmodel
+ {
+ std::string name;
+ std::vector<item> items;
+ };
+
+ struct keyframe
+ {
+ std::string cmd;
+ uint32_t time;
+ };
+
+ struct animitem
+ {
+ abc::item item;
+ uint32_t unknown, unknown2;
+ };
+
+ struct nodeanim
+ {
+ std::vector<animitem> animitems;
+ };
+
+ struct animation
+ {
+ std::string name;
+ vector dimensions;
+ uint32_t compression_type, interp_time;
+ std::vector<keyframe> keyframes;
+ std::vector<nodeanim> nodeanims;
+ };
+
+ struct socket
+ {
+ std::string name;
+ uint32_t node_index;
+ rotation quat;
+ vector pos;
+ };
+
+ struct childanim
+ {
+ std::string name;
+ vector dimensions, translation;
+ };
+
+ struct animbinding
+ {
+ std::vector<childanim> childanims;
+ };
+
+ int parse_header(rez::file &f);
+ int parse_pieces(rez::file &f);
+ int parse_nodes(rez::file &f);
+ int parse_sets(rez::file &f);
+ int parse_childmodels(rez::file &f);
+ int parse_animation(rez::file &f);
+ int parse_sockets(rez::file &f);
+ int parse_animbindings(rez::file &f);
+ int parse_hitgroups(rez::file &f);
+ int parse(rez::file &f, std::string &name) const;
+ int parse(rez::file &f, node &parent);
+ int parse(rez::file &f, piece &piece) const;
+ int parse(rez::file &f, triangle &triangle) const;
+ int parse(rez::file &f, vertex &vertex) const;
+ int parse(rez::file &f, weight &weight) const;
+ int parse(rez::file &f, vector &vector) const;
+ int parse(rez::file &f, rotation &rot) const;
+ int parse(rez::file &f, set &set) const;
+ int parse(rez::file &f, childmodel &cm, bool self = false) const;
+ int parse(rez::file &f, item &item) const;
+ int parse(rez::file &f, animation &animation) const;
+ int parse(rez::file &f, keyframe &keyframe) const;
+ int parse(rez::file &f, nodeanim &nodeanim, uint32_t n_keyframes) const;
+ int parse(rez::file &f, animitem &animitem) const;
+ int parse(rez::file &f, socket &socket) const;
+ int parse(rez::file &f, animbinding &animbinding) const;
+ int parse(rez::file &f, childanim &childanim) const;
+ int print(const node &node, unsigned level) const;
+ size_t count(const node &node) const;
+
+ node root;
+ header header;
+ std::vector<piece> pieces;
+ std::vector<abc::set> sets;
+ std::vector<childmodel> childmodels;
+ std::vector<animation> animations;
+ std::vector<socket> sockets;
+ std::vector<animbinding> animbindings;
+ float int_radius;
+};
+
+#endif
diff --git a/src/dtx/CMakeLists.txt b/src/dtx/CMakeLists.txt
new file mode 100644
index 0000000..40a8c1f
--- /dev/null
+++ b/src/dtx/CMakeLists.txt
@@ -0,0 +1,4 @@
+target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_LIST_DIR})
+target_sources(${PROJECT_NAME} PRIVATE
+ dtx.cpp
+)
diff --git a/src/dtx/dtx.cpp b/src/dtx/dtx.cpp
new file mode 100644
index 0000000..463bb1d
--- /dev/null
+++ b/src/dtx/dtx.cpp
@@ -0,0 +1,207 @@
+#include <dtx.h>
+#include <rez/file.h>
+#include <GL/gl.h>
+#include <GL/glext.h>
+#include <cstdint>
+#include <cstdio>
+#include <iostream>
+
+int dtx::draw() const
+{
+ int ret = 0;
+ GLenum error;
+
+ glEnable(GL_TEXTURE_2D);
+ glBindTexture(GL_TEXTURE_2D, texture);
+
+ if (bpp == bpp_s3tc_dxt5)
+ {
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_ONE_MINUS_SRC_ALPHA, GL_SRC_ALPHA);
+ }
+
+ glBegin(GL_QUADS);
+ glTexCoord2f(0.0f, 0.0f); glVertex2f(-0.5f, 0.5f);
+ glTexCoord2f(0.0f, 1.0f); glVertex2f(0.5f, 0.5f);
+ glTexCoord2f(1.0f, 1.0f); glVertex2f(0.5f, -0.5f);
+ glTexCoord2f(1.0f, 0.0f); glVertex2f(-0.5f, -0.5f);
+ glEnd();
+
+ glFlush();
+ glDisable(GL_BLEND);
+ glDisable(GL_TEXTURE_2D);
+
+ while ((error = glGetError()) != GL_NO_ERROR)
+ {
+ ret = -1;
+ fprintf(stderr, "%s:%d: glGetError returned %x\n", __FILE__,
+ __LINE__, error);
+ }
+
+ return ret;
+}
+
+int dtx::parse(rez::file &f)
+{
+ uint32_t restype, uversion;
+
+ if (f.read_le(restype) || f.read_le(uversion))
+ return -1;
+
+ const uint32_t exp_type = 0;
+ const int32_t exp_version = -5;
+ int32_t version = uversion;
+
+ if (restype != exp_type)
+ {
+ std::cerr << f.path() << ": invalid resource type " << restype
+ << ", expected " << exp_type << '\n';
+ return -1;
+ }
+ else if (version != exp_version)
+ {
+ std::cerr << f.path() << ": invalid or unsupported version " << version
+ << ", expected " << exp_version << '\n';
+ return -1;
+ }
+
+ const uint16_t max_mipmaps = 8;
+ uint16_t w, h, n_mipmaps, n_sections, tex_angle;
+ uint32_t iflags, userflags, tex_scale;
+ unsigned char tex_group, n_rtmipmaps, bpp, mipmap_off_nc, mipmap_off,
+ tex_prio;
+ char cmd[128];
+
+ if (f.read_le(w)
+ || f.read_le(h)
+ || f.read_le(n_mipmaps)
+ || f.read_le(n_sections))
+ {
+ std::cerr << f.path() << ": failed to read header\n";
+ return -1;
+ }
+ else if (!n_mipmaps || n_mipmaps > max_mipmaps)
+ {
+ std::cerr << f.path() << ": invalid number of mipmaps: "
+ << n_mipmaps << '\n';
+ return -1;
+ }
+ else if (f.read_le(iflags) || f.read_le(userflags))
+ {
+ std::cerr << f.path() << ": failed to read flags\n";
+ return -1;
+ }
+ else if (f.read(tex_group)
+ || f.read(n_rtmipmaps)
+ || f.read(bpp)
+ || f.read(mipmap_off_nc)
+ || f.read(mipmap_off)
+ || f.read(tex_prio)
+ || f.read_le(tex_scale)
+ || f.read_le(tex_angle))
+ {
+ std::cerr << f.path() << ": failed to read extra data\n";
+ return -1;
+ }
+ else if (bpp != bpp::bpp_32
+ && bpp != bpp::bpp_s3tc_dxt1
+ && bpp != bpp::bpp_s3tc_dxt5)
+ {
+ std::cerr << f.path() << ": invalid or unsupported bpp "
+ << std::to_string(bpp) << ", expected " << bpp::bpp_s3tc_dxt1
+ << ", " << bpp::bpp_32 << " or " << bpp::bpp_s3tc_dxt5 << '\n';
+ return -1;
+ }
+ else if (f.read(cmd, sizeof cmd))
+ {
+ std::cerr << f.path() << ": failed to read cmd\n";
+ return -1;
+ }
+
+ this->bpp = static_cast<enum bpp>(bpp);
+ this->w = w;
+ this->h = h;
+
+ glGenTextures(1, &texture);
+ glBindTexture(GL_TEXTURE_2D, texture);
+
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
+
+ GLint level = 0;
+
+ for (uint32_t i = 0; i < n_mipmaps; i++)
+ {
+ GLenum format;
+ unsigned long n_pix = (w >> i) * (h >> i);
+ pixdata mipmap;
+
+ switch (this->bpp)
+ {
+ case bpp_32:
+ format = GL_BGRA;
+ n_pix *= sizeof (uint32_t);
+ break;
+
+ case bpp_s3tc_dxt1:
+ format = GL_COMPRESSED_RGB_S3TC_DXT1_EXT;
+ n_pix >>= 1;
+ break;
+
+ case bpp_s3tc_dxt5:
+ format = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
+ break;
+
+ default:
+ return -1;
+ }
+
+ mipmap.reserve(n_pix);
+
+ while (n_pix)
+ {
+ unsigned char buf[BUFSIZ];
+ size_t n = n_pix > sizeof buf ? sizeof buf : n_pix;
+
+ if (f.read(buf, n))
+ return -1;
+
+ mipmap.insert(mipmap.end(), buf, &buf[n]);
+ n_pix -= n;
+ }
+
+ GLenum error;
+ int ret = 0;
+
+ if (bpp == bpp::bpp_s3tc_dxt1 || bpp == bpp::bpp_s3tc_dxt5)
+ glCompressedTexImage2D(GL_TEXTURE_2D, level++, format,
+ w >> i, h >> i, 0, mipmap.size(), mipmap.data());
+ else if (bpp == bpp::bpp_32)
+ glTexImage2D(GL_TEXTURE_2D, level++, format, w >> i, h >> i, 0,
+ format, GL_UNSIGNED_BYTE, mipmap.data());
+
+ while ((error = glGetError()) != GL_NO_ERROR)
+ {
+ fprintf(stderr, "%s:%d: glGetError returned %x\n", __FILE__,
+ __LINE__, error);
+ ret = -1;
+ }
+
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+dtx::~dtx()
+{
+ if (texture)
+ glDeleteTextures(1, &texture);
+}
+
+dtx::dtx() :
+ texture(0)
+{
+}
diff --git a/src/dtx/dtx.h b/src/dtx/dtx.h
new file mode 100644
index 0000000..24a7a7b
--- /dev/null
+++ b/src/dtx/dtx.h
@@ -0,0 +1,37 @@
+#ifndef DTX_H
+#define DTX_H
+
+#include <rez/file.h>
+#include <GL/gl.h>
+
+class dtx
+{
+public:
+ typedef std::vector<unsigned char> pixdata;
+
+ dtx();
+ ~dtx();
+ int parse(rez::file &f);
+ int draw() const;
+
+private:
+
+ enum bpp
+ {
+ bpp_8_pal,
+ bpp_8,
+ bpp_16,
+ bpp_32,
+ bpp_s3tc_dxt1,
+ bpp_s3tc_dxt3,
+ bpp_s3tc_dxt5,
+ bpp_32_pal,
+
+ n_bpp
+ } bpp;
+
+ GLuint texture;
+ unsigned w, h;
+};
+
+#endif
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..620e97e
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,99 @@
+#define SDL_MAIN_HANDLED
+
+#include <SDL2/SDL.h>
+#include <SDL2/SDL_video.h>
+#include <abc.h>
+#include <dtx.h>
+#include <rez.h>
+#include <cstdlib>
+#include <cstdio>
+#include <iostream>
+#include <GL/gl.h>
+
+int main(int argc, char *argv[])
+{
+ int ret = EXIT_FAILURE;
+ rez::ball b("globalops.rez");
+ std::unique_ptr<rez::file> f, abcf;
+ SDL_Window *window = nullptr;
+ SDL_GLContext glcontext = nullptr;
+ static const char extension[] = "GL_EXT_texture_compression_s3tc";
+ abc abc;
+ dtx dtx;
+
+ if (SDL_Init(SDL_INIT_VIDEO) < 0)
+ {
+ std::cerr << "SDL_Init failed: " << SDL_GetError() << '\n';
+ goto end;
+ }
+
+ SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
+ SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
+
+ if (!(window = SDL_CreateWindow("GlobalOps", SDL_WINDOWPOS_CENTERED,
+ SDL_WINDOWPOS_CENTERED, 640, 480,
+ SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE)))
+ {
+ std::cerr << "SDL_CreateWindow failed: " << SDL_GetError() << '\n';
+ goto end;
+ }
+ else if (!(glcontext = SDL_GL_CreateContext(window)))
+ {
+ std::cerr << "SDL_GL_CreateContext failed: " << SDL_GetError() << '\n';
+ goto end;
+ }
+ else if (!SDL_GL_ExtensionSupported(extension))
+ {
+ std::cerr << "OpenGL extension " << extension << " not supported\n";
+ goto end;
+ }
+ else if (SDL_GL_MakeCurrent(window, glcontext))
+ {
+ std::cerr << "SDL_GL_MakeCurrent failed: " << SDL_GetError()
+ << std::endl;
+ goto end;
+ }
+ else if (b.parse()
+ || !(f = b.open("interface/blueprints/antarctica_tacmap.dtx"))
+ || dtx.parse(*f)
+ || !(f = b.open("models/grenades/v_frag.abc"))
+ || abc.parse(*f))
+ goto end;
+
+ for (;;)
+ {
+ SDL_Event event;
+
+ while (SDL_PollEvent(&event))
+ {
+ switch (event.type)
+ {
+ case SDL_QUIT:
+ ret = EXIT_SUCCESS;
+ goto end;
+ }
+ }
+
+ glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ if (dtx.draw())
+ goto end;
+
+ SDL_GL_SwapWindow(window);
+ SDL_Delay(1.0f / 60.f * 1000.0f);
+ }
+
+end:
+
+ if (glcontext)
+ SDL_GL_DeleteContext(glcontext);
+
+ if (window)
+ SDL_DestroyWindow(window);
+
+ if (SDL_WasInit(SDL_INIT_VIDEO))
+ SDL_QuitSubSystem(SDL_INIT_VIDEO);
+
+ return ret;
+}
diff --git a/src/rez/CMakeLists.txt b/src/rez/CMakeLists.txt
new file mode 100644
index 0000000..7a767fc
--- /dev/null
+++ b/src/rez/CMakeLists.txt
@@ -0,0 +1,9 @@
+target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_LIST_DIR})
+target_sources(${PROJECT_NAME} PRIVATE
+ dir.cpp
+ entry.cpp
+ file.cpp
+ io.cpp
+ resource.cpp
+ rez.cpp
+)
diff --git a/src/rez/dir.cpp b/src/rez/dir.cpp
new file mode 100644
index 0000000..62e37fb
--- /dev/null
+++ b/src/rez/dir.cpp
@@ -0,0 +1,117 @@
+#include <rez/dir.h>
+#include <rez/io.h>
+#include <iostream>
+
+const rez::dir::entrymap &rez::dir::entries() const
+{
+ return m_entries;
+}
+
+const std::string &rez::dir::name() const
+{
+ return m_name;
+}
+
+int rez::dir::read(const struct entry::cfg &cfg, rez::io &io,
+ std::string &name, direntry &de, unsigned level)
+{
+ rez::entry entry;
+
+ if (entry.read(io))
+ return -1;
+
+ switch (entry.type)
+ {
+ case entry::type::dir:
+ {
+ long offset;
+ dir subdir;
+
+ if (io.read(subdir.m_name, cfg.maxdirlen)
+ || (offset = io.tell()) < 0)
+ return -1;
+
+ struct entry::cfg scfg = cfg;
+
+#if 0
+ for (unsigned i = 0; i < level + 1; i++)
+ std::cout << "|\t";
+
+ std::cout << "name: " << subdir.m_name << std::endl;
+#endif
+
+ subdir.entry = entry;
+ scfg.size = entry.size;
+ scfg.offset = entry.pos;
+
+ if (subdir.read(scfg, io, level + 1) || io.seek(offset))
+ return -1;
+
+ name = subdir.m_name;
+ de = subdir;
+ }
+ break;
+
+ case entry::type::resource:
+ {
+ resource resource;
+
+ resource.entry = entry;
+
+ if (resource.read(cfg, io))
+ return -1;
+
+#if 0
+ for (unsigned i = 0; i < level + 1; i++)
+ std::cout << "|\t";
+
+ std::cout << "name: " << resource.name() << std::endl;
+#endif
+
+ name = resource.name();
+ de = resource;
+ }
+ break;
+
+ default:
+ std::cerr << "Invalid entry type " << entry.type << std::endl;
+ return -1;
+ }
+
+ return 0;
+}
+
+int rez::dir::read(const struct entry::cfg &cfg, rez::io &io, unsigned level)
+{
+ unsigned long size = cfg.size;
+ long before = cfg.offset;
+
+ if (io.seek(cfg.offset))
+ return -1;
+
+ while (size)
+ {
+ std::string name;
+ direntry de;
+ long after;
+
+ if (read(cfg, io, name, de, level)
+ || (after = io.tell()) < 0)
+ return -1;
+
+ unsigned long r = after - before;
+
+ if (r > size)
+ {
+ std::cerr << m_name << ": exceeded maximum direntry length "
+ "(" << cfg.size << ")" << std::endl;
+ return -1;
+ }
+
+ m_entries[name] = de;
+ before = after;
+ size -= r;
+ }
+
+ return 0;
+}
diff --git a/src/rez/entry.cpp b/src/rez/entry.cpp
new file mode 100644
index 0000000..2e2f73e
--- /dev/null
+++ b/src/rez/entry.cpp
@@ -0,0 +1,10 @@
+#include <rez/entry.h>
+#include <rez/io.h>
+
+int rez::entry::read(rez::io &io)
+{
+ return io.read_le(type)
+ || io.read_le(pos)
+ || io.read_le(size)
+ || io.read_le(time);
+}
diff --git a/src/rez/file.cpp b/src/rez/file.cpp
new file mode 100644
index 0000000..9a6a1f3
--- /dev/null
+++ b/src/rez/file.cpp
@@ -0,0 +1,100 @@
+#include <rez/file.h>
+#include <rez/entry.h>
+#include <rez/resource.h>
+#include <cstddef>
+#include <iostream>
+
+std::string rez::file::path() const
+{
+ return resource.name();
+}
+
+long rez::file::tell()
+{
+ return offset;
+}
+
+int rez::file::seek(long offset)
+{
+ if (offset < 0 || offset > resource.entry.size)
+ return -1;
+ else if (io.seek(resource.entry.pos + offset))
+ return -1;
+
+ this->offset = offset;
+ return 0;
+}
+
+int rez::file::eof() const
+{
+ return offset >= resource.entry.size;
+}
+
+int rez::file::read(void *b, size_t n)
+{
+ const rez::entry &entry = resource.entry;
+ unsigned long roffset = offset + entry.pos,
+ end = entry.pos + entry.size, remaining = end - roffset;
+
+ if (n > remaining)
+ {
+ std::cerr << path() << ": requested " << n << " bytes, only "
+ << remaining << " remaining" << std::endl;
+ return -1;
+ }
+
+ if (seek(offset) || io.read(b, n))
+ return -1;
+
+ offset += n;
+ return 0;
+}
+
+int rez::file::read(unsigned char &b)
+{
+ return read(&b, sizeof b);
+}
+
+int rez::file::read(std::string &s, size_t len)
+{
+ for (size_t i = 0; i < len; i++)
+ {
+ char b;
+
+ if (read(&b, sizeof b))
+ return -1;
+
+ s += b;
+ }
+
+ return 0;
+}
+
+int rez::file::read_le(uint16_t &w)
+{
+ unsigned char b[sizeof w];
+
+ if (read(b, sizeof b))
+ return -1;
+
+ w = b[0] | (b[1] << 8);
+ return 0;
+}
+
+int rez::file::read_le(uint32_t &w)
+{
+ unsigned char b[sizeof w];
+
+ if (read(b, sizeof b))
+ return -1;
+
+ w = b[0] | (b[1] << 8u) | (b[2] << 16u) | (b[3] << 24u);
+ return 0;
+}
+
+rez::file::file(const rez::resource &resource, rez::io &io) :
+ resource(resource),
+ offset(0),
+ io(io)
+{
+}
diff --git a/src/rez/io.cpp b/src/rez/io.cpp
new file mode 100644
index 0000000..019fdab
--- /dev/null
+++ b/src/rez/io.cpp
@@ -0,0 +1,174 @@
+#include <rez/io.h>
+#include <cerrno>
+#include <cstring>
+#include <fstream>
+#include <iostream>
+
+int rez::io::read(unsigned char &b)
+{
+ std::ifstream::pos_type off = f.tellg();
+
+ f.read((char *)&b, sizeof b);
+
+ if (!f || f.eof())
+ {
+ std::cerr << m_path << ": failed to read at offset " << off
+ << ", eof=" << f.eof() << std::endl;
+ return -1;
+ }
+
+ return 0;
+}
+
+int rez::io::check(char byte)
+{
+ char b;
+ std::ifstream::pos_type off = f.tellg();
+
+ f.read(&b, sizeof b);
+
+ if (!f || f.eof())
+ {
+ std::cerr << m_path << ": failed to read at offset " << off
+ << ", eof=" << f.eof() << std::endl;
+ return -1;
+ }
+ else if (b != byte)
+ {
+ std::cerr << m_path << ": expected " << byte << " at offset: " << off
+ << ", got " << b << std::endl;
+ return -1;
+ }
+
+ return 0;
+}
+
+int rez::io::check(const char *s)
+{
+ while (*s)
+ if (check(*s++))
+ return -1;
+
+ return 0;
+}
+
+int rez::io::read_le(uint32_t &w)
+{
+ std::ifstream::pos_type off = f.tellg();
+ unsigned char buf[sizeof w];
+
+ f.read((char *)buf, sizeof buf);
+
+ if (!f || f.eof())
+ {
+ std::cerr << m_path << ": failed to read word at offset " << off
+ << ", eof=" << f.eof() << std::endl;
+ return -1;
+ }
+
+ w = buf[0] | (buf[1] << 8) | (buf[2] << 16) | (buf[3] << 24);
+ return 0;
+}
+
+int rez::io::read(std::string &s, unsigned maxlen)
+{
+ unsigned i = 0;
+
+ while (i++ < maxlen)
+ {
+ unsigned char b;
+
+ if (read(b))
+ return -1;
+ else if (!b)
+ return 0;
+
+ s += b;
+ }
+
+ std::cerr << m_path << ": exceeded maximum string length (" << maxlen
+ << "): \"" << s << "\"" << std::endl;
+ return -1;
+}
+
+int rez::io::read(void *b, size_t n)
+{
+ std::ifstream::pos_type off = f.tellg();
+
+ f.read((char *)(b), n);
+
+ if (!f)
+ {
+ std::cerr << m_path << ": failed to read " << n << " bytes at offset "
+ << off << ", eof=" << f.eof() << std::endl;
+ return -1;
+ }
+
+ return 0;
+}
+
+long rez::io::tell()
+{
+ std::ifstream::pos_type offset = f.tellg();
+
+ if (!f)
+ {
+ std::cerr << m_path << ": failed to retrieve offset" << std::endl;
+ return -1;
+ }
+
+ return offset;
+}
+
+int rez::io::seek(long offset)
+{
+ long cur = f.tellg();
+
+ if (!f)
+ {
+ std::cerr << m_path << ": failed to tell offset " << std::endl;
+ return -1;
+ }
+ else if (cur != offset)
+ {
+ f.seekg(static_cast<std::ifstream::pos_type>(offset));
+
+ if (!f)
+ {
+ std::cerr << m_path << ": failed to seek to offset " << offset
+ << std::endl;
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
+int rez::io::open()
+{
+ if (!f.is_open())
+ {
+ const char *error;
+
+#ifdef _WIN32
+ error = GetLastError();
+#else
+ error = strerror(errno);
+#endif
+ std::cerr << "Failed to open " << m_path << ": " << error << std::endl;
+ return -1;
+ }
+
+ return 0;
+}
+
+const std::string &rez::io::path() const
+{
+ return m_path;
+}
+
+rez::io::io(const char *path) :
+ f(path, std::ios::binary),
+ m_path(path)
+{
+}
diff --git a/src/rez/resource.cpp b/src/rez/resource.cpp
new file mode 100644
index 0000000..3fc94a7
--- /dev/null
+++ b/src/rez/resource.cpp
@@ -0,0 +1,46 @@
+#include <rez/resource.h>
+#include <rez/io.h>
+
+std::string rez::resource::name() const
+{
+ std::string ret = m_name;
+
+ if (*type)
+ ret += std::string(".") + type;
+
+ return ret;
+}
+
+const std::string &rez::resource::description() const
+{
+ return descr;
+}
+
+int rez::resource::read(const struct entry::cfg &cfg, rez::io &io)
+{
+ char letype[sizeof type];
+ uint32_t n_keys;
+
+ if (io.read_le(id)
+ || io.read(letype, sizeof letype)
+ || io.read_le(n_keys)
+ || io.read(m_name, cfg.maxreslen)
+ || io.read(descr, sizeof ".XYZ"))
+ return -1;
+
+ for (uint32_t i = 0; i < n_keys; i++)
+ {
+ uint32_t key;
+
+ if (io.read_le(key))
+ return -1;
+
+ keys.push_back(key);
+ }
+
+ type[0] = letype[2];
+ type[1] = letype[1];
+ type[2] = letype[0];
+ type[3] = '\0';
+ return 0;
+}
diff --git a/src/rez/rez.cpp b/src/rez/rez.cpp
new file mode 100644
index 0000000..2351c05
--- /dev/null
+++ b/src/rez/rez.cpp
@@ -0,0 +1,124 @@
+#include <rez.h>
+#include <rez/dir.h>
+#include <rez/io.h>
+#include <cctype>
+#include <iostream>
+#include <memory>
+#include <sstream>
+#include <variant>
+
+std::string rez::ball::toupper(const std::string &s) const
+{
+ std::string ret;
+
+ // Assume ASCII encoding.
+ for (const auto &c: s)
+ ret += std::toupper(c);
+
+ return ret;
+}
+
+std::unique_ptr<rez::file> rez::ball::open(const char *path)
+{
+ std::istringstream stream(path);
+ std::string token;
+ const rez::dir *dir = &root_dir;
+ const rez::resource *resource = nullptr;
+
+ while (std::getline(stream, token, '/'))
+ {
+ std::string uctoken = toupper(token);
+ const auto &map = dir->entries();
+ auto it = map.find(uctoken);
+
+ if (it == map.end())
+ return nullptr;
+
+ const rez::dir::direntry &de = it->second;
+
+ if (std::holds_alternative<rez::dir>(de))
+ dir = &std::get<rez::dir>(de);
+ else if (std::holds_alternative<rez::resource>(de))
+ {
+ dir = nullptr;
+ resource = &std::get<rez::resource>(de);
+ }
+ }
+
+ if (dir)
+ {
+ std::cerr << path << " is a directory\n";
+ return nullptr;
+ }
+ else if (!resource)
+ {
+ std::cerr << io.path() << ": could not find " << path << '\n';
+ return nullptr;
+ }
+
+ return std::make_unique<rez::file>(*resource, io);
+}
+
+int rez::ball::parse()
+{
+ static const char title[63] =
+ "\r\nRezMgr Version 1 Copyright (C) 1995 MONOLITH INC. ",
+ subtitle[65] =
+ "\r\nLithTech Resource File \r\n";
+
+ if (io.open())
+ return -1;
+ else if (io.check(title))
+ {
+ std::cerr << path << ": wrong title\n";
+ return -1;
+ }
+ else if (io.check(subtitle))
+ {
+ std::cerr << path << ": wrong subtitle\n";
+ return -1;
+ }
+ else if (io.check(0x1a))
+ {
+ std::cerr << path << ": wrong EOF\n";
+ return -1;
+ }
+
+ struct
+ {
+ uint32_t w[N_WORDS];
+ uint8_t is_sorted;
+ } header;
+
+ for (auto &w : header.w)
+ if (io.read_le(w))
+ return -1;
+
+ if (header.w[FILE_FORMAT_VERSION] != 1)
+ {
+ std::cerr << path << ": expected file format version 1, got "
+ << header.w[FILE_FORMAT_VERSION] << '\n';
+ return -1;
+ }
+ else if (io.read(header.is_sorted))
+ {
+ std::cerr << path << ": could not read sorted flag" << '\n';
+ return -1;
+ }
+
+ struct rez::entry::cfg cfg = {0};
+
+ cfg.size = header.w[ROOT_DIR_SIZE];
+ cfg.offset = header.w[ROOT_DIR_POS];
+ cfg.maxcomlen = header.w[MAX_COMMENT_SIZE];
+ cfg.maxdirlen = header.w[MAX_DIR_NAME_SIZE];
+ cfg.maxreslen = header.w[MAX_REZ_NAME_SIZE];
+ root_dir.read(cfg, io, 0);
+ return 0;
+}
+
+rez::ball::ball(const char *path) :
+ path(path),
+ io(path)
+{
+}
diff --git a/src/rez/rez.h b/src/rez/rez.h
new file mode 100644
index 0000000..5bbe206
--- /dev/null
+++ b/src/rez/rez.h
@@ -0,0 +1,46 @@
+#ifndef REZ_H
+#define REZ_H
+
+#include <rez/dir.h>
+#include <rez/file.h>
+#include <rez/io.h>
+#include <memory>
+#include <string>
+
+namespace rez
+{
+
+class ball
+{
+public:
+ ball(const char *path);
+ int parse();
+ std::unique_ptr<rez::file> open(const char *path);
+
+private:
+ std::string toupper(const std::string &s) const;
+
+ enum
+ {
+ FILE_FORMAT_VERSION,
+ ROOT_DIR_POS,
+ ROOT_DIR_SIZE,
+ ROOT_DIR_TIME,
+ NEXT_WRITE_POS,
+ TIME,
+ MAX_KEY_ARRAY,
+ MAX_DIR_NAME_SIZE,
+ MAX_REZ_NAME_SIZE,
+ MAX_COMMENT_SIZE,
+
+ N_WORDS
+ };
+
+ const std::string path;
+ rez::io io;
+ rez::dir root_dir;
+};
+
+}
+
+#endif
diff --git a/src/rez/rez/dir.h b/src/rez/rez/dir.h
new file mode 100644
index 0000000..5485052
--- /dev/null
+++ b/src/rez/rez/dir.h
@@ -0,0 +1,34 @@
+#ifndef REZ_DIR_H
+#define REZ_DIR_H
+
+#include <rez/io.h>
+#include <rez/entry.h>
+#include <rez/resource.h>
+#include <map>
+#include <string>
+#include <variant>
+
+namespace rez
+{
+
+class dir
+{
+public:
+ typedef std::variant<resource, rez::dir> direntry;
+ typedef std::map<std::string, direntry> entrymap;
+ int read(const struct entry::cfg &cfg, rez::io &io, unsigned level);
+ const std::string &name() const;
+ const entrymap &entries() const;
+
+private:
+ int read(const struct entry::cfg &cfg, rez::io &io, std::string &name,
+ direntry &de, unsigned level);
+
+ rez::entry entry;
+ std::string m_name;
+ entrymap m_entries;
+};
+
+}
+
+#endif
diff --git a/src/rez/rez/entry.h b/src/rez/rez/entry.h
new file mode 100644
index 0000000..0e76321
--- /dev/null
+++ b/src/rez/rez/entry.h
@@ -0,0 +1,31 @@
+#ifndef REZ_ENTRY_H
+#define REZ_ENTRY_H
+
+#include <rez/io.h>
+#include <cstdint>
+
+namespace rez
+{
+
+struct entry
+{
+ int read(rez::io &io);
+
+ struct cfg
+ {
+ long offset;
+ unsigned long size, maxdirlen, maxreslen, maxcomlen;
+ };
+
+ enum type
+ {
+ resource,
+ dir
+ };
+
+ uint32_t type, pos, size, time;
+};
+
+}
+
+#endif
diff --git a/src/rez/rez/file.h b/src/rez/rez/file.h
new file mode 100644
index 0000000..20b2310
--- /dev/null
+++ b/src/rez/rez/file.h
@@ -0,0 +1,34 @@
+#ifndef REZ_FILE_H
+#define REZ_FILE_H
+
+#include <rez/io.h>
+#include <rez/resource.h>
+#include <cstddef>
+#include <cstdint>
+
+namespace rez
+{
+
+class file
+{
+public:
+ file(const rez::resource &resource, rez::io &io);
+ std::string path() const;
+ int read(void *b, size_t n);
+ int read(unsigned char &b);
+ int read(std::string &s, size_t len);
+ int read_le(uint16_t &w);
+ int read_le(uint32_t &w);
+ long tell();
+ int seek(long offset);
+ int eof() const;
+
+private:
+ const rez::resource &resource;
+ long offset;
+ rez::io &io;
+};
+
+}
+
+#endif
diff --git a/src/rez/rez/io.h b/src/rez/rez/io.h
new file mode 100644
index 0000000..238a925
--- /dev/null
+++ b/src/rez/rez/io.h
@@ -0,0 +1,34 @@
+#ifndef REZ_IO_H
+#define REZ_IO_H
+
+#include <cstddef>
+#include <cstdint>
+#include <fstream>
+#include <string>
+
+namespace rez
+{
+
+class io
+{
+public:
+ io(const char *path);
+ int open();
+ int read(unsigned char &b);
+ int read(std::string &s, unsigned maxlen);
+ int read(void *b, size_t n);
+ int check(char byte);
+ int check(const char *s);
+ int read_le(uint32_t &w);
+ long tell();
+ int seek(long offset);
+ const std::string &path() const;
+
+private:
+ std::ifstream f;
+ const std::string m_path;
+};
+
+}
+
+#endif
diff --git a/src/rez/rez/resource.h b/src/rez/rez/resource.h
new file mode 100644
index 0000000..4daafa0
--- /dev/null
+++ b/src/rez/rez/resource.h
@@ -0,0 +1,30 @@
+#ifndef REZ_RESOURCE_H
+#define REZ_RESOURCE_H
+
+#include <rez/entry.h>
+#include <rez/io.h>
+#include <cstdint>
+#include <string>
+#include <vector>
+
+namespace rez
+{
+
+class resource
+{
+public:
+ int read(const struct entry::cfg &cfg, rez::io &io);
+ const std::string &description() const;
+ std::string name() const;
+ rez::entry entry;
+
+private:
+ uint32_t id;
+ char type[sizeof "xyz"];
+ std::string m_name, descr;
+ std::vector<uint32_t> keys;
+};
+
+}
+
+#endif