16
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-07-17 23:39:18 +02:00

New Components/ Prepare to new scene loading system

This commit is contained in:
2026-06-16 21:33:29 +02:00
parent 2c599cb419
commit cb739365b4
15 changed files with 712 additions and 57 deletions

View File

@@ -164,6 +164,7 @@ set(SOURCES
"audio/audiorenderer.cpp"
"utilities/motiontelemetry.cpp"
"utilities/utilities.cpp"
"utilities/AsyncFilePreloader.cpp"
"application/uitranscripts.cpp"
"world/station.cpp"
"application/application.cpp"
@@ -431,7 +432,9 @@ if (${CMAKE_CXX_COMPILER_ID} STREQUAL MSVC)
entitysystem/systems/VehicleSyncSystem.cpp
entitysystem/systems/VehicleSyncSystem.h
entitysystem/systems/BillboardSystem.cpp
entitysystem/systems/BillboardSystem.h)
entitysystem/systems/BillboardSystem.h
entitysystem/ECSPersistence.cpp
entitysystem/ECSPersistence.h)
add_compile_options("/utf-8")
endif()

View File

@@ -22,6 +22,7 @@ http://mozilla.org/MPL/2.0/.
#include "entitysystem/ECScene.h"
#include "entitysystem/components/BasicComponents.h"
#include "entitysystem/components/RenderComponents.h"
#include "entitysystem/ECSPersistence.h"
driver_ui::driver_ui()
{
@@ -366,6 +367,100 @@ void driver_ui::register_driver_commands()
dev::Console.print_ok("ParticleEmitter added to: " + args[1]);
});
con.register_command("ecs.addspotlight",
"Spawn SpotLight at position: ecs.addspotlight <name> <x> <y> <z> [r g b] [intensity] [range]",
[](const dev::args_t &args) {
if (args.size() < 5) { dev::Console.print_error("Usage: ecs.addspotlight <name> <x> <y> <z> [r g b] [intensity] [range]"); return; }
ECScene *scene = Application.sceneManager.CurrentScene();
if (!scene) { dev::Console.print_warn("No active scene"); return; }
ECWorld &world = scene->World();
auto entity = world.CreateEntity();
auto& id = world.AddComponent<ECSComponent::Identification>(entity);
id.Name = args[1];
auto& t = world.AddComponent<ECSComponent::Transform>(entity);
t.Position = glm::dvec3(std::stod(args[2]), std::stod(args[3]), std::stod(args[4]));
auto& spot = world.AddComponent<ECSComponent::SpotLight>(entity);
if (args.size() > 7) {
spot.Color = glm::vec3(std::stof(args[5]), std::stof(args[6]), std::stof(args[7]));
}
if (args.size() > 8) spot.Intensity = std::stof(args[8]);
if (args.size() > 9) spot.Range = std::stof(args[9]);
spot.Enabled = true;
dev::Console.print_ok("SpotLight '" + args[1] + "' spawned at ("
+ args[2] + ", " + args[3] + ", " + args[4] + ")");
});
con.register_command("ecs.clone",
"Clone entity with optional offset: ecs.clone <src> <newname> [dx dy dz]",
[](const dev::args_t &args) {
if (args.size() < 3) { dev::Console.print_error("Usage: ecs.clone <src> <newname> [dx dy dz]"); return; }
ECScene *scene = Application.sceneManager.CurrentScene();
if (!scene) { dev::Console.print_warn("No active scene"); return; }
ECWorld &world = scene->World();
entt::entity src = world.FindEntityByName(args[1]);
if (src == entt::null) { dev::Console.print_error("Entity not found: " + args[1]); return; }
if (world.FindEntityByName(args[2]) != entt::null) { dev::Console.print_error("Name already exists: " + args[2]); return; }
glm::dvec3 offset{0.0};
if (args.size() >= 6) {
offset = glm::dvec3(std::stod(args[3]), std::stod(args[4]), std::stod(args[5]));
}
auto dst = world.CreateEntity();
auto &id = world.AddComponent<ECSComponent::Identification>(dst);
id.Name = args[2];
if (auto *t = world.GetComponent<ECSComponent::Transform>(src)) {
auto &nt = world.AddComponent<ECSComponent::Transform>(dst);
nt = *t;
nt.Position += offset;
}
if (auto *s = world.GetComponent<ECSComponent::SpotLight>(src))
world.AddComponent<ECSComponent::SpotLight>(dst) = *s;
if (auto *p = world.GetComponent<ECSComponent::ParticleEmitter>(src))
world.AddComponent<ECSComponent::ParticleEmitter>(dst) = *p;
if (auto *b = world.GetComponent<ECSComponent::Billboard>(src))
world.AddComponent<ECSComponent::Billboard>(dst) = *b;
if (auto *l = world.GetComponent<ECSComponent::Line>(src))
world.AddComponent<ECSComponent::Line>(dst) = *l;
if (auto *lod = world.GetComponent<ECSComponent::LODController>(src))
world.AddComponent<ECSComponent::LODController>(dst) = *lod;
dev::Console.print_ok("Cloned '" + args[1] + "' → '" + args[2] + "'");
});
con.register_command("ecs.save",
"Save ECS entities to JSON: ecs.save [filename=ecs_save.json]",
[](const dev::args_t &args) {
ECScene *scene = Application.sceneManager.CurrentScene();
if (!scene) { dev::Console.print_warn("No active scene"); return; }
std::string filename = (args.size() > 1) ? args[1] : "ecs_save.json";
int n = ecs_persistence::save(scene->World(), filename);
if (n >= 0)
dev::Console.print_ok("Saved " + std::to_string(n) + " entities to " + filename);
else
dev::Console.print_error("Cannot write: " + filename);
});
con.register_command("ecs.load",
"Load ECS entities from JSON: ecs.load [filename=ecs_save.json]",
[](const dev::args_t &args) {
ECScene *scene = Application.sceneManager.CurrentScene();
if (!scene) { dev::Console.print_warn("No active scene"); return; }
std::string filename = (args.size() > 1) ? args[1] : "ecs_save.json";
int n = ecs_persistence::load(scene->World(), filename);
if (n >= 0)
dev::Console.print_ok("Loaded " + std::to_string(n) + " entities from " + filename);
else
dev::Console.print_error("Cannot read or parse: " + filename);
});
con.print_info("Driver console ready. Type 'help' for commands.");
}

