Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 230 additions & 8 deletions editor/Exporter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

#include <algorithm>
#include <cctype>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <fstream>
Expand All @@ -32,6 +33,15 @@ namespace {
constexpr float BUILD_PROGRESS_START = 0.6f;
constexpr float BUILD_PROGRESS_END = 0.95f;
constexpr float MSBUILD_COMPILE_END = 0.9f;
constexpr char RESOURCE_PACK_MAGIC[] = {'D', 'X', 'P', 'K', '1'};

struct ResourcePackBuildEntry {
std::string path;
std::vector<unsigned char> data;
uint64_t offset = 0;
uint8_t key = 0;
uint32_t shift = 0;
};

// Parses make-style "[ 47%]" and ninja-style "[123/456]" build-line prefixes
// into a 0..1 fraction so the compile step can drive the progress bar.
Expand Down Expand Up @@ -59,6 +69,51 @@ namespace {
return false;
}
}

void writeU8(std::ofstream& out, uint8_t value) {
out.put(static_cast<char>(value));
}

void writeU16(std::ofstream& out, uint16_t value) {
out.put(static_cast<char>(value & 0xff));
out.put(static_cast<char>((value >> 8) & 0xff));
}

void writeU32(std::ofstream& out, uint32_t value) {
for (int i = 0; i < 4; i++) {
out.put(static_cast<char>((value >> (i * 8)) & 0xff));
}
}

void writeU64(std::ofstream& out, uint64_t value) {
for (int i = 0; i < 8; i++) {
out.put(static_cast<char>((value >> (i * 8)) & 0xff));
}
}

void shiftRight(std::vector<unsigned char>& data, uint32_t shift) {
if (data.empty()) return;
shift %= static_cast<uint32_t>(data.size());
if (shift == 0) return;
std::rotate(data.begin(), data.end() - shift, data.end());
}

void applyXor(std::vector<unsigned char>& data, uint8_t key) {
for (unsigned char& byte : data) {
byte ^= key;
}
}

bool readWholeFile(const fs::path& path, std::vector<unsigned char>& data) {
std::ifstream in(path, std::ios::binary);
if (!in) return false;
in.seekg(0, std::ios::end);
std::streamoff size = in.tellg();
if (size < 0) return false;
in.seekg(0, std::ios::beg);
data.resize(static_cast<size_t>(size));
return data.empty() || static_cast<bool>(in.read(reinterpret_cast<char*>(data.data()), size));
}
}

