tibia-rme/source/sqlite_materials_inspector.cpp
Victor ce8d5093ee
feat: add SQLite materials backend with XML import pipeline, schema migrations, and inspection tools (#183)
New Features:
- Added "SQLite Materials Inspector" to the Window menu — a read-only multi-tab viewer for summary stats, brushes by type, and tilesets with reload and detail panes.
- Background initialization and async bootstrap/import of the materials database to avoid blocking the UI; startup/shutdown now synchronize to prevent concurrent access.

Chores:
- New runtime option to enable the SQLite materials backend and control migration/bootstrap behavior (default enabled).
2026-05-22 19:41:30 -03:00

295 lines
11 KiB
C++

#include "main.h"
#include "sqlite_materials_inspector.h"
#include "gui.h"
namespace {
wxString BoolToText(bool value) {
return value ? "yes" : "no";
}
wxString FormatAuditReport(const BrushDatabase &database, const MaterialsDatabaseAuditReport &report) {
wxString text;
text << "Database: " << database.getDatabasePath() << "\n";
text << "Schema version: " << database.getExpectedSchemaVersion() << "\n\n";
text << "Brushes: " << report.brushCount << "\n";
text << "Border sets: " << report.borderSetCount << "\n";
text << "Tilesets: " << report.tilesetCount << "\n";
text << "Tileset sections: " << report.tilesetSectionCount << "\n";
text << "Tileset entries: " << report.tilesetEntryCount << "\n\n";
text << "Unresolved ground targets: " << report.unresolvedGroundTargets << "\n";
text << "Unresolved brush links: " << report.unresolvedBrushLinks << "\n";
text << "Unresolved tileset entries: " << report.unresolvedTilesetEntries << "\n\n";
text << "Brush counts by type:\n";
for (const BrushTypeCountRecord &typeCount : report.brushTypeCounts) {
text << " - " << typeCount.type << ": " << typeCount.count << "\n";
}
return text;
}
wxString FormatBrushDetails(const BrushStorageRecord &storage) {
const BrushRecord &brush = storage.brush;
wxString text;
text << "Name: " << brush.name << "\n";
text << "Type: " << brush.type << "\n";
text << "ID: " << brush.id << "\n";
text << "Source: " << brush.sourceFile << "\n\n";
text << "lookId: " << brush.lookId << "\n";
text << "serverLookId: " << brush.serverLookId << "\n";
text << "zOrder: " << brush.zOrder << "\n";
text << "thickness: " << brush.thickness << "\n";
text << "thicknessCeiling: " << brush.thicknessCeiling << "\n\n";
text << "Flags:\n";
text << " draggable: " << BoolToText(brush.draggable) << "\n";
text << " onBlocking: " << BoolToText(brush.onBlocking) << "\n";
text << " onDuplicate: " << BoolToText(brush.onDuplicate) << "\n";
text << " redoBorders: " << BoolToText(brush.redoBorders) << "\n";
text << " randomize: " << BoolToText(brush.randomize) << "\n";
text << " oneSize: " << BoolToText(brush.oneSize) << "\n";
text << " soloOptional: " << BoolToText(brush.soloOptional) << "\n\n";
text << "Brush items: " << storage.items.size() << "\n";
for (const BrushItemRecord &item : storage.items) {
text << " - item=" << item.itemId << " chance=" << item.chance << " sort=" << item.sortOrder << "\n";
}
text << "\nGround borders: " << storage.borders.size() << "\n";
for (const GroundBrushBorderRecord &border : storage.borders) {
text << " - role=" << border.borderRole
<< " align=" << border.align
<< " targetMode=" << border.targetMode
<< " targetBrush=" << border.targetBrushName
<< " borderSetId=" << border.borderSetId
<< " cases=" << border.cases.size() << "\n";
}
text << "\nLinks: " << storage.links.size() << "\n";
for (const BrushLinkRecord &link : storage.links) {
text << " - " << link.relationType << " -> " << link.targetBrushName << " (id=" << link.targetBrushId << ")\n";
}
text << "\nWall parts: " << storage.wallParts.size() << "\n";
for (const WallPartRecord &part : storage.wallParts) {
text << " - " << part.partType << " items=" << part.items.size() << " doors=" << part.doors.size() << "\n";
}
text << "\nCarpet nodes: " << storage.carpetNodes.size() << "\n";
for (const CarpetNodeRecord &node : storage.carpetNodes) {
text << " - align=" << node.align << " items=" << node.items.size() << "\n";
}
text << "\nTable nodes: " << storage.tableNodes.size() << "\n";
for (const TableNodeRecord &node : storage.tableNodes) {
text << " - align=" << node.align << " items=" << node.items.size() << "\n";
}
text << "\nDoodad alternatives: " << storage.doodadAlternatives.size() << "\n";
for (const DoodadAlternativeRecord &alternative : storage.doodadAlternatives) {
text << " - singleItems=" << alternative.singleItems.size() << " composites=" << alternative.composites.size() << "\n";
}
return text;
}
wxString FormatTilesetDetails(const TilesetStorageRecord &tileset) {
wxString text;
text << "Name: " << tileset.name << "\n";
text << "Source: " << tileset.sourceFile << "\n";
text << "Sections: " << tileset.sections.size() << "\n\n";
for (const TilesetSectionRecord &section : tileset.sections) {
text << "[" << section.sectionType << "] entries=" << section.entries.size() << "\n";
for (const TilesetEntryRecord &entry : section.entries) {
text << " - kind=" << entry.entryKind;
if (!entry.brushName.IsEmpty()) {
text << " brush=" << entry.brushName;
}
if (entry.itemId > 0) {
text << " item=" << entry.itemId;
}
if (entry.fromItemId > 0 || entry.toItemId > 0) {
text << " range=" << entry.fromItemId << "-" << entry.toItemId;
}
if (!entry.afterBrushName.IsEmpty()) {
text << " after=" << entry.afterBrushName;
}
if (entry.afterItemId > 0) {
text << " afterItem=" << entry.afterItemId;
}
text << "\n";
}
text << "\n";
}
return text;
}
} // namespace
SQLiteMaterialsInspectorDialog::SQLiteMaterialsInspectorDialog(wxWindow* parent) :
wxDialog(parent, wxID_ANY, "SQLite Materials Inspector", wxDefaultPosition, wxSize(1000, 700), wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) {
wxBoxSizer* topSizer = new wxBoxSizer(wxVERTICAL);
wxBoxSizer* toolbarSizer = new wxBoxSizer(wxHORIZONTAL);
wxButton* reloadButton = new wxButton(this, wxID_REFRESH, "Reload");
toolbarSizer->Add(reloadButton, 0, wxALL, FromDIP(5));
topSizer->Add(toolbarSizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(5));
notebook_ = new wxNotebook(this, wxID_ANY);
wxPanel* summaryPanel = new wxPanel(notebook_);
wxBoxSizer* summarySizer = new wxBoxSizer(wxVERTICAL);
summaryText_ = new wxTextCtrl(summaryPanel, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE | wxTE_READONLY);
summarySizer->Add(summaryText_, 1, wxEXPAND | wxALL, FromDIP(5));
summaryPanel->SetSizer(summarySizer);
notebook_->AddPage(summaryPanel, "Summary");
wxPanel* brushesPanel = new wxPanel(notebook_);
wxBoxSizer* brushesSizer = new wxBoxSizer(wxVERTICAL);
wxArrayString brushTypes;
brushTypes.Add("ground");
brushTypes.Add("wall");
brushTypes.Add("doodad");
brushTypes.Add("carpet");
brushTypes.Add("table");
brushTypeChoice_ = new wxChoice(brushesPanel, wxID_ANY, wxDefaultPosition, wxDefaultSize, brushTypes);
brushTypeChoice_->SetSelection(0);
brushesSizer->Add(brushTypeChoice_, 0, wxEXPAND | wxALL, FromDIP(5));
wxSplitterWindow* brushesSplitter = new wxSplitterWindow(brushesPanel, wxID_ANY);
brushList_ = new wxListBox(brushesSplitter, wxID_ANY);
brushDetailsText_ = new wxTextCtrl(brushesSplitter, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE | wxTE_READONLY);
brushesSplitter->SplitVertically(brushList_, brushDetailsText_, FromDIP(280));
brushesSplitter->SetMinimumPaneSize(FromDIP(180));
brushesSizer->Add(brushesSplitter, 1, wxEXPAND | wxALL, FromDIP(5));
brushesPanel->SetSizer(brushesSizer);
notebook_->AddPage(brushesPanel, "Brushes");
wxPanel* tilesetsPanel = new wxPanel(notebook_);
wxBoxSizer* tilesetsSizer = new wxBoxSizer(wxVERTICAL);
wxSplitterWindow* tilesetsSplitter = new wxSplitterWindow(tilesetsPanel, wxID_ANY);
tilesetList_ = new wxListBox(tilesetsSplitter, wxID_ANY);
tilesetDetailsText_ = new wxTextCtrl(tilesetsSplitter, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE | wxTE_READONLY);
tilesetsSplitter->SplitVertically(tilesetList_, tilesetDetailsText_, FromDIP(280));
tilesetsSplitter->SetMinimumPaneSize(FromDIP(180));
tilesetsSizer->Add(tilesetsSplitter, 1, wxEXPAND | wxALL, FromDIP(5));
tilesetsPanel->SetSizer(tilesetsSizer);
notebook_->AddPage(tilesetsPanel, "Tilesets");
topSizer->Add(notebook_, 1, wxEXPAND | wxALL, FromDIP(5));
topSizer->Add(CreateSeparatedButtonSizer(wxCLOSE), 0, wxEXPAND | wxALL, FromDIP(5));
SetSizer(topSizer);
reloadButton->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { ReloadData(); });
brushTypeChoice_->Bind(wxEVT_CHOICE, [this](wxCommandEvent &) { RefreshBrushList(); });
brushList_->Bind(wxEVT_LISTBOX, [this](wxCommandEvent &) { RefreshBrushDetails(); });
tilesetList_->Bind(wxEVT_LISTBOX, [this](wxCommandEvent &) { RefreshTilesetDetails(); });
ReloadData();
}
void SQLiteMaterialsInspectorDialog::ReloadData() {
const auto resetInspectorState = [this](const wxString &summary, const wxString &brushDetails, const wxString &tilesetDetails) {
currentBrushes_.clear();
tilesets_.clear();
brushList_->Clear();
brushDetailsText_->SetValue(brushDetails);
tilesetList_->Clear();
tilesetDetailsText_->SetValue(tilesetDetails);
summaryText_->SetValue(summary);
};
if (g_gui.IsAsyncSqliteBootstrapRunning()) {
const wxString message = "SQLite materials database is currently being built in background. Please reopen or reload this inspector when the bootstrap import finishes.";
resetInspectorState(message, message, message);
return;
}
if (!g_brush_database.isOpen()) {
resetInspectorState("SQLite brush database is not open.", "", "");
inspectorDatabase_.close();
return;
}
if (!inspectorDatabase_.openReadOnly(g_brush_database.getDatabasePath())) {
const wxString error = inspectorDatabase_.getLastError();
resetInspectorState(error, error, error);
return;
}
if (!inspectorDatabase_.generateAuditReport(auditReport_)) {
const wxString error = inspectorDatabase_.getLastError();
resetInspectorState(error, error, error);
return;
}
if (!inspectorDatabase_.getAllTilesets(tilesets_)) {
const wxString error = inspectorDatabase_.getLastError();
resetInspectorState(error, error, error);
return;
}
RefreshSummary();
RefreshBrushList();
RefreshTilesetList();
}
void SQLiteMaterialsInspectorDialog::RefreshSummary() {
summaryText_->SetValue(FormatAuditReport(inspectorDatabase_, auditReport_));
}
void SQLiteMaterialsInspectorDialog::RefreshBrushList() {
currentBrushes_.clear();
brushList_->Clear();
brushDetailsText_->Clear();
const wxString selectedType = brushTypeChoice_->GetStringSelection();
if (!inspectorDatabase_.listBrushesByType(selectedType, currentBrushes_)) {
brushDetailsText_->SetValue(inspectorDatabase_.getLastError());
return;
}
for (const BrushRecord &brush : currentBrushes_) {
brushList_->Append(brush.name);
}
if (!currentBrushes_.empty()) {
brushList_->SetSelection(0);
RefreshBrushDetails();
}
}
void SQLiteMaterialsInspectorDialog::RefreshBrushDetails() {
brushDetailsText_->Clear();
const int selection = brushList_->GetSelection();
if (selection == wxNOT_FOUND || selection >= static_cast<int>(currentBrushes_.size())) {
return;
}
BrushStorageRecord storage;
if (!inspectorDatabase_.getCompleteBrushById(currentBrushes_[selection].id, storage)) {
brushDetailsText_->SetValue(inspectorDatabase_.getLastError());
return;
}
brushDetailsText_->SetValue(FormatBrushDetails(storage));
}
void SQLiteMaterialsInspectorDialog::RefreshTilesetList() {
tilesetList_->Clear();
tilesetDetailsText_->Clear();
for (const TilesetStorageRecord &tileset : tilesets_) {
tilesetList_->Append(tileset.name);
}
if (!tilesets_.empty()) {
tilesetList_->SetSelection(0);
RefreshTilesetDetails();
}
}
void SQLiteMaterialsInspectorDialog::RefreshTilesetDetails() {
tilesetDetailsText_->Clear();
const int selection = tilesetList_->GetSelection();
if (selection == wxNOT_FOUND || selection >= static_cast<int>(tilesets_.size())) {
return;
}
tilesetDetailsText_->SetValue(FormatTilesetDetails(tilesets_[selection]));
}