View File

@@ -579,18 +579,22 @@ void ui_layer::render_entity_hierarchy(ECWorld& world)
{
if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen))
{
float pos[3] = { static_cast<float>(transform->Position.x), static_cast<float>(transform->Position.y), static_cast<float>(transform->Position.z) };
float pos[3] = {
static_cast<float>(transform->Position.x),
static_cast<float>(transform->Position.y),
static_cast<float>(transform->Position.z) };
if (ImGui::DragFloat3("Position", pos, 0.1f)) {
transform->Position.x = pos[0];
transform->Position.y = pos[1];
transform->Position.z = pos[2];
}
if(ImGui::DragFloat3("Position", pos, 0.1f)){
glm::vec3 euler = glm::degrees(glm::eulerAngles(transform->Rotation));
if (ImGui::DragFloat3("Rotation (deg)", &euler.x, 0.5f)) {
transform->Rotation = glm::quat(glm::radians(euler));
}
}
ImGui::DragFloat3("Rotation",
&transform->Rotation.x, 0.1, -360.0, 360.0);
ImGui::DragFloat3("Scale",
&transform->Scale.x, 0.01, 0.0, 100000.0);
ImGui::DragFloat3("Scale", &transform->Scale.x, 0.01f, 0.0f, 100000.0f);
}
}
@@ -599,13 +603,9 @@ void ui_layer::render_entity_hierarchy(ECWorld& world)
{
if (ImGui::CollapsingHeader("Velocity", ImGuiTreeNodeFlags_DefaultOpen))
{
ImGui::DragFloat3("Value",
&velocity->Value.x, 0.1);
if (ImGui::Button("Stop"))
{
velocity->Value = glm::dvec3(0.0);
}
ImGui::DragFloat3("Value", &velocity->Value.x, 0.1f);
if (ImGui::Button("Stop"))
velocity->Value = glm::vec3(0.0f);
}
}
@@ -614,10 +614,12 @@ void ui_layer::render_entity_hierarchy(ECWorld& world)
{
if (ImGui::CollapsingHeader("LODController", ImGuiTreeNodeFlags_DefaultOpen))
{
float range_min = lod->RangeMin;
float range_max = lod->RangeMax;
ImGui::DragFloat("Range Min", &range_min, 0.1, 0.0, 100000.0);
ImGui::DragFloat("Range Max", &range_max, 0.1, 0.0, 100000.0);
float range_min = static_cast<float>(lod->RangeMin);
float range_max = static_cast<float>(lod->RangeMax);
if (ImGui::DragFloat("Range Min", &range_min, 0.1f, 0.0f, 100000.0f))
lod->RangeMin = range_min;
if (ImGui::DragFloat("Range Max", &range_max, 0.1f, 0.0f, 100000.0f))
lod->RangeMax = range_max;
}
}
@@ -646,7 +648,6 @@ void ui_layer::render_entity_hierarchy(ECWorld& world)
}
// SpotLight
if (auto* light = world.GetComponent<ECSComponent::SpotLight>(m_selected_entity))
{
if (ImGui::CollapsingHeader("SpotLight", ImGuiTreeNodeFlags_DefaultOpen))
@@ -661,6 +662,58 @@ void ui_layer::render_entity_hierarchy(ECWorld& world)
}
}
// SunLight
if (auto* sun = world.GetComponent<ECSComponent::SunLight>(m_selected_entity))
{
if (ImGui::CollapsingHeader("SunLight", ImGuiTreeNodeFlags_DefaultOpen))
{
ImGui::Checkbox("Enabled", &sun->Enabled);
ImGui::ColorEdit3("Color", &sun->Color.x);
ImGui::DragFloat("Intensity", &sun->Intensity, 0.01f, 0.0f, 10.0f);
}
}
// ParticleEmitter
if (auto* em = world.GetComponent<ECSComponent::ParticleEmitter>(m_selected_entity))
{
if (ImGui::CollapsingHeader("ParticleEmitter", ImGuiTreeNodeFlags_DefaultOpen))
{
ImGui::Checkbox("Active", &em->isActive);
ImGui::DragFloat("Spawn Rate", &em->spawnRate, 0.5f, 0.0f, 1000.0f);
ImGui::DragFloat("Lifetime", &em->particleLifetime, 0.1f, 0.1f, 60.0f);
ImGui::DragFloat("Air Resistance", &em->airResistance, 0.01f, 0.0f, 5.0f);
ImGui::DragFloat3("Gravity", &em->gravity.x, 0.01f);
ImGui::DragFloat4("Color Fade", &em->colorFade.x, 0.01f, -1.0f, 1.0f);
ImGui::Text("Particles alive: %zu", em->particles.size());
if (ImGui::Button("Clear particles"))
em->particles.clear();
}
}
// Billboard
if (auto* bill = world.GetComponent<ECSComponent::Billboard>(m_selected_entity))
{
if (ImGui::CollapsingHeader("Billboard", ImGuiTreeNodeFlags_DefaultOpen))
{
ImGui::Checkbox("Active", &bill->active);
ImGui::DragFloat("Size", &bill->size, 0.05f, 0.01f, 100.0f);
ImGui::Text("Texture: %s", bill->texturePath.ToString().c_str());
}
}
// Line
if (auto* line = world.GetComponent<ECSComponent::Line>(m_selected_entity))
{
if (ImGui::CollapsingHeader("Line", ImGuiTreeNodeFlags_DefaultOpen))
{
ImGui::Checkbox("Active", &line->active);
ImGui::DragFloat3("Start", &line->start.x, 0.1f);
ImGui::DragFloat3("End", &line->end.x, 0.1f);
ImGui::ColorEdit3("Color", &line->color.x);
ImGui::DragFloat("Thickness", &line->thickness, 0.1f, 0.1f, 20.0f);
}
}
ImGui::EndChild();
ImGui::End();
}

