Implemented crude support for custom encodings via thingy files
Relevant issue: #26
This commit is contained in:
parent
424bba71f7
commit
b4c2f7d371
@ -42,6 +42,7 @@ add_executable(imhex ${application_type}
|
||||
source/helpers/project_file_handler.cpp
|
||||
source/helpers/loader_script_handler.cpp
|
||||
source/helpers/plugin_handler.cpp
|
||||
source/helpers/encoding_file.cpp
|
||||
|
||||
source/providers/file_provider.cpp
|
||||
|
||||
|
125
external/ImGui/include/imgui_memory_editor.h
vendored
125
external/ImGui/include/imgui_memory_editor.h
vendored
@ -50,6 +50,8 @@
|
||||
|
||||
#include <hex/views/view.hpp>
|
||||
|
||||
#include <string>
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#define _PRISizeT "I"
|
||||
#define ImSnprintf _snprintf
|
||||
@ -75,12 +77,19 @@ struct MemoryEditor
|
||||
DataFormat_COUNT
|
||||
};
|
||||
|
||||
struct DecodeData {
|
||||
std::string data;
|
||||
size_t advance;
|
||||
ImColor color;
|
||||
};
|
||||
|
||||
// Settings
|
||||
bool ReadOnly; // = false // disable any editing.
|
||||
int Cols; // = 16 // number of columns to display.
|
||||
bool OptShowOptions; // = true // display options button/context menu. when disabled, options will be locked unless you provide your own UI for them.
|
||||
bool OptShowHexII; // = false // display values in HexII representation instead of regular hexadecimal: hide null/zero bytes, ascii values as ".X".
|
||||
bool OptShowAscii; // = true // display ASCII representation on the right side.
|
||||
bool OptShowAdvancedDecoding; // = true // display advanced decoding data on the right side.
|
||||
bool OptGreyOutZeroes; // = true // display null/zero bytes using the TextDisabled color.
|
||||
bool OptUpperCaseHex; // = true // display hexadecimal values as "FF" instead of "ff".
|
||||
int OptMidColsCount; // = 8 // set to 0 to disable extra spacing between every mid-cols.
|
||||
@ -90,6 +99,7 @@ struct MemoryEditor
|
||||
void (*WriteFn)(ImU8* data, size_t off, ImU8 d); // = 0 // optional handler to write bytes.
|
||||
bool (*HighlightFn)(const ImU8* data, size_t off, bool next);//= 0 // optional handler to return Highlight property (to support non-contiguous highlighting).
|
||||
void (*HoverFn)(const ImU8 *data, size_t off);
|
||||
DecodeData (*DecodeFn)(const ImU8 *data, size_t off);
|
||||
|
||||
// [Internal State]
|
||||
bool ContentsWidthChanged;
|
||||
@ -114,6 +124,7 @@ struct MemoryEditor
|
||||
OptShowOptions = true;
|
||||
OptShowHexII = false;
|
||||
OptShowAscii = true;
|
||||
OptShowAdvancedDecoding = true;
|
||||
OptGreyOutZeroes = true;
|
||||
OptUpperCaseHex = true;
|
||||
OptMidColsCount = 8;
|
||||
@ -123,6 +134,7 @@ struct MemoryEditor
|
||||
WriteFn = NULL;
|
||||
HighlightFn = NULL;
|
||||
HoverFn = NULL;
|
||||
DecodeFn = NULL;
|
||||
|
||||
// State/Internals
|
||||
ContentsWidthChanged = false;
|
||||
@ -155,6 +167,8 @@ struct MemoryEditor
|
||||
float PosHexEnd;
|
||||
float PosAsciiStart;
|
||||
float PosAsciiEnd;
|
||||
float PosDecodingStart;
|
||||
float PosDecodingEnd;
|
||||
float WindowWidth;
|
||||
|
||||
Sizes() { memset(this, 0, sizeof(*this)); }
|
||||
@ -174,12 +188,27 @@ struct MemoryEditor
|
||||
s.PosHexStart = (s.AddrDigitsCount + 2) * s.GlyphWidth;
|
||||
s.PosHexEnd = s.PosHexStart + (s.HexCellWidth * Cols);
|
||||
s.PosAsciiStart = s.PosAsciiEnd = s.PosHexEnd;
|
||||
if (OptShowAscii)
|
||||
{
|
||||
|
||||
if (OptShowAscii && OptShowAdvancedDecoding) {
|
||||
s.PosAsciiStart = s.PosHexEnd + s.GlyphWidth * 1;
|
||||
if (OptMidColsCount > 0)
|
||||
s.PosAsciiStart += (float)((Cols + OptMidColsCount - 1) / OptMidColsCount) * s.SpacingBetweenMidCols;
|
||||
s.PosAsciiEnd = s.PosAsciiStart + Cols * s.GlyphWidth;
|
||||
|
||||
s.PosDecodingStart = s.PosAsciiEnd + s.GlyphWidth * 1;
|
||||
if (OptMidColsCount > 0)
|
||||
s.PosDecodingStart += (float)((Cols + OptMidColsCount - 1) / OptMidColsCount) * s.SpacingBetweenMidCols;
|
||||
s.PosDecodingEnd = s.PosDecodingStart + Cols * s.GlyphWidth;
|
||||
} else if (OptShowAscii) {
|
||||
s.PosAsciiStart = s.PosHexEnd + s.GlyphWidth * 1;
|
||||
if (OptMidColsCount > 0)
|
||||
s.PosAsciiStart += (float)((Cols + OptMidColsCount - 1) / OptMidColsCount) * s.SpacingBetweenMidCols;
|
||||
s.PosAsciiEnd = s.PosAsciiStart + Cols * s.GlyphWidth;
|
||||
} else if (OptShowAdvancedDecoding) {
|
||||
s.PosDecodingStart = s.PosHexEnd + s.GlyphWidth * 1;
|
||||
if (OptMidColsCount > 0)
|
||||
s.PosDecodingStart += (float)((Cols + OptMidColsCount - 1) / OptMidColsCount) * s.SpacingBetweenMidCols;
|
||||
s.PosDecodingEnd = s.PosDecodingStart + Cols * s.GlyphWidth;
|
||||
}
|
||||
s.WindowWidth = s.PosAsciiEnd + style.ScrollbarSize + style.WindowPadding.x * 2 + s.GlyphWidth;
|
||||
}
|
||||
@ -222,15 +251,6 @@ struct MemoryEditor
|
||||
CalcSizes(s, mem_size, base_display_addr);
|
||||
ImGuiStyle& style = ImGui::GetStyle();
|
||||
|
||||
if (mem_size == 0x00) {
|
||||
constexpr const char *noDataString = "No data loaded!";
|
||||
|
||||
auto pos = ImGui::GetCursorScreenPos();
|
||||
pos.x += (ImGui::GetWindowWidth() - (ImGui::CalcTextSize(noDataString).x)) / 2;
|
||||
ImGui::GetWindowDrawList()->AddText(pos, 0xFFFFFFFF, noDataString);
|
||||
return;
|
||||
}
|
||||
|
||||
// We begin into our scrolling region with the 'ImGuiWindowFlags_NoMove' in order to prevent click from moving the window.
|
||||
// This is used as a facility since our main click detection code doesn't assign an ActiveId so the click would normally be caught as a window-move.
|
||||
const float height_separator = style.ItemSpacing.y;
|
||||
@ -327,6 +347,8 @@ struct MemoryEditor
|
||||
ImVec2 window_pos = ImGui::GetWindowPos();
|
||||
if (OptShowAscii)
|
||||
draw_list->AddLine(ImVec2(window_pos.x + s.PosAsciiStart - s.GlyphWidth, window_pos.y), ImVec2(window_pos.x + s.PosAsciiStart - s.GlyphWidth, window_pos.y + 9999), ImGui::GetColorU32(ImGuiCol_Border));
|
||||
if (OptShowAdvancedDecoding)
|
||||
draw_list->AddLine(ImVec2(window_pos.x + s.PosDecodingStart - s.GlyphWidth, window_pos.y), ImVec2(window_pos.x + s.PosDecodingStart - s.GlyphWidth, window_pos.y + 9999), ImGui::GetColorU32(ImGuiCol_Border));
|
||||
|
||||
const ImU32 color_text = ImGui::GetColorU32(ImGuiCol_Text);
|
||||
const ImU32 color_disabled = OptGreyOutZeroes ? ImGui::GetColorU32(ImGuiCol_TextDisabled) : color_text;
|
||||
@ -541,6 +563,86 @@ struct MemoryEditor
|
||||
|
||||
pos.x += s.GlyphWidth;
|
||||
}
|
||||
|
||||
ImGui::PushID(-1);
|
||||
ImGui::SameLine();
|
||||
ImGui::Dummy(ImVec2(s.GlyphWidth, s.LineHeight));
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
if (OptShowAdvancedDecoding && DecodeFn) {
|
||||
// Draw decoded bytes
|
||||
ImGui::SameLine(s.PosDecodingStart);
|
||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
addr = line_i * Cols;
|
||||
|
||||
ImGui::PushID(-1);
|
||||
ImGui::SameLine();
|
||||
ImGui::Dummy(ImVec2(s.GlyphWidth, s.LineHeight));
|
||||
|
||||
ImGui::PopID();
|
||||
|
||||
for (int n = 0; n < Cols && addr < mem_size;)
|
||||
{
|
||||
auto decodedData = DecodeFn(mem_data, addr);
|
||||
|
||||
auto displayData = decodedData.data;
|
||||
auto decodedDataLength = displayData.length();
|
||||
|
||||
if (addr == DataEditingAddr)
|
||||
{
|
||||
draw_list->AddRectFilled(pos, ImVec2(pos.x + s.GlyphWidth * decodedDataLength, pos.y + s.LineHeight), ImGui::GetColorU32(ImGuiCol_FrameBg));
|
||||
draw_list->AddRectFilled(pos, ImVec2(pos.x + s.GlyphWidth * decodedDataLength, pos.y + s.LineHeight), ImGui::GetColorU32(ImGuiCol_TextSelectedBg));
|
||||
}
|
||||
|
||||
draw_list->AddText(pos, decodedData.color, displayData.c_str(), displayData.c_str() + decodedDataLength);
|
||||
|
||||
// Draw highlight
|
||||
bool is_highlight_from_user_range = (addr >= HighlightMin && addr < HighlightMax);
|
||||
bool is_highlight_from_user_func = (HighlightFn && HighlightFn(mem_data, addr, false));
|
||||
bool is_highlight_from_preview = (addr >= DataPreviewAddr && addr <= DataPreviewAddrEnd) || (addr >= DataPreviewAddrEnd && addr <= DataPreviewAddr);
|
||||
if (is_highlight_from_user_range || is_highlight_from_user_func || is_highlight_from_preview)
|
||||
{
|
||||
ImU32 color = HighlightColor;
|
||||
if ((is_highlight_from_user_range + is_highlight_from_user_func + is_highlight_from_preview) > 1)
|
||||
color = (ImAlphaBlendColors(HighlightColor, 0x60C08080) & 0x00FFFFFF) | 0x90000000;
|
||||
|
||||
draw_list->AddRectFilled(pos, ImVec2(pos.x + s.GlyphWidth * decodedDataLength, pos.y + s.LineHeight), color);
|
||||
}
|
||||
|
||||
|
||||
ImGui::PushID(line_i * Cols + n);
|
||||
ImGui::SameLine();
|
||||
ImGui::Dummy(ImVec2(s.GlyphWidth * decodedDataLength, s.LineHeight));
|
||||
|
||||
ImGui::PopID();
|
||||
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0) && !ImGui::GetIO().KeyShift)
|
||||
{
|
||||
if (!ReadOnly && ImGui::IsMouseDoubleClicked(0)) {
|
||||
DataEditingTakeFocus = true;
|
||||
data_editing_addr_next = addr;
|
||||
}
|
||||
|
||||
DataPreviewAddr = addr;
|
||||
DataPreviewAddrEnd = addr;
|
||||
|
||||
}
|
||||
if (ImGui::IsItemHovered() && ((ImGui::IsMouseClicked(0) && ImGui::GetIO().KeyShift) || ImGui::IsMouseDragging(0))) {
|
||||
DataPreviewAddrEnd = addr;
|
||||
}
|
||||
|
||||
pos.x += s.GlyphWidth * decodedDataLength;
|
||||
|
||||
if (addr <= 1) {
|
||||
n++;
|
||||
addr++;
|
||||
} else {
|
||||
n += decodedData.advance;
|
||||
addr += decodedData.advance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
IM_ASSERT(clipper.Step() == false);
|
||||
@ -585,6 +687,7 @@ struct MemoryEditor
|
||||
ImGui::PopItemWidth();
|
||||
ImGui::Checkbox("Show HexII", &OptShowHexII);
|
||||
if (ImGui::Checkbox("Show Ascii", &OptShowAscii)) { ContentsWidthChanged = true; }
|
||||
if (ImGui::Checkbox("Show Advanced Decoding", &OptShowAdvancedDecoding)) { ContentsWidthChanged = true; }
|
||||
ImGui::Checkbox("Grey out zeroes", &OptGreyOutZeroes);
|
||||
ImGui::Checkbox("Uppercase Hex", &OptUpperCaseHex);
|
||||
|
||||
|
37
include/helpers/encoding_file.hpp
Normal file
37
include/helpers/encoding_file.hpp
Normal file
@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <hex.hpp>
|
||||
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
namespace hex {
|
||||
|
||||
template<typename T>
|
||||
struct SizeSorter {
|
||||
bool operator() (const T& lhs, const T& rhs) const {
|
||||
return lhs.size() < rhs.size();
|
||||
}
|
||||
};
|
||||
|
||||
class EncodingFile {
|
||||
public:
|
||||
enum class Type {
|
||||
Thingy,
|
||||
CSV
|
||||
};
|
||||
|
||||
EncodingFile() = default;
|
||||
EncodingFile(Type type, std::string_view path);
|
||||
|
||||
std::pair<std::string_view, size_t> getEncodingFor(const std::vector<u8> &buffer) const;
|
||||
size_t getLongestSequence() const { return this->m_longestSequence; }
|
||||
|
||||
private:
|
||||
void parseThingyFile(std::ifstream &content);
|
||||
|
||||
std::map<u32, std::map<std::vector<u8>, std::string>> m_mapping;
|
||||
size_t m_longestSequence = 0;
|
||||
};
|
||||
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
|
||||
#include <hex/helpers/utils.hpp>
|
||||
#include <hex/views/view.hpp>
|
||||
#include "helpers/encoding_file.hpp"
|
||||
|
||||
#include <imgui_memory_editor.h>
|
||||
#include <ImGuiFileBrowser.h>
|
||||
@ -54,6 +55,8 @@ namespace hex {
|
||||
std::string m_loaderScriptScriptPath;
|
||||
std::string m_loaderScriptFilePath;
|
||||
|
||||
hex::EncodingFile m_currEncodingFile;
|
||||
|
||||
void drawSearchPopup();
|
||||
void drawGotoPopup();
|
||||
void drawEditPopup();
|
||||
|
@ -130,6 +130,7 @@ namespace hex::plugin::builtin {
|
||||
{ "hex.view.hexeditor.save_project", "Save Project" },
|
||||
{ "hex.view.hexeditor.save_data", "Save Data" },
|
||||
{ "hex.view.hexeditor.open_base64", "Open Base64 File" },
|
||||
{ "hex.view.hexeditor.load_enconding_file", "Load custom encoding File" },
|
||||
{ "hex.view.hexeditor.page", "Page %d / %d" },
|
||||
{ "hex.view.hexeditor.save_as", "Save As" },
|
||||
{ "hex.view.hexeditor.save_changes.title", "Save Changes" },
|
||||
@ -146,6 +147,7 @@ namespace hex::plugin::builtin {
|
||||
{ "hex.view.hexeditor.menu.file.save_as", "Save As..." },
|
||||
{ "hex.view.hexeditor.menu.file.open_project", "Open Project..." },
|
||||
{ "hex.view.hexeditor.menu.file.save_project", "Save Project..." },
|
||||
{ "hex.view.hexeditor.menu.file.load_encoding_file", "Load custom encoding..." },
|
||||
{ "hex.view.hexeditor.menu.file.import", "Import..." },
|
||||
{ "hex.view.hexeditor.menu.file.import.base64", "Base64 File" },
|
||||
{ "hex.view.hexeditor.base64.import_error", "File is not in a valid Base64 format!" },
|
||||
|
@ -180,6 +180,23 @@ namespace hex {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
inline std::vector<u8> parseByteString(std::string_view string) {
|
||||
auto byteString = std::string(string);
|
||||
byteString.erase(std::remove(byteString.begin(), byteString.end(), ' '), byteString.end());
|
||||
|
||||
if ((byteString.length() % 2) != 0) return { };
|
||||
|
||||
std::vector<u8> result;
|
||||
for (u32 i = 0; i < byteString.length(); i += 2) {
|
||||
if (!std::isxdigit(byteString[i]) || !std::isxdigit(byteString[i + 1]))
|
||||
return { };
|
||||
|
||||
result.push_back(std::strtoul(byteString.substr(i, 2).c_str(), nullptr, 16));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
inline std::string toBinaryString(hex::integral auto number) {
|
||||
if (number == 0) return "0";
|
||||
|
||||
@ -190,6 +207,23 @@ namespace hex {
|
||||
return result;
|
||||
}
|
||||
|
||||
inline void trimLeft(std::string &s) {
|
||||
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
|
||||
return !std::isspace(ch);
|
||||
}));
|
||||
}
|
||||
|
||||
inline void trimRight(std::string &s) {
|
||||
s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
|
||||
return !std::isspace(ch);
|
||||
}).base(), s.end());
|
||||
}
|
||||
|
||||
inline void trim(std::string &s) {
|
||||
trimLeft(s);
|
||||
trimRight(s);
|
||||
}
|
||||
|
||||
#define SCOPE_EXIT(func) ScopeExit TOKEN_CONCAT(scopeGuard, __COUNTER__)([&] { func })
|
||||
class ScopeExit {
|
||||
public:
|
||||
|
@ -30,8 +30,6 @@ namespace hex {
|
||||
json[unlocalizedCategory.data()] = nlohmann::json::object();
|
||||
if (!json[unlocalizedCategory.data()].contains(unlocalizedName.data()))
|
||||
json[unlocalizedCategory.data()][unlocalizedName.data()] = defaultValue;
|
||||
|
||||
Settings::store();
|
||||
}
|
||||
|
||||
void ContentRegistry::Settings::add(std::string_view unlocalizedCategory, std::string_view unlocalizedName, std::string_view defaultValue, const std::function<bool(std::string_view, nlohmann::json&)> &callback) {
|
||||
@ -43,8 +41,6 @@ namespace hex {
|
||||
json[unlocalizedCategory.data()] = nlohmann::json::object();
|
||||
if (!json[unlocalizedCategory.data()].contains(unlocalizedName.data()))
|
||||
json[unlocalizedCategory.data()][unlocalizedName.data()] = defaultValue;
|
||||
|
||||
Settings::store();
|
||||
}
|
||||
|
||||
void ContentRegistry::Settings::write(std::string_view unlocalizedCategory, std::string_view unlocalizedName, s64 value) {
|
||||
|
53
source/helpers/encoding_file.cpp
Normal file
53
source/helpers/encoding_file.cpp
Normal file
@ -0,0 +1,53 @@
|
||||
#include "helpers/encoding_file.hpp"
|
||||
|
||||
#include <hex/helpers/utils.hpp>
|
||||
|
||||
#include <fstream>
|
||||
|
||||
namespace hex {
|
||||
|
||||
EncodingFile::EncodingFile(Type type, std::string_view path) {
|
||||
std::ifstream encodingFile(path.data());
|
||||
|
||||
switch (type) {
|
||||
case Type::Thingy: parseThingyFile(encodingFile); break;
|
||||
default: throw std::runtime_error("Invalid encoding file type");
|
||||
}
|
||||
}
|
||||
|
||||
std::pair<std::string_view, size_t> EncodingFile::getEncodingFor(const std::vector<u8> &buffer) const {
|
||||
for (auto iter = this->m_mapping.rbegin(); iter != this->m_mapping.rend(); iter++) {
|
||||
auto &[size, mapping] = *iter;
|
||||
|
||||
if (size > buffer.size()) continue;
|
||||
|
||||
auto key = std::vector<u8>(buffer.begin(), buffer.begin() + size);
|
||||
if (mapping.contains(key))
|
||||
return { mapping.at(key), size };
|
||||
}
|
||||
|
||||
return { ".", 1 };
|
||||
}
|
||||
|
||||
void EncodingFile::parseThingyFile(std::ifstream &content) {
|
||||
for (std::string line; std::getline(content, line);) {
|
||||
auto entry = hex::splitString(line, "=");
|
||||
|
||||
if (entry.size() != 2) return;
|
||||
|
||||
auto &from = entry[0];
|
||||
auto &to = entry[1];
|
||||
|
||||
hex::trim(from);
|
||||
hex::trim(to);
|
||||
|
||||
auto fromBytes = hex::parseByteString(from);
|
||||
if (!this->m_mapping.contains(fromBytes.size()))
|
||||
this->m_mapping.insert({ fromBytes.size(), { } });
|
||||
this->m_mapping[fromBytes.size()].insert({ fromBytes, to });
|
||||
|
||||
this->m_longestSequence = std::max(this->m_longestSequence, fromBytes.size());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -99,6 +99,30 @@ namespace hex {
|
||||
ImGui::EndTooltip();
|
||||
};
|
||||
|
||||
this->m_memoryEditor.DecodeFn = [](const ImU8 *data, size_t addr) -> MemoryEditor::DecodeData {
|
||||
ViewHexEditor *_this = (ViewHexEditor *) data;
|
||||
|
||||
if (_this->m_currEncodingFile.getLongestSequence() == 0)
|
||||
return { ".", 1, 0xFFFF8000 };
|
||||
|
||||
auto &provider = SharedData::currentProvider;
|
||||
size_t size = std::min<size_t>(_this->m_currEncodingFile.getLongestSequence(), provider->getActualSize() - addr);
|
||||
|
||||
std::vector<u8> buffer(size);
|
||||
provider->read(addr, buffer.data(), size);
|
||||
|
||||
auto [decoded, advance] = _this->m_currEncodingFile.getEncodingFor(buffer);
|
||||
|
||||
ImColor color;
|
||||
if (decoded.length() == 1 && std::isalnum(decoded[0])) color = 0xFFFF8000;
|
||||
else if (decoded.length() == 1 && advance == 1) color = 0xFF0000FF;
|
||||
else if (decoded.length() > 1 && advance == 1) color = 0xFF00FFFF;
|
||||
else if (advance > 1) color = 0xFFFFFFFF;
|
||||
else color = 0xFFFF8000;
|
||||
|
||||
return { std::string(decoded), advance, color };
|
||||
};
|
||||
|
||||
View::subscribeEvent(Events::FileDropped, [this](auto userData) {
|
||||
auto filePath = std::any_cast<const char*>(userData);
|
||||
|
||||
@ -358,6 +382,12 @@ namespace hex {
|
||||
ProjectFile::store();
|
||||
}
|
||||
|
||||
if (ImGui::MenuItem("hex.view.hexeditor.menu.file.load_encoding_file"_lang)) {
|
||||
View::openFileBrowser("hex.view.hexeditor.load_enconding_file"_lang, imgui_addons::ImGuiFileBrowser::DialogMode::OPEN, "*.*", [this](auto path) {
|
||||
this->m_currEncodingFile = EncodingFile(EncodingFile::Type::Thingy, path);
|
||||
});
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::BeginMenu("hex.view.hexeditor.menu.file.import"_lang)) {
|
||||
@ -515,6 +545,9 @@ namespace hex {
|
||||
|
||||
if (!provider->isAvailable()) {
|
||||
View::showErrorPopup("hex.view.hexeditor.error.open"_lang);
|
||||
delete provider;
|
||||
provider = nullptr;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user