mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
feat: UGC server that makes and serves player models' meshes and icons
A new server (dUgcServer, started by master with enable_ugc_server=1) that takes unprocessed ugc and ugc_modular_build rows from the database and makes what the client downloads with UGCUSE3DSERVICES: an optimized NIF (hidden faces removed, ambient occlusion baked into vertex colors) and a 128px DDS icon for player models, rendered by a software rasterizer from the client's LDD brick primitives, and icons for cars and rockets assembled from their modules per ModularBuildComponent/ModuleComponent. It serves them, with the models' LXFML, over HTTP in the client's UGCC<dc>/3DOPTIMIZED and IMAGE128DDS layout with .gz and .checksum files, and keeps its folder under a size cap. Processing state lives in ugc.is_optimized plus new processed_at, process_attempts and process_error columns (and the same on ugc_modular_build). ServiceType::UGC is appended. NifFile moves to dCommon and records named node transforms for the modules' attach points. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,7 @@ set(DCOMMON_SOURCES
|
||||
"Raw.cpp"
|
||||
"Process.cpp"
|
||||
"Permissions.cpp"
|
||||
"NifFile.cpp"
|
||||
)
|
||||
|
||||
# Workaround for compiler bug where the optimized code could result in a memcpy of 0 bytes, even though that isnt possible.
|
||||
|
||||
732
dCommon/NifFile.cpp
Normal file
732
dCommon/NifFile.cpp
Normal file
@@ -0,0 +1,732 @@
|
||||
#include "NifFile.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <set>
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
namespace {
|
||||
constexpr uint32_t Version(uint32_t a, uint32_t b, uint32_t c, uint32_t d) { return (a << 24) | (b << 16) | (c << 8) | d; }
|
||||
constexpr uint32_t MIN_VERSION = Version(20, 2, 0, 5); // strings in a table and block sizes in the header
|
||||
constexpr uint32_t MAX_VERSION = Version(20, 3, 0, 9);
|
||||
constexpr uint32_t MAX_BLOCKS = 200000;
|
||||
constexpr uint32_t MAX_DEPTH = 64;
|
||||
constexpr uint16_t APP_CULLED = 1; // NiAVObject flag: hidden
|
||||
|
||||
// Little-endian reads that fail (and stay failed) instead of running past the end
|
||||
class Reader {
|
||||
public:
|
||||
explicit Reader(std::string_view data) : m_Data(data) {}
|
||||
|
||||
template<typename T>
|
||||
T Read() {
|
||||
T value{};
|
||||
if (!m_Ok || sizeof(T) > m_Data.size() - m_Pos) {
|
||||
m_Ok = false;
|
||||
return value;
|
||||
}
|
||||
std::memcpy(&value, m_Data.data() + m_Pos, sizeof(T));
|
||||
m_Pos += sizeof(T);
|
||||
return value;
|
||||
}
|
||||
|
||||
float Float() { return Read<float>(); }
|
||||
uint8_t U8() { return Read<uint8_t>(); }
|
||||
uint16_t U16() { return Read<uint16_t>(); }
|
||||
uint32_t U32() { return Read<uint32_t>(); }
|
||||
int32_t I32() { return Read<int32_t>(); }
|
||||
|
||||
void Skip(uint64_t bytes) {
|
||||
if (!m_Ok || bytes > m_Data.size() - m_Pos) {
|
||||
m_Ok = false;
|
||||
return;
|
||||
}
|
||||
m_Pos += static_cast<size_t>(bytes);
|
||||
}
|
||||
|
||||
// `count` values of T, or empty (and failed) if there aren't that many
|
||||
template<typename T>
|
||||
std::vector<T> Array(uint64_t count) {
|
||||
std::vector<T> values;
|
||||
if (!m_Ok || count > (m_Data.size() - m_Pos) / sizeof(T)) {
|
||||
m_Ok = false;
|
||||
return values;
|
||||
}
|
||||
values.resize(static_cast<size_t>(count));
|
||||
std::memcpy(values.data(), m_Data.data() + m_Pos, static_cast<size_t>(count) * sizeof(T));
|
||||
m_Pos += static_cast<size_t>(count) * sizeof(T);
|
||||
return values;
|
||||
}
|
||||
|
||||
std::string SizedString() {
|
||||
const auto length = U32();
|
||||
if (!m_Ok || length > m_Data.size() - m_Pos) {
|
||||
m_Ok = false;
|
||||
return {};
|
||||
}
|
||||
std::string value(m_Data.substr(m_Pos, length));
|
||||
m_Pos += length;
|
||||
return value;
|
||||
}
|
||||
|
||||
bool Ok() const { return m_Ok; }
|
||||
size_t Position() const { return m_Pos; }
|
||||
|
||||
private:
|
||||
std::string_view m_Data;
|
||||
size_t m_Pos = 0;
|
||||
bool m_Ok = true;
|
||||
};
|
||||
|
||||
// Rotation (row-major, for column vectors), translation and uniform scale: p' = t + s * R p
|
||||
struct Transform {
|
||||
std::array<float, 9> r{ 1, 0, 0, 0, 1, 0, 0, 0, 1 };
|
||||
std::array<float, 3> t{};
|
||||
float s{ 1.0f };
|
||||
|
||||
std::array<float, 3> Rotate(const std::array<float, 3>& v) const {
|
||||
return { r[0] * v[0] + r[1] * v[1] + r[2] * v[2], r[3] * v[0] + r[4] * v[1] + r[5] * v[2], r[6] * v[0] + r[7] * v[1] + r[8] * v[2] };
|
||||
}
|
||||
|
||||
std::array<float, 3> Apply(const std::array<float, 3>& v) const {
|
||||
const auto rotated = Rotate(v);
|
||||
return { t[0] + s * rotated[0], t[1] + s * rotated[1], t[2] + s * rotated[2] };
|
||||
}
|
||||
|
||||
// This transform, then `local` inside it (parent * child)
|
||||
Transform Then(const Transform& local) const {
|
||||
Transform out;
|
||||
for (int row = 0; row < 3; row++) {
|
||||
for (int col = 0; col < 3; col++) {
|
||||
out.r[row * 3 + col] = r[row * 3] * local.r[col] + r[row * 3 + 1] * local.r[3 + col] + r[row * 3 + 2] * local.r[6 + col];
|
||||
}
|
||||
}
|
||||
out.t = Apply(local.t);
|
||||
out.s = s * local.s;
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
// The properties in effect at a point of the tree: a child's own property of a type replaces its parent's
|
||||
struct Properties {
|
||||
int32_t material = -1;
|
||||
int32_t alpha = -1;
|
||||
int32_t texturing = -1;
|
||||
int32_t vertexColor = -1;
|
||||
int32_t stencil = -1;
|
||||
int32_t shaderTag = -1; // the nearest multishader tag ("S05__...") on the way down the tree
|
||||
};
|
||||
|
||||
struct NetHeader {
|
||||
std::string name;
|
||||
};
|
||||
|
||||
struct AvHeader {
|
||||
std::string name;
|
||||
uint16_t flags{};
|
||||
Transform transform;
|
||||
std::vector<int32_t> properties;
|
||||
};
|
||||
|
||||
class Parser {
|
||||
public:
|
||||
Parser(std::string_view data, uint32_t lod) : m_Data(data), m_Lod(lod) {}
|
||||
|
||||
std::optional<NifFile::Model> Run(std::string& error) {
|
||||
if (!ReadHeader(error)) return std::nullopt;
|
||||
m_Model.version = m_Version;
|
||||
|
||||
// The footer lists the roots; the first block is the root when it can't be read
|
||||
std::vector<int32_t> roots;
|
||||
Reader footer(m_Data.substr(m_FooterStart));
|
||||
const auto count = footer.U32();
|
||||
if (footer.Ok() && count <= m_Blocks.size()) roots = footer.Array<int32_t>(count);
|
||||
if (roots.empty() && !m_Blocks.empty()) roots.push_back(0);
|
||||
|
||||
for (const auto root : roots) Visit(root, Transform{}, Properties{}, 0);
|
||||
|
||||
for (size_t i = 0; i < m_Blocks.size(); i++) {
|
||||
if (!m_Used.contains(static_cast<int32_t>(i))) {
|
||||
const auto& type = m_Types[m_Blocks[i].type];
|
||||
if (!IsDrawnType(type)) m_Model.skipped[type]++;
|
||||
}
|
||||
}
|
||||
|
||||
bool first = true;
|
||||
for (const auto& mesh : m_Model.meshes) {
|
||||
for (size_t i = 0; i + 2 < mesh.positions.size(); i += 3) {
|
||||
for (int axis = 0; axis < 3; axis++) {
|
||||
const auto value = mesh.positions[i + axis];
|
||||
m_Model.min[axis] = first ? value : std::min(m_Model.min[axis], value);
|
||||
m_Model.max[axis] = first ? value : std::max(m_Model.max[axis], value);
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
return std::move(m_Model);
|
||||
}
|
||||
|
||||
// The DDS file for pixel data block `index` (see NifFile::EmbeddedTexture)
|
||||
std::optional<std::string> Dds(int32_t index, std::string& error) {
|
||||
if (!ReadHeader(error)) return std::nullopt;
|
||||
const auto* type = TypeOf(index);
|
||||
if (!type || (*type != "NiPixelData" && *type != "NiPersistentSrcTextureRendererData")) return std::nullopt;
|
||||
auto reader = BlockReader(index);
|
||||
const auto format = reader.U32();
|
||||
reader.U8(); // bits per pixel
|
||||
reader.U32(); // renderer hint
|
||||
reader.U32(); // extra data
|
||||
reader.U8(); // flags
|
||||
const auto tiling = reader.U32();
|
||||
if (m_Version >= Version(20, 3, 0, 4)) reader.U8(); // sRGB
|
||||
reader.Skip(4 * 10); // channels
|
||||
reader.I32(); // palette
|
||||
const auto mipCount = reader.U32();
|
||||
const auto bytesPerPixel = reader.U32();
|
||||
if (!reader.Ok() || mipCount == 0 || mipCount > 16 || tiling != 0) return std::nullopt;
|
||||
std::vector<std::array<uint32_t, 3>> mips; // width, height, offset
|
||||
for (uint32_t i = 0; i < mipCount; i++) mips.push_back({ reader.U32(), reader.U32(), reader.U32() });
|
||||
const auto pixelCount = reader.U32();
|
||||
if (*type == "NiPersistentSrcTextureRendererData") {
|
||||
reader.U32(); // padded pixel count
|
||||
reader.U32(); // faces
|
||||
reader.U32(); // platform
|
||||
} else {
|
||||
reader.U32(); // faces
|
||||
}
|
||||
std::string_view pixels;
|
||||
if (reader.Ok() && pixelCount <= m_Blocks[index].size - reader.Position()) pixels = m_Data.substr(m_Blocks[index].offset + reader.Position(), pixelCount);
|
||||
const auto width = mips[0][0], height = mips[0][1];
|
||||
if (pixels.empty() || width == 0 || height == 0 || width > 8192 || height > 8192 || mips[0][2] != 0) return std::nullopt;
|
||||
|
||||
// DDS header (Microsoft's DDS_HEADER and DDS_PIXELFORMAT)
|
||||
std::array<uint32_t, 31> header{};
|
||||
header[0] = 124;
|
||||
header[1] = 0x1 | 0x2 | 0x4 | 0x1000 | 0x20000; // caps, height, width, pixel format, mipmap count
|
||||
header[2] = height;
|
||||
header[3] = width;
|
||||
header[6] = mipCount;
|
||||
header[18] = 32; // pixel format size
|
||||
if (format >= 4 && format <= 6) {
|
||||
header[19] = 0x4; // four CC
|
||||
const char* fourCc = format == 4 ? "DXT1" : format == 5 ? "DXT3" : "DXT5";
|
||||
std::memcpy(&header[20], fourCc, 4);
|
||||
} else if ((format == 0 && bytesPerPixel == 3) || (format == 1 && bytesPerPixel == 4)) {
|
||||
header[19] = format == 1 ? 0x41 : 0x40; // RGB, with alpha
|
||||
header[21] = bytesPerPixel * 8;
|
||||
header[22] = 0x000000FF; // red first in memory
|
||||
header[23] = 0x0000FF00;
|
||||
header[24] = 0x00FF0000;
|
||||
header[25] = format == 1 ? 0xFF000000 : 0;
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
header[26] = 0x1000 | (mipCount > 1 ? 0x400008 : 0); // texture, mipmaps
|
||||
std::string out = "DDS ";
|
||||
out.append(reinterpret_cast<const char*>(header.data()), header.size() * 4);
|
||||
out.append(pixels);
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
struct Block {
|
||||
uint16_t type{};
|
||||
size_t offset{};
|
||||
uint32_t size{};
|
||||
};
|
||||
|
||||
std::string_view m_Data;
|
||||
uint32_t m_Lod;
|
||||
uint32_t m_Version{};
|
||||
std::vector<std::string> m_Types;
|
||||
std::vector<std::string> m_Strings;
|
||||
std::vector<Block> m_Blocks;
|
||||
size_t m_FooterStart{};
|
||||
std::set<int32_t> m_Used;
|
||||
std::set<int32_t> m_Visiting;
|
||||
NifFile::Model m_Model;
|
||||
|
||||
static bool IsDrawnType(const std::string& type) {
|
||||
return type == "NiNode" || type == "NiLODNode" || type == "NiBillboardNode" || type == "NiSwitchNode" ||
|
||||
type == "NiTriShape" || type == "NiTriStrips" || type == "NiTriShapeData" || type == "NiTriStripsData" ||
|
||||
type == "NiMaterialProperty" || type == "NiAlphaProperty" || type == "NiTexturingProperty" || type == "NiSourceTexture" ||
|
||||
type == "NiVertexColorProperty" || type == "NiStencilProperty" || type == "NiRangeLODData" ||
|
||||
// Read and ignored: they change nothing a still picture shows
|
||||
type == "NiSpecularProperty" || type == "NiZBufferProperty" || type == "NiShadeProperty" || type == "NiStringExtraData";
|
||||
}
|
||||
|
||||
bool ReadHeader(std::string& error) {
|
||||
const auto newline = m_Data.substr(0, 128).find('\n');
|
||||
if (newline == std::string_view::npos || !(m_Data.starts_with("Gamebryo File Format") || m_Data.starts_with("NetImmerse File Format"))) {
|
||||
error = "not a Gamebryo file";
|
||||
return false;
|
||||
}
|
||||
Reader header(m_Data.substr(newline + 1));
|
||||
m_Version = header.U32();
|
||||
if (m_Version < MIN_VERSION || m_Version > MAX_VERSION) {
|
||||
error = "unsupported version";
|
||||
return false;
|
||||
}
|
||||
const auto endian = header.U8();
|
||||
const auto userVersion = header.U32();
|
||||
const auto blockCount = header.U32();
|
||||
if (!header.Ok() || endian != 1 || userVersion != 0 || blockCount > MAX_BLOCKS) {
|
||||
error = "unsupported header (big-endian or another game's user version)";
|
||||
return false;
|
||||
}
|
||||
const auto typeCount = header.U16();
|
||||
for (uint32_t i = 0; i < typeCount && header.Ok(); i++) m_Types.push_back(header.SizedString());
|
||||
const auto typeIndex = header.Array<uint16_t>(blockCount);
|
||||
const auto sizes = header.Array<uint32_t>(blockCount);
|
||||
const auto stringCount = header.U32();
|
||||
header.U32(); // longest string
|
||||
for (uint32_t i = 0; i < stringCount && header.Ok(); i++) m_Strings.push_back(header.SizedString());
|
||||
const auto groupCount = header.U32();
|
||||
header.Skip(static_cast<uint64_t>(groupCount) * 4);
|
||||
if (!header.Ok()) {
|
||||
error = "truncated header";
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t offset = newline + 1 + header.Position();
|
||||
m_Blocks.reserve(blockCount);
|
||||
for (uint32_t i = 0; i < blockCount; i++) {
|
||||
const uint16_t type = typeIndex[i] & 0x7FFF; // the high bit marks PhysX blocks
|
||||
if (type >= m_Types.size() || sizes[i] > m_Data.size() - offset) {
|
||||
error = "block " + std::to_string(i) + " is out of range";
|
||||
return false;
|
||||
}
|
||||
m_Blocks.push_back({ type, offset, sizes[i] });
|
||||
offset += sizes[i];
|
||||
}
|
||||
m_FooterStart = offset;
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::string* TypeOf(int32_t index) const {
|
||||
if (index < 0 || static_cast<size_t>(index) >= m_Blocks.size()) return nullptr;
|
||||
return &m_Types[m_Blocks[index].type];
|
||||
}
|
||||
|
||||
Reader BlockReader(int32_t index) const {
|
||||
const auto& block = m_Blocks[index];
|
||||
return Reader(m_Data.substr(block.offset, block.size));
|
||||
}
|
||||
|
||||
std::string String(uint32_t index) const {
|
||||
return index < m_Strings.size() ? m_Strings[index] : std::string{};
|
||||
}
|
||||
|
||||
NetHeader ReadNet(Reader& reader) {
|
||||
NetHeader net;
|
||||
net.name = String(reader.U32());
|
||||
const auto extra = reader.U32();
|
||||
reader.Skip(static_cast<uint64_t>(extra) * 4);
|
||||
reader.I32(); // controller
|
||||
return net;
|
||||
}
|
||||
|
||||
AvHeader ReadAv(Reader& reader) {
|
||||
AvHeader av;
|
||||
av.name = ReadNet(reader).name;
|
||||
av.flags = reader.U16();
|
||||
for (auto& value : av.transform.t) value = reader.Float();
|
||||
// Matrix33 is stored m11, m21, m31, m12, ... (nif.xml): column by column
|
||||
std::array<float, 9> stored{};
|
||||
for (auto& value : stored) value = reader.Float();
|
||||
for (int row = 0; row < 3; row++) {
|
||||
for (int col = 0; col < 3; col++) av.transform.r[row * 3 + col] = stored[col * 3 + row];
|
||||
}
|
||||
av.transform.s = reader.Float();
|
||||
const auto count = reader.U32();
|
||||
av.properties = reader.Array<int32_t>(count);
|
||||
reader.I32(); // collision object
|
||||
return av;
|
||||
}
|
||||
|
||||
Properties Inherit(Properties properties, const std::vector<int32_t>& own) {
|
||||
for (const auto ref : own) {
|
||||
const auto* type = TypeOf(ref);
|
||||
if (!type) continue;
|
||||
if (*type == "NiMaterialProperty") properties.material = ref;
|
||||
else if (*type == "NiAlphaProperty") properties.alpha = ref;
|
||||
else if (*type == "NiTexturingProperty") properties.texturing = ref;
|
||||
else if (*type == "NiVertexColorProperty") properties.vertexColor = ref;
|
||||
else if (*type == "NiStencilProperty") properties.stencil = ref;
|
||||
m_Used.insert(ref);
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
|
||||
void Visit(int32_t index, const Transform& parent, Properties properties, uint32_t depth) {
|
||||
const auto* type = TypeOf(index);
|
||||
if (!type || depth > MAX_DEPTH || m_Visiting.contains(index)) return;
|
||||
m_Visiting.insert(index);
|
||||
if (*type == "NiNode" || *type == "NiLODNode" || *type == "NiBillboardNode" || *type == "NiSwitchNode") {
|
||||
m_Used.insert(index);
|
||||
VisitNode(index, *type, parent, properties, depth);
|
||||
} else if (*type == "NiTriShape" || *type == "NiTriStrips") {
|
||||
m_Used.insert(index);
|
||||
VisitGeometry(index, parent, properties);
|
||||
}
|
||||
m_Visiting.erase(index);
|
||||
}
|
||||
|
||||
void VisitNode(int32_t index, const std::string& type, const Transform& parent, Properties properties, uint32_t depth) {
|
||||
auto reader = BlockReader(index);
|
||||
const auto av = ReadAv(reader);
|
||||
const auto childCount = reader.U32();
|
||||
const auto children = reader.Array<int32_t>(childCount);
|
||||
const auto effectCount = reader.U32();
|
||||
reader.Skip(static_cast<uint64_t>(effectCount) * 4);
|
||||
if (!reader.Ok()) return;
|
||||
|
||||
const auto world = parent.Then(av.transform);
|
||||
// Recorded even when hidden: attach points often are
|
||||
if (!av.name.empty() && !m_Model.nodes.contains(av.name)) {
|
||||
NifFile::NodeTransform node;
|
||||
for (int i = 0; i < 9; i++) node.rotation[i] = world.r[i] * world.s;
|
||||
node.translation = world.t;
|
||||
m_Model.nodes.emplace(av.name, node);
|
||||
}
|
||||
if (av.flags & APP_CULLED) return;
|
||||
properties = Inherit(properties, av.properties);
|
||||
if (const auto tag = NifFile::ShaderTag(av.name); tag >= 0) properties.shaderTag = tag;
|
||||
|
||||
std::vector<int32_t> drawn = children;
|
||||
if (type == "NiSwitchNode" || type == "NiLODNode") {
|
||||
reader.U16(); // switch flags
|
||||
const auto active = reader.U32();
|
||||
drawn.clear();
|
||||
if (type == "NiSwitchNode") {
|
||||
if (reader.Ok() && active < children.size()) drawn.push_back(children[active]);
|
||||
} else if (const auto chosen = ChooseLod(reader.I32(), children)) {
|
||||
drawn.push_back(*chosen);
|
||||
}
|
||||
}
|
||||
for (const auto child : drawn) Visit(child, world, properties, depth + 1);
|
||||
}
|
||||
|
||||
// The child of an NiLODNode for m_Lod: children ordered nearest range first (the most detailed)
|
||||
std::optional<int32_t> ChooseLod(int32_t dataRef, const std::vector<int32_t>& children) {
|
||||
if (children.empty()) return std::nullopt;
|
||||
std::vector<std::pair<float, int32_t>> order;
|
||||
const auto* dataType = TypeOf(dataRef);
|
||||
if (dataType && *dataType == "NiRangeLODData") {
|
||||
m_Used.insert(dataRef);
|
||||
auto data = BlockReader(dataRef);
|
||||
data.Skip(12); // center
|
||||
const auto levels = data.U32();
|
||||
for (uint32_t i = 0; i < levels && data.Ok() && i < children.size(); i++) {
|
||||
const auto nearExtent = data.Float();
|
||||
data.Float(); // far extent
|
||||
if (data.Ok()) order.emplace_back(nearExtent, children[i]);
|
||||
}
|
||||
}
|
||||
if (order.size() != children.size()) {
|
||||
order.clear();
|
||||
for (size_t i = 0; i < children.size(); i++) order.emplace_back(static_cast<float>(i), children[i]);
|
||||
}
|
||||
std::stable_sort(order.begin(), order.end(), [](const auto& a, const auto& b) { return a.first < b.first; });
|
||||
return order[std::min<size_t>(m_Lod, order.size() - 1)].second;
|
||||
}
|
||||
|
||||
void VisitGeometry(int32_t index, const Transform& parent, Properties properties) {
|
||||
auto reader = BlockReader(index);
|
||||
const auto av = ReadAv(reader);
|
||||
const auto dataRef = reader.I32();
|
||||
const auto skin = reader.I32();
|
||||
if (!reader.Ok() || (av.flags & APP_CULLED)) return;
|
||||
properties = Inherit(properties, av.properties);
|
||||
if (const auto tag = NifFile::ShaderTag(av.name); tag >= 0) properties.shaderTag = tag;
|
||||
const auto* dataType = TypeOf(dataRef);
|
||||
if (!dataType || (*dataType != "NiTriShapeData" && *dataType != "NiTriStripsData")) return;
|
||||
m_Used.insert(dataRef);
|
||||
|
||||
NifFile::Mesh mesh;
|
||||
if (!ReadGeometryData(dataRef, *dataType == "NiTriStripsData", parent.Then(av.transform), mesh) || mesh.indices.empty()) return;
|
||||
mesh.material = ReadMaterial(properties);
|
||||
mesh.material.shaderTag = properties.shaderTag;
|
||||
if (skin >= 0) {
|
||||
m_Model.skinned++;
|
||||
m_Used.insert(skin);
|
||||
}
|
||||
m_Model.meshes.push_back(std::move(mesh));
|
||||
}
|
||||
|
||||
bool ReadGeometryData(int32_t index, bool strips, const Transform& transform, NifFile::Mesh& mesh) {
|
||||
auto reader = BlockReader(index);
|
||||
reader.I32(); // group ID
|
||||
const auto count = reader.U16();
|
||||
reader.U8(); // keep flags
|
||||
reader.U8(); // compress flags
|
||||
if (reader.U8()) {
|
||||
const auto vertices = reader.Array<float>(static_cast<uint64_t>(count) * 3);
|
||||
mesh.positions.reserve(vertices.size());
|
||||
for (size_t i = 0; i + 2 < vertices.size(); i += 3) {
|
||||
const auto p = transform.Apply({ vertices[i], vertices[i + 1], vertices[i + 2] });
|
||||
mesh.positions.insert(mesh.positions.end(), p.begin(), p.end());
|
||||
}
|
||||
}
|
||||
const auto dataFlags = reader.U16();
|
||||
if (reader.U8()) {
|
||||
const auto normals = reader.Array<float>(static_cast<uint64_t>(count) * 3);
|
||||
mesh.normals.reserve(normals.size());
|
||||
for (size_t i = 0; i + 2 < normals.size(); i += 3) {
|
||||
auto n = transform.Rotate({ normals[i], normals[i + 1], normals[i + 2] });
|
||||
const auto length = std::sqrt(n[0] * n[0] + n[1] * n[1] + n[2] * n[2]);
|
||||
if (length > 0.0f) for (auto& value : n) value /= length;
|
||||
mesh.normals.insert(mesh.normals.end(), n.begin(), n.end());
|
||||
}
|
||||
if (dataFlags & 4096) reader.Skip(static_cast<uint64_t>(count) * 24); // tangents and bitangents
|
||||
}
|
||||
reader.Skip(16); // bounding sphere
|
||||
if (reader.U8()) {
|
||||
const auto colors = reader.Array<float>(static_cast<uint64_t>(count) * 4);
|
||||
mesh.colors.reserve(colors.size());
|
||||
for (const auto value : colors) mesh.colors.push_back(static_cast<uint8_t>(std::lround(std::clamp(value, 0.0f, 1.0f) * 255.0f)));
|
||||
}
|
||||
const auto uvSets = dataFlags & 63;
|
||||
if (uvSets > 0) {
|
||||
mesh.uvs = reader.Array<float>(static_cast<uint64_t>(count) * 2);
|
||||
reader.Skip(static_cast<uint64_t>(uvSets - 1) * count * 8);
|
||||
}
|
||||
reader.U16(); // consistency flags
|
||||
reader.I32(); // additional data
|
||||
const auto triangles = reader.U16();
|
||||
if (!strips) {
|
||||
reader.U32(); // triangle points
|
||||
if (reader.U8()) mesh.indices = reader.Array<uint16_t>(static_cast<uint64_t>(triangles) * 3);
|
||||
} else {
|
||||
const auto stripCount = reader.U16();
|
||||
const auto lengths = reader.Array<uint16_t>(stripCount);
|
||||
if (reader.U8()) {
|
||||
for (const auto length : lengths) {
|
||||
const auto points = reader.Array<uint16_t>(length);
|
||||
for (size_t i = 2; i < points.size(); i++) {
|
||||
const auto a = points[i - 2], b = points[i - 1], c = points[i];
|
||||
if (a == b || b == c || a == c) continue;
|
||||
if (i % 2 == 0) mesh.indices.insert(mesh.indices.end(), { a, b, c });
|
||||
else mesh.indices.insert(mesh.indices.end(), { a, c, b });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!reader.Ok() || mesh.positions.size() != static_cast<size_t>(count) * 3) return false;
|
||||
if (mesh.normals.size() != mesh.positions.size()) mesh.normals.clear();
|
||||
if (mesh.colors.size() != static_cast<size_t>(count) * 4) mesh.colors.clear();
|
||||
if (mesh.uvs.size() != static_cast<size_t>(count) * 2) mesh.uvs.clear();
|
||||
// Drop triangles pointing past the vertices
|
||||
std::vector<uint16_t> valid;
|
||||
valid.reserve(mesh.indices.size());
|
||||
for (size_t i = 0; i + 2 < mesh.indices.size(); i += 3) {
|
||||
if (mesh.indices[i] < count && mesh.indices[i + 1] < count && mesh.indices[i + 2] < count) {
|
||||
valid.insert(valid.end(), { mesh.indices[i], mesh.indices[i + 1], mesh.indices[i + 2] });
|
||||
}
|
||||
}
|
||||
mesh.indices = std::move(valid);
|
||||
return true;
|
||||
}
|
||||
|
||||
NifFile::Material ReadMaterial(const Properties& properties) {
|
||||
NifFile::Material material;
|
||||
if (properties.material >= 0) {
|
||||
auto reader = BlockReader(properties.material);
|
||||
ReadNet(reader);
|
||||
reader.Skip(12); // ambient
|
||||
std::array<float, 3> diffuse{}, emissive{};
|
||||
for (auto& value : diffuse) value = reader.Float();
|
||||
reader.Skip(12); // specular
|
||||
for (auto& value : emissive) value = reader.Float();
|
||||
reader.Float(); // glossiness
|
||||
const auto alpha = reader.Float();
|
||||
if (reader.Ok()) {
|
||||
material.diffuse = diffuse;
|
||||
material.emissive = emissive;
|
||||
material.alpha = std::clamp(alpha, 0.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
if (properties.alpha >= 0) {
|
||||
auto reader = BlockReader(properties.alpha);
|
||||
ReadNet(reader);
|
||||
const auto flags = reader.U16();
|
||||
const auto threshold = reader.U8();
|
||||
if (reader.Ok()) {
|
||||
material.alphaBlend = flags & 1;
|
||||
material.alphaTest = flags & 0x200;
|
||||
material.alphaThreshold = threshold;
|
||||
}
|
||||
}
|
||||
if (properties.vertexColor >= 0) {
|
||||
auto reader = BlockReader(properties.vertexColor);
|
||||
ReadNet(reader);
|
||||
const auto flags = reader.U16();
|
||||
if (reader.Ok()) material.vertexColorMode = static_cast<uint8_t>((flags >> 4) & 3);
|
||||
}
|
||||
if (properties.stencil >= 0) {
|
||||
auto reader = BlockReader(properties.stencil);
|
||||
ReadNet(reader);
|
||||
const auto flags = reader.U16();
|
||||
if (reader.Ok()) material.doubleSided = ((flags >> 10) & 3) == 3; // DRAW_BOTH
|
||||
}
|
||||
if (properties.texturing >= 0) {
|
||||
auto reader = BlockReader(properties.texturing);
|
||||
ReadNet(reader);
|
||||
reader.U16(); // flags
|
||||
reader.U32(); // texture count
|
||||
if (reader.U8()) { // has base texture
|
||||
const auto source = reader.I32();
|
||||
const auto flags = reader.U16();
|
||||
const auto* type = TypeOf(source);
|
||||
if (reader.Ok() && type && *type == "NiSourceTexture") {
|
||||
m_Used.insert(source);
|
||||
auto texture = BlockReader(source);
|
||||
ReadNet(texture);
|
||||
const auto external = texture.U8();
|
||||
const auto file = String(texture.U32());
|
||||
const auto pixels = texture.I32();
|
||||
const auto* pixelType = TypeOf(pixels);
|
||||
if (texture.Ok() && external == 1) material.texture = file;
|
||||
else if (texture.Ok() && pixelType && (*pixelType == "NiPixelData" || *pixelType == "NiPersistentSrcTextureRendererData")) {
|
||||
material.embeddedTexture = pixels;
|
||||
m_Used.insert(pixels);
|
||||
}
|
||||
const auto clamp = (flags >> 12) & 0xF;
|
||||
material.clampU = clamp == 0 || clamp == 1;
|
||||
material.clampV = clamp == 0 || clamp == 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
return material;
|
||||
}
|
||||
};
|
||||
|
||||
void Append(std::string& out, const void* data, size_t bytes) {
|
||||
out.append(static_cast<const char*>(data), bytes);
|
||||
}
|
||||
|
||||
void Pad(std::string& out) {
|
||||
while (out.size() % 4) out.push_back('\0');
|
||||
}
|
||||
|
||||
nlohmann::json Color(const std::array<float, 3>& color) {
|
||||
return { std::round(color[0] * 1000.0f) / 1000.0f, std::round(color[1] * 1000.0f) / 1000.0f, std::round(color[2] * 1000.0f) / 1000.0f };
|
||||
}
|
||||
}
|
||||
|
||||
namespace NifFile {
|
||||
int32_t ShaderTag(std::string_view name) {
|
||||
// The client reads the tag with sscanf: "S%d" at the start of the name, else "_S%d" after the first "_S"
|
||||
const auto number = [](std::string_view digits) -> int32_t {
|
||||
size_t i = 0;
|
||||
while (i < digits.size() && (digits[i] == ' ' || digits[i] == '\t')) i++;
|
||||
// A signed number names no mapShaders row, like no number at all
|
||||
if (i >= digits.size() || digits[i] < '0' || digits[i] > '9') return -1;
|
||||
int32_t value = 0;
|
||||
for (; i < digits.size() && digits[i] >= '0' && digits[i] <= '9' && value < 100000; i++) value = value * 10 + (digits[i] - '0');
|
||||
return value;
|
||||
};
|
||||
if (name.starts_with('S')) {
|
||||
const auto tag = number(name.substr(1));
|
||||
if (tag >= 0) return tag;
|
||||
}
|
||||
const auto at = name.find("_S");
|
||||
if (at == std::string_view::npos || at + 3 >= name.size()) return -1;
|
||||
return number(name.substr(at + 2));
|
||||
}
|
||||
|
||||
int32_t MultishaderPart(std::optional<int32_t> tagShader) {
|
||||
return tagShader && *tagShader >= 3 && *tagShader <= 0x6C ? *tagShader : LEGO_SHADER;
|
||||
}
|
||||
|
||||
eTextureAlpha TextureAlphaFor(int32_t shader) {
|
||||
switch (shader) {
|
||||
// LEGOPPLighting: textured alone the texture's alpha is forced to 1; with vertex colors it only lays the
|
||||
// texture over them (lerp by its alpha) and the vertex alpha is what shows through
|
||||
case 4: case 5: case 12: case 25: case 27: case 28: case 29: case 30: case 50: case 72: case 88:
|
||||
// Darkling: the same lay-over; alpha from lighting or the fade
|
||||
case 75: case 76: case 77: case 102: case 103: case 104:
|
||||
return eTextureAlpha::DECAL;
|
||||
// LEGOPPLighting_Item: texture alpha forced to 1, multiplied by the vertex colors
|
||||
case 31: case 48:
|
||||
// TerrainMeshLighting_Rim: texture times vertex colors, alpha only the fade
|
||||
case 3:
|
||||
return eTextureAlpha::IGNORED;
|
||||
default:
|
||||
return eTextureAlpha::OPACITY;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<Model> Parse(std::string_view data, uint32_t lod, std::string& error) {
|
||||
return Parser(data, lod).Run(error);
|
||||
}
|
||||
|
||||
std::optional<std::string> EmbeddedTexture(std::string_view data, int32_t block) {
|
||||
std::string error;
|
||||
return Parser(data, 0).Dds(block, error);
|
||||
}
|
||||
|
||||
std::string Encode(const Model& model, const std::vector<std::string>& textures) {
|
||||
std::string body;
|
||||
nlohmann::json meshes = nlohmann::json::array();
|
||||
std::vector<std::string> names;
|
||||
for (size_t m = 0; m < model.meshes.size(); m++) {
|
||||
const auto& mesh = model.meshes[m];
|
||||
const auto& material = mesh.material;
|
||||
const auto vertices = mesh.positions.size() / 3;
|
||||
const std::string texture = m < textures.size() ? textures[m] : std::string{};
|
||||
int32_t textureIndex = -1;
|
||||
if (!texture.empty()) {
|
||||
const auto it = std::find(names.begin(), names.end(), texture);
|
||||
textureIndex = static_cast<int32_t>(it - names.begin());
|
||||
if (it == names.end()) names.push_back(texture);
|
||||
}
|
||||
nlohmann::json entry{
|
||||
{"offset", body.size()}, {"vertices", vertices}, {"indices", mesh.indices.size()},
|
||||
{"normals", !mesh.normals.empty()}, {"uv", !mesh.uvs.empty() && textureIndex >= 0}, {"colors", !mesh.colors.empty()},
|
||||
{"diffuse", Color(material.diffuse)}, {"emissive", Color(material.emissive)}, {"alpha", std::round(material.alpha * 1000.0f) / 1000.0f},
|
||||
{"blend", material.alphaBlend}, {"test", material.alphaTest ? material.alphaThreshold : -1}, {"doubleSided", material.doubleSided},
|
||||
{"vertexColors", material.vertexColorMode}, {"texture", textureIndex}, {"clampU", material.clampU}, {"clampV", material.clampV},
|
||||
{"shaderTag", material.shaderTag}
|
||||
};
|
||||
Append(body, mesh.positions.data(), mesh.positions.size() * sizeof(float));
|
||||
if (!mesh.normals.empty()) {
|
||||
std::vector<int8_t> packed(mesh.normals.size());
|
||||
for (size_t i = 0; i < packed.size(); i++) packed[i] = static_cast<int8_t>(std::lround(std::clamp(mesh.normals[i], -1.0f, 1.0f) * 127.0f));
|
||||
Append(body, packed.data(), packed.size());
|
||||
Pad(body);
|
||||
}
|
||||
if (entry["uv"].get<bool>()) Append(body, mesh.uvs.data(), mesh.uvs.size() * sizeof(float));
|
||||
if (!mesh.colors.empty()) Append(body, mesh.colors.data(), mesh.colors.size());
|
||||
Append(body, mesh.indices.data(), mesh.indices.size() * sizeof(uint16_t));
|
||||
Pad(body);
|
||||
meshes.push_back(std::move(entry));
|
||||
}
|
||||
nlohmann::json header{
|
||||
{"version", 1}, {"meshes", meshes}, {"textures", names},
|
||||
{"min", { model.min[0], model.min[1], model.min[2] }}, {"max", { model.max[0], model.max[1], model.max[2] }}
|
||||
};
|
||||
auto text = header.dump();
|
||||
while (text.size() % 4) text.push_back(' ');
|
||||
std::string out;
|
||||
const auto length = static_cast<uint32_t>(text.size());
|
||||
Append(out, &length, sizeof(length));
|
||||
out += text;
|
||||
out += body;
|
||||
return out;
|
||||
}
|
||||
|
||||
std::optional<std::string> KfmModelPath(std::string_view data) {
|
||||
const auto newline = data.substr(0, 128).find('\n');
|
||||
if (newline == std::string_view::npos || data.substr(0, newline).find("KFM") == std::string_view::npos) return std::nullopt;
|
||||
Reader reader(data.substr(newline + 1));
|
||||
reader.U8(); // little endian
|
||||
auto path = reader.SizedString();
|
||||
if (!reader.Ok() || path.empty()) return std::nullopt;
|
||||
return path;
|
||||
}
|
||||
}
|
||||
114
dCommon/NifFile.h
Normal file
114
dCommon/NifFile.h
Normal file
@@ -0,0 +1,114 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* Reads the game client's Gamebryo meshes (.nif) into flat, drawable meshes for the dashboard's 3D views, following
|
||||
* the NifTools project's format description (nif.xml). Pure (bytes in, meshes out) so it can be unit tested.
|
||||
*
|
||||
* The client's files are versions 20.2.0.8 and 20.3.0.9 with user version 0. What is drawn: the scene graph
|
||||
* (NiNode, NiLODNode, NiBillboardNode and other nodes) with its transforms baked into the vertices, NiTriShape and
|
||||
* NiTriStrips geometry (positions, normals, the first UV set, vertex colors), and the properties Gamebryo passes down
|
||||
* the tree: NiMaterialProperty, NiAlphaProperty, NiTexturingProperty's base texture (an external NiSourceTexture),
|
||||
* NiVertexColorProperty and NiStencilProperty's draw mode (double sided). Skipped: hidden subtrees, animation,
|
||||
* particles, lights and cameras; skinned geometry is drawn in its bind pose. Blocks are skipped by their stored sizes,
|
||||
* so a block this reader doesn't know never breaks the rest of the file.
|
||||
*/
|
||||
namespace NifFile {
|
||||
struct Material {
|
||||
std::array<float, 3> diffuse{ 1.0f, 1.0f, 1.0f }; // sRGB 0..1
|
||||
std::array<float, 3> emissive{ 0.0f, 0.0f, 0.0f };
|
||||
float alpha{ 1.0f };
|
||||
bool alphaBlend{};
|
||||
bool alphaTest{};
|
||||
uint8_t alphaThreshold{ 128 };
|
||||
bool doubleSided{};
|
||||
// NiVertexColorProperty's source vertex mode: 0 ignore vertex colors, 1 they're emissive, 2 they're ambient and diffuse
|
||||
uint8_t vertexColorMode{ 2 };
|
||||
std::string texture; // the base texture's file name as stored (often relative to the .nif's folder)
|
||||
int32_t embeddedTexture{ -1 }; // else the block with the texture's pixels in the file (EmbeddedTexture)
|
||||
bool clampU{};
|
||||
bool clampV{};
|
||||
int32_t shaderTag{ -1 }; // mapShaders id from a multishader tag in the name of the mesh or a node above it
|
||||
};
|
||||
|
||||
struct Mesh {
|
||||
Material material;
|
||||
std::vector<float> positions; // x, y, z per vertex, in the model's space
|
||||
std::vector<float> normals; // empty when the geometry has none
|
||||
std::vector<float> uvs; // u, v per vertex, empty when none
|
||||
std::vector<uint8_t> colors; // r, g, b, a per vertex (sRGB), empty when none
|
||||
std::vector<uint16_t> indices; // triangles
|
||||
};
|
||||
|
||||
/**
|
||||
* What a texture's alpha channel does, which the client's shader decides (res/shaders/*.fx), not the .nif:
|
||||
* OPACITY it is see-through where the alpha is (Basic, AlphaAsAlpha, fixed function and most others); DECAL the
|
||||
* texture is laid over the vertex colors by its alpha and the mesh itself stays as opaque as its vertex colors
|
||||
* (the LEGO lighting shaders); IGNORED the alpha does nothing (LEGO items, terrain meshes).
|
||||
*/
|
||||
enum class eTextureAlpha : uint8_t { OPACITY, DECAL, IGNORED };
|
||||
|
||||
// mapShaders.gameValue of the LEGO shader, the client's default
|
||||
constexpr int32_t LEGO_SHADER = 5;
|
||||
// RenderComponent's shader for models whose parts name their own shaders (mapShaders "Multishader")
|
||||
constexpr int32_t MULTISHADER = 9999;
|
||||
|
||||
/**
|
||||
* The mapShaders id in a multishader part's name: "S05__TRUNKS" or "rock_S30" (LWOBaseRenderComponent::
|
||||
* AddObjectToRenderPipe reads "S%d", else "_S%d"); -1 for none.
|
||||
*/
|
||||
int32_t ShaderTag(std::string_view name);
|
||||
|
||||
// The shader (gameValue) a multishader part is drawn with, from its tag's gameValue: outside 3..108 the LEGO shader
|
||||
int32_t MultishaderPart(std::optional<int32_t> tagShader);
|
||||
|
||||
// What a texture's alpha does under a shader (mapShaders.gameValue); -1 is fixed function (opacity)
|
||||
eTextureAlpha TextureAlphaFor(int32_t shader);
|
||||
|
||||
// Where a node is in the model's space: p' = rotation * p + translation (row-major, scale folded in)
|
||||
struct NodeTransform {
|
||||
std::array<float, 9> rotation{ 1, 0, 0, 0, 1, 0, 0, 0, 1 };
|
||||
std::array<float, 3> translation{};
|
||||
};
|
||||
|
||||
struct Model {
|
||||
uint32_t version{};
|
||||
std::vector<Mesh> meshes;
|
||||
std::map<std::string, NodeTransform> nodes; // named nodes that are drawn (the first of each name), e.g. attach points
|
||||
std::array<float, 3> min{};
|
||||
std::array<float, 3> max{};
|
||||
std::map<std::string, uint32_t> skipped; // block types in the file that aren't drawn, with how many
|
||||
uint32_t skinned{}; // meshes drawn in their bind pose
|
||||
};
|
||||
|
||||
/**
|
||||
* The meshes of a .nif. `lod` picks among an NiLODNode's children: 0 the most detailed (the nearest range), higher
|
||||
* numbers coarser ones, clamped to what the node has. nullopt and `error` set when the file can't be read at all.
|
||||
*/
|
||||
std::optional<Model> Parse(std::string_view data, uint32_t lod, std::string& error);
|
||||
|
||||
/**
|
||||
* A model for the browser: a little-endian uint32 with the length of a JSON header, the header (padded with spaces
|
||||
* to a multiple of 4), then the binary data it describes. Per mesh at "offset": float32 positions (3 per vertex),
|
||||
* int8 normals (3 per vertex, times 127, padded to 4 bytes) when "normals", float32 UVs (2 per vertex) when "uv",
|
||||
* uint8 RGBA colors when "colors", then uint16 indices (padded to 4 bytes). `textures[i]` is where mesh i's texture
|
||||
* is (empty: none); the header lists each once in "textures" and a mesh's "texture" indexes it (-1: none).
|
||||
*/
|
||||
std::string Encode(const Model& model, const std::vector<std::string>& textures);
|
||||
|
||||
/**
|
||||
* A texture stored inside a .nif (NiPixelData or NiPersistentSrcTextureRendererData, block `block`) as a DDS file
|
||||
* with its mipmaps: DXT1/3/5 as they are, 24 and 32-bit RGB(A) uncompressed. nullopt for other formats.
|
||||
*/
|
||||
std::optional<std::string> EmbeddedTexture(std::string_view data, int32_t block);
|
||||
|
||||
// The model a Gamebryo animation set (.kfm) is for, as stored (relative to the .kfm's folder, backslashes)
|
||||
std::optional<std::string> KfmModelPath(std::string_view data);
|
||||
}
|
||||
@@ -46,5 +46,55 @@ namespace ZCompression {
|
||||
inflateEnd(&zInfo); // zlib function
|
||||
return(nRet);
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Inflate everything in `data` with zlib's `windowBits` (31: gzip, -15: raw); nullopt on any error
|
||||
std::optional<std::string> Inflate(std::string_view data, int windowBits, size_t sizeHint) {
|
||||
z_stream stream{};
|
||||
if (inflateInit2(&stream, windowBits) != Z_OK) return std::nullopt;
|
||||
std::string out;
|
||||
out.reserve(sizeHint);
|
||||
stream.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(data.data()));
|
||||
stream.avail_in = static_cast<uInt>(data.size());
|
||||
char buffer[65536];
|
||||
int result = Z_OK;
|
||||
while (result == Z_OK) {
|
||||
stream.next_out = reinterpret_cast<Bytef*>(buffer);
|
||||
stream.avail_out = sizeof(buffer);
|
||||
result = inflate(&stream, Z_NO_FLUSH);
|
||||
if (result != Z_OK && result != Z_STREAM_END) break;
|
||||
out.append(buffer, sizeof(buffer) - stream.avail_out);
|
||||
if (result == Z_OK && stream.avail_in == 0 && stream.avail_out != 0) break; // truncated
|
||||
}
|
||||
inflateEnd(&stream);
|
||||
if (result != Z_STREAM_END) return std::nullopt;
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
std::string Gzip(std::string_view data) {
|
||||
z_stream stream{};
|
||||
// 31: a gzip header and trailer instead of zlib's
|
||||
if (deflateInit2(&stream, Z_BEST_COMPRESSION, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY) != Z_OK) return {};
|
||||
std::string out;
|
||||
out.resize(deflateBound(&stream, static_cast<uLong>(data.size())) + 32);
|
||||
stream.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(data.data()));
|
||||
stream.avail_in = static_cast<uInt>(data.size());
|
||||
stream.next_out = reinterpret_cast<Bytef*>(out.data());
|
||||
stream.avail_out = static_cast<uInt>(out.size());
|
||||
const auto result = deflate(&stream, Z_FINISH);
|
||||
out.resize(stream.total_out);
|
||||
deflateEnd(&stream);
|
||||
return result == Z_STREAM_END ? out : std::string{};
|
||||
}
|
||||
|
||||
std::optional<std::string> Gunzip(std::string_view data) {
|
||||
return Inflate(data, 31, data.size() * 4);
|
||||
}
|
||||
|
||||
std::optional<std::string> InflateRaw(std::string_view data, size_t size) {
|
||||
auto out = Inflate(data, -15, size);
|
||||
if (!out || out->size() != size) return std::nullopt;
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace ZCompression {
|
||||
uint32_t GetMaxCompressedLength(uint32_t nLenSrc);
|
||||
@@ -8,5 +11,14 @@ namespace ZCompression {
|
||||
int32_t Compress(const uint8_t* abSrc, uint32_t nLenSrc, uint8_t* abDst, uint32_t nLenDst);
|
||||
|
||||
int32_t Decompress(const uint8_t* abSrc, uint32_t nLenSrc, uint8_t* abDst, uint32_t nLenDst, int32_t& nErr);
|
||||
|
||||
// `data` as a gzip file (RFC 1952), what HTTP clients and the game client's UGC downloads inflate
|
||||
std::string Gzip(std::string_view data);
|
||||
|
||||
// A gzip file's contents; nullopt when it isn't one or is damaged
|
||||
std::optional<std::string> Gunzip(std::string_view data);
|
||||
|
||||
// Raw deflate data (no zlib or gzip header, as in zip files) that inflates to `size` bytes; nullopt when it doesn't
|
||||
std::optional<std::string> InflateRaw(std::string_view data, size_t size);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ enum class ServiceType : uint16_t {
|
||||
WORLD,
|
||||
CLIENT,
|
||||
MASTER,
|
||||
UNKNOWN
|
||||
UNKNOWN,
|
||||
UGC // the UGC server (dUgcServer): appended, the values above are on the wire
|
||||
};
|
||||
|
||||
#endif //!__SERVICETYPE__H__
|
||||
|
||||
Reference in New Issue
Block a user