-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmodule.cpp
More file actions
290 lines (257 loc) · 12.2 KB
/
module.cpp
File metadata and controls
290 lines (257 loc) · 12.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 Fernando Sahmkow
//
// Top-level pybind11 module entry. The autogenerated *_bindings.cpp files
// expose `void bind_<format>(py::module_& m)`; we set up one submodule per
// format and dispatch to each.
//
// Hand-written supplementary bindings (texture parsers, model parsers,
// in-memory file system) live in this file too.
#include <pybind11/pybind11.h>
#include <span>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#include <whiteout/common_types.h>
// VectorU8 and VectorString are shared across every submodule — bind them
// once at the root so every TU's PYBIND11_MAKE_OPAQUE sees the same
// registered class. Must precede <pybind11/stl.h>.
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::u8>);
PYBIND11_MAKE_OPAQUE(std::vector<std::string>);
#include <pybind11/stl.h>
#include <pybind11/stl_bind.h>
#include <pybind11/functional.h>
#include <whiteout/textures/texture.h>
#include <whiteout/models/mdx/parser.h>
#include <whiteout/models/mdx/writer.h>
#include <whiteout/models/m3/parser.h>
#include <whiteout/models/m3/writer.h>
#include <whiteout/models/m2/parser.h>
#include <whiteout/interfaces.h>
namespace py = pybind11;
using namespace whiteout;
// Forward declarations for the per-format generated binding entry points.
void bind_mdx (py::module_& m);
void bind_m2 (py::module_& m);
void bind_m3 (py::module_& m);
void bind_textures(py::module_& m); // texture parsers/writers + PixelFormat
void bind_utils (py::module_& m); // VertexBuffer + VertexBufferBuilder
void bind_host (py::module_& m); // OsFileSystem, SimpleThreadPool,
// HttpHandler, SimpleHttpHandler
#if defined(WHITEOUT_HAS_MPQ)
void bind_mpq (py::module_& m);
#endif
#if defined(WHITEOUT_HAS_CASC)
void bind_casc (py::module_& m);
#endif
namespace {
// ── Tiny in-memory VirtualPathFileSystem for M2 sibling files ────────────
class InMemoryFs : public interfaces::VirtualPathFileSystem {
public:
void addFile(const std::string& path, py::bytes data) {
std::string s = data; // py::bytes -> std::string via operator
m_files[path] = std::vector<u8>(s.begin(), s.end());
}
std::vector<u8> readFile(const std::string& path) const override {
auto it = m_files.find(path);
return it == m_files.end() ? std::vector<u8>{} : it->second;
}
bool writeFile(const std::string& path, const std::vector<u8>& data) override {
m_files[path] = data;
return true;
}
bool fileExists(const std::string& path) const override {
return m_files.count(path) > 0;
}
std::vector<interfaces::DirectoryEntry> listDirectory(const std::string&) const override {
return {}; // unused by the parsers
}
private:
std::unordered_map<std::string, std::vector<u8>> m_files;
};
py::bytes vecToPyBytes(const std::vector<u8>& v) {
return py::bytes(reinterpret_cast<const char*>(v.data()), v.size());
}
// ── HttpHandler trampoline — lets Python users subclass HttpHandler ──────
// The codegen skips HttpHandler (see modules/host.py::skip_class_js_names)
// so we can attach this trampoline here. SimpleHttpHandler (codegen'd in
// host_bindings.cpp) declares HttpHandler as its base and binds correctly
// because pybind11 looks up the runtime registration done in
// bind_supplementary, not the template parameter alone.
class PyHttpHandler : public interfaces::HttpHandler {
public:
using HttpHandler::HttpHandler;
u32 capabilities() const noexcept override {
try {
PYBIND11_OVERRIDE_NAME(u32, interfaces::HttpHandler,
"capabilities", capabilities);
} catch (...) {
return interfaces::HttpCapability::None;
}
}
void getAsync(const std::string& url,
interfaces::HttpCallback callback) override {
PYBIND11_OVERRIDE_PURE_NAME(void, interfaces::HttpHandler,
"get_async", getAsync,
url, std::move(callback));
}
void getRangeAsync(const std::string& url, u64 start, u64 end,
interfaces::HttpCallback callback) override {
PYBIND11_OVERRIDE_PURE_NAME(void, interfaces::HttpHandler,
"get_range_async", getRangeAsync,
url, start, end, std::move(callback));
}
};
} // namespace
void bind_supplementary(py::module_& m) {
// Texture, PixelFormat and every per-format Parser/Writer are bound
// by the auto-generated textures_bindings.cpp (called via bind_textures
// in this same module).
// ── HttpResponse (bytes-friendly accessor + kwargs ctor) ────────────
// Skipped by the codegen (modules/host.py::skip_class_js_names)
// because its `body` is `std::vector<u8>` which we want to surface
// as py::bytes for Python ergonomics — the codegen's stock
// def_readwrite would expose VectorU8 instead.
py::class_<interfaces::HttpResponse>(m, "HttpResponse",
"HTTP response payload returned by an HttpHandler callback.")
.def(py::init<>())
.def(py::init([](i32 status, py::bytes body, std::string error) {
interfaces::HttpResponse r;
r.statusCode = status;
std::string s = body;
r.body = std::vector<u8>(s.begin(), s.end());
r.error = std::move(error);
return r;
}),
py::arg("status_code") = 0,
py::arg("body") = py::bytes(),
py::arg("error") = std::string{})
.def_readwrite("status_code", &interfaces::HttpResponse::statusCode)
.def_property(
"body",
[](const interfaces::HttpResponse& r) {
return py::bytes(reinterpret_cast<const char*>(r.body.data()),
r.body.size());
},
[](interfaces::HttpResponse& r, py::bytes data) {
std::string s = data;
r.body = std::vector<u8>(s.begin(), s.end());
})
.def_readwrite("error", &interfaces::HttpResponse::error);
// ── VirtualPathFileSystem base + InMemoryFs subclass ────────────────
// Registered here so InMemoryFs can declare it as parent; the codegen
// (host_bindings.cpp) skips this class via skip_class_js_names so we
// own its registration. OsFileSystem (codegen'd) inherits from the
// same C++ type — pybind11 looks up the runtime registration so the
// ownership boundary doesn't matter to consumers.
py::class_<interfaces::VirtualPathFileSystem>(m, "VirtualPathFileSystem");
py::class_<InMemoryFs, interfaces::VirtualPathFileSystem>(m, "InMemoryFileSystem")
.def(py::init<>())
.def("add_file", &InMemoryFs::addFile)
.def("file_exists", &InMemoryFs::fileExists);
// ── HttpHandler base + Python trampoline ────────────────────────────
// PyHttpHandler lets Python users subclass HttpHandler and override
// get_async / get_range_async / capabilities — required for plugging
// a Python HTTP client into online CASC. The codegen skips this class
// (modules/host.py::skip_class_js_names) so we can attach the
// trampoline here; SimpleHttpHandler (codegen'd) inherits from the
// C++ HttpHandler type and finds this registration at runtime.
py::class_<interfaces::HttpHandler, PyHttpHandler>(m, "HttpHandler",
"Abstract HTTP handler. Subclass and implement `get_async` (and "
"optionally `get_range_async` / `capabilities`) to plug a Python "
"HTTP client into CASC online storage.")
.def(py::init<>())
.def("capabilities", &interfaces::HttpHandler::capabilities)
.def("get_async",
[](interfaces::HttpHandler& self, const std::string& url,
interfaces::HttpCallback cb) {
self.getAsync(url, std::move(cb));
},
py::arg("url"), py::arg("complete"))
.def("get_range_async",
[](interfaces::HttpHandler& self, const std::string& url,
u64 start, u64 end, interfaces::HttpCallback cb) {
self.getRangeAsync(url, start, end, std::move(cb));
},
py::arg("url"), py::arg("start"), py::arg("end"),
py::arg("complete"));
// Capability flag constants on the module (mirrors the C++ namespace).
m.attr("HTTP_CAPABILITY_NONE") = interfaces::HttpCapability::None;
m.attr("HTTP_CAPABILITY_HTTP2_MULTIPLEXING") =
interfaces::HttpCapability::Http2Multiplexing;
}
// ── Format-specific parser/writer helpers (live alongside the generated
// type-tree bindings — pure convenience surface for Python users). ────
namespace mdx_extra {
mdx::Model parse_bytes(mdx::Parser& self, py::bytes data) {
std::string s = data;
return self.parse(std::span<const u8>(reinterpret_cast<const u8*>(s.data()), s.size()),
mdx::MDLXFormat::MDX);
}
py::bytes write_bytes(mdx::Writer& self, const mdx::Model& m) {
auto v = self.write(m, mdx::MDLXFormat::MDX, mdx::MdlFormat::WarcraftIII);
return vecToPyBytes(v);
}
} // namespace mdx_extra
namespace m3_extra {
m3::Model parse_bytes(m3::Parser& self, py::bytes data) {
std::string s = data;
return self.parse(std::span<const u8>(reinterpret_cast<const u8*>(s.data()), s.size()));
}
py::bytes write_bytes(m3::Writer& self, const m3::Model& mod) {
return vecToPyBytes(self.write(mod));
}
} // namespace m3_extra
namespace m2_extra {
m2::Model parse_bytes(m2::Parser& self, interfaces::VirtualPathFileSystem& fs,
const std::string& main_path) {
return self.parse(fs, main_path);
}
} // namespace m2_extra
PYBIND11_MODULE(whiteout, root) {
root.doc() = "Native bindings for whiteout_lib (model & texture parsers)";
// Shared opaque vector containers. Bound once at root so all submodules
// see the same registered type (mirrors WASM bindings.cpp::register_vector).
// Buffer protocol on VectorU8 enables zero-copy `np.asarray(vec)` access.
py::bind_vector<std::vector<u8>>(root, "VectorU8", py::buffer_protocol());
py::bind_vector<std::vector<std::string>>(root, "VectorString");
bind_supplementary(root);
// Host extras (disk I/O, threading, HTTP). Must run AFTER
// bind_supplementary because OsFileSystem declares VirtualPathFileSystem
// as its base, and that base is registered there.
bind_host(root);
// Texture parsers/writers + PixelFormat live at root for backwards
// compatibility with the original hand-written API.
bind_textures(root);
auto mdx_m = root.def_submodule("mdx", "Warcraft III MDX/MDL");
bind_mdx(mdx_m);
// Shared math types live in the first format module that binds them
// (mdx). Re-export at the root so callers can write w.Vector3f rather
// than w.mdx.Vector3f.
for (auto name : {"Vector2f", "Vector3f", "Vector4f", "Quaternion"}) {
root.attr(name) = mdx_m.attr(name);
}
// mdx::Parser / Writer / ParseMode / MDLXFormat / MdlFormat /
// UpgradeMode are now fully bound by the codegen (bind_mdx). The
// span<u8> ↔ py::bytes round-trip is handled by the codegen's
// slow-path lambda emission, so the previous mdx_extra helpers
// here became redundant.
auto m3_m = root.def_submodule("m3", "StarCraft II / Heroes of the Storm M3");
bind_m3(m3_m);
auto utils_m = root.def_submodule("utils", "Vertex-buffer building + other helpers.");
bind_utils(utils_m);
#if defined(WHITEOUT_HAS_MPQ)
auto mpq_m = root.def_submodule("mpq", "MPQ archives (Warcraft III, classic WoW, etc).");
bind_mpq(mpq_m);
#endif
#if defined(WHITEOUT_HAS_CASC)
auto casc_m = root.def_submodule("casc", "CASC archives (modern Blizzard games).");
bind_casc(casc_m);
#endif
auto m2_m = root.def_submodule("m2", "World of Warcraft M2");
bind_m2(m2_m);
// m2::Parser is fully bound by bind_m2 (takes VirtualPathFileSystem&
// / CascFileSystem& which the codegen now handles).
}