View File

@@ -0,0 +1,172 @@
#include "stdafx.h"
#include "ECSPersistence.h"
#include "entitysystem/ECWorld.h"
#include "entitysystem/components/BasicComponents.h"
#include "entitysystem/components/RenderComponents.h"
#include "utilities/Logs.h"
#include "nlohmann/json.hpp"
#include <fstream>
namespace ecs_persistence
{
int save(ECWorld &world, const std::string &path)
{
nlohmann::json root = nlohmann::json::array();
for (auto entity : world.GetEntities()) {
auto *idComp = world.GetComponent<ECSComponent::Identification>(entity);
if (!idComp) continue;
nlohmann::json ent;
ent["name"] = idComp->Name.ToString();
if (auto *t = world.GetComponent<ECSComponent::Transform>(entity)) {
ent["Transform"] = {
{"px", t->Position.x}, {"py", t->Position.y}, {"pz", t->Position.z},
{"rx", t->Rotation.x}, {"ry", t->Rotation.y}, {"rz", t->Rotation.z}, {"rw", t->Rotation.w},
{"sx", t->Scale.x}, {"sy", t->Scale.y}, {"sz", t->Scale.z}
};
}
if (auto *s = world.GetComponent<ECSComponent::SpotLight>(entity)) {
ent["SpotLight"] = {
{"r", s->Color.x}, {"g", s->Color.y}, {"b", s->Color.z},
{"intensity", s->Intensity}, {"range", s->Range},
{"innerAngle", s->InnerAngle}, {"outerAngle", s->OuterAngle},
{"enabled", s->Enabled}, {"castShadows", s->CastShadows}
};
}
if (auto *p = world.GetComponent<ECSComponent::ParticleEmitter>(entity)) {
ent["ParticleEmitter"] = {
{"active", p->isActive}, {"spawnRate", p->spawnRate},
{"lifetime", p->particleLifetime}, {"airResistance", p->airResistance},
{"gx", p->gravity.x}, {"gy", p->gravity.y}, {"gz", p->gravity.z},
{"fadex", p->colorFade.x}, {"fadey", p->colorFade.y},
{"fadez", p->colorFade.z}, {"fadew", p->colorFade.w}
};
}
if (auto *b = world.GetComponent<ECSComponent::Billboard>(entity)) {
ent["Billboard"] = {
{"texture", b->texturePath.ToString()},
{"size", b->size}, {"active", b->active}
};
}
if (auto *l = world.GetComponent<ECSComponent::Line>(entity)) {
ent["Line"] = {
{"sx", l->start.x}, {"sy", l->start.y}, {"sz", l->start.z},
{"ex", l->end.x}, {"ey", l->end.y}, {"ez", l->end.z},
{"r", l->color.x}, {"g", l->color.y}, {"b", l->color.z},
{"thickness", l->thickness}, {"active", l->active}
};
}
if (auto *lod = world.GetComponent<ECSComponent::LODController>(entity)) {
ent["LODController"] = {{"min", lod->RangeMin}, {"max", lod->RangeMax}};
}
if (world.HasComponent<ECSComponent::Disabled>(entity))
ent["disabled"] = true;
// skip entities that are purely runtime-managed (VehicleRef, MeshRenderer linked to legacy)
bool runtimeOnly =
world.HasComponent<ECSComponent::VehicleRef>(entity) ||
(world.HasComponent<ECSComponent::MeshRenderer>(entity) &&
world.GetComponent<ECSComponent::MeshRenderer>(entity)->modelInstance != nullptr);
if (runtimeOnly) continue;
root.push_back(ent);
}
std::ofstream f(path);
if (!f) {
WriteLog("[ECS] ECSPersistence: cannot write to " + path);
return -1;
}
f << root.dump(2);
WriteLog("[ECS] ECSPersistence: saved " + std::to_string(root.size()) + " entities to " + path);
return static_cast<int>(root.size());
}
int load(ECWorld &world, const std::string &path)
{
std::ifstream f(path);
if (!f) return -1; // file doesn't exist — silently skip
nlohmann::json root;
try { root = nlohmann::json::parse(f); }
catch (const std::exception &e) {
WriteLog("[ECS] ECSPersistence: JSON parse error in " + path + ": " + e.what());
return -1;
}
int created = 0;
for (auto const &ent : root) {
if (!ent.contains("name")) continue;
std::string name = ent["name"].get<std::string>();
if (world.FindEntityByName(name) != entt::null) continue;
auto entity = world.CreateEntity();
auto &id = world.AddComponent<ECSComponent::Identification>(entity);
id.Name = name;
if (ent.contains("Transform")) {
auto &j = ent["Transform"];
auto &t = world.AddComponent<ECSComponent::Transform>(entity);
t.Position = glm::dvec3(double(j["px"]), double(j["py"]), double(j["pz"]));
t.Rotation = glm::quat(float(j["rw"]), float(j["rx"]), float(j["ry"]), float(j["rz"]));
t.Scale = glm::vec3(float(j["sx"]), float(j["sy"]), float(j["sz"]));
}
if (ent.contains("SpotLight")) {
auto &j = ent["SpotLight"];
auto &s = world.AddComponent<ECSComponent::SpotLight>(entity);
s.Color = glm::vec3(float(j["r"]), float(j["g"]), float(j["b"]));
s.Intensity = j["intensity"];
s.Range = j["range"];
s.InnerAngle = j["innerAngle"];
s.OuterAngle = j["outerAngle"];
s.Enabled = j["enabled"];
s.CastShadows = j["castShadows"];
}
if (ent.contains("ParticleEmitter")) {
auto &j = ent["ParticleEmitter"];
auto &p = world.AddComponent<ECSComponent::ParticleEmitter>(entity);
p.isActive = j["active"];
p.spawnRate = j["spawnRate"];
p.particleLifetime = j["lifetime"];
p.airResistance = j["airResistance"];
p.gravity = glm::vec3(float(j["gx"]), float(j["gy"]), float(j["gz"]));
p.colorFade = glm::vec4(float(j["fadex"]), float(j["fadey"]),
float(j["fadez"]), float(j["fadew"]));
}
if (ent.contains("Billboard")) {
auto &j = ent["Billboard"];
auto &b = world.AddComponent<ECSComponent::Billboard>(entity);
b.texturePath = FName(j["texture"].get<std::string>());
b.size = j["size"];
b.active = j["active"];
}
if (ent.contains("Line")) {
auto &j = ent["Line"];
auto &l = world.AddComponent<ECSComponent::Line>(entity);
l.start = glm::vec3(float(j["sx"]), float(j["sy"]), float(j["sz"]));
l.end = glm::vec3(float(j["ex"]), float(j["ey"]), float(j["ez"]));
l.color = glm::vec3(float(j["r"]), float(j["g"]), float(j["b"]));
l.thickness = j["thickness"];
l.active = j["active"];
}
if (ent.contains("LODController")) {
auto &j = ent["LODController"];
auto &lod = world.AddComponent<ECSComponent::LODController>(entity);
lod.RangeMin = j["min"];
lod.RangeMax = j["max"];
}
if (ent.value("disabled", false))
world.AddComponent<ECSComponent::Disabled>(entity);
++created;
}
WriteLog("[ECS] ECSPersistence: loaded " + std::to_string(created) + " entities from " + path);
return created;
}
} // namespace ecs_persistence

