diff --git a/.gitignore b/.gitignore index e27ce7d..828958c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ build *.blend1 +assets/scene-baked.glb diff --git a/src/graphics/camera.cpp b/src/graphics/camera.cpp index 4bdf2a4..b855ae5 100644 --- a/src/graphics/camera.cpp +++ b/src/graphics/camera.cpp @@ -89,9 +89,12 @@ void camera::update(double dt) velocity.x += rotated.x * m * dt; velocity.y += rotated.y * m * dt; - if(std::abs(pos.x + velocity.x * dt) > 2.9) + double nx = pos.x + velocity.x * dt; + double ny = pos.y + velocity.y * dt; + + if(nx > 8.9 || nx < -2.9) velocity.x = 0; - if(std::abs(pos.y + velocity.y * dt) > 3.9) + if(std::abs(ny) > 3.9) velocity.y = 0; float m2 = std::pow(0.5, dt / (on_ground ? 0.05 : 1)); diff --git a/src/graphics/mesh/arrays.cpp b/src/graphics/mesh/arrays.cpp index e85edea..63023f3 100644 --- a/src/graphics/mesh/arrays.cpp +++ b/src/graphics/mesh/arrays.cpp @@ -27,7 +27,7 @@ void arrays::vertex_attrib_pointers() glVertexAttribPointer(1, 2, GL_FLOAT, false, sizeof(v), ptr_diff(&v.texpos, &v)); glEnableVertexAttribArray(1); - glVertexAttribPointer(2, 3, GL_FLOAT, false, sizeof(v), ptr_diff(&v.pos, &v)); + glVertexAttribPointer(2, 4, GL_FLOAT, false, sizeof(v), ptr_diff(&v.pos, &v)); glEnableVertexAttribArray(2); glVertexAttribPointer(3, 3, GL_FLOAT, false, sizeof(v), ptr_diff(&v.normal, &v)); diff --git a/src/graphics/mesh/arrays.hpp b/src/graphics/mesh/arrays.hpp index b56ce7e..2e42132 100644 --- a/src/graphics/mesh/arrays.hpp +++ b/src/graphics/mesh/arrays.hpp @@ -3,6 +3,7 @@ #include #include +#include namespace sim::graphics::arrays { @@ -11,7 +12,7 @@ struct vertex { unsigned long texid = 0; glm::vec2 texpos = {0, 0}; - glm::vec3 pos = {0, 0, 0}; + glm::vec4 pos = {0, 0, 0, 1}; glm::vec3 normal = {0, 0, 0}; }; diff --git a/src/graphics/mesh/font.cpp b/src/graphics/mesh/font.cpp index 960519a..147d39e 100644 --- a/src/graphics/mesh/font.cpp +++ b/src/graphics/mesh/font.cpp @@ -125,10 +125,10 @@ void font::generate(mesh& m, const char* text, double size) float ex = sx + ch.size.x * size; float ey = sy + ch.size.y * size; - vertices.push_back(arrays::vertex(ch.handle, {0, 0}, {sx, sy, 0}, {0, 0, -1})); - vertices.push_back(arrays::vertex(ch.handle, {0, 1}, {sx, ey, 0}, {0, 0, -1})); - vertices.push_back(arrays::vertex(ch.handle, {1, 0}, {ex, sy, 0}, {0, 0, -1})); - vertices.push_back(arrays::vertex(ch.handle, {1, 1}, {ex, ey, 0}, {0, 0, -1})); + vertices.push_back(arrays::vertex(ch.handle, {0, 0}, {sx, sy, 0, 1}, {0, 0, -1})); + vertices.push_back(arrays::vertex(ch.handle, {0, 1}, {sx, ey, 0, 1}, {0, 0, -1})); + vertices.push_back(arrays::vertex(ch.handle, {1, 0}, {ex, sy, 0, 1}, {0, 0, -1})); + vertices.push_back(arrays::vertex(ch.handle, {1, 1}, {ex, ey, 0, 1}, {0, 0, -1})); indices.insert(indices.end(), &index[0], &index[6]); at += 4; diff --git a/src/graphics/mesh/font.hpp b/src/graphics/mesh/font.hpp index bff5902..7af4f77 100644 --- a/src/graphics/mesh/font.hpp +++ b/src/graphics/mesh/font.hpp @@ -4,6 +4,7 @@ #include "mesh.hpp" #include +#include namespace sim::graphics::font { @@ -11,5 +12,13 @@ namespace sim::graphics::font void init(); void generate(mesh& m, const char* text, double size); +template +void generate(mesh& m, const char* header, T* item, double size) +{ + std::stringstream ss; + ss << header << *item; + generate(m, ss.str().c_str(), size); +} + }; diff --git a/src/graphics/mesh/mesh.cpp b/src/graphics/mesh/mesh.cpp index 016c3aa..3a1ed76 100644 --- a/src/graphics/mesh/mesh.cpp +++ b/src/graphics/mesh/mesh.cpp @@ -66,7 +66,10 @@ void mesh::bind() glm::mat4 m = camera::get_matrix() * model_matrix; glUniformMatrix4fv(shader::gl_model, 1, false, &m[0][0]); glUniformMatrix4fv(shader::gl_tex_mat, 1, false, &colour_matrix[0][0]); + glBindVertexArray(vao); + glBindBuffer(GL_ARRAY_BUFFER, vbo); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); } void mesh::render() diff --git a/src/graphics/mesh/model.cpp b/src/graphics/mesh/model.cpp index d6811ab..1735f56 100644 --- a/src/graphics/mesh/model.cpp +++ b/src/graphics/mesh/model.cpp @@ -5,7 +5,9 @@ #include #include #include +#include +#include #include #include @@ -22,28 +24,45 @@ struct proc_state std::string base; std::vector vertices; std::vector indices; + std::unordered_map handles; }; -static unsigned int proc_texture(const proc_state& state, aiMaterial* mat, aiTextureType type) +static unsigned int proc_texture(const proc_state& state, aiMaterial* mat, const aiScene* scene) { - if(mat->GetTextureCount(type) == 0) + for(int i = 0; i < 0x0d; i++) { - return 0; + aiTextureType type = (aiTextureType)i; + + if(mat->GetTextureCount(type) == 0) + { + continue; + } + + aiString str; + mat->GetTexture(type, 0, &str); + + const aiTexture* tex = scene->GetEmbeddedTexture(str.C_Str()); + + if(tex != nullptr) + { + unsigned int handle = state.handles.find(tex)->second; + std::cout << "Using preloaded texture: " << tex->mFilename.C_Str() << "\n"; + return handle; + } + + std::string filename(str.C_Str()); + std::replace(filename.begin(), filename.end(), '\\', '/'); + + return texture::load(state.base + "/" + filename); } - aiString str; - mat->GetTexture(type, 0, &str); - - std::string filename(str.C_Str()); - std::replace(filename.begin(), filename.end(), '\\', '/'); - - return texture::load(state.base + "/" + filename); + return 0; } -static void proc_mesh(proc_state& state, aiMesh* mesh, const aiScene* scene) +static void proc_mesh(proc_state& state, glm::mat4 mat, aiMesh* mesh, const aiScene* scene) { aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex]; - unsigned int texid = proc_texture(state, material, aiTextureType_DIFFUSE); + unsigned int texid = proc_texture(state, material, scene); unsigned int offset = state.offset; for(unsigned int i = 0; i < mesh->mNumVertices; i++) @@ -51,13 +70,13 @@ static void proc_mesh(proc_state& state, aiMesh* mesh, const aiScene* scene) arrays::vertex vertex; auto [x, y, z] = mesh->mVertices[i]; - vertex.pos = {y, x, -z}; + vertex.pos = glm::vec4(x, y, z, 1) * mat; vertex.texid = texid; if(mesh->HasNormals()) { auto [x, y, z] = mesh->mNormals[i]; - vertex.normal = {y, x, -z}; + vertex.normal = glm::vec3(x, y, z) * glm::mat3(mat); } if(mesh->mTextureCoords[0]) @@ -82,20 +101,47 @@ static void proc_mesh(proc_state& state, aiMesh* mesh, const aiScene* scene) state.offset += mesh->mNumVertices; } -static void proc_node(proc_state& state, aiNode* node, const aiScene* scene) +static void proc_node(proc_state& state, glm::mat4 mat, aiNode* node, const aiScene* scene) { + auto m = node->mTransformation; + mat = glm::mat4( + m.a1, m.a2, m.a3, m.a4, + m.b1, m.b2, m.b3, m.b4, + m.c1, m.c2, m.c3, m.c4, + m.d1, m.d2, m.d3, m.d4 + ) * mat; + for(size_t i = 0; i < node->mNumMeshes; i++) { aiMesh* mesh = scene->mMeshes[node->mMeshes[i]]; - proc_mesh(state, mesh, scene); + proc_mesh(state, mat, mesh, scene); } for(size_t i = 0; i < node->mNumChildren; i++) { - proc_node(state, node->mChildren[i], scene); + proc_node(state, mat, node->mChildren[i], scene); } } +static unsigned int proc_embedded_texture(aiTexture* tex) +{ + std::cout << "Loading embedded data: " << tex->mFilename.C_Str() << "\n"; + + if(tex->mHeight == 0) + { + return texture::load_mem((unsigned char*)tex->pcData, tex->mWidth); + } + + // swizzle each pixel to get RGBA + for(int i = 0; i < tex->mWidth * tex->mHeight; i++) + { + aiTexel t = tex->pcData[i]; + tex->pcData[i] = {t.r, t.g, t.b, t.a}; + } + + return texture::load_mem((unsigned char*)tex->pcData, tex->mWidth, tex->mHeight, 4); +} + void mesh::load_model(std::string base, std::string filename) { proc_state state {.base = base}; @@ -103,7 +149,21 @@ void mesh::load_model(std::string base, std::string filename) Assimp::Importer importer; const aiScene *scene = importer.ReadFile(path.c_str(), aiProcess_Triangulate | aiProcess_FlipUVs); - proc_node(state, scene->mRootNode, scene); + + if(scene == nullptr) + { + std::cerr << "AssImp: Error loading model\n"; + return; + } + + for(int i = 0; i < scene->mNumTextures; i++) + { + aiTexture* tex = scene->mTextures[i]; + unsigned int handle = proc_embedded_texture(tex); + state.handles[tex] = handle; + } + + proc_node(state, glm::mat4(1), scene->mRootNode, scene); set_vertices(&state.vertices[0], state.vertices.size(), GL_STATIC_DRAW); set_indices(&state.indices[0], state.indices.size(), GL_STATIC_DRAW); diff --git a/src/graphics/mesh/texture.cpp b/src/graphics/mesh/texture.cpp index 8a35abb..5f933c8 100644 --- a/src/graphics/mesh/texture.cpp +++ b/src/graphics/mesh/texture.cpp @@ -12,22 +12,11 @@ using namespace sim::graphics; static std::unordered_map loaded; -unsigned int texture::load(std::string path) +unsigned int texture::load_mem(const unsigned char* data, int width, int height, int channels) { - const auto it = loaded.find(path); - - if(it != loaded.end()) - { - return it->second; - } - - int width, height, channels; - unsigned char* data = stbi_load(path.c_str(), &width, &height, &channels, 0); - if(!data) { - stbi_image_free(data); - throw std::runtime_error("Failed to load path: " + path); + return 0; } GLenum format, format_in; @@ -57,8 +46,6 @@ unsigned int texture::load(std::string path) glTextureStorage2D(texid, 8, format_in, width, height); glTextureSubImage2D(texid, 0, 0, 0, width, height, format, GL_UNSIGNED_BYTE, data); - stbi_image_free(data); - glTextureParameteri(texid, GL_TEXTURE_WRAP_S, GL_REPEAT); glTextureParameteri(texid, GL_TEXTURE_WRAP_T, GL_REPEAT); glTextureParameteri(texid, GL_TEXTURE_MIN_FILTER, GL_LINEAR); @@ -67,6 +54,36 @@ unsigned int texture::load(std::string path) unsigned int handle = glGetTextureHandleARB(texid); glMakeTextureHandleResidentARB(handle); + return handle; +} + +unsigned int texture::load_mem(const unsigned char* filedata, size_t len) +{ + int width, height, channels; + unsigned char* data = stbi_load_from_memory(filedata, len, &width, &height, &channels, 0); + unsigned int handle = load_mem(data, width, height, channels); + stbi_image_free(data); + return handle; +} + +unsigned int texture::load(std::string path) +{ + const auto it = loaded.find(path); + + if(it != loaded.end()) + { + return it->second; + } + + int width, height, channels; + unsigned char* data = stbi_load(path.c_str(), &width, &height, &channels, 0); + unsigned int handle = load_mem(data, width, height, channels); + stbi_image_free(data); + + if(handle == 0) + { + throw std::runtime_error("Failed to load path: " + path); + } std::cout << "Loaded Image: " << path << "\n"; diff --git a/src/graphics/mesh/texture.hpp b/src/graphics/mesh/texture.hpp index a29d157..d8b402d 100644 --- a/src/graphics/mesh/texture.hpp +++ b/src/graphics/mesh/texture.hpp @@ -7,6 +7,8 @@ namespace sim::graphics::texture { unsigned int load(std::string path); +unsigned int load_mem(const unsigned char* data, int width, int height, int channels); +unsigned int load_mem(const unsigned char* data, size_t len); }; diff --git a/src/graphics/resize.cpp b/src/graphics/resize.cpp index 238292a..cf562c0 100644 --- a/src/graphics/resize.cpp +++ b/src/graphics/resize.cpp @@ -17,14 +17,14 @@ static int win_restore_h; static int win_restore_x; static int win_restore_y; -glm::vec2 resize::get_size() +glm::vec<2, int> resize::get_size() { return {win_w, win_h}; } -double resize::get_aspect() +float resize::get_aspect() { - return win_w / win_h; + return (float)win_w / (float)win_h; } void resize::toggle_fullscreen() diff --git a/src/graphics/resize.hpp b/src/graphics/resize.hpp index ca060e8..ae7c3a8 100644 --- a/src/graphics/resize.hpp +++ b/src/graphics/resize.hpp @@ -8,8 +8,8 @@ namespace sim::graphics::resize void init(); void toggle_fullscreen(); -glm::vec2 get_size(); -double get_aspect(); +glm::vec<2, int> get_size(); +float get_aspect(); }; diff --git a/src/graphics/shader.cpp b/src/graphics/shader.cpp index 86290c8..f893e93 100644 --- a/src/graphics/shader.cpp +++ b/src/graphics/shader.cpp @@ -15,7 +15,7 @@ static const char* VERTEX_SHADER = R"( layout (location = 0) in sampler2D aTex; layout (location = 1) in vec2 aTexPos; -layout (location = 2) in vec3 aPos; +layout (location = 2) in vec4 aPos; layout (location = 3) in vec3 aNormal; uniform mat4 model; @@ -27,7 +27,7 @@ out vec2 texPos; void main() { - vec4 pos = model * vec4(aPos, 1.0); + vec4 pos = model * aPos; vec3 cNormal = vec3(0.f, 0.f, 1.f) * mat3(model); brightness = dot(normalize(aNormal), normalize(cNormal)) * 0.25f + 0.75f; diff --git a/src/graphics/window.cpp b/src/graphics/window.cpp index 6ce2463..c93fec6 100644 --- a/src/graphics/window.cpp +++ b/src/graphics/window.cpp @@ -17,12 +17,15 @@ #include "window.hpp" #include "shader.hpp" #include "mesh/font.hpp" +#include "../parts.hpp" using namespace sim::graphics; static GLFWwindow* win; static bool win_should_close = false; + static mesh MeshScene, MeshText; +static mesh MeshMon1, MeshMon2, MeshMon3; void GLAPIENTRY cb_debug_message(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userParam) { @@ -75,7 +78,7 @@ void window::create() shader::init_program(); MeshScene.bind(); - MeshScene.load_model("../assets/scene", "scene.obj"); + MeshScene.load_model("../assets", "scene-baked.glb"); glm::mat4 mat = glm::mat4(1); mat = glm::translate(mat, glm::vec3(-2.949, -1.7778 + 0.05, 3 - 0.05)); @@ -90,15 +93,53 @@ void window::create() 0, 0, 0, 0 }; + mat = glm::mat4(1); + mat = glm::translate(mat, glm::vec3(-1.5 + 0.05, 3.949, 3 - 0.05)); + mat = glm::rotate(mat, glm::radians(-90), glm::vec3(1, 0, 0)); + + MeshMon1.model_matrix = mat; + MeshMon1.colour_matrix = { + 1, 0.5, 0.5, 1, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0 + }; + + mat = glm::translate(glm::mat4(1), glm::vec3(2.5, 0, 0)) * mat; + + MeshMon2.model_matrix = mat; + MeshMon2.colour_matrix = { + 0.5, 1, 0.5, 1, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0 + }; + + mat = glm::translate(glm::mat4(1), glm::vec3(2.5, 0, 0)) * mat; + + MeshMon3.model_matrix = mat; + MeshMon3.colour_matrix = { + 0.5, 0.5, 1, 1, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0 + }; + glViewport(0, 0, 800, 600); } -void window::loop(const char* str) +void window::loop() { MeshText.bind(); - font::generate(MeshText, str, 0.1); + font::generate(MeshText, "Reactor Core\n\nTODO", 0.1); + MeshMon1.bind(); + font::generate(MeshMon1, "Reactor Vessel\n\n", parts::vessel, 0.1); + MeshMon2.bind(); + font::generate(MeshMon2, "Steam Valve\n\n", parts::valve, 0.1); + MeshMon3.bind(); + font::generate(MeshMon3, "Coolant Pump\n\n", parts::pump, 0.1); - glm::mat4 mat_projection = glm::perspective(glm::radians(90.0f), 1.0f, 0.01f, 20.f); + glm::mat4 mat_projection = glm::perspective(glm::radians(80.0f), resize::get_aspect(), 0.01f, 20.f); glUniformMatrix4fv(shader::gl_projection, 1, false, &mat_projection[0][0]); glClearColor(0, 0, 0, 1.0f); @@ -106,9 +147,14 @@ void window::loop(const char* str) MeshScene.bind(); MeshScene.render(); - MeshText.bind(); MeshText.render(); + MeshMon1.bind(); + MeshMon1.render(); + MeshMon2.bind(); + MeshMon2.render(); + MeshMon3.bind(); + MeshMon3.render(); glfwSwapBuffers(win); glfwPollEvents(); diff --git a/src/graphics/window.hpp b/src/graphics/window.hpp index a55c2a3..3b031fd 100644 --- a/src/graphics/window.hpp +++ b/src/graphics/window.hpp @@ -8,7 +8,7 @@ namespace sim::graphics::window void create(); bool should_close(); -void loop(const char* str); +void loop(); void destroy(); void close(); diff --git a/src/main.cpp b/src/main.cpp index dd32fde..f1466fd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,11 +5,6 @@ #include #include -#include "reactor/builder.hpp" -#include "reactor/control/control_rod.hpp" -#include "reactor/fuel/fuel_rod.hpp" -#include "reactor/coolant/pipe.hpp" -#include "reactor/coolant/heater.hpp" #include "reactor/coolant/vessel.hpp" #include "coolant/fluid_t.hpp" #include "coolant/valve.hpp" @@ -18,6 +13,8 @@ #include "graphics/window.hpp" #include "graphics/camera.hpp" +#include "parts.hpp" + using namespace sim; unsigned long get_now() @@ -31,22 +28,8 @@ int main() { std::random_device rd; std::mt19937 rand(rd()); - - sim::reactor::coolant::vessel vessel(8, 10, 300, sim::coolant::WATER); - sim::reactor::reactor<5, 5> reactor = sim::reactor::builder<5, 5>( - sim::reactor::fuel::fuel_rod(2000, 4000), - sim::reactor::control::control_rod(vessel, 10000, 1), - sim::reactor::coolant::pipe(vessel), { - "#C#C#", - "CFCFC", - "#C#C#", - "CFCFC", - "#C#C#" - }); - - sim::coolant::valve valve(vessel, 1, 500); - sim::coolant::pump pump(vessel, 1e4, 15); + parts::init(); graphics::window::create(); long clock = get_now(); @@ -58,17 +41,13 @@ int main() double dt = (double)passed / 1e6; clock += passed; - std::stringstream ss; - - reactor.update(rand, dt); - pump.update(dt); - valve.update(dt); - vessel.update(dt); - - ss << "Reactor Vessel\n\n" << vessel; + parts::reactor->update(rand, dt); + parts::pump->update(dt); + parts::valve->update(dt); + parts::vessel->update(dt); graphics::camera::update(dt); - graphics::window::loop(ss.str().c_str()); + graphics::window::loop(); } graphics::window::destroy(); diff --git a/src/parts.cpp b/src/parts.cpp new file mode 100644 index 0000000..e8069fe --- /dev/null +++ b/src/parts.cpp @@ -0,0 +1,34 @@ + +#include "parts.hpp" + +#include "reactor/builder.hpp" +#include "reactor/control/control_rod.hpp" +#include "reactor/fuel/fuel_rod.hpp" +#include "reactor/coolant/pipe.hpp" +#include "reactor/coolant/heater.hpp" + +using namespace sim; + +reactor::coolant::vessel* parts::vessel; +reactor::reactor<5, 5>* parts::reactor; +coolant::valve* parts::valve; +coolant::pump* parts::pump; + +void parts::init() +{ + vessel = new reactor::coolant::vessel(8, 10, 300, sim::coolant::WATER); + reactor = new reactor::reactor<5, 5>(sim::reactor::builder<5, 5>( + reactor::fuel::fuel_rod(2000, 4000), + reactor::control::control_rod(*vessel, 10000, 1), + reactor::coolant::pipe(*vessel), { + "#C#C#", + "CFCFC", + "#C#C#", + "CFCFC", + "#C#C#" + })); + + valve = new coolant::valve(*vessel, 1, 500); + pump = new coolant::pump(*vessel, 1e4, 15); +} + diff --git a/src/parts.hpp b/src/parts.hpp new file mode 100644 index 0000000..f52c5ad --- /dev/null +++ b/src/parts.hpp @@ -0,0 +1,20 @@ + +#pragma once + +#include "reactor/coolant/vessel.hpp" +#include "reactor/reactor.hpp" +#include "coolant/pump.hpp" +#include "coolant/valve.hpp" + +namespace sim::parts +{ + +extern sim::reactor::coolant::vessel* vessel; +extern sim::reactor::reactor<5, 5>* reactor; +extern sim::coolant::valve* valve; +extern sim::coolant::pump* pump; + +void init(); + +}; + diff --git a/src/reactor/coolant/vessel.cpp b/src/reactor/coolant/vessel.cpp index fa2bbf0..5cbd1ad 100644 --- a/src/reactor/coolant/vessel.cpp +++ b/src/reactor/coolant/vessel.cpp @@ -126,16 +126,3 @@ double vessel::get_pressure() const return (n * T * constants::R) / V; } -std::ostream& operator<<(std::ostream& o, const vessel& v) -{ - o << "Volume: " << v.get_volume() << " L\n"; - o << "Level: " << v.get_level() << " L\n"; - o << "Steam: " << v.get_steam() << " g\n"; - o << "Heat: " << v.get_heat() << " C\n"; - o << "Pressure: " << (v.get_pressure() * 0.001) << " kPa\n"; - o << "Void Ratio: " << (v.get_void_ratio() * 100) << " %\n"; - - return o; -} - - diff --git a/src/reactor/coolant/vessel.hpp b/src/reactor/coolant/vessel.hpp index b9dbc5a..f95a59d 100644 --- a/src/reactor/coolant/vessel.hpp +++ b/src/reactor/coolant/vessel.hpp @@ -41,9 +41,20 @@ public: constexpr double get_void_ratio() const { double s = steam_suspended / get_steam_density(); return s / (level + s); } double get_pressure() const; // pascals + + friend std::ostream& operator<<(std::ostream& o, const vessel& v) + { + o << "Volume: " << v.get_volume() << " L\n"; + o << "Level: " << v.get_level() << " L\n"; + o << "Steam: " << v.get_steam() << " g\n"; + o << "Heat: " << v.get_heat() << " C\n"; + o << "Pressure: " << (v.get_pressure() * 0.001) << " kPa\n"; + o << "Void Ratio: " << (v.get_void_ratio() * 100) << " %\n"; + + return o; + } + }; } -std::ostream& operator<<(std::ostream& o, const sim::reactor::coolant::vessel& v); -