diff --git a/launcher/keymapper.cpp b/launcher/keymapper.cpp new file mode 100644 index 00000000..0b23d5ad --- /dev/null +++ b/launcher/keymapper.cpp @@ -0,0 +1,89 @@ +#include "stdafx.h" +#include "keymapper.h" +#include "simulation.h" + +ui::keymapper_panel::keymapper_panel() + : ui_panel(STR("Keymapper"), true) +{ + keyboard.init(); +} + +bool ui::keymapper_panel::key(int key) +{ + if (!is_open || bind_active == user_command::none) + return false; + + auto it = keyboard.keytonamemap.find(key); + if (it == keyboard.keytonamemap.end()) + return false; + + if (Global.shiftState) + key |= keyboard_input::keymodifier::shift; + if (Global.ctrlState) + key |= keyboard_input::keymodifier::control; + + keyboard.bindings().insert_or_assign(bind_active, key); + bind_active = user_command::none; + keyboard.bind(); + + return true; +} + +void ui::keymapper_panel::render() +{ + if (!is_open) + return; + + auto const panelname{(title.empty() ? m_name : title) + "###" + m_name}; + + if (ImGui::Begin(panelname.c_str(), &is_open)) { + if (ImGui::Button(STR_C("Load"))) + keyboard.init(); + + ImGui::SameLine(); + + if (ImGui::Button(STR_C("Save"))) + keyboard.dump_bindings(); + + for (const std::pair &binding : keyboard.bindings()) { + ImGui::AlignTextToFramePadding(); + ImGui::Text(simulation::Commands_descriptions[static_cast(binding.first)].name.c_str()); + + std::string label; + + if (binding.second & keyboard_input::keymodifier::control) + label += "ctrl + "; + if (binding.second & keyboard_input::keymodifier::shift) + label += "shift + "; + + auto it = keyboard.keytonamemap.find(binding.second & 0xFFFF); + if (it != keyboard.keytonamemap.end()) + label += it->second; + + label = ToUpper(label); + + bool styles_pushed = false; + if (bind_active == binding.first) { + label = "?"; + ImVec4 active_col = ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive); + ImGui::PushStyleColor(ImGuiCol_Button, active_col); + styles_pushed = true; + } + + label += "##" + std::to_string((int)binding.first); + + ImGui::SameLine(ImGui::GetContentRegionAvailWidth() - 150); + if (ImGui::Button(label.c_str(), ImVec2(150, 0))) { + if (bind_active == binding.first) + bind_active = user_command::none; + else + bind_active = binding.first; + } + + if (styles_pushed) + ImGui::PopStyleColor(); + } + } + + ImGui::End(); +} diff --git a/launcher/keymapper.h b/launcher/keymapper.h new file mode 100644 index 00000000..bb2a6205 --- /dev/null +++ b/launcher/keymapper.h @@ -0,0 +1,21 @@ +#pragma once + +#include "uilayer.h" +#include "driverkeyboardinput.h" + +namespace ui +{ +class keymapper_panel : public ui_panel +{ + public: + keymapper_panel(); + + void render() override; + bool key(int key); + +private: + driverkeyboard_input keyboard; + + user_command bind_active = user_command::none; +}; +} // namespace ui diff --git a/launcher/launchermode.cpp b/launcher/launchermode.cpp new file mode 100644 index 00000000..2e0a54d7 --- /dev/null +++ b/launcher/launchermode.cpp @@ -0,0 +1,43 @@ +#include "stdafx.h" +#include "launcher/launchermode.h" +#include "launcher/launcheruilayer.h" +#include "application.h" +#include "simulation.h" +#include "Globals.h" + +launcher_mode::launcher_mode() +{ + m_userinterface = std::make_shared(); +} + +bool launcher_mode::init() +{ + return true; +} + +bool launcher_mode::update() +{ + return true; +} + +void launcher_mode::enter() +{ + Application.set_cursor( GLFW_CURSOR_NORMAL ); + + simulation::is_ready = false; + + m_userinterface->set_background( "logo" ); + Application.set_title(Global.AppName); +} + +void launcher_mode::exit() +{ + +} + +void launcher_mode::on_key(const int Key, const int Scancode, const int Action, const int Mods) +{ + Global.shiftState = ( Mods & GLFW_MOD_SHIFT ) ? true : false; + Global.ctrlState = ( Mods & GLFW_MOD_CONTROL ) ? true : false; + m_userinterface->on_key(Key, Action); +} diff --git a/launcher/launchermode.h b/launcher/launchermode.h new file mode 100644 index 00000000..df7dc192 --- /dev/null +++ b/launcher/launchermode.h @@ -0,0 +1,45 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "applicationmode.h" + +class launcher_mode : public application_mode { + +public: +// constructors + launcher_mode(); +// methods + // initializes internal data structures of the mode. returns: true on success, false otherwise + bool + init() override; + // mode-specific update of simulation data. returns: false on error, true otherwise + bool + update() override; + // maintenance method, called when the mode is activated + void + enter() override; + // maintenance method, called when the mode is deactivated + void + exit() override; + // input handlers + void + on_key( int const Key, int const Scancode, int const Action, int const Mods ); + void + on_cursor_pos( double const Horizontal, double const Vertical ) override { ; } + void + on_mouse_button( int const Button, int const Action, int const Mods ) override { ; } + void + on_scroll( double const Xoffset, double const Yoffset ) override { ; } + void + on_event_poll() override { ; } + bool + is_command_processor() override { return false; } +}; diff --git a/launcher/launcheruilayer.cpp b/launcher/launcheruilayer.cpp new file mode 100644 index 00000000..900cf58f --- /dev/null +++ b/launcher/launcheruilayer.cpp @@ -0,0 +1,18 @@ +#include "stdafx.h" +#include "launcher/launcheruilayer.h" + +launcher_ui::launcher_ui() +{ + add_external_panel(&m_scenerylist_panel); + add_external_panel(&m_keymapper_panel); + add_external_panel(&m_mainmenu_panel); + add_external_panel(&m_vehiclepicker_panel); +} + +bool launcher_ui::on_key(const int Key, const int Action) +{ + if (m_keymapper_panel.key(Key)) + return true; + + return ui_layer::on_key(Key, Action); +} diff --git a/launcher/launcheruilayer.h b/launcher/launcheruilayer.h new file mode 100644 index 00000000..9d20b51c --- /dev/null +++ b/launcher/launcheruilayer.h @@ -0,0 +1,28 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "uilayer.h" +#include "launcher/scenery_list.h" +#include "launcher/keymapper.h" +#include "launcher/mainmenu.h" +#include "launcher/vehicle_picker.h" + +class launcher_ui : public ui_layer { +public: + launcher_ui(); + bool on_key(const int Key, const int Action) override; + +private: + ui::scenerylist_panel m_scenerylist_panel; + ui::keymapper_panel m_keymapper_panel; + ui::mainmenu_panel m_mainmenu_panel; + ui::vehiclepicker_panel m_vehiclepicker_panel; +}; diff --git a/launcher/mainmenu.cpp b/launcher/mainmenu.cpp new file mode 100644 index 00000000..83994693 --- /dev/null +++ b/launcher/mainmenu.cpp @@ -0,0 +1,26 @@ +#include "stdafx.h" +#include "launcher/mainmenu.h" + +ui::mainmenu_panel::mainmenu_panel() + : ui_panel(STR("Main menu"), true) +{ + +} + +void ui::mainmenu_panel::render() +{ + if (!is_open) + return; + + ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoCollapse; + ImVec2 display_size = ImGui::GetIO().DisplaySize; + ImGui::SetNextWindowPos(ImVec2(display_size.x * 0.66f, display_size.y / 2.0f)); + ImGui::SetNextWindowSize(ImVec2(200, -1)); + + if (ImGui::Begin(m_name.c_str(), nullptr, flags)) { + ImGui::Button(STR_C("Scenario list"), ImVec2(-1, 0)); + ImGui::Button(STR_C("Settings"), ImVec2(-1, 0)); + ImGui::Button(STR_C("Quit"), ImVec2(-1, 0)); + } + ImGui::End(); +} diff --git a/launcher/mainmenu.h b/launcher/mainmenu.h new file mode 100644 index 00000000..db5bfec8 --- /dev/null +++ b/launcher/mainmenu.h @@ -0,0 +1,14 @@ +#pragma once + +#include "uilayer.h" + +namespace ui +{ +class mainmenu_panel : public ui_panel +{ + public: + mainmenu_panel(); + + void render() override; +}; +} // namespace ui diff --git a/launcher/scenery_list.cpp b/launcher/scenery_list.cpp new file mode 100644 index 00000000..3a6cb135 --- /dev/null +++ b/launcher/scenery_list.cpp @@ -0,0 +1,165 @@ +#include "stdafx.h" +#include "scenery_list.h" +#include "imgui/imgui.h" +#include "utilities.h" +#include "renderer.h" +#include + +ui::scenerylist_panel::scenerylist_panel() + : ui_panel(STR("Scenario list"), true) +{ + scan_scenarios(); + + std::sort(scenarios.begin(), scenarios.end(), + [](const scenery_desc &a, const scenery_desc &b) { return a.path < b.path; }); +} + +void ui::scenerylist_panel::render() +{ + if (!is_open) + return; + + auto const panelname{(title.empty() ? m_name : title) + "###" + m_name}; + + if (ImGui::Begin(panelname.c_str(), &is_open)) { + ImGui::Columns(3); + + if (ImGui::BeginChild("child1")) { + std::string prev_prefix; + bool collapse_open = false; + + for (auto const &desc : scenarios) { + std::string name = desc.path.stem(); + std::string prefix = name.substr(0, name.find_first_of("-_")); + if (prefix.empty()) + prefix = name; + + bool just_opened = false; + if (prefix != prev_prefix) { + collapse_open = ImGui::CollapsingHeader(prefix.c_str()); + just_opened = ImGui::IsItemDeactivated(); + prev_prefix = prefix; + } + + if (collapse_open) { + if (just_opened) + selected_scenery = &desc; + + ImGui::Indent(10.0f); + if (ImGui::Selectable(name.c_str(), &desc == selected_scenery)) + selected_scenery = &desc; + ImGui::Unindent(10.0f); + } + } + } ImGui::EndChild(); + + ImGui::NextColumn(); + + if (ImGui::BeginChild("child2")) { + if (selected_scenery) { + ImGui::TextWrapped("%s", selected_scenery->name.c_str()); + ImGui::TextWrapped("%s", selected_scenery->description.c_str()); + + ImGui::Separator(); + + for (auto const &link : selected_scenery->links) { + if (ImGui::Button(link.second.c_str(), ImVec2(-1, 0))) { + std::string file = ToLower(link.first); +#ifdef _WIN32 + system(("start \"" + file + "\"").c_str()); +#elif __linux__ + system(("xdg-open \"" + file + "\"").c_str()); +#elif __APPLE__ + system(("open \"" + file + "\"").c_str()); +#endif + } + } + } + } ImGui::EndChild(); + + ImGui::NextColumn(); + + if (selected_scenery) { + if (ImGui::BeginChild("child3", ImVec2(0, 0), false, ImGuiWindowFlags_NoScrollbar)) { + if (!selected_scenery->image_path.empty()) { + scenery_desc *desc = const_cast(selected_scenery); + desc->image = GfxRenderer.Fetch_Texture(selected_scenery->image_path, true); + desc->image_path.clear(); + } + + if (selected_scenery->image != null_handle) { + opengl_texture &tex = GfxRenderer.Texture(selected_scenery->image); + tex.create(); + + if (tex.is_ready) { + float avail_width = ImGui::GetContentRegionAvailWidth(); + float height = avail_width / tex.width() * tex.height(); + + ImGui::Image(reinterpret_cast(tex.id), ImVec2(avail_width, height), ImVec2(0, 1), ImVec2(1, 0)); + } + } + } ImGui::EndChild(); + + if (ImGui::Begin(STR_C("Select trainset"))) { + for (auto const &trainset : selected_scenery->trainsets) { + ImGui::Selectable("Trainset", false); + } + } + ImGui::End(); + } + } + + ImGui::End(); +} + +void ui::scenerylist_panel::scan_scenarios() +{ + for (auto &f : std::filesystem::directory_iterator("scenery")) { + std::filesystem::path path(f.path()); + + if (*(path.filename().string().begin()) == '$') + continue; + + if (string_ends_with(path, ".scn")) + scan_scn(path); + } +} + +void ui::scenerylist_panel::scan_scn(std::string path) +{ + scenarios.emplace_back(); + scenery_desc &desc = scenarios.back(); + desc.path = path; + + std::ifstream stream(path, std::ios_base::binary | std::ios_base::in); + + std::string line; + while (std::getline(stream, line)) + { + if (line.size() < 6 || !string_starts_with(line, "//$")) + continue; + + if (line[3] == 'i') + desc.image_path = "scenery/images/" + line.substr(5); + else if (line[3] == 'n') + desc.name = line.substr(5); + else if (line[3] == 'd') + desc.description += line.substr(5) + '\n'; + else if (line[3] == 'f') { + std::string lang; + std::string file; + std::string label; + + std::istringstream stream(line.substr(5)); + stream >> lang >> file; + std::getline(stream, label); + + desc.links.push_back(std::make_pair(file, label)); + } + else if (line[3] == 'o') { + desc.trainsets.emplace_back(); + trainset_desc &trainset = desc.trainsets.back(); + trainset.name = line.substr(5); + } + } +} diff --git a/launcher/scenery_list.h b/launcher/scenery_list.h new file mode 100644 index 00000000..2514790d --- /dev/null +++ b/launcher/scenery_list.h @@ -0,0 +1,53 @@ +#pragma once + +#include "uilayer.h" +#include + +struct trainset_desc { + std::string description; + + std::string name; + std::string track; + + float offset { 0.f }; + float velocity { 0.f }; + + std::vector vehicles; + std::vector couplings; +}; + +struct scenery_desc { + std::filesystem::path path; + std::string name; + std::string description; + std::string image_path; + texture_handle image = 0; + + std::vector> links; + std::vector trainsets; + + std::string title() { + if (!name.empty()) + return name; + else + return path.stem().string(); + } +}; + +namespace ui +{ +class scenerylist_panel : public ui_panel +{ + public: + scenerylist_panel(); + + void render() override; + +private: + std::vector scenarios; + scenery_desc const *selected_scenery = nullptr; + + void scan_scenarios(); + void scan_scn(std::string path); +}; +} // namespace ui diff --git a/launcher/vehicle_picker.cpp b/launcher/vehicle_picker.cpp new file mode 100644 index 00000000..ecbedfe0 --- /dev/null +++ b/launcher/vehicle_picker.cpp @@ -0,0 +1,324 @@ +#include "stdafx.h" +#include "launcher/vehicle_picker.h" +#include "renderer.h" + +GLuint ui::deferred_image::get() +{ + if (!path.empty()) { + image = GfxRenderer.Fetch_Texture(path, true); + path.clear(); + } + + if (image != null_handle) { + opengl_texture &tex = GfxRenderer.Texture(image); + tex.create(); + + if (tex.is_ready) + return tex.id; + } + + return -1; +} + +glm::ivec2 ui::deferred_image::size() +{ + if (image != null_handle) { + opengl_texture &tex = GfxRenderer.Texture(image); + return glm::ivec2(tex.width(), tex.height()); + } + return glm::ivec2(); +} + +ui::vehiclepicker_panel::vehiclepicker_panel() + : ui_panel(STR("Select vehicle"), true) +{ + auto now = std::chrono::high_resolution_clock::now(); + bank.scan_textures(); + auto diff = std::chrono::high_resolution_clock::now() - now; + std::cout << std::chrono::duration_cast(diff).count() << std::endl; + filter.resize(256, 0); +} + +void ui::vehiclepicker_panel::render() +{ + if (!is_open) + return; + + static std::map type_names = + { + { vehicle_type::electric_loco, STRN("Electric locos") }, + { vehicle_type::diesel_loco, STRN("Diesel locos") }, + { vehicle_type::steam_loco, STRN("Steam locos") }, + { vehicle_type::railcar, STRN("Railcars") }, + { vehicle_type::emu, STRN("EMU") }, + { vehicle_type::utility, STRN("Utility") }, + { vehicle_type::draisine, STRN("Draisines") }, + { vehicle_type::tram, STRN("Trams") }, + { vehicle_type::carriage, STRN("Carriages") }, + { vehicle_type::truck, STRN("Trucks") }, + { vehicle_type::bus, STRN("Buses") }, + { vehicle_type::car, STRN("Cars") }, + { vehicle_type::man, STRN("People") }, + { vehicle_type::animal, STRN("Animals") }, + { vehicle_type::unknown, STRN("Unknown") } + }; + + if (ImGui::Begin(m_name.c_str())) { + ImGui::InputText("##filter", &filter[0], filter.size()); + + ImGui::Columns(3); + + ImGui::PushStyleVar(ImGuiStyleVar_SelectableTextAlign, ImVec2(0.0f, 0.5f)); + + if (ImGui::BeginChild("box1")) { + for (auto const &e : type_names) { + deferred_image *image; + auto it = bank.category_icons.find(e.first); + if (it != bank.category_icons.end()) + image = &it->second; + + if (selectable_image(Translations.lookup_s(e.second).c_str(), e.first == selected_type, image)) + selected_type = e.first; + } + } + ImGui::EndChild(); + ImGui::NextColumn(); + + if (ImGui::BeginChild("box2")) { + for (auto const &v : bank.vehicles) { + auto desc = v.second; + if (selected_type == desc->type) { + if (desc->matching_skinsets.size() == 0) + continue; + + auto image = &desc->matching_skinsets[0].mini; + + if (selectable_image(desc->path.stem().c_str(), desc == selected_vehicle, image)) + selected_vehicle = desc; + } + } + } + ImGui::EndChild(); + ImGui::NextColumn(); + + if (ImGui::BeginChild("box3")) { + for (auto const &v : bank.vehicles) { + auto desc = v.second; + if (selected_vehicle == desc) { + for (auto &skin : desc->matching_skinsets) { + auto image = &skin.mini; + + if (selectable_image(skin.skins[0].stem().c_str(), &skin == selected_skinset, image)) + selected_skinset = &skin; + } + } + } + } + ImGui::EndChild(); + + ImGui::PopStyleVar(); + } + ImGui::End(); +} + +bool ui::vehiclepicker_panel::selectable_image(const char *desc, bool selected, deferred_image* image) +{ + bool ret = ImGui::Selectable(desc, selected, 0, ImVec2(0, 30)); + + if (!image) + return ret; + + GLuint tex = image->get(); + if (tex != -1) { + glm::ivec2 size = image->size(); + float width = 30.0f / size.y * size.x; + ImGui::SameLine(ImGui::GetContentRegionAvailWidth() - width); + ImGui::Image(reinterpret_cast(tex), ImVec2(width, 30), ImVec2(0, 1), ImVec2(1, 0)); + } + + return ret; +} + +void ui::vehicles_bank::scan_textures() +{ + for (auto &f : std::filesystem::recursive_directory_iterator("dynamic")) { + if (f.path().filename() != "textures.txt") + continue; + + std::fstream stream(f.path(), std::ios_base::binary | std::ios_base::in); + ctx_path = f.path().parent_path(); + + std::string line; + while (std::getline(stream, line)) + { + if (line.size() < 3) + continue; + if (line.back() == '\r') + line.pop_back(); + + parse_entry(line); + } + } +} + +void ui::vehicles_bank::parse_entry(const std::string &line) +{ + std::string content; + std::string comments; + + size_t pos = line.find("//"); + if (pos != -1) + comments = line.substr(pos); + content = line.substr(0, pos); + + std::istringstream stream(content); + + std::string target; + std::getline(stream, target, '='); + + std::string param; + while (std::getline(stream, param, '=')) { + if (line[0] == '!') + parse_category_entry(param); + else if (line[0] == '*' && line[1] == '*') + parse_texture_rule(target.substr(2), param); + else if (line[0] == '*') + parse_coupling_rule(target.substr(1), param); + else if (line[0] != '@') + parse_texture_info(target, param, comments); + } +} + +void ui::vehicles_bank::parse_category_entry(const std::string ¶m) +{ + static std::unordered_map type_map = { + { 'e', vehicle_type::electric_loco }, + { 's', vehicle_type::diesel_loco }, + { 'p', vehicle_type::steam_loco }, + { 'a', vehicle_type::railcar }, + { 'z', vehicle_type::emu }, + { 'r', vehicle_type::utility }, + { 'd', vehicle_type::draisine }, + { 't', vehicle_type::tram }, + { 'c', vehicle_type::truck }, + { 'b', vehicle_type::bus }, + { 'o', vehicle_type::car }, + { 'h', vehicle_type::man }, + { 'f', vehicle_type::animal }, + }; + + ctx_type = vehicle_type::unknown; + + std::istringstream stream(param); + + std::string tok; + std::getline(stream, tok, ','); + + if (tok.size() < 1) + return; + + auto it = type_map.find(tok[0]); + if (it != type_map.end()) + ctx_type = it->second; + else if (tok[0] >= 'A' && tok[0] <= 'Z') + ctx_type = vehicle_type::carriage; + + std::string mini; + std::getline(stream, mini, ','); + + category_icons.emplace(ctx_type, "textures/mini/" + ToLower(mini)); +} + +void ui::vehicles_bank::parse_texture_info(const std::string &target, const std::string ¶m, const std::string &comment) +{ + std::istringstream stream(param); + + std::string model, mini, miniplus; + + std::getline(stream, model, ','); + std::getline(stream, mini, ','); + std::getline(stream, miniplus, ','); + + skin_set set; + set.group = mini; + if (!miniplus.empty()) + set.mini = deferred_image("textures/mini/" + ToLower(miniplus)); + + std::istringstream tex_stream(target); + std::string texture; + while (std::getline(tex_stream, texture, '|')) { + auto path = ctx_path; + path.append(texture); + set.skins.push_back(path); + } + + get_vehicle(model)->matching_skinsets.push_back(set); +} + +void ui::vehicles_bank::parse_coupling_rule(const std::string &target, const std::string ¶m) +{ + if (param == "?") + return; + + std::istringstream stream(param); + + int coupling_flag; + stream >> coupling_flag; + stream.ignore(1000, ','); + + std::string connected; + std::getline(stream, connected, ','); + + std::string param1, param2; + std::getline(stream, param1, ','); + std::getline(stream, param2, ','); + + coupling_rule rule; + rule.coupled_vehicle = get_vehicle(target); + rule.coupling_flag = coupling_flag; + + get_vehicle(connected)->coupling_rules.push_back(rule); +} + +void ui::vehicles_bank::parse_texture_rule(const std::string &target, const std::string ¶m) +{ + std::istringstream stream(param); + + std::string prev; + std::getline(stream, prev, ','); + + std::string replace_rule; + while (std::getline(stream, replace_rule, ',')) { + if (replace_rule == "-") + break; + + std::istringstream rule_stream(replace_rule); + + std::string src, dst; + std::getline(rule_stream, src, '-'); + std::getline(rule_stream, dst, '-'); + + texture_rule rule; + rule.previous_vehicle = get_vehicle(prev); + rule.replace_rules.emplace_back(src, dst); + + get_vehicle(target)->texture_rules.push_back(rule); + } +} + +std::shared_ptr ui::vehicles_bank::get_vehicle(const std::string &name) +{ + auto path = ctx_path; + path.append(name); + auto it = vehicles.find(path); + if (it != vehicles.end()) { + return it->second; + } + else { + auto desc = std::make_shared(); + desc->type = ctx_type; + desc->path = path; + vehicles.emplace(path, desc); + return desc; + } +} diff --git a/launcher/vehicle_picker.h b/launcher/vehicle_picker.h new file mode 100644 index 00000000..d5606a8b --- /dev/null +++ b/launcher/vehicle_picker.h @@ -0,0 +1,102 @@ +#pragma once + +#include "uilayer.h" + +namespace ui { +enum class vehicle_type { + none = -1, + electric_loco, + diesel_loco, + steam_loco, + railcar, + emu, + utility, + draisine, + tram, + carriage, + truck, + bus, + car, + man, + animal, + unknown +}; + +class deferred_image { +public: + deferred_image() = default; + deferred_image(const std::string &p) : path(p) { } + + GLuint get(); + glm::ivec2 size(); + +private: + std::string path; + texture_handle image = 0; +}; + +struct skin_set { + std::vector skins; + deferred_image mini; + std::string group; +}; + +struct vehicle_desc; +struct texture_rule { + std::shared_ptr previous_vehicle; + std::vector> replace_rules; +}; + +struct coupling_rule { + std::shared_ptr coupled_vehicle; + int coupling_flag; +}; + +struct vehicle_desc { + vehicle_type type; + std::filesystem::path path; + + std::vector matching_skinsets; + std::vector coupling_rules; + std::vector texture_rules; +}; + +class vehicles_bank { +public: + std::unordered_map category_icons; + std::map> vehicles; + std::map>> group_map; + void scan_textures(); + +private: + void parse_entry(const std::string &line); + void parse_category_entry(const std::string ¶m); + void parse_coupling_rule(const std::string &target, const std::string ¶m); + void parse_texture_rule(const std::string &target, const std::string ¶m); + void parse_texture_info(const std::string &target, const std::string ¶m, const std::string &comment); + + std::shared_ptr get_vehicle(const std::string &name); + + vehicle_type ctx_type = vehicle_type::unknown; + std::filesystem::path ctx_path; + std::string ctx_model; +}; + +class vehiclepicker_panel : public ui_panel +{ + public: + vehiclepicker_panel(); + + void render() override; + +private: + bool selectable_image(const char *desc, bool selected, deferred_image *image); + + vehicle_type selected_type = vehicle_type::none; + std::shared_ptr selected_vehicle; + const skin_set *selected_skinset = nullptr; + + vehicles_bank bank; + std::string filter; +}; +} // namespace ui