View File

@@ -0,0 +1,12 @@
#pragma once
#include <string>
class ECWorld;
namespace ecs_persistence
{
// Save all named entities to a JSON file. Returns number of entities saved, or -1 on error.
int save(ECWorld &world, const std::string &path);
// Load entities from a JSON file. Skips entities whose name already exists. Returns count loaded, or -1 on error.
int load(ECWorld &world, const std::string &path);
}

View File

@@ -4,6 +4,7 @@
#include "ECScene.h"
#include "utilities/Logs.h"
#include "simulation/simulation.h"
ECScene::ECScene()
: m_loaded(false)
@@ -39,6 +40,9 @@ void ECScene::Update(float dt)
if (!m_loaded)
return;
if (!simulation::is_ready)
return;
m_systems.Update(m_world, dt);
OnUpdate(dt);
}

View File

@@ -12,15 +12,24 @@ entt::entity ECWorld::CreateEntity()
void ECWorld::DestroyEntity(entt::entity entity)
{
if (m_registry.valid(entity))
if (m_registry.valid(entity)) {
if (auto *id = m_registry.try_get<ECSComponent::Identification>(entity))
m_nameIndex.erase(id->Name.ToString());
m_registry.destroy(entity);
}
}
void ECWorld::Clear()
{
m_nameIndex.clear();
m_registry.clear();
}
void ECWorld::UpdateNameIndex(entt::entity entity, const std::string &name)
{
m_nameIndex[name] = entity;
}
bool ECWorld::IsAlive(entt::entity entity) const
{
return m_registry.valid(entity);
@@ -29,18 +38,8 @@ bool ECWorld::IsAlive(entt::entity entity) const
entt::entity ECWorld::FindEntityByName(const std::string& name) const
{
auto view = m_registry.view<ECSComponent::Identification>();
for (auto entity : view)
{
const auto& id = view.get<ECSComponent::Identification>(entity);
if (id.Name.ToString() == name)
{
return entity;
}
}
return entt::null;
auto it = m_nameIndex.find(name);
return (it != m_nameIndex.end()) ? it->second : entt::null;
}
entt::registry& ECWorld::Registry()

View File

@@ -12,6 +12,14 @@
#include <entt/entt.hpp>
#include <utility>
#include <type_traits>
#include <unordered_map>
#include <string>
// Forward declaration so the name-index branch in AddComponent<T> compiles in
// translation units that include ECWorld.h without BasicComponents.h.
// if constexpr discards the branch for non-Identification T, but the name must
// still resolve at parse time.
namespace ECSComponent { struct Identification; }
class ECWorld
{
@@ -22,6 +30,8 @@ public:
entt::entity CreateEntity();
void DestroyEntity(entt::entity entity);
void Clear();
// Updates the name→entity index after an Identification component's Name is changed at runtime.
void UpdateNameIndex(entt::entity entity, const std::string &name);
bool IsAlive(entt::entity entity) const;
@@ -105,6 +115,7 @@ public:
private:
entt::registry m_registry;
std::unordered_map<std::string, entt::entity> m_nameIndex;
};
template<typename T, typename... Args>
@@ -122,7 +133,10 @@ T& ECWorld::AddComponent(entt::entity entity, Args&&... args)
static T dummy{};
return dummy;
} else {
return m_registry.get<T>(entity);
auto &comp = m_registry.get<T>(entity);
if constexpr (std::is_same_v<T, ECSComponent::Identification>)
m_nameIndex[comp.Name.ToString()] = entity;
return comp;
}
}

