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
13 changes: 11 additions & 2 deletions src/editor/document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@ void Document::reset(splash::Scene scene, const fs::path& project, const std::st

void Document::refreshAssets(const std::vector<std::string>& projectPaths) {
for (const std::string& p : projectPaths) {
if (fs::path(p).extension() == ".mesh") m_meshes[p] = analyseMesh(resolve(p));
const fs::path ext = fs::path(p).extension();
if (ext == ".mesh") m_meshes[p] = analyseMesh(resolve(p));
else if (ext == ".lua" || ext == ".wav" || ext == ".anim") m_files[p] = isFile(resolve(p));
else m_textures[p] = analyseTexture(resolve(p));
}
++m_assetsId;
Expand All @@ -152,7 +154,14 @@ void Document::cacheAssets(const splash::Object& o) {
for (const splash::Material& mat : o.mesh->materials)
if (!mat.texture.empty() && !m_textures.count(mat.texture)) m_textures[mat.texture] = analyseTexture(resolve(mat.texture));
}
if (o.script && !o.script->lua.empty() && !m_files.count(o.script->lua)) m_files[o.script->lua] = isFile(resolve(o.script->lua));
auto file = [&](const std::string& p) {
if (!p.empty() && !m_files.count(p)) m_files[p] = isFile(resolve(p));
};
if (o.script) file(o.script->lua);
if (o.trigger) file(o.trigger->lua);
if (o.audio) file(o.audio->clip);
if (o.skin)
for (const std::string& c : o.skin->clips) file(c);
for (const splash::Object& c : o.children) cacheAssets(c);
}

Expand Down
56 changes: 56 additions & 0 deletions src/editor/project.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <algorithm>
#include <exception>
#include <fstream>
#include <iterator>
#include <system_error>

#include "scene.hh"
Expand Down Expand Up @@ -70,6 +71,61 @@ std::optional<fs::path> sceneInProject(const fs::path& project, const fs::path&
return rel;
}

static bool sameBytes(const fs::path& a, const fs::path& b) {
std::error_code ec;
if (fs::file_size(a, ec) != fs::file_size(b, ec) || ec) return false;
std::ifstream fa(a, std::ios::binary), fb(b, std::ios::binary);
if (!fa || !fb) return false;
return std::equal(std::istreambuf_iterator<char>(fa), {}, std::istreambuf_iterator<char>(fb), {});
}
static std::string genericUtf8(const fs::path& p) {
std::u8string u = p.generic_u8string();
return std::string(u.begin(), u.end());
}

AdoptedAsset adoptAsset(const fs::path& project, const fs::path& file, const std::string& folder) {
AdoptedAsset r;
std::error_code ec;
if (project.empty() || file.empty()) {
r.error = "no project is open";
return r;
}
if (!fs::is_regular_file(file, ec)) {
r.error = utf8(file) + " is not a file";
return r;
}
fs::path root = fs::weakly_canonical(project, ec);
fs::path src = fs::weakly_canonical(file, ec);
if (ec) {
r.error = ec.message();
return r;
}
fs::path rel = src.lexically_relative(root);
if (!rel.empty() && rel != "." && *rel.begin() != "..") {
r.path = genericUtf8(rel);
return r;
}
fs::path dir = root / fromUtf8(folder);
fs::create_directories(dir, ec);
if (ec) {
r.error = "could not create " + utf8(dir) + ": " + ec.message();
return r;
}
const std::string stem = utf8(src.stem()), ext = utf8(src.extension());
fs::path dest = dir / src.filename();
for (int n = 2; fs::exists(dest, ec) && !sameBytes(src, dest); ++n) dest = dir / fromUtf8(stem + "_" + std::to_string(n) + ext);
if (!fs::exists(dest, ec)) {
fs::copy_file(src, dest, ec);
if (ec) {
r.error = "could not copy " + utf8(src.filename()) + ": " + ec.message();
return r;
}
r.copied = true;
}
r.path = genericUtf8(dest.lexically_relative(root));
return r;
}

void RecentScenes::load() {
m_items.clear();
if (m_file.empty()) return;
Expand Down
11 changes: 11 additions & 0 deletions src/editor/project.hh
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ NewProject createProject(const std::filesystem::path& dir);
// project (with a .scene extension added if missing), or nullopt outside it.
std::optional<std::filesystem::path> sceneInProject(const std::filesystem::path& project, const std::filesystem::path& file);

// A file picked for an asset field, as the project-relative path the scene
// stores. A file already inside `project` is used where it is; one outside is
// copied into `project`/`folder`, reusing an identical file of the same name
// and numbering the copy ("rock_2.png") when a different one is there.
struct AdoptedAsset {
std::string path; // project-relative, '/'-separated; empty on failure
bool copied = false;
std::string error; // empty on success
};
AdoptedAsset adoptAsset(const std::filesystem::path& project, const std::filesystem::path& file, const std::string& folder);

// Recently opened scenes, newest first, as absolute paths. Stored one per line.
class RecentScenes {
public:
Expand Down
66 changes: 66 additions & 0 deletions src/editor/tests/editor_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,71 @@ void testProject() {
CHECK(std::find(back.items().begin(), back.items().end(), proj / "s9.scene") == back.items().end());
}

void testAdoptAsset() {
namespace fs = std::filesystem;
std::error_code ec;
fs::path dir = g_outDir / "adopt";
fs::remove_all(dir, ec);
fs::path proj = dir / "game", outside = dir / "elsewhere";
fs::create_directories(proj / "textures", ec);
fs::create_directories(outside, ec);
auto write = [](const fs::path& p, const std::string& bytes) { std::ofstream(p, std::ios::binary) << bytes; };
auto read = [](const fs::path& p) {
std::ifstream in(p, std::ios::binary);
return std::string((std::istreambuf_iterator<char>(in)), {});
};

// Inside the project: used where it is, never copied.
write(proj / "textures" / "rock.png", "rock");
editor::AdoptedAsset in = editor::adoptAsset(proj, proj / "textures" / "rock.png", "textures");
CHECK(in.error.empty() && in.path == "textures/rock.png" && !in.copied);

// Outside: copied into the folder, created if missing.
write(outside / "door.lua", "print(1)");
editor::AdoptedAsset lua = editor::adoptAsset(proj, outside / "door.lua", "scripts");
CHECK(lua.error.empty() && lua.path == "scripts/door.lua" && lua.copied);
CHECK(read(proj / "scripts" / "door.lua") == "print(1)");
// The same file again reuses the copy.
editor::AdoptedAsset again = editor::adoptAsset(proj, outside / "door.lua", "scripts");
CHECK(again.path == "scripts/door.lua" && !again.copied);
// A different file of the same name is numbered, never overwrites.
fs::create_directories(outside / "b", ec);
write(outside / "b" / "door.lua", "print(2)");
editor::AdoptedAsset other = editor::adoptAsset(proj, outside / "b" / "door.lua", "scripts");
CHECK(other.path == "scripts/door_2.lua" && other.copied);
CHECK(read(proj / "scripts" / "door.lua") == "print(1)" && read(proj / "scripts" / "door_2.lua") == "print(2)");

// Failures carry a reason and no path.
editor::AdoptedAsset missing = editor::adoptAsset(proj, outside / "nope.wav", "audio");
CHECK(!missing.error.empty() && missing.path.empty());
editor::AdoptedAsset noProject = editor::adoptAsset({}, outside / "door.lua", "scripts");
CHECK(!noProject.error.empty() && noProject.path.empty());

// Every file field is checked on disk: script, trigger script, audio clip,
// skin clips. Picking a file refreshes its entry.
write(proj / "scripts" / "zone.lua", "");
fs::create_directories(proj / "audio", ec);
write(proj / "audio" / "bell.wav", "RIFF");
splash::Scene sc;
splash::Object ob = named("Thing");
ob.trigger.emplace();
ob.trigger->lua = "scripts/zone.lua";
ob.audio.emplace();
ob.audio->clip = "audio/bell.wav";
ob.skin.emplace();
ob.skin->clips = {"anims/walk.anim"};
sc.objects.push_back(ob);
editor::Document doc;
doc.reset(sc, proj, "game");
CHECK(doc.fileExists("scripts/zone.lua") && doc.fileExists("audio/bell.wav"));
CHECK(!doc.fileExists("anims/walk.anim") && !doc.fileExists("scripts/new.lua"));
fs::create_directories(proj / "anims", ec);
write(proj / "anims" / "walk.anim", "x");
write(proj / "scripts" / "new.lua", "");
doc.refreshAssets({"anims/walk.anim", "scripts/new.lua"});
CHECK(doc.fileExists("anims/walk.anim") && doc.fileExists("scripts/new.lua"));
}

} // namespace

int main(int argc, char** argv) {
Expand All @@ -816,6 +881,7 @@ int main(int argc, char** argv) {
testLiveExport();
testPlay();
testProject();
testAdoptAsset();
if (g_failures) {
std::fprintf(stderr, "editor_tests: %d of %d checks failed\n", g_failures, g_checks);
return 1;
Expand Down
Loading
Loading