editor::Exporter::Exporter() {
Expand Down Expand Up @@ -224,6 +279,10 @@ void editor::Exporter::runExport() {
if (!copyEngine()) return;
if (isCancelled()) { setError("Export cancelled"); return; }
if (!buildAndSaveShaders()) return;
if (config.mode == ExportMode::SourceCode && config.packNativeResources) {
if (isCancelled()) { setError("Export cancelled"); return; }
if (!collectSourceResourcePack()) return;
}

if (buildMode) {
if (isCancelled()) { setError("Export cancelled"); return; }
Expand Down Expand Up @@ -583,6 +642,7 @@ bool editor::Exporter::collectDesktopArtifacts() {
#endif

std::error_code ec;
const fs::path projectRoot = getExportProjectRoot();
fs::path exePath = buildDir / exeName;
if (!fs::exists(exePath, ec)) {
// Multi-config generators (Visual Studio) place binaries in a per-config subdir.
Expand Down Expand Up @@ -612,19 +672,41 @@ bool editor::Exporter::collectDesktopArtifacts() {
return false;
}

// The desktop runtime resolves "assets" and "lua" relative to the working
// directory, so ship them next to the executable.
const fs::path projectRoot = getExportProjectRoot();
for (const char* dir : {"assets", "lua"}) {
fs::path src = projectRoot / dir;
if (!fs::exists(src, ec)) continue;
copyTree(src, config.destinationDir / dir, ec);
bool packCreated = false;
if (config.packNativeResources && !writeNativeResourcePack(config.destinationDir / "game.pak", packCreated)) {
return false;
}
if (!config.packNativeResources) {
ec.clear();
fs::remove(config.destinationDir / "game.pak", ec);
if (ec) {
setError(std::string("Failed to copy ") + dir + ": " + ec.message());
setError("Failed to remove stale resource pack: " + ec.message());
return false;
}
}

if (config.packNativeResources && packCreated) {
for (const char* dir : {"assets", "lua"}) {
ec.clear();
fs::remove_all(config.destinationDir / dir, ec);
if (ec) {
setError(std::string("Failed to remove unpacked ") + dir + ": " + ec.message());
return false;
}
}
} else {
// Default and fallback behavior: ship resource folders next to the executable.
for (const char* dir : {"assets", "lua"}) {
fs::path src = projectRoot / dir;
if (!fs::exists(src, ec)) continue;
copyTree(src, config.destinationDir / dir, ec);
if (ec) {
setError(std::string("Failed to copy ") + dir + ": " + ec.message());
return false;
}
}
}

#if defined(__linux__)
// Linux executables cannot embed icons, and Wayland windows only get one
// through an installed .desktop entry matched by app_id. When the project
Expand Down Expand Up @@ -689,6 +771,146 @@ bool editor::Exporter::collectDesktopArtifacts() {
return true;
}

bool editor::Exporter::writeNativeResourcePack(const fs::path& outputPath, bool& created) {
setProgressRaw("Packing resources...", 0.95f);

created = false;
std::vector<ResourcePackBuildEntry> entries;
const fs::path projectRoot = getExportProjectRoot();

std::error_code ec;
for (const char* rootName : {"assets", "lua"}) {
const fs::path root = projectRoot / rootName;
if (!fs::exists(root, ec)) continue;

for (const auto& file : fs::recursive_directory_iterator(root, fs::directory_options::skip_permission_denied, ec)) {
if (!file.is_regular_file()) continue;

fs::path relPath = fs::relative(file.path(), projectRoot, ec);
if (ec || relPath.empty()) continue;

ResourcePackBuildEntry entry;
entry.path = relPath.generic_string();
if (entry.path == "assets/game.pak") continue;

if (!readWholeFile(file.path(), entry.data)) {
setError("Failed to read resource for pack: " + file.path().string());
return false;
}

size_t hash = std::hash<std::string>{}(entry.path) ^ (entry.data.size() << 1);
entry.key = static_cast<uint8_t>((hash % 255) + 1);
entry.shift = entry.data.empty() ? 0 : static_cast<uint32_t>(hash % entry.data.size());

shiftRight(entry.data, entry.shift);
applyXor(entry.data, entry.key);

entries.push_back(std::move(entry));
}
}

if (entries.empty()) {
fs::remove(outputPath, ec);
return true;
}

std::sort(entries.begin(), entries.end(), [](const auto& a, const auto& b) {
return a.path < b.path;
});

uint64_t offset = sizeof(RESOURCE_PACK_MAGIC) + 4;
for (const auto& entry : entries) {
if (entry.path.size() > UINT16_MAX) {
setError("Resource path is too long for pack: " + entry.path);
return false;
}
offset += 2 + entry.path.size() + 8 + 8 + 1 + 4;
}
for (auto& entry : entries) {
entry.offset = offset;
offset += entry.data.size();
}

fs::create_directories(outputPath.parent_path(), ec);
if (ec) {
setError("Failed to create resource pack directory: " + ec.message());
return false;
}

std::ofstream out(outputPath, std::ios::binary);
if (!out) {
setError("Failed to create resource pack: " + outputPath.string());
return false;
}

out.write(RESOURCE_PACK_MAGIC, sizeof(RESOURCE_PACK_MAGIC));
writeU32(out, static_cast<uint32_t>(entries.size()));

for (const auto& entry : entries) {
writeU16(out, static_cast<uint16_t>(entry.path.size()));
out.write(entry.path.data(), static_cast<std::streamsize>(entry.path.size()));
writeU64(out, entry.offset);
writeU64(out, static_cast<uint64_t>(entry.data.size()));
writeU8(out, entry.key);
writeU32(out, entry.shift);
}

for (const auto& entry : entries) {
if (!entry.data.empty()) {
out.write(reinterpret_cast<const char*>(entry.data.data()), static_cast<std::streamsize>(entry.data.size()));
}
}

if (!out) {
setError("Failed to write resource pack: " + outputPath.string());
return false;
}

created = true;
return true;
}

bool editor::Exporter::collectSourceResourcePack() {
const fs::path projectRoot = getExportProjectRoot();
const fs::path assetsDir = projectRoot / "assets";
const fs::path luaDir = projectRoot / "lua";
const fs::path packPath = assetsDir / "game.pak";

bool packCreated = false;
if (!writeNativeResourcePack(packPath, packCreated)) {
return false;
}

std::error_code ec;
if (!packCreated) {
return true;
}

for (const auto& entry : fs::directory_iterator(assetsDir, ec)) {
if (ec) {
setError("Failed to read exported assets directory: " + ec.message());
return false;
}
if (entry.path().filename() == "game.pak") {
continue;
}

fs::remove_all(entry.path(), ec);
if (ec) {
setError("Failed to remove unpacked asset after packing: " + ec.message());
return false;
}
}

fs::remove_all(luaDir, ec);
if (ec) {
setError("Failed to remove unpacked Lua directory after packing: " + ec.message());
return false;
}

return true;
}

bool editor::Exporter::collectWebArtifacts() {
setProgressRaw("Copying artifacts...", 0.95f);

Expand Down
5 changes: 5 additions & 0 deletions editor/Exporter.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ namespace doriax::editor {
// Desktop/Web: value passed to the exported CMake GRAPHIC_BACKEND
// setting (glcore, gles3, d3d11, metal, or vulkan).
std::string graphicBackend;
// Native builds: pack assets/lua into game.pak instead of copying folders.
// Experimental and disabled by default.
bool packNativeResources = false;
// Web: emsdk root override ("" = auto-detect via EMSDK env / PATH)
std::string emsdkPath;
};
Expand Down Expand Up @@ -124,6 +127,8 @@ namespace doriax::editor {
bool runBuild();
bool collectDesktopArtifacts();
bool collectWebArtifacts();
bool collectSourceResourcePack();
bool writeNativeResourcePack(const fs::path& outputPath, bool& created);
bool clearGenerated();
bool loadAndSaveAllScenes();
bool copyGenerated();
Expand Down
9 changes: 9 additions & 0 deletions editor/Project.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2287,6 +2287,14 @@ unsigned int editor::Project::getCMakeBuildJobs() const{
return cmakeBuildJobs;
}

void editor::Project::setPackNativeResources(bool enabled){
packNativeResources = enabled;
}

bool editor::Project::shouldPackNativeResources() const{
return packNativeResources;
}

uint32_t editor::Project::getStartSceneId() const{
return startSceneId;
}
Expand Down Expand Up @@ -3448,6 +3456,7 @@ void editor::Project::resetConfigs() {
cmakeCxxCompiler = "";
cmakeGenerator = "";
cmakeBuildJobs = defaultCMakeBuildJobs;
packNativeResources = defaultPackNativeResources;
selectedScene = NULL_PROJECT_SCENE;
selectedSceneForProperties = NULL_PROJECT_SCENE;
nextSceneId = 0;
Expand Down
4 changes: 4 additions & 0 deletions editor/Project.h
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ namespace doriax::editor{
// Atomic: read by the play-startup thread while the settings dialog can
// write it from the UI thread.
std::atomic<unsigned int> cmakeBuildJobs{0};
bool packNativeResources;
CommandHistory projectHistory;

uint32_t startSceneId;
Expand Down Expand Up @@ -335,6 +336,7 @@ namespace doriax::editor{
static constexpr const char* defaultAssetsDir = ".";
static constexpr const char* defaultLuaDir = ".";
static constexpr unsigned int defaultCMakeBuildJobs = 0;
static constexpr bool defaultPackNativeResources = false;

Project();

Expand Down Expand Up @@ -413,6 +415,8 @@ namespace doriax::editor{
std::string getCMakeGenerator() const;
void setCMakeBuildJobs(unsigned int jobs);
unsigned int getCMakeBuildJobs() const;
void setPackNativeResources(bool enabled);
bool shouldPackNativeResources() const;

uint32_t getStartSceneId() const;
void setStartSceneId(uint32_t sceneId);
Expand Down
6 changes: 6 additions & 0 deletions editor/Stream.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1565,6 +1565,9 @@ YAML::Node editor::Stream::encodeProject(Project* project) {
if (project->getCMakeBuildJobs() != 0) {
root["cmakeBuildJobs"] = project->getCMakeBuildJobs();
}
if (project->shouldPackNativeResources() != Project::defaultPackNativeResources) {
root["packNativeResources"] = project->shouldPackNativeResources();
}

if (project->getStartSceneId() != NULL_PROJECT_SCENE) {
root["startSceneId"] = project->getStartSceneId();
Expand Down Expand Up @@ -1752,6 +1755,9 @@ void editor::Stream::decodeProject(Project* project, const YAML::Node& node) {
const long long maxJobs = static_cast<long long>(Generator::MAX_SUPPORTED_PARALLEL_BUILD_JOBS);
project->setCMakeBuildJobs(static_cast<unsigned int>(std::clamp(jobs, 0LL, maxJobs)));
}
if (node["packNativeResources"].IsDefined()) {
project->setPackNativeResources(node["packNativeResources"].as<bool>());
}

if (node["startSceneId"]) {
project->setStartSceneId(node["startSceneId"].as<uint32_t>());
Expand Down
2 changes: 2 additions & 0 deletions editor/window/dialog/ExportWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,7 @@ void ExportWindow::startConfiguredExport(bool overwriteTarget) {

if (m_mode == ExportMode::SourceCode) {
exportConfig.targetDir = m_targetDir;
exportConfig.packNativeResources = m_project->shouldPackNativeResources();
for (const auto& entry : m_backendEntries) {
if (entry.selected) {
exportConfig.selectedBackends.insert(entry.backend);
Expand All @@ -732,6 +733,7 @@ void ExportWindow::startConfiguredExport(bool overwriteTarget) {
exportConfig.cmakeCxxCompiler = m_project->getCMakeCxxCompiler();
exportConfig.cmakeGenerator = m_project->getCMakeGenerator();
exportConfig.buildJobs = m_project->getCMakeBuildJobs();
exportConfig.packNativeResources = m_project->shouldPackNativeResources();
} else {
exportConfig.emsdkPath = m_emsdkOverride;
}
Expand Down
Loading