View File

@@ -16,6 +16,7 @@
#include "entitysystem/systems/LineSystem.h"
#include "entitysystem/systems/VehicleSyncSystem.h"
#include "entitysystem/systems/BillboardSystem.h"
#include "entitysystem/ECSPersistence.h"
#include "utilities/Logs.h"
GameScene::GameScene() = default;
@@ -37,6 +38,8 @@ void GameScene::OnLoad()
#ifdef _DEBUG
CreateTestEntities();
#endif
ecs_persistence::load(World(), "ecs_save.json");
}
void GameScene::OnUpdate(float dt)
@@ -45,6 +48,7 @@ void GameScene::OnUpdate(float dt)
void GameScene::OnUnload()
{
ecs_persistence::save(World(), "ecs_save.json");
World().Clear();
}

View File

@@ -44,7 +44,7 @@ void LightSystem::Update(ECWorld& world, float dt)
light_array::free_light_record rec;
rec.position = transform.Position;
rec.direction = glm::vec3(0.f, -1.f, 0.f); // default downward; rotation support can be added later
rec.direction = glm::normalize(glm::mat3_cast(transform.Rotation) * glm::vec3(0.f, -1.f, 0.f));
rec.color = spot.Color;
rec.intensity = spot.Intensity;
rec.range = spot.Range;

View File

