diff options
Diffstat (limited to 'src/factories/oot/OoTCutsceneFactory.cpp')
| -rw-r--r-- | src/factories/oot/OoTCutsceneFactory.cpp | 323 |
1 files changed, 323 insertions, 0 deletions
diff --git a/src/factories/oot/OoTCutsceneFactory.cpp b/src/factories/oot/OoTCutsceneFactory.cpp new file mode 100644 index 0000000..da5ac76 --- /dev/null +++ b/src/factories/oot/OoTCutsceneFactory.cpp @@ -0,0 +1,323 @@ +#include "OoTCutsceneFactory.h" +#include "OoTSceneUtils.h" +#include "spdlog/spdlog.h" +#include "Companion.h" + +#include <set> + +namespace OoT { + +// Camera commands use variable-length entries terminated by a 0xFF marker byte. +bool CutsceneSerializer::IsCameraCmd(uint32_t id) { + return id == 1 || id == 2 || id == 5 || id == 6; +} + +// These commands use 0x0C-byte entries instead of the standard 0x30. +bool CutsceneSerializer::IsSmallEntryCmd(uint32_t id) { + return id == 0x09 || id == 0x13 || id == 0x8C; +} + +// Single-entry commands (transition: 0x2D, destination: 0x3E8). +bool CutsceneSerializer::IsSingleEntryCmd(uint32_t id) { + return id == 0x2D || id == 0x3E8; +} + +// Commands that are in the valid range (0x0Eā0x90) but not implemented. +const std::set<uint32_t> CutsceneSerializer::sUnimplementedCmds = { + 0x0B, 0x0C, 0x0D, 0x14, 0x15, 0x16, 0x1A, 0x1B, 0x1C, 0x20, 0x21, 0x38, 0x3B, 0x3D, + 0x47, 0x49, 0x5B, 0x5C, 0x5F, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, + 0x6D, 0x70, 0x71, 0x7A +}; + +bool CutsceneSerializer::IsHandledCmd(uint32_t cid) { + // Basic commands (camera, misc, rumble) + if (cid >= 0x01 && cid <= 0x0A) return true; + + // Transition and destination + if (IsSingleEntryCmd(cid)) return true; + + // Text, time, and non-actor-cue commands + if (cid == 0x13 || cid == 0x56 || cid == 0x57 || cid == 0x7C || cid == 0x8C) return true; + + // Actor cue commands (0x0Eā0x90 range, excluding known unimplemented ones) + if (cid >= 0x0E && cid <= 0x90 && !sUnimplementedCmds.count(cid)) return true; + + return false; +} + +// Cutscene serialization ā shared with OoTSceneFactory's SetCutscenes handler. +// Walk through cutscene commands to determine total byte size. +// Returns 0 if the data is corrupt or too small. +uint32_t CutsceneSerializer::CalculateSize(std::vector<uint8_t>& buffer, uint32_t segAddr) { + auto reader = ReadSubArray(buffer, segAddr, 0x10000); + uint32_t endOffset = reader.GetLength(); + + // Need at least numCommands + endFrame + if (endOffset < 8) return 0; + + uint32_t numCommands = reader.ReadUInt32(); + reader.ReadUInt32(); // endFrame + + for (uint32_t cmd = 0; cmd < numCommands; cmd++) { + // Each command is at least 8 bytes (id + entryCount) + if (reader.GetBaseAddress() + 8 > endOffset) return 0; + + uint32_t cmdId = reader.ReadUInt32(); + if (cmdId == 0xFFFFFFFF) break; // end marker + + uint32_t entryCount = reader.ReadUInt32(); + + if (IsCameraCmd(cmdId)) { + // Camera commands have an extra uint32 before the entry list + if (reader.GetBaseAddress() + 4 > endOffset) return 0; + + reader.ReadUInt32(); + for (uint32_t ci = 0; ci < 1000; ci++) { + // Each camera entry is 0x10 bytes + if (reader.GetBaseAddress() + 0x10 > endOffset) return 0; + + uint8_t marker = reader.ReadUByte(); + reader.Seek(reader.GetBaseAddress() + 0x0F, LUS::SeekOffsetType::Start); + if (marker == 0xFF) break; // end of camera entries + } + continue; + } + + if (IsSingleEntryCmd(cmdId)) { + // Single entry is 0x08 bytes + if (reader.GetBaseAddress() + 0x08 > endOffset) return 0; + + reader.Seek(reader.GetBaseAddress() + 0x08, LUS::SeekOffsetType::Start); + continue; + } + + // Default: fixed-size entries (0x0C for rumble/text/time, 0x30 for everything else) + uint32_t entrySize = IsSmallEntryCmd(cmdId) ? 0x0C : 0x30; + uint32_t dataSize = entryCount * entrySize; + + // Ensure all entries fit within the cutscene data + if (reader.GetBaseAddress() + dataSize > endOffset) return 0; + + reader.Seek(reader.GetBaseAddress() + dataSize, LUS::SeekOffsetType::Start); + } + + // Read the end marker (0xFFFFFFFF + 0x00000000) to get final size + if (reader.GetBaseAddress() + 8 > endOffset) return 0; + reader.ReadUInt32(); + reader.ReadUInt32(); + + return reader.GetBaseAddress(); +} + +void CutsceneSerializer::WriteCameraCmd(LUS::BinaryReader& reader, LUS::BinaryWriter& w) { + uint16_t startFrame = reader.ReadUInt16(); + uint16_t endFrame = reader.ReadUInt16(); + w.Write(CS_CMD_HH(startFrame, endFrame)); + + uint16_t unk1 = reader.ReadUInt16(); + uint16_t unk2 = reader.ReadUInt16(); + w.Write(CS_CMD_HH(unk1, unk2)); + + while (true) { + int8_t marker = reader.ReadInt8(); + int8_t interpType = reader.ReadInt8(); + int16_t numPointsForFrame = reader.ReadInt16(); + float viewAngle = reader.ReadFloat(); + int16_t posX = reader.ReadInt16(); + int16_t posY = reader.ReadInt16(); + int16_t posZ = reader.ReadInt16(); + int16_t unused = reader.ReadInt16(); + + w.Write(CS_CMD_BBH(marker, interpType, numPointsForFrame)); + w.Write(viewAngle); + w.Write(CS_CMD_HH(posX, posY)); + w.Write(CS_CMD_HH(posZ, unused)); + + if ((uint8_t)marker == 0xFF) break; + } +} + +void CutsceneSerializer::WriteSingleEntryCmd(LUS::BinaryReader& reader, LUS::BinaryWriter& w) { + reader.ReadUInt32(); // skip original count + w.Write(static_cast<uint32_t>(1)); + + uint16_t startFrame = reader.ReadUInt16(); + uint16_t endFrame = reader.ReadUInt16(); + uint16_t type = reader.ReadUInt16(); + reader.ReadUInt16(); // padding + + w.Write(CS_CMD_HH(startFrame, endFrame)); + w.Write(CS_CMD_HH(type, type)); +} + +// Helper: check if a command uses actor cue format (0x30-byte entries with rotation fields) +static bool IsActorCueCmd(uint32_t cid) { + return cid != 0x03 && cid != 0x04 && cid != 0x56 && cid != 0x57 && cid != 0x7C; +} + +void CutsceneSerializer::WriteEntryCountCmd(uint32_t cid, LUS::BinaryReader& reader, LUS::BinaryWriter& w) { + uint32_t entryCount = reader.ReadUInt32(); + w.Write(entryCount); + + for (uint32_t i = 0; i < entryCount; i++) { + // All entry types share a common header + uint16_t base = reader.ReadUInt16(); + uint16_t startFrame = reader.ReadUInt16(); + uint16_t endFrame = reader.ReadUInt16(); + + w.Write(CS_CMD_HH(base, startFrame)); + + if (cid == 0x09) { + // Rumble (0x0C bytes) + uint8_t sourceStrength = reader.ReadUByte(); + uint8_t duration = reader.ReadUByte(); + uint8_t decreaseRate = reader.ReadUByte(); + uint8_t unk = reader.ReadUByte(); + uint16_t unkA = reader.ReadUInt16(); + + w.Write(CS_CMD_HBB(endFrame, sourceStrength, duration)); + w.Write(CS_CMD_BBH(decreaseRate, unk, unkA)); + continue; + } + + if (cid == 0x13) { + // Text (0x0C bytes) + uint16_t type = reader.ReadUInt16(); + uint16_t textId1 = reader.ReadUInt16(); + uint16_t textId2 = reader.ReadUInt16(); + + w.Write(CS_CMD_HH(endFrame, type)); + w.Write(CS_CMD_HH(textId1, textId2)); + continue; + } + + if (cid == 0x8C) { + // Time (0x0C bytes) + uint8_t hour = reader.ReadUByte(); + uint8_t minute = reader.ReadUByte(); + reader.ReadUInt32(); // padding + + w.Write(CS_CMD_HBB(endFrame, hour, minute)); + w.Write(static_cast<uint32_t>(0)); + continue; + } + + // Actor cue (0x30 bytes) + if (IsActorCueCmd(cid)) { + w.Write(CS_CMD_HH(endFrame, reader.ReadUInt16())); + uint16_t rotY = reader.ReadUInt16(); + uint16_t rotZ = reader.ReadUInt16(); + w.Write(CS_CMD_HH(rotY, rotZ)); + for (int j = 0; j < 9; j++) w.Write(reader.ReadUInt32()); + continue; + } + + // Non-actor-cue (0x30 bytes, commands 0x03, 0x04, 0x56, 0x57, 0x7C) + w.Write(CS_CMD_HH(endFrame, reader.ReadUInt16())); + for (int j = 0; j < 10; j++) w.Write(reader.ReadUInt32()); + } +} + +std::vector<char> CutsceneSerializer::Serialize(std::vector<uint8_t>& buffer, uint32_t segAddr) { + auto size = CalculateSize(buffer, segAddr); + + // Size of 0 means corrupt or empty cutscene data + if (size == 0) return {}; + + return Write(buffer, segAddr, size); +} + +std::vector<char> CutsceneSerializer::Write(std::vector<uint8_t>& buffer, uint32_t segAddr, uint32_t size) { + auto csReader = ReadSubArray(buffer, segAddr, size); + + // Use the actual bytes available (ReadSubArray may return less than requested) + size = csReader.GetLength(); + + LUS::BinaryWriter w; + BaseExporter::WriteHeader(w, Torch::ResourceType::OoTCutscene, 0); + + // Write a placeholder for the cutscene word count. + // We don't know the final count until all commands are serialized, + // so we'll seek back and fill this in at the end. + uint32_t wordCountPos = w.GetStream()->GetLength(); + w.Write(static_cast<uint32_t>(0)); + + // Used to calculate word count at the end + uint32_t dataStartPos = w.GetStream()->GetLength(); + + uint32_t numCmds = csReader.ReadUInt32(); + uint32_t endFrame = csReader.ReadUInt32(); + w.Write(numCmds); + w.Write(endFrame); + + for (uint32_t ci = 0; ci < numCmds && csReader.GetBaseAddress() + 8 <= size; ci++) { + uint32_t cid = csReader.ReadUInt32(); + + // End marker + if (cid == 0xFFFFFFFF) break; + + // Skip unhandled commands (entryCount * 0x30 bytes) + if (!IsHandledCmd(cid)) { + uint32_t entryCount = csReader.ReadUInt32(); + uint32_t dataSize = entryCount * 0x30; + if (csReader.GetBaseAddress() + dataSize <= size) + csReader.Seek(csReader.GetBaseAddress() + dataSize, LUS::SeekOffsetType::Start); + continue; + } + + // Write command ID, then serialize its data + w.Write(cid); + + if (IsCameraCmd(cid)) { + WriteCameraCmd(csReader, w); + continue; + } + + if (IsSingleEntryCmd(cid)) { + WriteSingleEntryCmd(csReader, w); + continue; + } + + // Default: entry-count-based commands + WriteEntryCountCmd(cid, csReader, w); + } + + // Write end marker + w.Write(static_cast<uint32_t>(0xFFFFFFFF)); + w.Write(static_cast<uint32_t>(0)); + + // Fill in the reserved word count now that we know the final size + uint32_t dataEndPos = w.GetStream()->GetLength(); + w.Seek(wordCountPos, LUS::SeekOffsetType::Start); + w.Write(static_cast<uint32_t>((dataEndPos - dataStartPos) / 4)); + w.Seek(dataEndPos, LUS::SeekOffsetType::Start); + + std::stringstream ss; + w.Finish(ss); + std::string str = ss.str(); + return std::vector<char>(str.begin(), str.end()); +} + +class OoTCutsceneData : public IParsedData { +public: + std::vector<char> mBinary; + OoTCutsceneData(std::vector<char> data) : mBinary(std::move(data)) {} +}; + +std::optional<std::shared_ptr<IParsedData>> OoTCutsceneFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) { + auto data = CutsceneSerializer::Serialize(buffer, GetSafeNode<uint32_t>(node, "offset")); + if (data.empty()) { + SPDLOG_WARN("OoTCutsceneFactory: Failed to serialize cutscene"); + return std::nullopt; + } + return std::make_shared<OoTCutsceneData>(std::move(data)); +} + +ExportResult OoTCutsceneBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + auto cs = std::static_pointer_cast<OoTCutsceneData>(raw); + write.write(cs->mBinary.data(), cs->mBinary.size()); + return std::nullopt; +} + +} // namespace OoT |
