aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-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
21 files changed, 2233 insertions, 0 deletions
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