@@ -18,6 +18,7 @@ Copyright (C) 2001-2004 Marcin Wozniak, Maciej Czapkiewicz and others
#include "utilities/Globals.h"
#include "utilities/Logs.h"
#include "utilities/utilities.h"
#include "utilities/AsyncFilePreloader.h"
#include "rendering/renderer.h"
#include "utilities/Timer.h"
#include "simulation/simulation.h"
@@ -2357,10 +2358,34 @@ bool TSubModel::HasAnyVertexUserData() const
return false;
};
// Helper: a read-only streambuf backed by an existing memory buffer.
struct membuf : std::streambuf {
membuf(const char* begin, const char* end) {
setg(const_cast<char*>(begin), const_cast<char*>(begin), const_cast<char*>(end));
}
};
void TModel3d::LoadFromBinFile(std::string const &FileName, bool dynamic)
{ // wczytanie modelu z pliku binarnego
WriteLog("Loading binary format 3d model data from \"" + FileName + "\"...", logtype::model);
auto preloaded = GModelPreloader.get(FileName);
if (preloaded && !preloaded->empty()) {
// Fast path: file already in RAM — parse directly from memory, no disk I/O.
membuf mb(preloaded->data(), preloaded->data() + preloaded->size());
std::istream stream(&mb);
uint32_t type = sn_utils::ld_uint32(stream);
uint32_t size = sn_utils::ld_uint32(stream) - 8;
if (type == MAKE_ID4('E', '3', 'D', '0')) {
deserialize(stream, size, dynamic);
WriteLog("Finished loading 3d model data from \"" + FileName + "\"", logtype::model);
} else {
ErrorLog("Bad model: unknown main chunk in file \"" + FileName + "\"", logtype::model);
}
return;
}
// Slow path: synchronous disk read.
std::ifstream file(FileName, std::ios::binary);
uint32_t type = sn_utils::ld_uint32(file);
@@ -2375,7 +2400,6 @@ void TModel3d::LoadFromBinFile(std::string const &FileName, bool dynamic)
}
else
{
// throw std::runtime_error("e3d: unknown main chunk");
ErrorLog("Bad model: unknown main chunk in file \"" + FileName + "\"", logtype::model);
file.close();
}
@@ -2414,7 +2438,13 @@ void TModel3d::LoadFromTextFile(std::string const &FileName, bool dynamic)
{ // wczytanie submodelu z pliku tekstowego
WriteLog("Loading text format 3d model data from \"" + FileName + "\"...", logtype::model);
iFlags |= 0x0200; // wczytano z pliku tekstowego (właścicielami tablic są submodle)
cParser parser(FileName, cParser::buffer_FILE); // Ra: tu powinno być "models/"...
auto preloaded = GModelPreloader.get(FileName);
cParser parser(
preloaded && !preloaded->empty()
? std::string(preloaded->data(), preloaded->size())
: FileName,
preloaded && !preloaded->empty() ? cParser::buffer_TEXT : cParser::buffer_FILE); // Ra: tu powinno być "models/"...
TSubModel *SubModel;
std::string token = parser.getToken<std::string>();
while (token != "" || parser.eof())

View File

@@ -30,9 +30,69 @@ http://mozilla.org/MPL/2.0/.
#include "entitysystem/components/RenderComponents.h"
#include "rendering/renderer.h"
#include "utilities/Logs.h"
#include "utilities/AsyncFilePreloader.h"
namespace simulation {
// Resolve a raw scenario token to the exact model file path that
// TModel3d::LoadFromFile will eventually open, or "" if it isn't a model.
// Mirrors TModelsManager::find_on_disk + TModel3d::LoadFromFile path logic so
// the preloader caches under the same key the loader later looks up.
static std::string resolve_model_path( std::string name ) {
erase_extension( name );
name = ToLower( name );
std::string base;
for( auto const &ext : { std::string(".e3d"), std::string(".t3d") } ) {
if( FileExists( name + ext ) ) { base = name; break; }
if( FileExists( paths::models + name + ext ) ) { base = paths::models + name; break; }
}
if( base.empty() )
return {};
if( Global.priorityLoadText3D && FileExists( base + ".t3d" ) )
return base + ".t3d";
if( FileExists( base + ".e3d" ) )
return base + ".e3d";
if( FileExists( base + ".t3d" ) )
return base + ".t3d";
return {};
}
// Runs on a background thread: tokenizes the scenario (includes expanded by
// cParser) ahead of the main parse and queues any token that resolves to a
// model file. Best-effort — false positives fail to load harmlessly and
// missed models simply fall back to synchronous disk reads.
static void prefetch_scan_worker( std::string scenariofile, std::string scenerypath,
bool loadtraction, std::atomic<bool> *cancel ) {
try {
cParser scan( scenariofile, cParser::buffer_FILE, scenerypath, loadtraction );
std::string tok;
while( !( tok = scan.getToken<std::string>( false ) ).empty() ) {
if( cancel->load( std::memory_order_relaxed ) )
return;
// cheap filter: model references carry a path separator or extension;
// skips the millions of numeric vertex/coordinate tokens instantly.
if( tok.size() < 4 )
continue;
bool pathlike =
tok.find( '/' ) != std::string::npos
|| tok.find( '\\' ) != std::string::npos
|| ends_with( ToLower( tok ), ".t3d" )
|| ends_with( ToLower( tok ), ".e3d" );
if( !pathlike )
continue;
std::string resolved = resolve_model_path( tok );
if( !resolved.empty() )
GModelPreloader.queue( resolved );
}
}
catch( ... ) {
// best-effort prefetch — never let a scan failure affect loading
}
}
std::shared_ptr<deserializer_state>
state_serializer::deserialize_begin( std::string const &Scenariofile ) {
@@ -112,6 +172,14 @@ state_serializer::deserialize_begin( std::string const &Scenariofile ) {
state->input.injectString(Global.prepend_scn);
}
// kick off background disk preloading: a scan thread races ahead of the main
// parse discovering model files, worker threads read them into RAM, and
// LoadFromBinFile/LoadFromTextFile parse straight from memory on the main thread.
GModelPreloader.start();
state->prefetch_thread = std::thread(
prefetch_scan_worker, Scenariofile, Global.asCurrentSceneryPath,
Global.bLoadTraction, &state->prefetch_cancel );
return state;
}
@@ -161,6 +229,13 @@ state_serializer::deserialize_continue(std::shared_ptr<deserializer_state> state
Region->serialize( state->scenariofile );
}
// loading finished — stop preloader threads and free the cached file buffers
state->prefetch_cancel = true;
if( state->prefetch_thread.joinable() )
state->prefetch_thread.join();
GModelPreloader.stop();
GModelPreloader.clear();
return false;
}
@@ -435,24 +510,38 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
return;
ECWorld& world = currentScene->World();
entt::entity entity = world.CreateEntity();
// Only create ECS entities for node types that actually use them.
// Geometry (triangles, lines) and infrastructure (track, traction) go through
// the legacy pipeline exclusively — creating entities for them wastes time and memory.
static const std::unordered_set<std::string> ecs_node_types = {
"dynamic", "model", "sound"
};
const bool needs_entity = ecs_node_types.count(nodedata.type) > 0;
auto& transformComponent = world.AddComponent<ECSComponent::Transform>(entity);
transformComponent.Position = Scratchpad.location.offset.empty() ? glm::dvec3(0.0) : Scratchpad.location.offset.top();
transformComponent.Rotation = Scratchpad.location.rotation;
entt::entity entity = entt::null;
if (needs_entity) {
entity = world.CreateEntity();
if (nodedata.range_max > 0.0 || nodedata.range_min >= 0.0) {
auto& lodComponent = world.AddComponent<ECSComponent::LODController>(entity);
lodComponent.RangeMin = nodedata.range_min;
lodComponent.RangeMax = nodedata.range_max;
auto& transformComponent = world.AddComponent<ECSComponent::Transform>(entity);
transformComponent.Position = Scratchpad.location.offset.empty() ? glm::dvec3(0.0) : Scratchpad.location.offset.top();
transformComponent.Rotation = Scratchpad.location.rotation;
if (nodedata.range_max > 0.0 || nodedata.range_min >= 0.0) {
auto& lodComponent = world.AddComponent<ECSComponent::LODController>(entity);
lodComponent.RangeMin = nodedata.range_min;
lodComponent.RangeMax = nodedata.range_max;
}
if (nodedata.name == "none") {
nodedata.name.clear();
} else {
auto& id = world.AddComponent<ECSComponent::Identification>(entity);
id.Name = nodedata.name;
}
} else {
if (nodedata.name == "none")
nodedata.name.clear();
}
if( nodedata.name == "none" ) {
nodedata.name.clear();
} else {
auto& id = world.AddComponent<ECSComponent::Identification>(entity);
id.Name = nodedata.name;
}
// type-based deserialization. not elegant but it'll do
if( nodedata.type == "dynamic" ) {
@@ -588,7 +677,7 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
}
scene::Groups.insert( scene::Groups.handle(), instance );
simulation::Region->insert( instance );
{
entt::entity modelEntity = world.FindEntityByName(instance->name());
if (modelEntity != entt::null) {
@@ -601,7 +690,6 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
t->Rotation = { instance->Angles().x, instance->Angles().y, instance->Angles().z, 1.0f };
}
// smoke sources on static models
if (instance->Model() != nullptr) {
for (auto const &smokesource : instance->Model()->smoke_sources()) {
auto& emitter = world.AddComponent<ECSComponent::ParticleEmitter>(modelEntity);
@@ -609,6 +697,25 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
emitter.isActive = true;
}
}
{
std::string lname = instance->name();
std::transform(lname.begin(), lname.end(), lname.begin(), ::tolower);
bool isLamp = lname.find("lamp") != std::string::npos
|| lname.find("latarni") != std::string::npos
|| lname.find("reflektor") != std::string::npos
|| lname.find("swiatlo") != std::string::npos
|| lname.find("light") != std::string::npos;
if (isLamp && !world.HasComponent<ECSComponent::SpotLight>(modelEntity)) {
auto& spot = world.AddComponent<ECSComponent::SpotLight>(modelEntity);
spot.Color = glm::vec3(1.0f, 0.92f, 0.75f);
spot.Intensity = 1.5f;
spot.Range = 40.0f;
spot.InnerAngle = 20.0f;
spot.OuterAngle = 45.0f;
spot.Enabled = true;
}
}
}
}

View File

@@ -11,6 +11,10 @@ http://mozilla.org/MPL/2.0/.
#include "utilities/parser.h"
#include "scene/scene.h"
#include "utilities/AsyncFilePreloader.h"
#include <thread>
#include <atomic>
namespace simulation {
@@ -23,8 +27,23 @@ struct deserializer_state {
std::string,
deserializefunctionbind> functionmap;
// background thread that scans the scenario ahead of the main parse and
// queues model files for asynchronous disk preloading
std::thread prefetch_thread;
std::atomic<bool> prefetch_cancel { false };
deserializer_state(std::string const &File, cParser::buffertype const Type, const std::string &Path, bool const Loadtraction)
: scenariofile(File), input(File, Type, Path, Loadtraction) { }
~deserializer_state() {
// safety net for the aborted-load path; on success deserialize_continue
// has already joined and stopped the preloader (idempotent here)
prefetch_cancel = true;
if (prefetch_thread.joinable())
prefetch_thread.join();
GModelPreloader.stop();
GModelPreloader.clear();
}
};
class state_serializer {

View File

@@ -0,0 +1,86 @@
#include "stdafx.h"
#include "AsyncFilePreloader.h"
#include <fstream>
#include <algorithm>
#include <thread>
AsyncFilePreloader GModelPreloader;
void AsyncFilePreloader::start(int thread_count) {
if (m_running.load()) return;
int n = thread_count > 0 ? thread_count
: (int)std::thread::hardware_concurrency();
n = std::clamp(n, 1, 8);
m_running = true;
m_workers.reserve(n);
for (int i = 0; i < n; ++i)
m_workers.emplace_back([this] { worker(); });
}
void AsyncFilePreloader::stop() {
{
std::lock_guard<std::mutex> lk(m_mutex);
m_running = false;
// drain the queue so workers don't pick up more work
while (!m_pending.empty()) m_pending.pop();
}
m_cv.notify_all();
for (auto& t : m_workers)
if (t.joinable()) t.join();
m_workers.clear();
}
void AsyncFilePreloader::queue(std::string path) {
std::lock_guard<std::mutex> lk(m_mutex);
if (!m_running) return;
if (!m_queued.insert(path).second) return; // already queued / cached
m_pending.push(std::move(path));
m_cv.notify_one();
}
AsyncFilePreloader::Buffer AsyncFilePreloader::get(const std::string& path) const {
std::lock_guard<std::mutex> lk(m_mutex);
auto it = m_cache.find(path);
return (it != m_cache.end()) ? it->second : nullptr;
}
void AsyncFilePreloader::clear() {
std::lock_guard<std::mutex> lk(m_mutex);
m_cache.clear();
m_queued.clear();
}
void AsyncFilePreloader::worker() {
while (true) {
std::string path;
{
std::unique_lock<std::mutex> lk(m_mutex);
m_cv.wait(lk, [this] {
return !m_pending.empty() || !m_running.load(std::memory_order_relaxed);
});
if (!m_running && m_pending.empty()) return;
if (m_pending.empty()) continue;
path = std::move(m_pending.front());
m_pending.pop();
}
auto buf = std::make_shared<std::vector<char>>();
{
std::ifstream f(path, std::ios::binary | std::ios::ate);
if (f) {
auto sz = static_cast<std::streamsize>(f.tellg());
f.seekg(0);
buf->resize(static_cast<size_t>(sz));
f.read(buf->data(), sz);
if (!f) buf->clear(); // read error — fall back to sync load
}
}
{
std::lock_guard<std::mutex> lk(m_mutex);
m_cache[path] = std::move(buf);
}
}
}

View File

@@ -0,0 +1,57 @@
#pragma once
#include <string>
#include <vector>
#include <memory>
#include <unordered_map>
#include <unordered_set>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <queue>
#include <atomic>
// Thread pool that reads files from disk into memory so the main thread can
// skip disk I/O and parse directly from RAM. Only covers the binary path
// (E3D); text models fall back to synchronous load.
class AsyncFilePreloader {
public:
using Buffer = std::shared_ptr<std::vector<char>>;
AsyncFilePreloader() = default;
~AsyncFilePreloader() { stop(); }
AsyncFilePreloader(const AsyncFilePreloader&) = delete;
AsyncFilePreloader& operator=(const AsyncFilePreloader&) = delete;
// Start N worker threads (clamped to [1, hardware_concurrency]).
void start(int thread_count = 0);
// Signal workers to stop and join them. Clears the queue but keeps
// already-loaded buffers so late callers still get cache hits.
void stop();
// Queue a file for background loading. Silently skips duplicates.
void queue(std::string path);
// Return the preloaded buffer for `path`, or nullptr if not ready yet.
// Does NOT block — callers must fall back to synchronous I/O if nullptr.
Buffer get(const std::string& path) const;
// Drop all cached buffers (call after loading finishes to free memory).
void clear();
bool is_running() const { return m_running.load(std::memory_order_relaxed); }
private:
void worker();
mutable std::mutex m_mutex;
std::condition_variable m_cv;
std::queue<std::string> m_pending;
std::unordered_set<std::string> m_queued; // prevents duplicate queuing
std::unordered_map<std::string, Buffer> m_cache;
std::vector<std::thread> m_workers;
std::atomic<bool> m_running { false };
};
extern AsyncFilePreloader GModelPreloader;