#include "ebx_writer.h" #include #include #include #include #include #include #include #include #include namespace dingosdk::frostbite::ebx { namespace { std::size_t align_up(const std::size_t value, const std::size_t alignment) { if (alignment <= 1) return value; if (value > std::numeric_limits::max() - alignment + 1) { throw std::overflow_error("EBX alignment overflow"); } return (value + alignment - 1) / alignment * alignment; } template Type number(const Value& value, const Type fallback = {}) { if (const auto* signedValue = std::get_if(&value.data)) { if constexpr (std::is_integral_v && !std::is_same_v) { if constexpr (std::is_unsigned_v) { if (*signedValue < 0 || static_cast(*signedValue) > std::numeric_limits::max()) throw std::overflow_error("EBX integer field exceeds its native width"); } else if (*signedValue < std::numeric_limits::min() || *signedValue > std::numeric_limits::max()) { throw std::overflow_error("EBX integer field exceeds its native width"); } } return static_cast(*signedValue); } if (const auto* unsignedValue = std::get_if(&value.data)) { if constexpr (std::is_integral_v && !std::is_same_v) { if (*unsignedValue > static_cast(std::numeric_limits::max())) throw std::overflow_error("EBX integer field exceeds its native width"); } return static_cast(*unsignedValue); } if (const auto* floatValue = std::get_if(&value.data)) { if (!std::isfinite(*floatValue)) throw std::overflow_error("EBX floating-point field must be finite"); if constexpr (std::is_floating_point_v) { if (*floatValue < std::numeric_limits::lowest() || *floatValue > std::numeric_limits::max()) throw std::overflow_error("EBX floating-point field exceeds its native width"); } return static_cast(*floatValue); } if (const auto* booleanValue = std::get_if(&value.data)) return static_cast(*booleanValue); return fallback; } std::size_t scalar_size(const FieldType type) { switch (type) { case FieldType::boolean: case FieldType::int8: case FieldType::uint8: return 1; case FieldType::int16: case FieldType::uint16: return 2; case FieldType::int32: case FieldType::uint32: case FieldType::float32: case FieldType::enumeration: return 4; case FieldType::int64: case FieldType::uint64: case FieldType::float64: case FieldType::resourceRef: case FieldType::pointer: case FieldType::cString: case FieldType::fileRef: case FieldType::typeRef: case FieldType::delegate: return 8; case FieldType::guid: return 16; case FieldType::sha1: return 20; case FieldType::string: return 32; case FieldType::boxedValueRef: return 16; default: return 8; } } class Builder final { public: explicit Builder(const Document& document) : document_(document) {} std::vector run() { validate(); write_instances(); write_arrays(); write_boxed_values(); const auto stringOffset = checked_u32(payload_.size(), "EBX string offset"); write_strings(); resolve_pointers(); const auto payload = std::move(payload_); const auto fixup = write_fixup(stringOffset); const auto extra = write_extra(); return write_riff(payload, fixup, extra); } private: struct PendingArray { std::uint32_t slot{}; const Value::Array* values{}; std::size_t descriptor{}; bool emitted{}; }; struct PendingString { std::uint32_t slot{}; std::string value; }; struct PendingPointer { std::uint32_t slot{}; PointerReference value; }; struct PendingBoxed { std::uint32_t slot{}; BoxedReference value; }; static std::uint32_t checked_u32(const std::size_t value, const char* label) { if (value > std::numeric_limits::max()) throw std::overflow_error(label); return static_cast(value); } void validate() const { if (document_.reflectionData.empty()) throw std::runtime_error("EBX has no donor reflection table"); if (document_.fixupTypeSignatures.size() > document_.fixupTypeGuids.size()) { throw std::runtime_error("EBX fixup type table is invalid"); } bool internalSeen{}; for (const auto& instance : document_.instances) { internalSeen |= !instance.exported; if (internalSeen && instance.exported) throw std::runtime_error("exported EBX instances must precede internal instances"); if (instance.descriptor < 0 || static_cast(instance.descriptor) >= document_.types.size()) { throw std::runtime_error("EBX instance has no valid type descriptor"); } if (instance.fixupType >= document_.fixupTypeGuids.size()) { throw std::runtime_error("EBX instance has no valid fixup type"); } } } void grow(const std::size_t end) { if (end > payload_.size()) payload_.resize(end); } template void put_number(const std::size_t offset, const Type value) { grow(offset + sizeof(value)); std::memcpy(payload_.data() + offset, &value, sizeof(value)); } void put(const std::size_t offset, const std::span bytes) { grow(offset + bytes.size()); std::ranges::copy(bytes, payload_.begin() + static_cast(offset)); } const Value* value_for(const Object* object, const std::size_t descriptor) const { if (!object) return nullptr; const auto found = std::ranges::find_if(object->fields, [descriptor](const FieldValue& field) { return field.descriptor == descriptor; }); return found == object->fields.end() ? nullptr : &found->value; } void write_instances() { std::size_t cursor{}; instanceOffsets_.reserve(document_.instances.size()); for (const auto& instance : document_.instances) { const auto& type = document_.types[static_cast(instance.descriptor)]; const auto start = align_up(cursor + (instance.exported ? 16 : 0), std::max(type.alignment, 1)); if (instance.exported) put(start - 16, instance.instanceGuid.bytes); grow(start + type.size); if (!instance.rawImage.empty()) { put(start, std::span(instance.rawImage).first(std::min(instance.rawImage.size(), type.size))); } put_number(start, instance.fixupType); write_type(static_cast(instance.descriptor), start, instance.object.get()); instanceOffsets_.push_back(checked_u32(start, "EBX instance offset")); cursor = start + type.size; } } void write_type(const std::size_t typeIndex, const std::size_t start, const Object* object) { const auto& type = document_.types[typeIndex]; grow(start + type.size); for (std::size_t ordinal = 0; ordinal < type.fieldCount; ++ordinal) { const auto descriptorIndex = static_cast(type.fieldIndex) + ordinal; if (descriptorIndex >= document_.fields.size()) throw std::runtime_error("EBX field range is invalid"); const auto& field = document_.fields[descriptorIndex]; if (field.type() == FieldType::inherited) { if (field.classRef >= document_.types.size()) throw std::runtime_error("EBX base type is invalid"); write_type(field.classRef, start, object); continue; } const auto position = start + field.dataOffset; const auto* value = value_for(object, descriptorIndex); if (field.category() == FieldCategory::array) { grow(position + 8); pointerOffsets_.push_back(checked_u32(position, "EBX array slot")); static const Value::Array empty; const auto* array = value ? std::get_if(&value->data) : nullptr; arraysPending_.push_back({checked_u32(position, "EBX array slot"), array ? array : &empty, descriptorIndex}); } else if (field.type() == FieldType::structure) { if (field.classRef >= document_.types.size()) throw std::runtime_error("EBX struct type is invalid"); const auto nestedStart = align_up(position, std::max(document_.types[field.classRef].alignment, 1)); const auto* nested = value ? std::get_if>(&value->data) : nullptr; write_type(field.classRef, nestedStart, nested && *nested ? nested->get() : nullptr); } else { write_scalar(field, position, value); } } } void write_scalar(const FieldDescriptor& field, const std::size_t position, const Value* value) { static const Value empty; const auto& source = value ? *value : empty; switch (field.type()) { case FieldType::boolean: put_number(position, static_cast(number(source))); break; case FieldType::int8: put_number(position, number(source)); break; case FieldType::uint8: put_number(position, number(source)); break; case FieldType::int16: put_number(position, number(source)); break; case FieldType::uint16: put_number(position, number(source)); break; case FieldType::int32: case FieldType::enumeration: put_number(position, number(source)); break; case FieldType::uint32: put_number(position, number(source)); break; case FieldType::int64: put_number(position, number(source)); break; case FieldType::uint64: put_number(position, number(source)); break; case FieldType::float32: put_number(position, number(source)); break; case FieldType::float64: put_number(position, number(source)); break; case FieldType::guid: { const auto* guid = std::get_if(&source.data); static const Guid zero; put(position, guid ? std::span(guid->bytes) : std::span(zero.bytes)); break; } case FieldType::sha1: { const auto* sha = std::get_if(&source.data); static const Sha1 zero; put(position, sha ? std::span(sha->bytes) : std::span(zero.bytes)); break; } case FieldType::string: { grow(position + 32); std::fill_n(payload_.begin() + static_cast(position), 32, std::byte{}); const auto* string = std::get_if(&source.data); if (string) { if (string->size() > 31) throw std::overflow_error("EBX fixed string exceeds 31 bytes"); std::memcpy(payload_.data() + position, string->data(), string->size()); } break; } case FieldType::cString: case FieldType::fileRef: { grow(position + 8); put_number(position, std::uint64_t{}); pointerOffsets_.push_back(checked_u32(position, "EBX string slot")); const auto* string = std::get_if(&source.data); if (string && string->size() > 1024 * 1024) throw std::overflow_error("EBX string exceeds the authoring limit"); stringsPending_.push_back({checked_u32(position, "EBX string slot"), string ? *string : std::string{}}); break; } case FieldType::resourceRef: { const auto* reference = std::get_if(&source.data); put_number(position, reference ? reference->id : std::uint64_t{}); resourceOffsets_.push_back(checked_u32(position, "EBX resource slot")); break; } case FieldType::pointer: { grow(position + 8); put_number(position, std::uint64_t{}); const auto* pointer = std::get_if(&source.data); pointersPending_.push_back({checked_u32(position, "EBX pointer slot"), pointer ? *pointer : PointerReference{}}); break; } case FieldType::typeRef: case FieldType::delegate: { grow(position + 8); const auto* reference = std::get_if(&source.data); const auto encoded = reference ? reference->encoded : 0U; put_number(position, encoded); put_number(position + 4, std::uint32_t{}); if (encoded != 0 && field.type() == FieldType::typeRef) { typeInfoOffsets_.push_back(checked_u32(position, "EBX type-info slot")); } break; } case FieldType::boxedValueRef: { grow(position + 16); const auto* reference = std::get_if(&source.data); if (!reference || reference->encodedType == 0) { std::fill_n(payload_.begin() + static_cast(position), 16, std::byte{}); break; } put_number(position, reference->encodedType); put_number(position + 4, std::uint32_t{}); typeInfoOffsets_.push_back(checked_u32(position, "EBX boxed type slot")); pointerOffsets_.push_back(checked_u32(position + 8, "EBX boxed pointer slot")); boxesPending_.push_back({checked_u32(position, "EBX boxed slot"), *reference}); break; } default: grow(position + scalar_size(field.type())); break; } } std::size_t element_stride(const FieldDescriptor& field) const { if (field.type() == FieldType::structure) { if (field.classRef >= document_.types.size()) throw std::runtime_error("EBX array struct type is invalid"); return std::max(document_.types[field.classRef].size, 1); } return scalar_size(field.type()); } std::size_t element_alignment(const FieldDescriptor& field) const { if (field.type() == FieldType::structure) { return std::max(document_.types[field.classRef].alignment, 1); } return std::max(std::min(element_stride(field), 8), 1); } std::uint16_t fixup_slot_for_descriptor(const std::uint16_t descriptor) const { if (descriptor >= document_.types.size()) return 0xFFFF; const auto& type = document_.types[descriptor]; const auto count = std::min(document_.fixupTypeGuids.size(), document_.fixupTypeSignatures.size()); for (std::size_t index = 0; index < count; ++index) { if (document_.fixupTypeGuids[index] == type.guid && document_.fixupTypeSignatures[index] == type.signature) { return static_cast(index); } } return 0xFFFF; } std::uint32_t array_hash(const FieldDescriptor& field) const { const auto rawType = static_cast(field.encodedType << 1); const auto classRef = field.type() == FieldType::structure ? fixup_slot_for_descriptor(field.classRef) : 0xFFFF; const auto matching = std::ranges::find_if(document_.arrays, [&](const ArrayRecord& value) { return value.encodedType == rawType && value.classRef == classRef; }); if (matching != document_.arrays.end()) return matching->hash; if (!document_.arrays.empty()) return document_.arrays.front().hash; return field.nameHash; } void write_arrays() { arraysOffset_ = checked_u32(align_up(payload_.size(), 16), "EBX array section"); grow(static_cast(arraysOffset_) + 32); emptyArrayTarget_ = arraysOffset_ + 16; for (std::size_t index = 0; index < arraysPending_.size(); ++index) { if (!arraysPending_[index].emitted) emit_array(index); } } void emit_array(const std::size_t index) { arraysPending_[index].emitted = true; const auto slot = arraysPending_[index].slot; const auto* values = arraysPending_[index].values; const auto descriptor = arraysPending_[index].descriptor; const auto& field = document_.fields[descriptor]; if (values->empty()) { put_number(slot, static_cast(emptyArrayTarget_ - slot)); return; } const auto data = align_up(payload_.size() + 4, element_alignment(field)); const auto stride = element_stride(field); put_number(data - 4, checked_u32(values->size(), "EBX array count")); grow(data + stride * values->size()); put_number(slot, static_cast(data - slot)); const auto firstChild = arraysPending_.size(); for (std::size_t element = 0; element < values->size(); ++element) { const auto elementStart = data + element * stride; const auto& value = (*values)[element]; if (field.type() == FieldType::structure) { const auto* object = std::get_if>(&value.data); write_type(field.classRef, elementStart, object && *object ? object->get() : nullptr); } else { write_scalar(field, elementStart, &value); } } arrays_.push_back({checked_u32(data, "EBX array data"), checked_u32(values->size(), "EBX array count"), array_hash(field), static_cast(field.encodedType << 1), field.type() == FieldType::structure ? fixup_slot_for_descriptor(field.classRef) : static_cast(0xFFFF)}); for (std::size_t child = firstChild; child < arraysPending_.size(); ++child) { if (!arraysPending_[child].emitted) emit_array(child); } } void write_boxed_values() { boxedOffset_ = checked_u32(align_up(payload_.size(), 16), "EBX boxed section"); grow(boxedOffset_); std::unordered_map moved; std::vector records = document_.boxedValues; std::ranges::sort(records, {}, &BoxedValueRecord::offset); for (const auto& source : records) { if (source.offset < document_.boxedValuesOffset) throw std::runtime_error("EBX boxed record precedes its section"); const auto offset = static_cast(boxedOffset_) + source.offset - document_.boxedValuesOffset; put(offset, source.rawBytes); const auto relocate = [&](const std::vector& sourceOffsets, std::vector& destinationOffsets) { for (const auto old : sourceOffsets) { if (old < source.offset || old - source.offset >= source.rawBytes.size()) continue; destinationOffsets.push_back(checked_u32(offset + old - source.offset, "EBX boxed relocation")); } }; relocate(document_.pointerOffsets, pointerOffsets_); relocate(document_.resourceRefOffsets, resourceOffsets_); relocate(document_.importOffsets, importOffsets_); relocate(document_.typeInfoOffsets, typeInfoOffsets_); moved[source.offset] = checked_u32(offset, "EBX boxed value"); auto authored = source; authored.offset = checked_u32(offset, "EBX boxed value"); boxed_.push_back(std::move(authored)); } for (const auto& pending : boxesPending_) { put_number(pending.slot, pending.value.encodedType); put_number(static_cast(pending.slot) + 4, std::uint32_t{}); const auto found = moved.find(static_cast(pending.value.dataOffset)); const auto relative = found == moved.end() ? pending.value.relativeOffset : static_cast(found->second) - static_cast(pending.slot + 8); put_number(static_cast(pending.slot) + 8, relative); } } void write_strings() { std::unordered_map offsets; for (const auto& pending : stringsPending_) { auto found = offsets.find(pending.value); if (found == offsets.end()) { const auto offset = checked_u32(payload_.size(), "EBX string table"); found = offsets.emplace(pending.value, offset).first; const auto bytes = std::as_bytes(std::span(pending.value.data(), pending.value.size())); put(payload_.size(), bytes); grow(payload_.size() + 1); } put_number(pending.slot, static_cast(found->second - pending.slot)); put_number(static_cast(pending.slot) + 4, std::uint32_t{}); } } void resolve_pointers() { for (const auto& pending : pointersPending_) { switch (pending.value.kind) { case PointerKind::null: break; case PointerKind::internal: if (pending.value.index < 0 || static_cast(pending.value.index) >= instanceOffsets_.size()) { throw std::runtime_error("EBX internal pointer index is invalid"); } put_number(pending.slot, static_cast(instanceOffsets_[pending.value.index] - pending.slot)); pointerOffsets_.push_back(pending.slot); break; case PointerKind::external: if (pending.value.index < 0 || static_cast(pending.value.index) >= document_.imports.size()) { throw std::runtime_error("EBX import pointer index is invalid"); } put_number(pending.slot, (static_cast(pending.value.index) << 1U) | 1U); importOffsets_.push_back(pending.slot); break; } put_number(static_cast(pending.slot) + 4, std::uint32_t{}); } } static void write_table(BinaryWriter& writer, std::vector values) { std::ranges::sort(values); values.erase(std::unique(values.begin(), values.end()), values.end()); writer.u32(checked_u32(values.size(), "EBX fixup table count")); for (const auto value : values) writer.u32(value); } std::vector write_fixup(const std::uint32_t stringOffset) const { BinaryWriter writer; writer.guid(document_.fileGuid); writer.u32(checked_u32(document_.fixupTypeGuids.size(), "EBX type GUID count")); for (const auto& value : document_.fixupTypeGuids) writer.guid(value); writer.u32(checked_u32(document_.fixupTypeSignatures.size(), "EBX signature count")); for (const auto value : document_.fixupTypeSignatures) writer.u32(value); writer.u32(checked_u32(std::ranges::count_if(document_.instances, &InstanceRecord::exported), "EBX exported instance count")); write_table(writer, instanceOffsets_); write_table(writer, pointerOffsets_); write_table(writer, resourceOffsets_); writer.u32(checked_u32(document_.imports.size(), "EBX import count")); for (const auto& value : document_.imports) { writer.guid(value.fileGuid); writer.guid(value.classGuid); } write_table(writer, importOffsets_); write_table(writer, typeInfoOffsets_); writer.u32(arraysOffset_); writer.u32(boxedOffset_); writer.u32(stringOffset); auto oldSlots = document_.boxedValueSlots; std::vector newSlots; newSlots.reserve(boxesPending_.size()); for (const auto& pending : boxesPending_) newSlots.push_back(pending.slot); std::ranges::sort(oldSlots); std::ranges::sort(newSlots); std::vector trailing; for (const auto old : document_.boxedReferenceOffsets) { const auto found = std::ranges::lower_bound(oldSlots, old); if (found == oldSlots.end() || *found != old) continue; const auto index = static_cast(found - oldSlots.begin()); if (index < newSlots.size()) trailing.push_back(newSlots[index]); } write_table(writer, std::move(trailing)); return writer.take(); } std::vector write_extra() const { BinaryWriter writer; writer.u32(checked_u32(arrays_.size(), "EBX array descriptor count")); writer.u32(checked_u32(boxed_.size(), "EBX boxed descriptor count")); auto arrays = arrays_; std::ranges::sort(arrays, {}, &ArrayRecord::offset); for (const auto& value : arrays) { writer.u32(value.offset); writer.u32(value.count); writer.u32(value.hash); writer.u16(value.encodedType); writer.u16(value.classRef); } auto boxed = boxed_; std::ranges::sort(boxed, {}, &BoxedValueRecord::offset); for (const auto& value : boxed) { writer.u32(value.offset); writer.u32(value.count); writer.u32(value.hash); writer.u16(value.encodedType); writer.u16(value.classRef); } return writer.take(); } std::vector write_riff(const std::vector& payload, const std::vector& fixup, const std::vector& extra) const { BinaryWriter writer; writer.bytes(std::as_bytes(std::span("RIFF", 4))); writer.u32(0); // EBXS requires native-layout conversion when loaded by the game. writer.bytes(std::as_bytes(std::span(document_.serializedLayout ? "EBXS" : "EBX\0", 4))); auto chunk = [&](const std::string_view name, const std::span bytes) { writer.bytes(std::as_bytes(std::span(name.data(), name.size()))); writer.u32(checked_u32(bytes.size(), "EBX RIFF chunk size")); writer.bytes(bytes); if ((bytes.size() & 1U) != 0) writer.u8(0); }; std::vector data(12); data.insert(data.end(), payload.begin(), payload.end()); chunk("EBXD", data); chunk("EFIX", fixup); chunk("EBXX", extra); chunk(document_.hashedReflectionNames ? "RFL2" : "REFL", document_.reflectionData); auto result = writer.take(); const auto size = checked_u32(result.size() - 8, "EBX RIFF size"); std::memcpy(result.data() + 4, &size, sizeof(size)); return result; } const Document& document_; std::vector payload_; std::vector instanceOffsets_; std::vector pointerOffsets_; std::vector resourceOffsets_; std::vector importOffsets_; std::vector typeInfoOffsets_; std::vector arraysPending_; std::vector stringsPending_; std::vector pointersPending_; std::vector boxesPending_; std::vector arrays_; std::vector boxed_; std::uint32_t arraysOffset_{}; std::uint32_t boxedOffset_{}; std::uint32_t emptyArrayTarget_{}; }; } std::uint16_t ensure_fixup_type(Document& document, const std::int32_t descriptor) { if (descriptor < 0 || static_cast(descriptor) >= document.types.size()) { throw std::out_of_range("EBX type descriptor is invalid"); } const auto& type = document.types[static_cast(descriptor)]; const auto count = std::min(document.fixupTypeGuids.size(), document.fixupTypeSignatures.size()); for (std::size_t index = 0; index < count; ++index) { if (document.fixupTypeGuids[index] == type.guid && document.fixupTypeSignatures[index] == type.signature) { if (index > std::numeric_limits::max()) throw std::overflow_error("EBX type slot overflow"); return static_cast(index); } } if (count >= std::numeric_limits::max()) throw std::overflow_error("EBX type slot overflow"); document.fixupTypeGuids.insert(document.fixupTypeGuids.begin() + static_cast(count), type.guid); document.fixupTypeSignatures.push_back(type.signature); return static_cast(count); } std::vector write_document(const Document& document) { return Builder(document).run(); } }