mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-07-22 16:19:19 +02:00
build 200709. milek7/sim branch scenery editor node bank code import
This commit is contained in:
@@ -929,7 +929,12 @@ TAnimModel::export_as_text_( std::ostream &Output ) const {
|
|||||||
// don't include 'textures/' in the path
|
// don't include 'textures/' in the path
|
||||||
texturefile.erase( 0, std::string{ szTexturePath }.size() );
|
texturefile.erase( 0, std::string{ szTexturePath }.size() );
|
||||||
}
|
}
|
||||||
Output << texturefile << ' ';
|
if( texturefile.find( ' ' ) == std::string::npos ) {
|
||||||
|
Output << texturefile << ' ';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Output << "\"" << texturefile << "\"" << ' ';
|
||||||
|
}
|
||||||
// light submodels activation configuration
|
// light submodels activation configuration
|
||||||
if( iNumLights > 0 ) {
|
if( iNumLights > 0 ) {
|
||||||
Output << "lights ";
|
Output << "lights ";
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/.
|
|||||||
#include "Timer.h"
|
#include "Timer.h"
|
||||||
#include "Console.h"
|
#include "Console.h"
|
||||||
#include "renderer.h"
|
#include "renderer.h"
|
||||||
|
#include "AnimModel.h"
|
||||||
|
|
||||||
bool
|
bool
|
||||||
editor_mode::editormode_input::init() {
|
editor_mode::editormode_input::init() {
|
||||||
@@ -175,6 +176,22 @@ editor_mode::on_key( int const Key, int const Scancode, int const Action, int co
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: ensure delete method can play nice with history stack
|
||||||
|
case GLFW_KEY_DELETE: {
|
||||||
|
TAnimModel *model = dynamic_cast<TAnimModel*>(m_node);
|
||||||
|
if (!model)
|
||||||
|
break;
|
||||||
|
|
||||||
|
m_node = nullptr;
|
||||||
|
m_dragging = false;
|
||||||
|
Application.set_cursor( GLFW_CURSOR_NORMAL );
|
||||||
|
static_cast<editor_ui*>( m_userinterface.get() )->set_node(nullptr);
|
||||||
|
|
||||||
|
simulation::State.delete_model(model);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -224,6 +241,8 @@ editor_mode::on_cursor_pos( double const Horizontal, double const Vertical ) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
TODO: re-enable in post-merge cleanup
|
||||||
void
|
void
|
||||||
editor_mode::on_mouse_button( int const Button, int const Action, int const Mods ) {
|
editor_mode::on_mouse_button( int const Button, int const Action, int const Mods ) {
|
||||||
|
|
||||||
@@ -255,6 +274,81 @@ editor_mode::on_mouse_button( int const Button, int const Action, int const Mods
|
|||||||
|
|
||||||
m_input.mouse.button( Button, Action );
|
m_input.mouse.button( Button, Action );
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
editor_mode::on_mouse_button( int const Button, int const Action, int const Mods ) {
|
||||||
|
|
||||||
|
// give the ui first shot at the input processing...
|
||||||
|
if( true == m_userinterface->on_mouse_button( Button, Action, Mods ) ) { return; }
|
||||||
|
|
||||||
|
if( Button == GLFW_MOUSE_BUTTON_LEFT ) {
|
||||||
|
|
||||||
|
if( Action == GLFW_PRESS ) {
|
||||||
|
// left button press
|
||||||
|
auto const mode = static_cast<editor_ui*>( m_userinterface.get() )->mode();
|
||||||
|
|
||||||
|
m_node = nullptr;
|
||||||
|
|
||||||
|
GfxRenderer->Pick_Node_Callback([this, mode](scene::basic_node *node)
|
||||||
|
{
|
||||||
|
editor_ui *ui = static_cast<editor_ui*>( m_userinterface.get() );
|
||||||
|
|
||||||
|
if (mode == nodebank_panel::MODIFY) {
|
||||||
|
if (!m_dragging)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_node = node;
|
||||||
|
if( m_node )
|
||||||
|
Application.set_cursor( GLFW_CURSOR_DISABLED );
|
||||||
|
else
|
||||||
|
m_dragging = false;
|
||||||
|
ui->set_node( m_node );
|
||||||
|
}
|
||||||
|
else if (mode == nodebank_panel::COPY) {
|
||||||
|
if (node && typeid(*node) == typeid(TAnimModel)) {
|
||||||
|
std::string as_text;
|
||||||
|
node->export_as_text(as_text);
|
||||||
|
|
||||||
|
ui->add_node_template(as_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_dragging = false;
|
||||||
|
}
|
||||||
|
else if (mode == nodebank_panel::ADD) {
|
||||||
|
const std::string *src = ui->get_active_node_template();
|
||||||
|
std::string name = "editor_" + std::to_string(LocalRandom(0.0, 100000.0));
|
||||||
|
|
||||||
|
if (!src)
|
||||||
|
return;
|
||||||
|
|
||||||
|
TAnimModel *cloned = simulation::State.create_model(*src, name, Camera.Pos + GfxRenderer->Mouse_Position());
|
||||||
|
|
||||||
|
if (!cloned)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!m_dragging)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_node = cloned;
|
||||||
|
Application.set_cursor( GLFW_CURSOR_DISABLED );
|
||||||
|
ui->set_node( m_node );
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
m_dragging = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// left button release
|
||||||
|
if( m_node )
|
||||||
|
Application.set_cursor( GLFW_CURSOR_NORMAL );
|
||||||
|
m_dragging = false;
|
||||||
|
// prime history stack for another snapshot
|
||||||
|
m_takesnapshot = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_input.mouse.button( Button, Action );
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
editor_mode::on_scroll( double const Xoffset, double const Yoffset ) {
|
editor_mode::on_scroll( double const Xoffset, double const Yoffset ) {
|
||||||
|
|||||||
@@ -89,5 +89,5 @@ private:
|
|||||||
scene::basic_editor m_editor;
|
scene::basic_editor m_editor;
|
||||||
scene::basic_node *m_node; // currently selected scene node
|
scene::basic_node *m_node; // currently selected scene node
|
||||||
bool m_takesnapshot { true }; // helper, hints whether snapshot of selected node(s) should be taken before modification
|
bool m_takesnapshot { true }; // helper, hints whether snapshot of selected node(s) should be taken before modification
|
||||||
|
bool m_dragging = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ editor_ui::editor_ui() {
|
|||||||
clear_panels();
|
clear_panels();
|
||||||
// bind the panels with ui object. maybe not the best place for this but, eh
|
// bind the panels with ui object. maybe not the best place for this but, eh
|
||||||
push_back( &m_itempropertiespanel );
|
push_back( &m_itempropertiespanel );
|
||||||
|
push_back( &m_nodebankpanel );
|
||||||
}
|
}
|
||||||
|
|
||||||
// updates state of UI elements
|
// updates state of UI elements
|
||||||
@@ -46,3 +47,18 @@ editor_ui::set_node( scene::basic_node * Node ) {
|
|||||||
|
|
||||||
m_node = Node;
|
m_node = Node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
editor_ui::add_node_template(const std::string &desc) {
|
||||||
|
m_nodebankpanel.add_template(desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string const *
|
||||||
|
editor_ui::get_active_node_template() {
|
||||||
|
return m_nodebankpanel.get_active_template();
|
||||||
|
}
|
||||||
|
|
||||||
|
nodebank_panel::edit_mode
|
||||||
|
editor_ui::mode() {
|
||||||
|
return m_nodebankpanel.mode;
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,9 +29,16 @@ public:
|
|||||||
update() override;
|
update() override;
|
||||||
void
|
void
|
||||||
set_node( scene::basic_node * Node );
|
set_node( scene::basic_node * Node );
|
||||||
|
void
|
||||||
|
add_node_template(const std::string &desc);
|
||||||
|
const std::string *
|
||||||
|
get_active_node_template();
|
||||||
|
nodebank_panel::edit_mode
|
||||||
|
mode();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// members
|
// members
|
||||||
itemproperties_panel m_itempropertiespanel { "Node Properties", true };
|
itemproperties_panel m_itempropertiespanel { "Node Properties", true };
|
||||||
|
nodebank_panel m_nodebankpanel{ "Node Bank", true };
|
||||||
scene::basic_node * m_node { nullptr }; // currently bound scene node, if any
|
scene::basic_node * m_node { nullptr }; // currently bound scene node, if any
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -297,3 +297,79 @@ itemproperties_panel::render_group() {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
nodebank_panel::nodebank_panel( std::string const &Name, bool const Isopen ) : ui_panel( Name, Isopen ) {
|
||||||
|
size_min = { 100, 50 };
|
||||||
|
size_max = { 1000, 1000 };
|
||||||
|
|
||||||
|
std::ifstream file;
|
||||||
|
file.open("nodebank.txt", std::ios_base::in | std::ios_base::binary);
|
||||||
|
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(file, line))
|
||||||
|
if (line.size() > 2)
|
||||||
|
m_nodebank.push_back(std::make_unique<std::string>(line));
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
nodebank_panel::render() {
|
||||||
|
|
||||||
|
if( false == is_open ) { return; }
|
||||||
|
|
||||||
|
auto flags =
|
||||||
|
ImGuiWindowFlags_NoFocusOnAppearing
|
||||||
|
| ImGuiWindowFlags_NoCollapse
|
||||||
|
| ( size.x > 0 ? ImGuiWindowFlags_NoResize : 0 );
|
||||||
|
|
||||||
|
if( size.x > 0 ) {
|
||||||
|
ImGui::SetNextWindowSize( ImVec2( size.x, size.y ) );
|
||||||
|
}
|
||||||
|
if( size_min.x > 0 ) {
|
||||||
|
ImGui::SetNextWindowSizeConstraints( ImVec2( size_min.x, size_min.y ), ImVec2( size_max.x, size_max.y ) );
|
||||||
|
}
|
||||||
|
auto const panelname { (
|
||||||
|
title.empty() ?
|
||||||
|
name :
|
||||||
|
title )
|
||||||
|
+ "###" + name };
|
||||||
|
|
||||||
|
if( true == ImGui::Begin( panelname.c_str(), nullptr, flags ) ) {
|
||||||
|
|
||||||
|
ImGui::RadioButton("modify node", (int*)&mode, MODIFY);
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::RadioButton("insert from bank", (int*)&mode, ADD);
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::RadioButton( "copy to bank", (int*)&mode, COPY );
|
||||||
|
|
||||||
|
ImGui::PushItemWidth(-1);
|
||||||
|
if (ImGui::ListBoxHeader("##nodebank", ImVec2(-1, -1)))
|
||||||
|
{
|
||||||
|
int i = 0;
|
||||||
|
for (auto const entry : m_nodebank) {
|
||||||
|
std::string label = *entry + "##" + std::to_string(i);
|
||||||
|
if (ImGui::Selectable(label.c_str(), entry == m_selectedtemplate))
|
||||||
|
m_selectedtemplate = entry;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::ListBoxFooter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
nodebank_panel::add_template(const std::string &desc) {
|
||||||
|
std::ofstream file;
|
||||||
|
file.open("nodebank.txt", std::ios_base::out | std::ios_base::app | std::ios_base::binary);
|
||||||
|
file << desc;
|
||||||
|
|
||||||
|
m_nodebank.push_back(std::make_unique<std::string>(desc));
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string *nodebank_panel::get_active_template() {
|
||||||
|
return m_selectedtemplate.get();
|
||||||
|
}
|
||||||
|
|||||||
@@ -47,3 +47,23 @@ private:
|
|||||||
std::string m_groupprefix;
|
std::string m_groupprefix;
|
||||||
std::vector<text_line> m_grouplines;
|
std::vector<text_line> m_grouplines;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class nodebank_panel : public ui_panel {
|
||||||
|
std::vector<std::shared_ptr<std::string>> m_nodebank;
|
||||||
|
std::shared_ptr<std::string> m_selectedtemplate;
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum edit_mode {
|
||||||
|
MODIFY,
|
||||||
|
COPY,
|
||||||
|
ADD
|
||||||
|
};
|
||||||
|
|
||||||
|
edit_mode mode = MODIFY;
|
||||||
|
|
||||||
|
nodebank_panel( std::string const &Name, bool const Isopen );
|
||||||
|
|
||||||
|
void render() override;
|
||||||
|
void add_template(const std::string &desc);
|
||||||
|
const std::string* get_active_template();
|
||||||
|
};
|
||||||
|
|||||||
@@ -749,7 +749,7 @@ basic_node::export_as_text( std::ostream &Output ) const {
|
|||||||
<< ' ' << ( m_rangesquaredmax < std::numeric_limits<double>::max() ? std::sqrt( m_rangesquaredmax ) : -1 )
|
<< ' ' << ( m_rangesquaredmax < std::numeric_limits<double>::max() ? std::sqrt( m_rangesquaredmax ) : -1 )
|
||||||
<< ' ' << std::sqrt( m_rangesquaredmin )
|
<< ' ' << std::sqrt( m_rangesquaredmin )
|
||||||
// name
|
// name
|
||||||
<< ' ' << m_name << ' ';
|
<< ' ' << ( m_name.empty() ? "none" : m_name ) << ' ';
|
||||||
// template method implementation
|
// template method implementation
|
||||||
export_as_text_( Output );
|
export_as_text_( Output );
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,8 +93,8 @@ node_groups::insert( scene::group_handle const Group, basic_event *Event ) {
|
|||||||
|
|
||||||
// sends basic content of the class in legacy (text) format to provided stream
|
// sends basic content of the class in legacy (text) format to provided stream
|
||||||
void
|
void
|
||||||
node_groups::export_as_text( std::ostream &Output ) const {
|
node_groups::export_as_text( std::ostream &Output, bool const Dirty ) const {
|
||||||
|
/*
|
||||||
for( auto const &group : m_groupmap ) {
|
for( auto const &group : m_groupmap ) {
|
||||||
|
|
||||||
Output << "group\n";
|
Output << "group\n";
|
||||||
@@ -112,6 +112,38 @@ node_groups::export_as_text( std::ostream &Output ) const {
|
|||||||
}
|
}
|
||||||
Output << "endgroup\n";
|
Output << "endgroup\n";
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
for( auto const &group : m_groupmap ) {
|
||||||
|
bool any = false;
|
||||||
|
for( auto *node : group.second.nodes ) {
|
||||||
|
if (node->dirty() != Dirty)
|
||||||
|
continue;
|
||||||
|
// HACK: auto-generated memory cells aren't exported, so we check for this
|
||||||
|
// TODO: is_exportable as basic_node method
|
||||||
|
if( ( typeid( *node ) == typeid( TMemCell ) )
|
||||||
|
&& ( false == static_cast<TMemCell *>( node )->is_exportable ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!any)
|
||||||
|
Output << "group\n";
|
||||||
|
any = true;
|
||||||
|
|
||||||
|
node->export_as_text( Output );
|
||||||
|
}
|
||||||
|
for( auto *event : group.second.events ) {
|
||||||
|
if (Dirty)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!any)
|
||||||
|
Output << "group\n";
|
||||||
|
any = true;
|
||||||
|
|
||||||
|
event->export_as_text( Output );
|
||||||
|
}
|
||||||
|
if (any)
|
||||||
|
Output << "endgroup\n";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// removes specified group from the group list and group information from the group's nodes
|
// removes specified group from the group list and group information from the group's nodes
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ public:
|
|||||||
return m_groupmap[ Group ]; }
|
return m_groupmap[ Group ]; }
|
||||||
// sends basic content of the class in legacy (text) format to provided stream
|
// sends basic content of the class in legacy (text) format to provided stream
|
||||||
void
|
void
|
||||||
export_as_text( std::ostream &Output ) const;
|
export_as_text( std::ostream &Output, bool const Dirty ) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// types
|
// types
|
||||||
|
|||||||
@@ -1012,7 +1012,7 @@ state_serializer::transform( glm::dvec3 Location, scene::scratch_data const &Scr
|
|||||||
return Location;
|
return Location;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
// stores class data in specified file, in legacy (text) format
|
// stores class data in specified file, in legacy (text) format
|
||||||
void
|
void
|
||||||
state_serializer::export_as_text( std::string const &Scenariofile ) const {
|
state_serializer::export_as_text( std::string const &Scenariofile ) const {
|
||||||
@@ -1083,6 +1083,89 @@ state_serializer::export_as_text( std::string const &Scenariofile ) const {
|
|||||||
|
|
||||||
WriteLog( "Scenery data export done." );
|
WriteLog( "Scenery data export done." );
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
state_serializer::export_as_text(std::string const &Scenariofile) const {
|
||||||
|
|
||||||
|
if( Scenariofile == "$.scn" ) {
|
||||||
|
ErrorLog( "Bad file: scenery export not supported for file \"$.scn\"" );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WriteLog( "Scenery data export in progress..." );
|
||||||
|
}
|
||||||
|
|
||||||
|
auto filename { Scenariofile };
|
||||||
|
while( filename[ 0 ] == '$' ) {
|
||||||
|
// trim leading $ char rainsted utility may add to the base name for modified .scn files
|
||||||
|
filename.erase( 0, 1 );
|
||||||
|
}
|
||||||
|
erase_extension( filename );
|
||||||
|
auto absfilename = Global.asCurrentSceneryPath + filename + "_export";
|
||||||
|
|
||||||
|
std::ofstream scmdirtyfile { absfilename + "_dirty.scm" };
|
||||||
|
export_nodes_to_stream(scmdirtyfile, true);
|
||||||
|
|
||||||
|
std::ofstream scmfile { absfilename + ".scm" };
|
||||||
|
export_nodes_to_stream(scmfile, false);
|
||||||
|
|
||||||
|
// sounds
|
||||||
|
// NOTE: sounds currently aren't included in groups
|
||||||
|
scmfile << "// sounds\n";
|
||||||
|
Region->export_as_text( scmfile );
|
||||||
|
|
||||||
|
scmfile << "// modified objects\ninclude " << filename << "_export_dirty.scm\n";
|
||||||
|
|
||||||
|
std::ofstream ctrfile { absfilename + ".ctr" };
|
||||||
|
// mem cells
|
||||||
|
ctrfile << "// memory cells\n";
|
||||||
|
for( auto const *memorycell : Memory.sequence() ) {
|
||||||
|
if( ( true == memorycell->is_exportable )
|
||||||
|
&& ( memorycell->group() == null_handle ) ) {
|
||||||
|
memorycell->export_as_text( ctrfile );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// events
|
||||||
|
Events.export_as_text( ctrfile );
|
||||||
|
|
||||||
|
WriteLog( "Scenery data export done." );
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
state_serializer::export_nodes_to_stream(std::ostream &scmfile, bool Dirty) const {
|
||||||
|
// groups
|
||||||
|
scmfile << "// groups\n";
|
||||||
|
scene::Groups.export_as_text( scmfile, Dirty );
|
||||||
|
|
||||||
|
// tracks
|
||||||
|
scmfile << "// paths\n";
|
||||||
|
for( auto const *path : Paths.sequence() ) {
|
||||||
|
if( path->dirty() == Dirty && path->group() == null_handle ) {
|
||||||
|
path->export_as_text( scmfile );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// traction
|
||||||
|
scmfile << "// traction\n";
|
||||||
|
for( auto const *traction : Traction.sequence() ) {
|
||||||
|
if( traction->dirty() == Dirty && traction->group() == null_handle ) {
|
||||||
|
traction->export_as_text( scmfile );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// power grid
|
||||||
|
scmfile << "// traction power sources\n";
|
||||||
|
for( auto const *powersource : Powergrid.sequence() ) {
|
||||||
|
if( powersource->dirty() == Dirty && powersource->group() == null_handle ) {
|
||||||
|
powersource->export_as_text( scmfile );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// models
|
||||||
|
scmfile << "// instanced models\n";
|
||||||
|
for( auto const *instance : Instances.sequence() ) {
|
||||||
|
if( instance && instance->dirty() == Dirty && instance->group() == null_handle ) {
|
||||||
|
instance->export_as_text( scmfile );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TAnimModel *state_serializer::create_model(const std::string &src, const std::string &name, const glm::dvec3 &position) {
|
TAnimModel *state_serializer::create_model(const std::string &src, const std::string &name, const glm::dvec3 &position) {
|
||||||
cParser parser(src);
|
cParser parser(src);
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ private:
|
|||||||
void skip_until( cParser &Input, std::string const &Token );
|
void skip_until( cParser &Input, std::string const &Token );
|
||||||
// transforms provided location by specifed rotation and offset
|
// transforms provided location by specifed rotation and offset
|
||||||
glm::dvec3 transform( glm::dvec3 Location, scene::scratch_data const &Scratchpad );
|
glm::dvec3 transform( glm::dvec3 Location, scene::scratch_data const &Scratchpad );
|
||||||
|
void export_nodes_to_stream( std::ostream &, bool Dirty ) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // simulation
|
} // simulation
|
||||||
|
|||||||
Reference in New Issue
Block a user