1015 lines
34 KiB
C++
1015 lines
34 KiB
C++
#include "ImXui/XurFormat.h"
|
|
#include "ImXui/XurWriter.h"
|
|
#include "ImXui/XurXml.h"
|
|
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <map>
|
|
#include <iostream>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
|
|
#include "zlib.h"
|
|
|
|
#ifdef _WIN32
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#include <windows.h>
|
|
#endif
|
|
|
|
namespace {
|
|
|
|
const char* TOOL_VERSION = "0.1.0";
|
|
|
|
enum class FileFormat { Unknown, XUI, XUR };
|
|
|
|
struct XurHeaderInfo {
|
|
bool valid;
|
|
uint32_t version;
|
|
uint32_t flags;
|
|
uint32_t fileSize;
|
|
uint16_t sectionsCount;
|
|
XurHeaderInfo() : valid(false), version(0), flags(0), fileSize(0), sectionsCount(0) {}
|
|
};
|
|
|
|
std::string lower(const std::string& s) {
|
|
std::string r = s;
|
|
for (size_t i = 0; i < r.size(); i++)
|
|
if (r[i] >= 'A' && r[i] <= 'Z') r[i] = (char)(r[i] - 'A' + 'a');
|
|
return r;
|
|
}
|
|
|
|
std::string ws2s(const std::wstring& ws) {
|
|
return std::string(ws.begin(), ws.end());
|
|
}
|
|
|
|
bool hasExtension(const std::string& path, const char* ext) {
|
|
if (path.size() < strlen(ext)) return false;
|
|
return lower(path.substr(path.size() - strlen(ext))) == ext;
|
|
}
|
|
|
|
|
|
FileFormat detectFormat(const std::string& path) {
|
|
std::vector<uint8_t> head(16, 0);
|
|
{
|
|
std::ifstream ifs(path, std::ios::binary);
|
|
if (ifs) ifs.read((char*)head.data(), 16);
|
|
}
|
|
if (head.size() >= 4 && memcmp(head.data(), "XUIB", 4) == 0)
|
|
return FileFormat::XUR;
|
|
if (head.size() >= 1 && (head[0] == '<' || head[0] == 0xEF || head[0] == 0xFF || head[0] == 0xFE))
|
|
return FileFormat::XUI;
|
|
if (hasExtension(path, ".xur")) return FileFormat::XUR;
|
|
if (hasExtension(path, ".xui")) return FileFormat::XUI;
|
|
return FileFormat::Unknown;
|
|
}
|
|
|
|
XurHeaderInfo readXurHeader(const std::string& path) {
|
|
XurHeaderInfo info;
|
|
std::ifstream ifs(path, std::ios::binary);
|
|
if (!ifs) return info;
|
|
uint8_t bytes[20];
|
|
ifs.read((char*)bytes, 20);
|
|
if (ifs.gcount() < 20) return info;
|
|
if (memcmp(bytes, "XUIB", 4) != 0) return info;
|
|
|
|
auto be32 = [&](size_t off) -> uint32_t {
|
|
return ((uint32_t)bytes[off] << 24) | ((uint32_t)bytes[off + 1] << 16) |
|
|
((uint32_t)bytes[off + 2] << 8) | (uint32_t)bytes[off + 3];
|
|
};
|
|
auto be16 = [&](size_t off) -> uint16_t {
|
|
return (uint16_t)(((uint16_t)bytes[off] << 8) | (uint16_t)bytes[off + 1]);
|
|
};
|
|
info.valid = true;
|
|
info.version = be32(4);
|
|
info.flags = be32(8);
|
|
|
|
info.fileSize = be32(14);
|
|
info.sectionsCount = be16(18);
|
|
return info;
|
|
}
|
|
|
|
const char* formatName(FileFormat f) {
|
|
switch (f) {
|
|
case FileFormat::XUI: return "XUI v12";
|
|
case FileFormat::XUR: return "XUR";
|
|
default: return "unknown";
|
|
}
|
|
}
|
|
|
|
void usage() {
|
|
std::cout <<
|
|
"XUITool " << TOOL_VERSION << " - ImXui alternative to XUIHelper / XuiTool\n"
|
|
"Reads, writes and converts XUI and XUR files (Xbox 360 UI format).\n"
|
|
"\n"
|
|
"Usage:\n"
|
|
" xuitool about\n"
|
|
" Print version and supported formats.\n"
|
|
" xuitool info <file>\n"
|
|
" Detect format and print a summary of a .xui / .xur file.\n"
|
|
" xuitool validate <file>\n"
|
|
" Parse a file and report validity. Exit code 0 = valid.\n"
|
|
" xuitool conv -s <src> -o <dst> [-f xurv5|xuiv12]\n"
|
|
" Convert a single file. Input format is auto-detected; output\n"
|
|
" format comes from -f or is inferred from the output extension.\n"
|
|
" xuitool massconv -s <dir> -o <dir> [-f xurv5|xuiv12] [-r]\n"
|
|
" Convert every .xui / .xur file in a directory (recursive with -r).\n"
|
|
" xuitool swf2xui <file.swf> [output.xui]\n"
|
|
" Convert a Flash SWF to XUI XML (shapes, sprites, buttons).\n";
|
|
}
|
|
|
|
|
|
bool loadObject(const std::string& path, FileFormat fmt, ImXui::XurFormat::XurObject& out) {
|
|
if (fmt == FileFormat::XUR) {
|
|
auto doc = ImXui::XurFormat::XurParser::parseFromFile(path);
|
|
if (!doc || !ImXui::XurFormat::XurParser::validate(doc.get())) return false;
|
|
out = doc->rootObject;
|
|
return true;
|
|
}
|
|
out = ImXui::XurFormat::xuiFileToObject(path);
|
|
return !out.className.empty();
|
|
}
|
|
|
|
|
|
bool writeObject(const std::string& dst, const ImXui::XurFormat::XurObject& obj, bool toXur) {
|
|
if (toXur)
|
|
return ImXui::XurFormat::XurWriter::writeToFile(dst, obj);
|
|
return ImXui::XurFormat::xurObjectToXuiXml(obj, dst);
|
|
}
|
|
|
|
void dumpObjectTree(const ImXui::XurFormat::XurObject& obj, int depth) {
|
|
std::string indent((size_t)depth * 2, ' ');
|
|
std::cout << indent << obj.className << "\n";
|
|
for (auto& p : obj.properties) {
|
|
if (p.name == "Id" && p.type == ImXui::XurFormat::XurPropertyType_String)
|
|
std::cout << indent << " id=" << ws2s(p.value.stringVal) << "\n";
|
|
else if (p.name == "Visual" && p.type == ImXui::XurFormat::XurPropertyType_String)
|
|
std::cout << indent << " visual=" << ws2s(p.value.stringVal) << "\n";
|
|
else if (p.name == "Text" && p.type == ImXui::XurFormat::XurPropertyType_String)
|
|
std::cout << indent << " text=\"" << ws2s(p.value.stringVal) << "\"\n";
|
|
}
|
|
for (auto& c : obj.children)
|
|
dumpObjectTree(c, depth + 1);
|
|
}
|
|
|
|
|
|
int cmdAbout() {
|
|
std::cout <<
|
|
"XUITool " << TOOL_VERSION << "\n"
|
|
"An ImXui-based alternative to XUIHelper / XuiTool for editing the Xbox 360 UI format.\n"
|
|
"\n"
|
|
"Supported formats:\n"
|
|
" XUI v12 - XML scene files (version=\"000c\"), read/write\n"
|
|
" XUR v5 - compiled binary scenes, read/write\n"
|
|
" XUR v8+ - detected and reported, but not read/write capable in this build\n"
|
|
"\n"
|
|
"Library: ImXui (XurFormat, XurParser, XurWriter, XurXml)\n";
|
|
return 0;
|
|
}
|
|
|
|
int cmdInfo(const std::string& path) {
|
|
FileFormat fmt = detectFormat(path);
|
|
std::cout << "File: " << path << "\n";
|
|
std::cout << "Type: " << formatName(fmt) << "\n";
|
|
|
|
if (fmt == FileFormat::XUR) {
|
|
XurHeaderInfo hdr = readXurHeader(path);
|
|
if (!hdr.valid) {
|
|
std::cerr << "Error: not a XUR file\n";
|
|
return 1;
|
|
}
|
|
std::cout << "XUR version: " << hdr.version << "\n";
|
|
std::cout << "Flags: " << hdr.flags << "\n";
|
|
std::cout << "File size: " << hdr.fileSize << " bytes\n";
|
|
std::cout << "Sections: " << hdr.sectionsCount << "\n";
|
|
if (hdr.version != 5) {
|
|
std::cout << "Note: this build can only read/write XUR v5.\n";
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
ImXui::XurFormat::XurObject obj;
|
|
if (!loadObject(path, fmt, obj)) {
|
|
std::cerr << "Error: failed to parse file\n";
|
|
return 1;
|
|
}
|
|
std::cout << "Root object: " << obj.className
|
|
<< " (" << obj.properties.size() << " props, "
|
|
<< obj.children.size() << " children)\n";
|
|
std::cout << "Object tree:\n";
|
|
dumpObjectTree(obj, 0);
|
|
return 0;
|
|
}
|
|
|
|
int cmdValidate(const std::string& path) {
|
|
FileFormat fmt = detectFormat(path);
|
|
if (fmt == FileFormat::XUR) {
|
|
XurHeaderInfo hdr = readXurHeader(path);
|
|
if (!hdr.valid) {
|
|
std::cerr << "Error: not a XUR file\n";
|
|
return 1;
|
|
}
|
|
if (hdr.version != 5) {
|
|
std::cerr << "Error: XUR v" << hdr.version << " is not supported (only v5)\n";
|
|
return 1;
|
|
}
|
|
auto doc = ImXui::XurFormat::XurParser::parseFromFile(path);
|
|
if (!doc || !ImXui::XurFormat::XurParser::validate(doc.get())) {
|
|
std::cerr << "Error: invalid XUR v5\n";
|
|
return 1;
|
|
}
|
|
std::cout << "OK: valid XUR v5\n";
|
|
return 0;
|
|
}
|
|
if (fmt == FileFormat::XUI) {
|
|
ImXui::XurFormat::XurObject obj = ImXui::XurFormat::xuiFileToObject(path);
|
|
if (obj.className.empty()) {
|
|
std::cerr << "Error: invalid XUI v12\n";
|
|
return 1;
|
|
}
|
|
std::cout << "OK: valid XUI v12\n";
|
|
return 0;
|
|
}
|
|
std::cerr << "Error: unrecognized file format\n";
|
|
return 1;
|
|
}
|
|
|
|
bool convertFile(const std::string& src, const std::string& dst,
|
|
FileFormat srcFmt, const std::string& outFormat) {
|
|
ImXui::XurFormat::XurObject obj;
|
|
if (!loadObject(src, srcFmt, obj)) {
|
|
std::cerr << "Error: failed to read '" << src << "'\n";
|
|
return false;
|
|
}
|
|
|
|
bool toXur;
|
|
if (outFormat == "xurv5") toXur = true;
|
|
else if (outFormat == "xuiv12") toXur = false;
|
|
else toXur = hasExtension(dst, ".xur");
|
|
|
|
if (!writeObject(dst, obj, toXur)) {
|
|
std::cerr << "Error: failed to write '" << dst << "'\n";
|
|
return false;
|
|
}
|
|
std::cout << "Converted " << src << " -> " << dst << "\n";
|
|
return true;
|
|
}
|
|
|
|
int cmdConv(int argc, char* argv[]) {
|
|
std::string src, dst, outFormat;
|
|
for (int i = 2; i < argc; i++) {
|
|
std::string a = argv[i];
|
|
if (a == "-s" && i + 1 < argc) src = argv[++i];
|
|
else if (a == "-o" && i + 1 < argc) dst = argv[++i];
|
|
else if (a == "-f" && i + 1 < argc) outFormat = lower(argv[++i]);
|
|
else {
|
|
std::cerr << "Unknown argument: " << a << "\n";
|
|
usage();
|
|
return 1;
|
|
}
|
|
}
|
|
if (src.empty() || dst.empty()) {
|
|
std::cerr << "conv requires -s <src> and -o <dst>\n";
|
|
usage();
|
|
return 1;
|
|
}
|
|
if (!outFormat.empty() && outFormat != "xurv5" && outFormat != "xuiv12") {
|
|
std::cerr << "Unknown output format '" << outFormat
|
|
<< "' (must be xurv5 or xuiv12)\n";
|
|
return 1;
|
|
}
|
|
|
|
FileFormat srcFmt = detectFormat(src);
|
|
if (srcFmt == FileFormat::Unknown) {
|
|
std::cerr << "Error: cannot detect format of '" << src << "'\n";
|
|
return 1;
|
|
}
|
|
if (srcFmt == FileFormat::XUR) {
|
|
XurHeaderInfo hdr = readXurHeader(src);
|
|
if (hdr.valid && hdr.version != 5) {
|
|
std::cerr << "Error: XUR v" << hdr.version << " is not supported (only v5)\n";
|
|
return 1;
|
|
}
|
|
}
|
|
return convertFile(src, dst, srcFmt, outFormat) ? 0 : 1;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
std::vector<std::string> listFiles(const std::string& dir, bool recursive) {
|
|
std::vector<std::string> files;
|
|
std::string pattern = dir + "\\*";
|
|
WIN32_FIND_DATAA fd;
|
|
HANDLE h = FindFirstFileA(pattern.c_str(), &fd);
|
|
if (h == INVALID_HANDLE_VALUE) return files;
|
|
do {
|
|
std::string name = fd.cFileName;
|
|
if (name == "." || name == "..") continue;
|
|
std::string full = dir + "\\" + name;
|
|
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
|
|
if (recursive) {
|
|
std::vector<std::string> sub = listFiles(full, recursive);
|
|
files.insert(files.end(), sub.begin(), sub.end());
|
|
}
|
|
} else if (hasExtension(name, ".xui") || hasExtension(name, ".xur")) {
|
|
files.push_back(full);
|
|
}
|
|
} while (FindNextFileA(h, &fd));
|
|
FindClose(h);
|
|
return files;
|
|
}
|
|
#else
|
|
#include <dirent.h>
|
|
#include <sys/stat.h>
|
|
|
|
static std::vector<std::string> listFiles(const std::string& dir, bool recursive) {
|
|
std::vector<std::string> files;
|
|
DIR* d = opendir(dir.c_str());
|
|
if (!d) return files;
|
|
struct dirent* ent;
|
|
while ((ent = readdir(d)) != nullptr) {
|
|
std::string name = ent->d_name;
|
|
if (name == "." || name == "..") continue;
|
|
std::string full = dir + "/" + name;
|
|
struct stat st;
|
|
if (stat(full.c_str(), &st) != 0) continue;
|
|
if (S_ISDIR(st.st_mode)) {
|
|
if (recursive) {
|
|
std::vector<std::string> sub = listFiles(full, recursive);
|
|
files.insert(files.end(), sub.begin(), sub.end());
|
|
}
|
|
} else if (hasExtension(name, ".xui") || hasExtension(name, ".xur")) {
|
|
files.push_back(full);
|
|
}
|
|
}
|
|
closedir(d);
|
|
return files;
|
|
}
|
|
#endif
|
|
|
|
int cmdMassConv(int argc, char* argv[]) {
|
|
std::string srcDir, dstDir, outFormat;
|
|
bool recursive = false;
|
|
for (int i = 2; i < argc; i++) {
|
|
std::string a = argv[i];
|
|
if (a == "-s" && i + 1 < argc) srcDir = argv[++i];
|
|
else if (a == "-o" && i + 1 < argc) dstDir = argv[++i];
|
|
else if (a == "-f" && i + 1 < argc) outFormat = lower(argv[++i]);
|
|
else if (a == "-r") recursive = true;
|
|
else {
|
|
std::cerr << "Unknown argument: " << a << "\n";
|
|
usage();
|
|
return 1;
|
|
}
|
|
}
|
|
if (srcDir.empty() || dstDir.empty()) {
|
|
std::cerr << "massconv requires -s <dir> and -o <dir>\n";
|
|
usage();
|
|
return 1;
|
|
}
|
|
if (!outFormat.empty() && outFormat != "xurv5" && outFormat != "xuiv12") {
|
|
std::cerr << "Unknown output format '" << outFormat
|
|
<< "' (must be xurv5 or xuiv12)\n";
|
|
return 1;
|
|
}
|
|
if (outFormat.empty()) outFormat = "xuiv12";
|
|
|
|
#ifdef _WIN32
|
|
CreateDirectoryA(dstDir.c_str(), nullptr);
|
|
#else
|
|
mkdir(dstDir.c_str(), 0755);
|
|
#endif
|
|
|
|
std::vector<std::string> files = listFiles(srcDir, recursive);
|
|
int ok = 0, failed = 0, skipped = 0;
|
|
for (auto& src : files) {
|
|
FileFormat srcFmt = detectFormat(src);
|
|
if (srcFmt == FileFormat::Unknown) { skipped++; continue; }
|
|
|
|
size_t sep = src.find_last_of("\\/");
|
|
std::string base = (sep == std::string::npos) ? src : src.substr(sep + 1);
|
|
size_t dot = base.find_last_of('.');
|
|
if (dot != std::string::npos) base = base.substr(0, dot);
|
|
|
|
std::string dst = dstDir + "\\" + base + (outFormat == "xurv5" ? ".xur" : ".xui");
|
|
|
|
bool srcIsXur = (srcFmt == FileFormat::XUR);
|
|
bool dstIsXur = (outFormat == "xurv5");
|
|
if (srcIsXur == dstIsXur) {
|
|
std::cout << "Skipping (already " << (srcIsXur ? "xurv5" : "xuiv12") << "): " << src << "\n";
|
|
skipped++;
|
|
continue;
|
|
}
|
|
|
|
if (srcFmt == FileFormat::XUR) {
|
|
XurHeaderInfo hdr = readXurHeader(src);
|
|
if (hdr.valid && hdr.version != 5) {
|
|
std::cout << "Skipping (unsupported XUR v" << hdr.version << "): " << src << "\n";
|
|
skipped++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (convertFile(src, dst, srcFmt, outFormat)) ok++;
|
|
else failed++;
|
|
}
|
|
std::cout << "massconv: " << ok << " converted, " << failed << " failed, "
|
|
<< skipped << " skipped\n";
|
|
return failed == 0 ? 0 : 1;
|
|
}
|
|
|
|
static bool decompressSWF(const uint8_t* compressed, size_t csize, std::vector<uint8_t>& out, size_t uncompressedSize) {
|
|
if (csize < 2) return false;
|
|
|
|
out.reserve(uncompressedSize > 0 ? uncompressedSize : 65536);
|
|
|
|
z_stream zs;
|
|
memset(&zs, 0, sizeof(zs));
|
|
// CWS is a zlib stream; skip the 2-byte header and inflate the raw deflate
|
|
if (inflateInit2(&zs, -MAX_WBITS) != Z_OK) return false;
|
|
|
|
zs.next_in = (Bytef*)(compressed + 2);
|
|
zs.avail_in = (uInt)(csize - 2);
|
|
|
|
std::vector<uint8_t> buf(65536);
|
|
int ret = Z_OK;
|
|
while (ret != Z_STREAM_END) {
|
|
zs.next_out = buf.data();
|
|
zs.avail_out = (uInt)buf.size();
|
|
ret = inflate(&zs, Z_NO_FLUSH);
|
|
if (ret != Z_OK && ret != Z_STREAM_END) {
|
|
inflateEnd(&zs);
|
|
return false;
|
|
}
|
|
out.insert(out.end(), buf.begin(), buf.end() - zs.avail_out);
|
|
}
|
|
inflateEnd(&zs);
|
|
return true;
|
|
}
|
|
|
|
#pragma pack(push, 1)
|
|
struct SwfHeader {
|
|
uint8_t signature[3];
|
|
uint8_t version;
|
|
uint32_t fileLength;
|
|
};
|
|
#pragma pack(pop)
|
|
|
|
struct SwfRect {
|
|
int32_t xMin, xMax, yMin, yMax;
|
|
};
|
|
|
|
struct SwfTag {
|
|
uint16_t type;
|
|
uint32_t length;
|
|
std::vector<uint8_t> data;
|
|
};
|
|
|
|
static uint8_t readU8(const uint8_t*& p) { return *p++; }
|
|
static uint16_t readU16(const uint8_t*& p) {
|
|
uint16_t v;
|
|
memcpy(&v, p, 2);
|
|
p += 2;
|
|
return v;
|
|
}
|
|
static uint32_t readU32(const uint8_t*& p) {
|
|
uint32_t v;
|
|
memcpy(&v, p, 4);
|
|
p += 4;
|
|
return v;
|
|
}
|
|
|
|
struct SwfMatrix {
|
|
float scaleX, scaleY;
|
|
float rotateSkew0, rotateSkew1;
|
|
float translateX, translateY;
|
|
};
|
|
|
|
static SwfRect readRect(const uint8_t*& p) {
|
|
SwfRect r = {};
|
|
uint8_t nBits = (*p) >> 3;
|
|
uint32_t bitBuf = 0;
|
|
int bitsRead = 0;
|
|
|
|
auto readBits = [&](int n) -> uint32_t {
|
|
uint32_t val = 0;
|
|
for (int i = 0; i < n; i++) {
|
|
if (bitsRead % 8 == 0) {
|
|
bitBuf = *p++;
|
|
}
|
|
val = (val << 1) | ((bitBuf >> (7 - (bitsRead % 8))) & 1);
|
|
bitsRead++;
|
|
}
|
|
return val;
|
|
};
|
|
|
|
bitBuf = *p++;
|
|
bitsRead = 5;
|
|
|
|
int32_t vals[4];
|
|
for (int i = 0; i < 4; i++) {
|
|
uint32_t v = readBits(nBits);
|
|
vals[i] = (int32_t)v;
|
|
}
|
|
|
|
r.xMin = vals[0] / 20;
|
|
r.xMax = vals[1] / 20;
|
|
r.yMin = vals[2] / 20;
|
|
r.yMax = vals[3] / 20;
|
|
|
|
if (bitsRead % 8 != 0) {
|
|
int skip = 8 - (bitsRead % 8);
|
|
readBits(skip);
|
|
}
|
|
|
|
return r;
|
|
}
|
|
|
|
static SwfMatrix readMatrix(const uint8_t*& p) {
|
|
SwfMatrix m = {1, 0, 0, 1, 0, 0};
|
|
uint32_t bitBuf = 0;
|
|
int bitsRead = 0;
|
|
|
|
auto readBits = [&](int n) -> uint32_t {
|
|
uint32_t val = 0;
|
|
for (int i = 0; i < n; i++) {
|
|
if (bitsRead % 8 == 0) {
|
|
bitBuf = *p++;
|
|
}
|
|
val = (val << 1) | ((bitBuf >> (7 - (bitsRead % 8))) & 1);
|
|
bitsRead++;
|
|
}
|
|
return val;
|
|
};
|
|
|
|
auto readSBits = [&](int n) -> int32_t {
|
|
uint32_t v = readBits(n);
|
|
if (v & (1u << (n - 1))) {
|
|
v |= ~((1u << n) - 1);
|
|
}
|
|
return (int32_t)v;
|
|
};
|
|
|
|
uint8_t hasScale = readBits(1);
|
|
if (hasScale) {
|
|
int nScaleBits = readBits(5);
|
|
m.scaleX = (float)readSBits(nScaleBits) / 65536.0f;
|
|
m.scaleY = (float)readSBits(nScaleBits) / 65536.0f;
|
|
}
|
|
uint8_t hasRotate = readBits(1);
|
|
if (hasRotate) {
|
|
int nRotateBits = readBits(5);
|
|
m.rotateSkew0 = (float)readSBits(nRotateBits) / 65536.0f;
|
|
m.rotateSkew1 = (float)readSBits(nRotateBits) / 65536.0f;
|
|
}
|
|
int nTranslateBits = readBits(5);
|
|
if (nTranslateBits > 0) {
|
|
m.translateX = (float)readSBits(nTranslateBits) / 20.0f;
|
|
m.translateY = (float)readSBits(nTranslateBits) / 20.0f;
|
|
}
|
|
|
|
if (bitsRead % 8 != 0) {
|
|
int skip = 8 - (bitsRead % 8);
|
|
readBits(skip);
|
|
}
|
|
|
|
return m;
|
|
}
|
|
|
|
class SwfParser {
|
|
public:
|
|
std::vector<uint8_t> data;
|
|
SwfHeader header;
|
|
SwfRect frameSize;
|
|
float frameRate;
|
|
uint16_t frameCount;
|
|
std::vector<SwfTag> tags;
|
|
|
|
bool parse(const std::string& path) {
|
|
std::ifstream ifs(path, std::ios::binary | std::ios::ate);
|
|
if (!ifs) return false;
|
|
std::streamsize sz = ifs.tellg();
|
|
ifs.seekg(0);
|
|
data.resize((size_t)sz);
|
|
if (!ifs.read((char*)data.data(), sz)) return false;
|
|
|
|
const uint8_t* p = data.data();
|
|
memcpy(&header, p, sizeof(SwfHeader));
|
|
p += sizeof(SwfHeader);
|
|
|
|
if (header.signature[0] != 'F' && header.signature[0] != 'C') return false;
|
|
if (header.signature[1] != 'W' || header.signature[2] != 'S') return false;
|
|
|
|
bool compressed = (header.signature[0] == 'C');
|
|
if (compressed) {
|
|
size_t uncompLen = header.fileLength;
|
|
const uint8_t* compStart = p;
|
|
size_t compSize = data.size() - (size_t)(p - data.data());
|
|
std::vector<uint8_t> decomp;
|
|
if (!decompressSWF(compStart, compSize, decomp, uncompLen)) {
|
|
std::cerr << " Decompression failed\n";
|
|
return false;
|
|
}
|
|
|
|
size_t hdrBytes = (size_t)(p - data.data());
|
|
std::vector<uint8_t> newData(hdrBytes + decomp.size());
|
|
memcpy(newData.data(), data.data(), hdrBytes);
|
|
if (!decomp.empty()) memcpy(newData.data() + hdrBytes, decomp.data(), decomp.size());
|
|
newData.swap(data);
|
|
p = data.data() + sizeof(SwfHeader);
|
|
header.signature[0] = 'F';
|
|
}
|
|
|
|
frameSize = readRect(p);
|
|
|
|
frameRate = (float)readU16(p) / 256.0f;
|
|
frameCount = readU16(p);
|
|
|
|
while ((size_t)(p - data.data()) < data.size()) {
|
|
uint16_t tagTypeAndLen = readU16(p);
|
|
uint16_t tagType = tagTypeAndLen >> 6;
|
|
uint32_t tagLen = tagTypeAndLen & 0x3F;
|
|
|
|
if (tagLen == 0x3F) {
|
|
tagLen = readU32(p);
|
|
}
|
|
|
|
if (tagType == 0) break;
|
|
|
|
SwfTag tag;
|
|
tag.type = tagType;
|
|
tag.length = tagLen;
|
|
if (tagLen > 0 && (size_t)(p - data.data() + tagLen) <= data.size()) {
|
|
tag.data.assign(p, p + tagLen);
|
|
p += tagLen;
|
|
}
|
|
|
|
tags.push_back(tag);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
};
|
|
|
|
static std::string escapeXml(const std::string& s) {
|
|
std::string r;
|
|
for (char c : s) {
|
|
switch (c) {
|
|
case '&': r += "&"; break;
|
|
case '<': r += "<"; break;
|
|
case '>': r += ">"; break;
|
|
case '\"': r += """; break;
|
|
case '\'': r += "'"; break;
|
|
default: r += c;
|
|
}
|
|
}
|
|
return r;
|
|
}
|
|
|
|
struct XuiElement {
|
|
std::string className;
|
|
std::map<std::string, std::string> props;
|
|
std::vector<XuiElement> children;
|
|
};
|
|
|
|
static void writeXuiElement(std::ostream& os, const XuiElement& el, int depth) {
|
|
std::string indent(depth * 2, ' ');
|
|
os << indent << "<" << el.className << ">\n";
|
|
if (!el.props.empty()) {
|
|
os << indent << " <Properties>\n";
|
|
for (auto& prop : el.props) {
|
|
os << indent << " <" << prop.first << ">" << escapeXml(prop.second) << "</" << prop.first << ">\n";
|
|
}
|
|
os << indent << " </Properties>\n";
|
|
}
|
|
for (auto& child : el.children) {
|
|
writeXuiElement(os, child, depth + 1);
|
|
}
|
|
os << indent << "</" << el.className << ">\n";
|
|
}
|
|
|
|
static void addProp(XuiElement& el, const std::string& key, const std::string& val) {
|
|
el.props[key] = val;
|
|
}
|
|
|
|
static std::string floatStr(float v) {
|
|
std::ostringstream ss;
|
|
ss << v;
|
|
return ss.str();
|
|
}
|
|
|
|
int cmdSwf2Xui(int argc, char* argv[]) {
|
|
if (argc < 2) {
|
|
std::cerr << "Usage: xuitool swf2xui <input.swf> [output.xui]\n";
|
|
std::cerr << " Converts basic Flash SWF files to XUI XML format.\n";
|
|
std::cerr << " Handles shapes, sprites, place/remove objects, and buttons.\n";
|
|
return 1;
|
|
}
|
|
|
|
std::string inPath = argv[1];
|
|
std::string outPath;
|
|
if (argc >= 3) {
|
|
outPath = argv[2];
|
|
} else {
|
|
if (inPath.size() >= 4 && inPath.substr(inPath.size() - 4) == ".swf") {
|
|
outPath = inPath.substr(0, inPath.size() - 4) + ".xui";
|
|
} else {
|
|
outPath = inPath + ".xui";
|
|
}
|
|
}
|
|
|
|
SwfParser parser;
|
|
if (!parser.parse(inPath)) {
|
|
std::cerr << "Error: failed to parse SWF file '" << inPath << "'\n";
|
|
std::cerr << "Note: Corrupt or unsupported SWF format\n";
|
|
return 1;
|
|
}
|
|
|
|
std::cout << "SWF: " << inPath << "\n";
|
|
std::cout << " Version: " << (int)parser.header.version << "\n";
|
|
std::cout << " FrameSize: " << parser.frameSize.xMax << "x" << parser.frameSize.yMax << "\n";
|
|
std::cout << " FrameRate: " << parser.frameRate << "\n";
|
|
std::cout << " FrameCount: " << parser.frameCount << "\n";
|
|
std::cout << " Tags: " << parser.tags.size() << "\n";
|
|
std::cout << " Tag types: ";
|
|
for (auto& tag : parser.tags) std::cout << tag.type << "(" << tag.length << ") ";
|
|
std::cout << "\n";
|
|
|
|
XuiElement root;
|
|
root.className = "XuiCanvas";
|
|
addProp(root, "Width", floatStr((float)parser.frameSize.xMax));
|
|
addProp(root, "Height", floatStr((float)parser.frameSize.yMax));
|
|
|
|
std::map<uint16_t, std::pair<uint16_t, std::vector<uint8_t>>> characters;
|
|
|
|
for (auto& tag : parser.tags) {
|
|
if (tag.type >= 1 && tag.type <= 88) {
|
|
switch (tag.type) {
|
|
case 76:
|
|
{
|
|
const uint8_t* sp = tag.data.data();
|
|
const uint8_t* send = tag.data.data() + tag.data.size();
|
|
uint16_t numSymbols = readU16(sp);
|
|
std::cout << " SymbolClass: " << numSymbols << " symbols\n";
|
|
for (uint16_t si = 0; si < numSymbols && sp < send; si++) {
|
|
uint16_t tagId = readU16(sp);
|
|
std::string name;
|
|
while (sp < send && *sp) { name += (char)*sp++; }
|
|
if (sp < send) sp++;
|
|
std::cout << " Symbol " << tagId << " -> " << name << "\n";
|
|
}
|
|
break;
|
|
}
|
|
case 2:
|
|
case 22:
|
|
case 32:
|
|
case 83:
|
|
case 11:
|
|
case 33:
|
|
case 37:
|
|
case 39:
|
|
case 34:
|
|
case 35:
|
|
{
|
|
const uint8_t* p = tag.data.data();
|
|
uint16_t charId = readU16(p);
|
|
characters[charId] = std::make_pair(tag.type, tag.data);
|
|
std::cout << " CharDef id=" << charId << " type=" << tag.type << " len=" << tag.length << "\n";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::map<int32_t, XuiElement> displayElements;
|
|
|
|
for (auto& tag : parser.tags) {
|
|
switch (tag.type) {
|
|
case 26:
|
|
{
|
|
const uint8_t* tp = tag.data.data();
|
|
const uint8_t* tend = tag.data.data() + tag.data.size();
|
|
uint8_t flags = readU8(tp);
|
|
uint8_t charIdByte = readU8(tp);
|
|
uint16_t charId = charIdByte;
|
|
SwfMatrix mat = {1, 0, 0, 1, 0, 0};
|
|
if (tp < tend) mat = readMatrix(tp);
|
|
|
|
static int po26Count = 0;
|
|
po26Count++;
|
|
|
|
auto it = characters.find(charId);
|
|
if (it != characters.end()) {
|
|
XuiElement el;
|
|
if (it->second.first == 39) {
|
|
el.className = "XuiScene";
|
|
} else if (it->second.first >= 11 && it->second.first <= 37) {
|
|
el.className = "XuiLabel";
|
|
} else {
|
|
el.className = "XuiImage";
|
|
}
|
|
addProp(el, "Id", "swf_char_" + std::to_string(charId) + "_p" + std::to_string(po26Count));
|
|
addProp(el, "X", floatStr(mat.translateX));
|
|
addProp(el, "Y", floatStr(mat.translateY));
|
|
addProp(el, "Width", "100.000000");
|
|
addProp(el, "Height", "100.000000");
|
|
addProp(el, "Show", "true");
|
|
std::cout << " PlaceObject26 flags=0x" << std::hex << (int)flags << std::dec << " charId=" << charId << " pos=(" << mat.translateX << "," << mat.translateY << ")\n";
|
|
displayElements[po26Count] = el;
|
|
} else {
|
|
std::cout << " PlaceObject26 flags=0x" << std::hex << (int)flags << std::dec << " charId=" << charId << " NOT FOUND\n";
|
|
}
|
|
break;
|
|
}
|
|
case 28:
|
|
{
|
|
const uint8_t* tp = tag.data.data();
|
|
uint16_t charId = readU16(tp);
|
|
int32_t depth = readU16(tp);
|
|
std::cout << " RemoveObject28 charId=" << charId << " depth=" << depth << "\n";
|
|
displayElements.erase(depth);
|
|
break;
|
|
}
|
|
case 33:
|
|
{
|
|
const uint8_t* tp = tag.data.data();
|
|
int32_t depth = readU16(tp);
|
|
std::cout << " RemoveObject33 depth=" << depth << "\n";
|
|
displayElements.erase(depth);
|
|
break;
|
|
}
|
|
case 43:
|
|
{
|
|
const uint8_t* tp = tag.data.data();
|
|
const uint8_t* tend = tag.data.data() + tag.data.size();
|
|
uint8_t flags = readU8(tp);
|
|
int32_t depth = readU16(tp);
|
|
|
|
uint16_t charId = 0;
|
|
SwfMatrix mat = {1, 0, 0, 1, 0, 0};
|
|
|
|
if (flags & 0x02) { charId = readU16(tp); }
|
|
if (flags & 0x04) {
|
|
if (tp < tend) mat = readMatrix(tp);
|
|
}
|
|
if (flags & 0x08) { }
|
|
if (flags & 0x10) { readU16(tp); }
|
|
if (flags & 0x20) {
|
|
while (tp < tend && *tp) tp++;
|
|
if (tp < tend) tp++;
|
|
}
|
|
if (flags & 0x40) { readU16(tp); }
|
|
if (flags & 0x80) { }
|
|
|
|
std::cout << " PlaceObject43 depth=" << depth << " charId=" << charId << " flags=0x" << std::hex << (int)flags << std::dec << " pos=(" << mat.translateX << "," << mat.translateY << ") found=" << characters.count(charId) << "\n";
|
|
|
|
if (flags & 0x01) {
|
|
displayElements.erase(depth);
|
|
} else {
|
|
auto it = characters.find(charId);
|
|
if (it != characters.end()) {
|
|
XuiElement el;
|
|
if (it->second.first == 39) {
|
|
el.className = "XuiScene";
|
|
} else if (it->second.first >= 34 && it->second.first <= 35) {
|
|
el.className = "XuiButton";
|
|
} else if (it->second.first >= 11 && it->second.first <= 37) {
|
|
el.className = "XuiLabel";
|
|
} else {
|
|
el.className = "XuiImage";
|
|
}
|
|
addProp(el, "Id", "swf_char_" + std::to_string(charId) + "_d" + std::to_string(depth));
|
|
addProp(el, "X", floatStr(mat.translateX));
|
|
addProp(el, "Y", floatStr(mat.translateY));
|
|
addProp(el, "Width", floatStr((float)parser.frameSize.xMax / 10.0f));
|
|
addProp(el, "Height", floatStr((float)parser.frameSize.yMax / 10.0f));
|
|
displayElements[depth] = el;
|
|
} else {
|
|
XuiElement el;
|
|
el.className = "XuiControl";
|
|
addProp(el, "Id", "swf_unknown_d" + std::to_string(depth));
|
|
addProp(el, "X", floatStr(mat.translateX));
|
|
addProp(el, "Y", floatStr(mat.translateY));
|
|
addProp(el, "Width", "50.000000");
|
|
addProp(el, "Height", "50.000000");
|
|
displayElements[depth] = el;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 70:
|
|
{
|
|
const uint8_t* tp = tag.data.data();
|
|
const uint8_t* tend = tag.data.data() + tag.data.size();
|
|
uint8_t flags = readU8(tp);
|
|
uint8_t flags2 = readU8(tp);
|
|
int32_t depth = readU16(tp);
|
|
|
|
std::string className;
|
|
while (tp < tend && *tp) { className += (char)*tp; }
|
|
if (tp < tend) tp++;
|
|
|
|
std::string instanceName;
|
|
{
|
|
const uint8_t* s = tend - 1;
|
|
while (s > tp && *s == 0) s--;
|
|
const uint8_t* strEnd = s + 1;
|
|
while (s > tp && *s != 0 && *s >= 0x20 && *s < 0x7F) s--;
|
|
s++;
|
|
if (strEnd > s) {
|
|
for (const uint8_t* p2 = s; p2 < strEnd; p2++) instanceName += (char)*p2;
|
|
while (!instanceName.empty() &&
|
|
!((instanceName[0] >= 'A' && instanceName[0] <= 'Z') ||
|
|
(instanceName[0] >= 'a' && instanceName[0] <= 'z') ||
|
|
(instanceName[0] >= '0' && instanceName[0] <= '9') ||
|
|
instanceName[0] == '_')) {
|
|
instanceName.erase(0, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
std::cout << " PlaceObject70 depth=" << depth << " class=\"" << className << "\"";
|
|
if (!instanceName.empty()) std::cout << " instance=\"" << instanceName << "\"";
|
|
std::cout << " tagLen=" << tag.length << "\n";
|
|
|
|
if (flags & 0x01) {
|
|
displayElements.erase(depth);
|
|
} else {
|
|
XuiElement el;
|
|
if (className.find("Button") != std::string::npos ||
|
|
className.find("button") != std::string::npos) {
|
|
el.className = "XuiButton";
|
|
} else if (className.find("Label") != std::string::npos ||
|
|
className.find("Text") != std::string::npos ||
|
|
className.find("text") != std::string::npos) {
|
|
el.className = "XuiLabel";
|
|
} else if (className.find("Image") != std::string::npos ||
|
|
className.find("Graphic") != std::string::npos) {
|
|
el.className = "XuiImage";
|
|
} else if (className.find("Scene") != std::string::npos ||
|
|
className.find("Menu") != std::string::npos ||
|
|
className.find("Panel") != std::string::npos ||
|
|
className.find("Dialog") != std::string::npos ||
|
|
className.find("Canvas") != std::string::npos) {
|
|
el.className = "XuiScene";
|
|
} else {
|
|
el.className = "XuiControl";
|
|
}
|
|
|
|
std::string id = !instanceName.empty() ? instanceName : className;
|
|
addProp(el, "Id", id);
|
|
addProp(el, "ClassName", className);
|
|
addProp(el, "X", "0.000000");
|
|
addProp(el, "Y", "0.000000");
|
|
addProp(el, "Width", floatStr((float)parser.frameSize.xMax / 10.0f));
|
|
addProp(el, "Height", floatStr((float)parser.frameSize.yMax / 10.0f));
|
|
displayElements[depth] = el;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (auto& de : displayElements) {
|
|
root.children.push_back(de.second);
|
|
}
|
|
|
|
std::ofstream ofs(outPath);
|
|
if (!ofs) {
|
|
std::cerr << "Error: cannot write to '" << outPath << "'\n";
|
|
return 1;
|
|
}
|
|
|
|
writeXuiElement(ofs, root, 0);
|
|
std::cout << "Wrote " << outPath << "\n";
|
|
return 0;
|
|
}
|
|
|
|
}
|
|
|
|
int main(int argc, char* argv[]) {
|
|
if (argc < 2) {
|
|
usage();
|
|
return 1;
|
|
}
|
|
|
|
std::string cmd = lower(argv[1]);
|
|
if (cmd == "about") return cmdAbout();
|
|
if (cmd == "info") {
|
|
if (argc < 3) { std::cerr << "Usage: xuitool info <file>\n"; return 1; }
|
|
return cmdInfo(argv[2]);
|
|
}
|
|
if (cmd == "validate") {
|
|
if (argc < 3) { std::cerr << "Usage: xuitool validate <file>\n"; return 1; }
|
|
return cmdValidate(argv[2]);
|
|
}
|
|
if (cmd == "conv") return cmdConv(argc, argv);
|
|
if (cmd == "massconv") return cmdMassConv(argc, argv);
|
|
if (cmd == "swf2xui") return cmdSwf2Xui(argc - 1, argv + 1);
|
|
|
|
std::cerr << "Unknown command: " << argv[1] << "\n";
|
|
usage();
|
|
return 1;
|
|
}
|