tibia-rme/source/brush_database.cpp

3510 lines
112 KiB
C++
Raw Permalink Normal View History

#include "main.h"
#include <sqlite3.h>
#include "brush_database.h"
BrushDatabase g_brush_database;
namespace {
constexpr int kBrushDatabaseSchemaVersion = 6;
constexpr const char* kBrushSelectColumns = "id, name, type, look_id, z_order, source_file, server_look_id, "
"draggable, on_blocking, on_duplicate, redo_borders, randomize, "
"one_size, solo_optional, thickness, thickness_ceiling";
constexpr const char* kRecreateTilesetTablesSql = "DROP TABLE IF EXISTS tileset_brush_entries;"
"DROP TABLE IF EXISTS tileset_sections;"
"DROP TABLE IF EXISTS tilesets;"
"CREATE TABLE tilesets ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"name TEXT NOT NULL UNIQUE,"
"source_file TEXT NOT NULL DEFAULT '',"
"created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,"
"updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
");"
"CREATE TABLE tileset_sections ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"tileset_id INTEGER NOT NULL,"
"section_type TEXT NOT NULL,"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"FOREIGN KEY (tileset_id) REFERENCES tilesets(id) ON DELETE CASCADE"
");"
"CREATE TABLE tileset_brush_entries ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"tileset_section_id INTEGER NOT NULL,"
"entry_kind TEXT NOT NULL DEFAULT 'brush',"
"brush_id INTEGER,"
"brush_name TEXT NOT NULL DEFAULT '',"
"item_id INTEGER NOT NULL DEFAULT 0,"
"from_item_id INTEGER NOT NULL DEFAULT 0,"
"to_item_id INTEGER NOT NULL DEFAULT 0,"
"after_brush_name TEXT NOT NULL DEFAULT '',"
"after_item_id INTEGER NOT NULL DEFAULT 0,"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"FOREIGN KEY (tileset_section_id) REFERENCES tileset_sections(id) ON DELETE CASCADE,"
"FOREIGN KEY (brush_id) REFERENCES brushes(id) ON DELETE SET NULL"
");"
"CREATE INDEX idx_tileset_brush_entries_section ON tileset_brush_entries(tileset_section_id, sort_order);"
"CREATE INDEX idx_tileset_brush_entries_name ON tileset_brush_entries(brush_name);"
"CREATE INDEX idx_tileset_brush_entries_item ON tileset_brush_entries(item_id, from_item_id, to_item_id);";
wxString ToWxString(const char* value) {
return value ? wxString::FromUTF8(value) : wxString();
}
wxString MakeTransactionSavepointName(int savepointId) {
return wxString::Format("brushdb_sp_%d", savepointId);
}
void FinalizeStatements(std::initializer_list<sqlite3_stmt*> statements) {
for (sqlite3_stmt* stmt : statements) {
if (stmt) {
sqlite3_finalize(stmt);
}
}
}
void BindNullableInt64(sqlite3_stmt* stmt, int index, int64_t value) {
if (value > 0) {
sqlite3_bind_int64(stmt, index, value);
} else {
sqlite3_bind_null(stmt, index);
}
}
int64_t ReadNullableInt64(sqlite3_stmt* stmt, int index) {
return sqlite3_column_type(stmt, index) == SQLITE_NULL ? 0 : sqlite3_column_int64(stmt, index);
}
int BindBrushRecordFields(sqlite3_stmt* stmt, const BrushRecord &brush, int parameterIndex) {
sqlite3_bind_text(stmt, parameterIndex++, brush.name.utf8_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, parameterIndex++, brush.type.utf8_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(stmt, parameterIndex++, brush.lookId);
sqlite3_bind_int(stmt, parameterIndex++, brush.zOrder);
sqlite3_bind_text(stmt, parameterIndex++, brush.sourceFile.utf8_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(stmt, parameterIndex++, brush.serverLookId);
sqlite3_bind_int(stmt, parameterIndex++, brush.draggable ? 1 : 0);
sqlite3_bind_int(stmt, parameterIndex++, brush.onBlocking ? 1 : 0);
sqlite3_bind_int(stmt, parameterIndex++, brush.onDuplicate ? 1 : 0);
sqlite3_bind_int(stmt, parameterIndex++, brush.redoBorders ? 1 : 0);
sqlite3_bind_int(stmt, parameterIndex++, brush.randomize ? 1 : 0);
sqlite3_bind_int(stmt, parameterIndex++, brush.oneSize ? 1 : 0);
sqlite3_bind_int(stmt, parameterIndex++, brush.soloOptional ? 1 : 0);
sqlite3_bind_int(stmt, parameterIndex++, brush.thickness);
sqlite3_bind_int(stmt, parameterIndex++, brush.thicknessCeiling);
return parameterIndex;
}
struct AlignedNodeWriteContext {
sqlite3_stmt* insertNodeStmt = nullptr;
sqlite3_stmt* insertItemStmt = nullptr;
wxString insertNodeError;
wxString insertItemError;
};
void ReadBrushRecordFromStatement(sqlite3_stmt* stmt, BrushRecord &outBrush) {
outBrush.id = sqlite3_column_int64(stmt, 0);
outBrush.name = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1)));
outBrush.type = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2)));
outBrush.lookId = sqlite3_column_int(stmt, 3);
outBrush.zOrder = sqlite3_column_int(stmt, 4);
outBrush.sourceFile = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 5)));
outBrush.serverLookId = sqlite3_column_int(stmt, 6);
outBrush.draggable = sqlite3_column_int(stmt, 7) != 0;
outBrush.onBlocking = sqlite3_column_int(stmt, 8) != 0;
outBrush.onDuplicate = sqlite3_column_int(stmt, 9) != 0;
outBrush.redoBorders = sqlite3_column_int(stmt, 10) != 0;
outBrush.randomize = sqlite3_column_int(stmt, 11) != 0;
outBrush.oneSize = sqlite3_column_int(stmt, 12) != 0;
outBrush.soloOptional = sqlite3_column_int(stmt, 13) != 0;
outBrush.thickness = sqlite3_column_int(stmt, 14);
outBrush.thicknessCeiling = sqlite3_column_int(stmt, 15);
}
template <typename NodeRecord, typename ItemRecord, typename SetErrorFn>
bool WriteAlignedNodesWithItems(
sqlite3* connection,
int64_t brushId,
const std::vector<NodeRecord> &nodes,
const AlignedNodeWriteContext &context,
SetErrorFn &&setErrorFromDatabase
) {
for (const NodeRecord &node : nodes) {
sqlite3_reset(context.insertNodeStmt);
sqlite3_clear_bindings(context.insertNodeStmt);
sqlite3_bind_int64(context.insertNodeStmt, 1, brushId);
sqlite3_bind_text(context.insertNodeStmt, 2, node.align.utf8_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(context.insertNodeStmt, 3, node.sortOrder);
int rc = sqlite3_step(context.insertNodeStmt);
if (rc != SQLITE_DONE) {
return setErrorFromDatabase(context.insertNodeError);
}
const int64_t nodeId = sqlite3_last_insert_rowid(connection);
for (const ItemRecord &item : node.items) {
sqlite3_reset(context.insertItemStmt);
sqlite3_clear_bindings(context.insertItemStmt);
sqlite3_bind_int64(context.insertItemStmt, 1, nodeId);
sqlite3_bind_int(context.insertItemStmt, 2, item.itemId);
sqlite3_bind_int(context.insertItemStmt, 3, item.chance);
sqlite3_bind_int(context.insertItemStmt, 4, item.sortOrder);
rc = sqlite3_step(context.insertItemStmt);
if (rc != SQLITE_DONE) {
return setErrorFromDatabase(context.insertItemError);
}
}
}
return true;
}
template <typename NodeRecord, typename ItemRecord, typename SetErrorFn>
bool ReadAlignedNodesWithItems(
sqlite3_stmt* nodeStmt,
sqlite3_stmt* itemStmt,
std::vector<NodeRecord> &outNodes,
const wxString &readNodesError,
const wxString &readItemsError,
SetErrorFn &&setErrorFromDatabase
) {
for (;;) {
const int nodeRc = sqlite3_step(nodeStmt);
if (nodeRc == SQLITE_DONE) {
break;
}
if (nodeRc != SQLITE_ROW) {
return setErrorFromDatabase(readNodesError);
}
const int64_t nodeId = sqlite3_column_int64(nodeStmt, 0);
NodeRecord node;
node.align = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(nodeStmt, 1)));
node.sortOrder = sqlite3_column_int(nodeStmt, 2);
sqlite3_reset(itemStmt);
sqlite3_clear_bindings(itemStmt);
sqlite3_bind_int64(itemStmt, 1, nodeId);
for (;;) {
const int itemRc = sqlite3_step(itemStmt);
if (itemRc == SQLITE_DONE) {
break;
}
if (itemRc != SQLITE_ROW) {
return setErrorFromDatabase(readItemsError);
}
ItemRecord item;
item.itemId = sqlite3_column_int(itemStmt, 0);
item.chance = sqlite3_column_int(itemStmt, 1);
item.sortOrder = sqlite3_column_int(itemStmt, 2);
node.items.push_back(item);
}
outNodes.push_back(node);
}
return true;
}
template <typename SetErrorFn>
bool WriteGroundBorderConditions(
int64_t caseId,
const std::vector<GroundBorderCaseConditionRecord> &conditions,
sqlite3_stmt* insertConditionStmt,
SetErrorFn &&setErrorFromDatabase
) {
for (const GroundBorderCaseConditionRecord &condition : conditions) {
sqlite3_reset(insertConditionStmt);
sqlite3_clear_bindings(insertConditionStmt);
sqlite3_bind_int64(insertConditionStmt, 1, caseId);
sqlite3_bind_text(insertConditionStmt, 2, condition.conditionType.utf8_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(insertConditionStmt, 3, condition.matchValue);
sqlite3_bind_text(insertConditionStmt, 4, condition.edge.utf8_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(insertConditionStmt, 5, condition.sortOrder);
if (sqlite3_step(insertConditionStmt) != SQLITE_DONE) {
return setErrorFromDatabase("Failed to insert ground border condition");
}
}
return true;
}
template <typename SetErrorFn>
bool WriteGroundBorderActions(
int64_t caseId,
const std::vector<GroundBorderCaseActionRecord> &actions,
sqlite3_stmt* insertActionStmt,
SetErrorFn &&setErrorFromDatabase
) {
for (const GroundBorderCaseActionRecord &action : actions) {
sqlite3_reset(insertActionStmt);
sqlite3_clear_bindings(insertActionStmt);
sqlite3_bind_int64(insertActionStmt, 1, caseId);
sqlite3_bind_text(insertActionStmt, 2, action.actionType.utf8_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(insertActionStmt, 3, action.targetValue);
sqlite3_bind_text(insertActionStmt, 4, action.edge.utf8_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(insertActionStmt, 5, action.replacementValue);
sqlite3_bind_int(insertActionStmt, 6, action.sortOrder);
if (sqlite3_step(insertActionStmt) != SQLITE_DONE) {
return setErrorFromDatabase("Failed to insert ground border action");
}
}
return true;
}
template <typename SetErrorFn>
bool WriteGroundBorderCases(
sqlite3* connection,
int64_t groundBrushBorderId,
const std::vector<GroundBorderCaseRecord> &cases,
sqlite3_stmt* insertCaseStmt,
sqlite3_stmt* insertConditionStmt,
sqlite3_stmt* insertActionStmt,
SetErrorFn &&setErrorFromDatabase
) {
for (const GroundBorderCaseRecord &caseRecord : cases) {
sqlite3_reset(insertCaseStmt);
sqlite3_clear_bindings(insertCaseStmt);
sqlite3_bind_int64(insertCaseStmt, 1, groundBrushBorderId);
sqlite3_bind_int(insertCaseStmt, 2, caseRecord.sortOrder);
if (sqlite3_step(insertCaseStmt) != SQLITE_DONE) {
return setErrorFromDatabase("Failed to insert ground border case");
}
const int64_t caseId = sqlite3_last_insert_rowid(connection);
if (!WriteGroundBorderConditions(caseId, caseRecord.conditions, insertConditionStmt, setErrorFromDatabase)) {
return false;
}
if (!WriteGroundBorderActions(caseId, caseRecord.actions, insertActionStmt, setErrorFromDatabase)) {
return false;
}
}
return true;
}
template <typename SetErrorFn>
bool ReadGroundBorderConditions(
int64_t caseId,
sqlite3_stmt* conditionStmt,
std::vector<GroundBorderCaseConditionRecord> &outConditions,
SetErrorFn &&setErrorFromDatabase
) {
sqlite3_reset(conditionStmt);
sqlite3_clear_bindings(conditionStmt);
sqlite3_bind_int64(conditionStmt, 1, caseId);
for (;;) {
const int conditionRc = sqlite3_step(conditionStmt);
if (conditionRc == SQLITE_DONE) {
break;
}
if (conditionRc != SQLITE_ROW) {
return setErrorFromDatabase("Failed to read ground border case conditions");
}
GroundBorderCaseConditionRecord condition;
condition.conditionType = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(conditionStmt, 0)));
condition.matchValue = sqlite3_column_int(conditionStmt, 1);
condition.edge = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(conditionStmt, 2)));
condition.sortOrder = sqlite3_column_int(conditionStmt, 3);
outConditions.push_back(condition);
}
return true;
}
template <typename SetErrorFn>
bool ReadGroundBorderActions(
int64_t caseId,
sqlite3_stmt* actionStmt,
std::vector<GroundBorderCaseActionRecord> &outActions,
SetErrorFn &&setErrorFromDatabase
) {
sqlite3_reset(actionStmt);
sqlite3_clear_bindings(actionStmt);
sqlite3_bind_int64(actionStmt, 1, caseId);
for (;;) {
const int actionRc = sqlite3_step(actionStmt);
if (actionRc == SQLITE_DONE) {
break;
}
if (actionRc != SQLITE_ROW) {
return setErrorFromDatabase("Failed to read ground border case actions");
}
GroundBorderCaseActionRecord action;
action.actionType = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(actionStmt, 0)));
action.targetValue = sqlite3_column_int(actionStmt, 1);
action.edge = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(actionStmt, 2)));
action.replacementValue = sqlite3_column_int(actionStmt, 3);
action.sortOrder = sqlite3_column_int(actionStmt, 4);
outActions.push_back(action);
}
return true;
}
template <typename SetErrorFn>
bool ReadGroundBorderCases(
sqlite3_stmt* caseStmt,
sqlite3_stmt* conditionStmt,
sqlite3_stmt* actionStmt,
std::vector<GroundBorderCaseRecord> &outCases,
SetErrorFn &&setErrorFromDatabase
) {
for (;;) {
const int caseRc = sqlite3_step(caseStmt);
if (caseRc == SQLITE_DONE) {
break;
}
if (caseRc != SQLITE_ROW) {
return setErrorFromDatabase("Failed to read ground border cases");
}
const int64_t caseId = sqlite3_column_int64(caseStmt, 0);
GroundBorderCaseRecord caseRecord;
caseRecord.sortOrder = sqlite3_column_int(caseStmt, 1);
if (!ReadGroundBorderConditions(caseId, conditionStmt, caseRecord.conditions, setErrorFromDatabase)) {
return false;
}
if (!ReadGroundBorderActions(caseId, actionStmt, caseRecord.actions, setErrorFromDatabase)) {
return false;
}
outCases.push_back(caseRecord);
}
return true;
}
template <typename SetErrorFn>
bool WriteDoodadSingleItems(
int64_t alternativeId,
const std::vector<DoodadSingleItemRecord> &singleItems,
sqlite3_stmt* insertSingleStmt,
SetErrorFn &&setErrorFromDatabase
) {
for (const DoodadSingleItemRecord &single : singleItems) {
sqlite3_reset(insertSingleStmt);
sqlite3_clear_bindings(insertSingleStmt);
sqlite3_bind_int64(insertSingleStmt, 1, alternativeId);
sqlite3_bind_int(insertSingleStmt, 2, single.itemId);
sqlite3_bind_int(insertSingleStmt, 3, single.chance);
sqlite3_bind_int(insertSingleStmt, 4, single.sortOrder);
if (sqlite3_step(insertSingleStmt) != SQLITE_DONE) {
return setErrorFromDatabase("Failed to insert doodad single item");
}
}
return true;
}
template <typename SetErrorFn>
bool WriteDoodadCompositeTileItems(
int64_t tileId,
const std::vector<DoodadCompositeTileItemRecord> &items,
sqlite3_stmt* insertTileItemStmt,
SetErrorFn &&setErrorFromDatabase
) {
for (const DoodadCompositeTileItemRecord &item : items) {
sqlite3_reset(insertTileItemStmt);
sqlite3_clear_bindings(insertTileItemStmt);
sqlite3_bind_int64(insertTileItemStmt, 1, tileId);
sqlite3_bind_int(insertTileItemStmt, 2, item.itemId);
sqlite3_bind_int(insertTileItemStmt, 3, item.sortOrder);
if (sqlite3_step(insertTileItemStmt) != SQLITE_DONE) {
return setErrorFromDatabase("Failed to insert doodad composite tile item");
}
}
return true;
}
template <typename SetErrorFn>
bool WriteDoodadCompositeTiles(
sqlite3* connection,
int64_t compositeId,
const std::vector<DoodadCompositeTileRecord> &tiles,
sqlite3_stmt* insertTileStmt,
sqlite3_stmt* insertTileItemStmt,
SetErrorFn &&setErrorFromDatabase
) {
for (const DoodadCompositeTileRecord &tile : tiles) {
sqlite3_reset(insertTileStmt);
sqlite3_clear_bindings(insertTileStmt);
sqlite3_bind_int64(insertTileStmt, 1, compositeId);
sqlite3_bind_int(insertTileStmt, 2, tile.offsetX);
sqlite3_bind_int(insertTileStmt, 3, tile.offsetY);
sqlite3_bind_int(insertTileStmt, 4, tile.offsetZ);
sqlite3_bind_int(insertTileStmt, 5, tile.sortOrder);
if (sqlite3_step(insertTileStmt) != SQLITE_DONE) {
return setErrorFromDatabase("Failed to insert doodad composite tile");
}
const int64_t tileId = sqlite3_last_insert_rowid(connection);
if (!WriteDoodadCompositeTileItems(tileId, tile.items, insertTileItemStmt, setErrorFromDatabase)) {
return false;
}
}
return true;
}
template <typename SetErrorFn>
bool WriteDoodadComposites(
sqlite3* connection,
int64_t alternativeId,
const std::vector<DoodadCompositeRecord> &composites,
sqlite3_stmt* insertCompositeStmt,
sqlite3_stmt* insertTileStmt,
sqlite3_stmt* insertTileItemStmt,
SetErrorFn &&setErrorFromDatabase
) {
for (const DoodadCompositeRecord &composite : composites) {
sqlite3_reset(insertCompositeStmt);
sqlite3_clear_bindings(insertCompositeStmt);
sqlite3_bind_int64(insertCompositeStmt, 1, alternativeId);
sqlite3_bind_int(insertCompositeStmt, 2, composite.chance);
sqlite3_bind_int(insertCompositeStmt, 3, composite.sortOrder);
if (sqlite3_step(insertCompositeStmt) != SQLITE_DONE) {
return setErrorFromDatabase("Failed to insert doodad composite");
}
const int64_t compositeId = sqlite3_last_insert_rowid(connection);
if (!WriteDoodadCompositeTiles(connection, compositeId, composite.tiles, insertTileStmt, insertTileItemStmt, setErrorFromDatabase)) {
return false;
}
}
return true;
}
template <typename SetErrorFn>
bool ReadDoodadSingleItems(
int64_t alternativeId,
sqlite3_stmt* singleStmt,
std::vector<DoodadSingleItemRecord> &outSingleItems,
SetErrorFn &&setErrorFromDatabase
) {
sqlite3_reset(singleStmt);
sqlite3_clear_bindings(singleStmt);
sqlite3_bind_int64(singleStmt, 1, alternativeId);
for (;;) {
const int singleRc = sqlite3_step(singleStmt);
if (singleRc == SQLITE_DONE) {
break;
}
if (singleRc != SQLITE_ROW) {
return setErrorFromDatabase("Failed to read doodad single items");
}
DoodadSingleItemRecord item;
item.itemId = sqlite3_column_int(singleStmt, 0);
item.chance = sqlite3_column_int(singleStmt, 1);
item.sortOrder = sqlite3_column_int(singleStmt, 2);
outSingleItems.push_back(item);
}
return true;
}
template <typename SetErrorFn>
bool ReadDoodadCompositeTileItems(
int64_t tileId,
sqlite3_stmt* tileItemStmt,
std::vector<DoodadCompositeTileItemRecord> &outItems,
SetErrorFn &&setErrorFromDatabase
) {
sqlite3_reset(tileItemStmt);
sqlite3_clear_bindings(tileItemStmt);
sqlite3_bind_int64(tileItemStmt, 1, tileId);
for (;;) {
const int tileItemRc = sqlite3_step(tileItemStmt);
if (tileItemRc == SQLITE_DONE) {
break;
}
if (tileItemRc != SQLITE_ROW) {
return setErrorFromDatabase("Failed to read doodad composite tile items");
}
DoodadCompositeTileItemRecord item;
item.itemId = sqlite3_column_int(tileItemStmt, 0);
item.sortOrder = sqlite3_column_int(tileItemStmt, 1);
outItems.push_back(item);
}
return true;
}
template <typename SetErrorFn>
bool ReadDoodadCompositeTiles(
int64_t compositeId,
sqlite3_stmt* tileStmt,
sqlite3_stmt* tileItemStmt,
std::vector<DoodadCompositeTileRecord> &outTiles,
SetErrorFn &&setErrorFromDatabase
) {
sqlite3_reset(tileStmt);
sqlite3_clear_bindings(tileStmt);
sqlite3_bind_int64(tileStmt, 1, compositeId);
for (;;) {
const int tileRc = sqlite3_step(tileStmt);
if (tileRc == SQLITE_DONE) {
break;
}
if (tileRc != SQLITE_ROW) {
return setErrorFromDatabase("Failed to read doodad composite tiles");
}
const int64_t tileId = sqlite3_column_int64(tileStmt, 0);
DoodadCompositeTileRecord tile;
tile.offsetX = sqlite3_column_int(tileStmt, 1);
tile.offsetY = sqlite3_column_int(tileStmt, 2);
tile.offsetZ = sqlite3_column_int(tileStmt, 3);
tile.sortOrder = sqlite3_column_int(tileStmt, 4);
if (!ReadDoodadCompositeTileItems(tileId, tileItemStmt, tile.items, setErrorFromDatabase)) {
return false;
}
outTiles.push_back(tile);
}
return true;
}
template <typename SetErrorFn>
bool ReadDoodadComposites(
int64_t alternativeId,
sqlite3_stmt* compositeStmt,
sqlite3_stmt* tileStmt,
sqlite3_stmt* tileItemStmt,
std::vector<DoodadCompositeRecord> &outComposites,
SetErrorFn &&setErrorFromDatabase
) {
sqlite3_reset(compositeStmt);
sqlite3_clear_bindings(compositeStmt);
sqlite3_bind_int64(compositeStmt, 1, alternativeId);
for (;;) {
const int compositeRc = sqlite3_step(compositeStmt);
if (compositeRc == SQLITE_DONE) {
break;
}
if (compositeRc != SQLITE_ROW) {
return setErrorFromDatabase("Failed to read doodad composites");
}
const int64_t compositeId = sqlite3_column_int64(compositeStmt, 0);
DoodadCompositeRecord composite;
composite.chance = sqlite3_column_int(compositeStmt, 1);
composite.sortOrder = sqlite3_column_int(compositeStmt, 2);
if (!ReadDoodadCompositeTiles(compositeId, tileStmt, tileItemStmt, composite.tiles, setErrorFromDatabase)) {
return false;
}
outComposites.push_back(composite);
}
return true;
}
template <typename SetErrorFn>
bool ResolveTilesetBrushReference(
sqlite3_stmt* findBrushStmt,
const TilesetEntryRecord &entry,
int64_t &resolvedBrushId,
SetErrorFn &&setErrorFromDatabase
) {
resolvedBrushId = entry.brushId;
if (resolvedBrushId != 0 || entry.brushName.IsEmpty()) {
return true;
}
sqlite3_reset(findBrushStmt);
sqlite3_clear_bindings(findBrushStmt);
sqlite3_bind_text(findBrushStmt, 1, entry.brushName.utf8_str(), -1, SQLITE_TRANSIENT);
const int firstRc = sqlite3_step(findBrushStmt);
if (firstRc == SQLITE_DONE) {
return true;
}
if (firstRc != SQLITE_ROW) {
return setErrorFromDatabase("Failed to resolve tileset brush reference");
}
resolvedBrushId = sqlite3_column_int64(findBrushStmt, 0);
const int secondRc = sqlite3_step(findBrushStmt);
if (secondRc == SQLITE_ROW) {
resolvedBrushId = 0;
return true;
}
if (secondRc != SQLITE_DONE) {
return setErrorFromDatabase("Failed to resolve tileset brush reference");
}
return true;
}
template <typename SetErrorFn>
bool WriteTilesetEntries(
int64_t sectionId,
const std::vector<TilesetEntryRecord> &entries,
sqlite3_stmt* insertEntryStmt,
sqlite3_stmt* findBrushStmt,
SetErrorFn &&setErrorFromDatabase
) {
for (const TilesetEntryRecord &entry : entries) {
int64_t resolvedBrushId = 0;
if (!ResolveTilesetBrushReference(findBrushStmt, entry, resolvedBrushId, setErrorFromDatabase)) {
return false;
}
sqlite3_reset(insertEntryStmt);
sqlite3_clear_bindings(insertEntryStmt);
sqlite3_bind_int64(insertEntryStmt, 1, sectionId);
sqlite3_bind_text(insertEntryStmt, 2, entry.entryKind.utf8_str(), -1, SQLITE_TRANSIENT);
if (resolvedBrushId != 0) {
sqlite3_bind_int64(insertEntryStmt, 3, resolvedBrushId);
} else {
sqlite3_bind_null(insertEntryStmt, 3);
}
sqlite3_bind_text(insertEntryStmt, 4, entry.brushName.utf8_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(insertEntryStmt, 5, entry.itemId);
sqlite3_bind_int(insertEntryStmt, 6, entry.fromItemId);
sqlite3_bind_int(insertEntryStmt, 7, entry.toItemId);
sqlite3_bind_text(insertEntryStmt, 8, entry.afterBrushName.utf8_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(insertEntryStmt, 9, entry.afterItemId);
sqlite3_bind_int(insertEntryStmt, 10, entry.sortOrder);
if (sqlite3_step(insertEntryStmt) != SQLITE_DONE) {
return setErrorFromDatabase("Failed to insert tileset entry");
}
}
return true;
}
template <typename SetErrorFn>
bool WriteWallPartItems(
int64_t wallPartId,
const std::vector<WallPartItemRecord> &items,
sqlite3_stmt* insertItemStmt,
SetErrorFn &&setErrorFromDatabase
) {
for (const WallPartItemRecord &item : items) {
sqlite3_reset(insertItemStmt);
sqlite3_clear_bindings(insertItemStmt);
sqlite3_bind_int64(insertItemStmt, 1, wallPartId);
sqlite3_bind_int(insertItemStmt, 2, item.itemId);
sqlite3_bind_int(insertItemStmt, 3, item.chance);
sqlite3_bind_int(insertItemStmt, 4, item.sortOrder);
if (sqlite3_step(insertItemStmt) != SQLITE_DONE) {
return setErrorFromDatabase("Failed to insert wall part item");
}
}
return true;
}
template <typename SetErrorFn>
bool WriteWallPartDoors(
int64_t wallPartId,
const std::vector<WallPartDoorRecord> &doors,
sqlite3_stmt* insertDoorStmt,
SetErrorFn &&setErrorFromDatabase
) {
for (const WallPartDoorRecord &door : doors) {
sqlite3_reset(insertDoorStmt);
sqlite3_clear_bindings(insertDoorStmt);
sqlite3_bind_int64(insertDoorStmt, 1, wallPartId);
sqlite3_bind_int(insertDoorStmt, 2, door.itemId);
sqlite3_bind_text(insertDoorStmt, 3, door.doorType.utf8_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(insertDoorStmt, 4, door.isOpen ? 1 : 0);
sqlite3_bind_int(insertDoorStmt, 5, door.wallHateMe ? 1 : 0);
sqlite3_bind_int(insertDoorStmt, 6, door.sortOrder);
if (sqlite3_step(insertDoorStmt) != SQLITE_DONE) {
return setErrorFromDatabase("Failed to insert wall part door");
}
}
return true;
}
} // namespace
BrushDatabaseSession::BrushDatabaseSession() = default;
BrushDatabaseSession::~BrushDatabaseSession() {
close();
}
BrushDatabaseComponent::BrushDatabaseComponent(BrushDatabaseSession &session) :
session_(session),
connection_(session.connection_),
lastError_(session.lastError_),
readOnly_(session.readOnly_) {
}
BrushDatabaseSchemaManager::BrushDatabaseSchemaManager(BrushDatabaseSession &session) :
BrushDatabaseComponent(session) {
}
BrushDatabaseBrushRepository::BrushDatabaseBrushRepository(BrushDatabaseSession &session) :
BrushDatabaseComponent(session) {
}
BrushDatabaseCatalogRepository::BrushDatabaseCatalogRepository(BrushDatabaseSession &session, BrushDatabaseSchemaManager &schemaManager) :
BrushDatabaseComponent(session),
schemaManager_(schemaManager) {
}
BrushDatabase::~BrushDatabase() = default;
bool BrushDatabase::initialize(const wxString &databasePath) {
if (!session_.open(databasePath)) {
return false;
}
return schemaManager_.initializeSchema();
}
bool BrushDatabase::openReadOnly(const wxString &databasePath) {
return session_.openReadOnly(databasePath);
}
bool BrushDatabase::open(const wxString &databasePath) {
return session_.open(databasePath);
}
void BrushDatabase::close() {
session_.close();
}
bool BrushDatabase::isOpen() const {
return session_.isOpen();
}
bool BrushDatabase::isReadOnly() const {
return session_.isReadOnly();
}
const wxString &BrushDatabase::getDatabasePath() const {
return session_.getDatabasePath();
}
const wxString &BrushDatabase::getLastError() const {
return session_.getLastError();
}
bool BrushDatabase::testDatabaseConnection() {
return session_.testDatabaseConnection();
}
bool BrushDatabase::upsertBrush(const BrushRecord &brush, int64_t &brushId) {
return brushRepository_.upsertBrush(brush, brushId);
}
bool BrushDatabase::listBrushesByType(const wxString &type, std::vector<BrushRecord> &outBrushes) {
return brushRepository_.listBrushesByType(type, outBrushes);
}
bool BrushDatabase::getCompleteBrushById(int64_t brushId, BrushStorageRecord &outBrush) {
return brushRepository_.getCompleteBrushById(brushId, outBrush);
}
bool BrushDatabase::deleteBrushesByType(const wxString &type) {
return brushRepository_.deleteBrushesByType(type);
}
bool BrushDatabase::replaceBrushItems(int64_t brushId, const std::vector<BrushItemRecord> &items) {
return brushRepository_.replaceBrushItems(brushId, items);
}
bool BrushDatabase::upsertBorderSet(const BorderSetRecord &borderSet, int64_t &borderSetId) {
return brushRepository_.upsertBorderSet(borderSet, borderSetId);
}
bool BrushDatabase::findBorderSetByXmlBorderId(int xmlBorderId, BorderSetRecord &outBorderSet) {
return brushRepository_.findBorderSetByXmlBorderId(xmlBorderId, outBorderSet);
}
bool BrushDatabase::replaceBorderSetItems(int64_t borderSetId, const std::vector<BorderSetItemRecord> &items) {
return brushRepository_.replaceBorderSetItems(borderSetId, items);
}
bool BrushDatabase::deleteBorderSetsByScope(const wxString &borderScope) {
return brushRepository_.deleteBorderSetsByScope(borderScope);
}
bool BrushDatabase::deleteOwnedBorderSetsForBrush(int64_t brushId) {
return brushRepository_.deleteOwnedBorderSetsForBrush(brushId);
}
bool BrushDatabase::replaceGroundBrushBorders(int64_t brushId, const std::vector<GroundBrushBorderRecord> &borders) {
return brushRepository_.replaceGroundBrushBorders(brushId, borders);
}
bool BrushDatabase::replaceBrushLinks(int64_t brushId, const std::vector<BrushLinkRecord> &links) {
return brushRepository_.replaceBrushLinks(brushId, links);
}
bool BrushDatabase::replaceWallParts(int64_t brushId, const std::vector<WallPartRecord> &parts) {
return brushRepository_.replaceWallParts(brushId, parts);
}
bool BrushDatabase::replaceCarpetNodes(int64_t brushId, const std::vector<CarpetNodeRecord> &nodes) {
return brushRepository_.replaceCarpetNodes(brushId, nodes);
}
bool BrushDatabase::replaceTableNodes(int64_t brushId, const std::vector<TableNodeRecord> &nodes) {
return brushRepository_.replaceTableNodes(brushId, nodes);
}
bool BrushDatabase::replaceDoodadAlternatives(int64_t brushId, const std::vector<DoodadAlternativeRecord> &alternatives) {
return brushRepository_.replaceDoodadAlternatives(brushId, alternatives);
}
bool BrushDatabase::resolveGroundReferenceNames() {
return brushRepository_.resolveGroundReferenceNames();
}
bool BrushDatabase::replaceAllTilesets(const std::vector<TilesetStorageRecord> &tilesets) {
return catalogRepository_.replaceAllTilesets(tilesets);
}
bool BrushDatabase::getAllTilesets(std::vector<TilesetStorageRecord> &outTilesets) {
return catalogRepository_.getAllTilesets(outTilesets);
}
bool BrushDatabase::generateAuditReport(MaterialsDatabaseAuditReport &outReport) {
return catalogRepository_.generateAuditReport(outReport);
}
bool BrushDatabase::hasCompleteImportForCurrentSchema(bool &outReady) {
return catalogRepository_.hasCompleteImportForCurrentSchema(outReady);
}
int BrushDatabase::getExpectedSchemaVersion() const {
return catalogRepository_.getExpectedSchemaVersion();
}
bool BrushDatabaseSession::initialize(const wxString &databasePath) {
return open(databasePath);
}
bool BrushDatabaseSession::openReadOnly(const wxString &databasePath) {
return openInternal(databasePath, true);
}
bool BrushDatabaseSession::open(const wxString &databasePath) {
return openInternal(databasePath, false);
}
bool BrushDatabaseSession::openInternal(const wxString &databasePath, bool readOnly) {
if (connection_ && databasePath_ == databasePath) {
if (readOnly_ == readOnly) {
return true;
}
close();
}
if (connection_) {
close();
}
if (!readOnly) {
wxFileName dbFile(databasePath);
if (!dbFile.DirExists() && !dbFile.Mkdir(0755, wxPATH_MKDIR_FULL)) {
return setError("Failed to create SQLite directory: " + dbFile.GetPath());
}
}
wxFileName dbFile(databasePath);
const int flags = readOnly ? SQLITE_OPEN_READONLY : (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
const int rc = sqlite3_open_v2(dbFile.GetFullPath().utf8_str(), &connection_, flags, nullptr);
if (rc != SQLITE_OK) {
const wxString dbError = ToWxString(sqlite3_errmsg(connection_));
close();
return setError("Failed to open SQLite database: " + dbError);
}
databasePath_ = dbFile.GetFullPath();
lastError_.clear();
readOnly_ = readOnly;
if (!execute("PRAGMA foreign_keys = ON;")) {
close();
return false;
}
if (!execute("PRAGMA busy_timeout = 3000;")) {
close();
return false;
}
if (!readOnly) {
if (!execute("PRAGMA journal_mode = WAL;")) {
close();
return false;
}
} else if (!execute("PRAGMA query_only = ON;")) {
close();
return false;
}
spdlog::info("SQLite brush database opened ({}): {}", readOnly ? "read-only" : "read-write", databasePath_.ToStdString());
return true;
}
void BrushDatabaseSession::close() {
if (connection_) {
sqlite3_close(connection_);
connection_ = nullptr;
}
databasePath_.clear();
readOnly_ = false;
transactionDepth_ = 0;
nextSavepointId_ = 0;
savepointIds_.clear();
}
bool BrushDatabaseSession::isOpen() const {
return connection_ != nullptr;
}
bool BrushDatabaseSession::isReadOnly() const {
return readOnly_;
}
const wxString &BrushDatabaseSession::getDatabasePath() const {
return databasePath_;
}
const wxString &BrushDatabaseSession::getLastError() const {
return lastError_;
}
sqlite3* BrushDatabaseSession::connection() const {
return connection_;
}
sqlite3* BrushDatabaseComponent::connection() const {
return session_.connection();
}
bool BrushDatabaseComponent::isOpen() const {
return session_.isOpen();
}
bool BrushDatabaseComponent::isReadOnly() const {
return session_.isReadOnly();
}
const wxString &BrushDatabaseComponent::lastError() const {
return session_.getLastError();
}
bool BrushDatabaseComponent::execute(const wxString &sql) {
return session_.execute(sql);
}
bool BrushDatabaseComponent::prepare(const char* sql, sqlite3_stmt** stmt) {
return session_.prepare(sql, stmt);
}
bool BrushDatabaseComponent::beginTransaction() {
return session_.beginTransaction();
}
bool BrushDatabaseComponent::commitTransaction() {
return session_.commitTransaction();
}
bool BrushDatabaseComponent::rollbackTransaction() {
return session_.rollbackTransaction();
}
bool BrushDatabaseComponent::setError(const wxString &message) {
return session_.setError(message);
}
bool BrushDatabaseComponent::setErrorFromDatabase(const wxString &prefix) {
return session_.setErrorFromDatabase(prefix);
}
bool BrushDatabaseSchemaManager::ensureSchemaVersionTable() {
if (!execute("CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL);")) {
return false;
}
return execute("INSERT INTO schema_version(version) "
"SELECT 0 WHERE NOT EXISTS (SELECT 1 FROM schema_version);");
}
bool BrushDatabaseSchemaManager::getCurrentSchemaVersion(int &version) {
version = 0;
sqlite3_stmt* stmt = nullptr;
if (!prepare("SELECT version FROM schema_version LIMIT 1;", &stmt)) {
return false;
}
const int rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
version = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt);
return true;
}
sqlite3_finalize(stmt);
if (rc == SQLITE_DONE) {
return setError("SQLite schema version row was not found.");
}
return setErrorFromDatabase("Failed to query SQLite schema version");
}
bool BrushDatabaseSchemaManager::setSchemaVersion(int version) {
sqlite3_stmt* stmt = nullptr;
if (!prepare("UPDATE schema_version SET version = ?;", &stmt)) {
return false;
}
sqlite3_bind_int(stmt, 1, version);
const int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
return setErrorFromDatabase("Failed to update SQLite schema version");
}
return true;
}
bool BrushDatabaseSchemaManager::columnExists(const wxString &tableName, const wxString &columnName, bool &exists) {
exists = false;
sqlite3_stmt* stmt = nullptr;
const wxString sql = "PRAGMA table_info(" + tableName + ");";
if (!prepare(sql.utf8_str(), &stmt)) {
return false;
}
while (!exists) {
const int rc = sqlite3_step(stmt);
if (rc == SQLITE_DONE) {
break;
}
if (rc != SQLITE_ROW) {
sqlite3_finalize(stmt);
return setErrorFromDatabase("Failed to inspect SQLite table info");
}
const wxString currentName = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1)));
if (currentName == columnName) {
exists = true;
}
}
sqlite3_finalize(stmt);
return true;
}
bool BrushDatabaseSchemaManager::migrateToVersion1() {
if (!execute("CREATE TABLE IF NOT EXISTS brushes ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"name TEXT NOT NULL,"
"type TEXT NOT NULL,"
"look_id INTEGER NOT NULL DEFAULT 0,"
"z_order INTEGER NOT NULL DEFAULT 0,"
"created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,"
"updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
");")) {
return false;
}
if (!execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_brushes_type_name "
"ON brushes(type, name);")) {
return false;
}
if (!execute("CREATE TABLE IF NOT EXISTS brush_items ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"brush_id INTEGER NOT NULL,"
"item_id INTEGER NOT NULL,"
"chance INTEGER NOT NULL DEFAULT 1 CHECK(chance >= 0),"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"FOREIGN KEY (brush_id) REFERENCES brushes(id) ON DELETE CASCADE"
");")) {
return false;
}
if (!execute("CREATE INDEX IF NOT EXISTS idx_brush_items_item_id "
"ON brush_items(item_id);")) {
return false;
}
if (!execute("CREATE INDEX IF NOT EXISTS idx_brush_items_brush_sort "
"ON brush_items(brush_id, sort_order);")) {
return false;
}
if (!execute("CREATE TABLE IF NOT EXISTS ground_borders ("
"brush_id INTEGER NOT NULL,"
"border_id INTEGER NOT NULL,"
"align TEXT NOT NULL DEFAULT 'outer',"
"to_brush_id INTEGER,"
"PRIMARY KEY (brush_id, border_id),"
"FOREIGN KEY (brush_id) REFERENCES brushes(id) ON DELETE CASCADE,"
"FOREIGN KEY (to_brush_id) REFERENCES brushes(id) ON DELETE SET NULL"
");")) {
return false;
}
if (!execute("CREATE TABLE IF NOT EXISTS ground_optional_borders ("
"brush_id INTEGER NOT NULL,"
"border_id INTEGER NOT NULL,"
"PRIMARY KEY (brush_id, border_id),"
"FOREIGN KEY (brush_id) REFERENCES brushes(id) ON DELETE CASCADE"
");")) {
return false;
}
if (!execute("CREATE TABLE IF NOT EXISTS brush_relationships ("
"from_brush_id INTEGER NOT NULL,"
"to_brush_id INTEGER NOT NULL,"
"relationship_type TEXT NOT NULL,"
"PRIMARY KEY (from_brush_id, to_brush_id, relationship_type),"
"FOREIGN KEY (from_brush_id) REFERENCES brushes(id) ON DELETE CASCADE,"
"FOREIGN KEY (to_brush_id) REFERENCES brushes(id) ON DELETE CASCADE"
");")) {
return false;
}
return execute("CREATE INDEX IF NOT EXISTS idx_brush_relationships_to "
"ON brush_relationships(to_brush_id, relationship_type);");
}
bool BrushDatabaseSchemaManager::addColumnIfMissing(const wxString &tableName, const wxString &columnName, const wxString &definition) {
bool exists = false;
if (!columnExists(tableName, columnName, exists)) {
return false;
}
if (exists) {
return true;
}
return execute("ALTER TABLE " + tableName + " ADD COLUMN " + definition + ";");
}
bool BrushDatabaseSchemaManager::executeStatements(std::initializer_list<const char*> statements) {
for (const char* statement : statements) {
if (!execute(statement)) {
return false;
}
}
return true;
}
bool BrushDatabaseSchemaManager::addVersion2BrushColumns() {
struct ColumnDefinition {
const char* name;
const char* definition;
};
static constexpr std::array<ColumnDefinition, 11> kBrushColumns = { {
{ "source_file", "source_file TEXT NOT NULL DEFAULT ''" },
{ "server_look_id", "server_look_id INTEGER NOT NULL DEFAULT 0" },
{ "draggable", "draggable INTEGER NOT NULL DEFAULT 0 CHECK(draggable IN (0, 1))" },
{ "on_blocking", "on_blocking INTEGER NOT NULL DEFAULT 0 CHECK(on_blocking IN (0, 1))" },
{ "on_duplicate", "on_duplicate INTEGER NOT NULL DEFAULT 0 CHECK(on_duplicate IN (0, 1))" },
{ "redo_borders", "redo_borders INTEGER NOT NULL DEFAULT 0 CHECK(redo_borders IN (0, 1))" },
{ "randomize", "randomize INTEGER NOT NULL DEFAULT 0 CHECK(randomize IN (0, 1))" },
{ "one_size", "one_size INTEGER NOT NULL DEFAULT 0 CHECK(one_size IN (0, 1))" },
{ "solo_optional", "solo_optional INTEGER NOT NULL DEFAULT 0 CHECK(solo_optional IN (0, 1))" },
{ "thickness", "thickness INTEGER NOT NULL DEFAULT 0" },
{ "thickness_ceiling", "thickness_ceiling INTEGER NOT NULL DEFAULT 0" },
} };
for (const ColumnDefinition &column : kBrushColumns) {
if (!addColumnIfMissing("brushes", column.name, column.definition)) {
return false;
}
}
return executeStatements({
"CREATE INDEX IF NOT EXISTS idx_brushes_name ON brushes(name);",
});
}
bool BrushDatabaseSchemaManager::createVersion2BorderSchema() {
return executeStatements({
"CREATE TABLE IF NOT EXISTS border_sets ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"xml_border_id INTEGER UNIQUE,"
"owner_brush_id INTEGER,"
"border_scope TEXT NOT NULL DEFAULT 'global',"
"border_type TEXT NOT NULL DEFAULT 'normal',"
"border_group INTEGER NOT NULL DEFAULT 0,"
"ground_equivalent INTEGER NOT NULL DEFAULT 0,"
"source_file TEXT NOT NULL DEFAULT '',"
"created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,"
"updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,"
"FOREIGN KEY (owner_brush_id) REFERENCES brushes(id) ON DELETE CASCADE"
");",
"CREATE INDEX IF NOT EXISTS idx_border_sets_owner_scope "
"ON border_sets(owner_brush_id, border_scope);",
"CREATE TABLE IF NOT EXISTS border_set_items ("
"border_set_id INTEGER NOT NULL,"
"edge TEXT NOT NULL,"
"item_id INTEGER NOT NULL,"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"PRIMARY KEY (border_set_id, edge, item_id),"
"FOREIGN KEY (border_set_id) REFERENCES border_sets(id) ON DELETE CASCADE"
");",
"CREATE INDEX IF NOT EXISTS idx_border_set_items_item "
"ON border_set_items(item_id);",
"CREATE TABLE IF NOT EXISTS ground_brush_borders ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"brush_id INTEGER NOT NULL,"
"border_set_id INTEGER NOT NULL,"
"border_role TEXT NOT NULL DEFAULT 'normal',"
"align TEXT NOT NULL DEFAULT 'outer',"
"target_mode TEXT NOT NULL DEFAULT 'all',"
"target_brush_id INTEGER,"
"target_brush_name TEXT NOT NULL DEFAULT '',"
"super_border INTEGER NOT NULL DEFAULT 0 CHECK(super_border IN (0, 1)),"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"FOREIGN KEY (brush_id) REFERENCES brushes(id) ON DELETE CASCADE,"
"FOREIGN KEY (border_set_id) REFERENCES border_sets(id) ON DELETE CASCADE,"
"FOREIGN KEY (target_brush_id) REFERENCES brushes(id) ON DELETE SET NULL"
");",
"CREATE INDEX IF NOT EXISTS idx_ground_brush_borders_brush "
"ON ground_brush_borders(brush_id, border_role, align, sort_order);",
"CREATE INDEX IF NOT EXISTS idx_ground_brush_borders_target "
"ON ground_brush_borders(target_brush_name, target_mode);",
"CREATE TABLE IF NOT EXISTS ground_border_cases ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"ground_brush_border_id INTEGER NOT NULL,"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"FOREIGN KEY (ground_brush_border_id) REFERENCES ground_brush_borders(id) ON DELETE CASCADE"
");",
"CREATE TABLE IF NOT EXISTS ground_border_case_conditions ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"ground_border_case_id INTEGER NOT NULL,"
"condition_type TEXT NOT NULL,"
"match_value INTEGER NOT NULL DEFAULT 0,"
"edge TEXT NOT NULL DEFAULT '',"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"FOREIGN KEY (ground_border_case_id) REFERENCES ground_border_cases(id) ON DELETE CASCADE"
");",
"CREATE TABLE IF NOT EXISTS ground_border_case_actions ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"ground_border_case_id INTEGER NOT NULL,"
"action_type TEXT NOT NULL,"
"target_value INTEGER NOT NULL DEFAULT 0,"
"edge TEXT NOT NULL DEFAULT '',"
"replacement_value INTEGER NOT NULL DEFAULT 0,"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"FOREIGN KEY (ground_border_case_id) REFERENCES ground_border_cases(id) ON DELETE CASCADE"
");",
});
}
bool BrushDatabaseSchemaManager::createVersion2BrushDetailSchema() {
return executeStatements({
"CREATE TABLE IF NOT EXISTS brush_links ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"brush_id INTEGER NOT NULL,"
"target_brush_id INTEGER,"
"target_brush_name TEXT NOT NULL DEFAULT '',"
"relation_type TEXT NOT NULL,"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"FOREIGN KEY (brush_id) REFERENCES brushes(id) ON DELETE CASCADE,"
"FOREIGN KEY (target_brush_id) REFERENCES brushes(id) ON DELETE SET NULL"
");",
"CREATE INDEX IF NOT EXISTS idx_brush_links_type "
"ON brush_links(brush_id, relation_type, sort_order);",
"CREATE INDEX IF NOT EXISTS idx_brush_links_target_name "
"ON brush_links(target_brush_name);",
"CREATE TABLE IF NOT EXISTS wall_parts ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"brush_id INTEGER NOT NULL,"
"part_type TEXT NOT NULL,"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"FOREIGN KEY (brush_id) REFERENCES brushes(id) ON DELETE CASCADE,"
"UNIQUE (brush_id, part_type)"
");",
"CREATE TABLE IF NOT EXISTS wall_part_items ("
"wall_part_id INTEGER NOT NULL,"
"item_id INTEGER NOT NULL,"
"chance INTEGER NOT NULL DEFAULT 1 CHECK(chance >= 0),"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"PRIMARY KEY (wall_part_id, item_id),"
"FOREIGN KEY (wall_part_id) REFERENCES wall_parts(id) ON DELETE CASCADE"
");",
"CREATE TABLE IF NOT EXISTS wall_part_doors ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"wall_part_id INTEGER NOT NULL,"
"item_id INTEGER NOT NULL,"
"door_type TEXT NOT NULL DEFAULT 'normal',"
"is_open INTEGER NOT NULL DEFAULT 0 CHECK(is_open IN (0, 1)),"
"wall_hate_me INTEGER NOT NULL DEFAULT 0 CHECK(wall_hate_me IN (0, 1)),"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"FOREIGN KEY (wall_part_id) REFERENCES wall_parts(id) ON DELETE CASCADE"
");",
"CREATE INDEX IF NOT EXISTS idx_wall_part_doors_part "
"ON wall_part_doors(wall_part_id, sort_order);",
"CREATE TABLE IF NOT EXISTS carpet_nodes ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"brush_id INTEGER NOT NULL,"
"align TEXT NOT NULL,"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"FOREIGN KEY (brush_id) REFERENCES brushes(id) ON DELETE CASCADE"
");",
"CREATE TABLE IF NOT EXISTS carpet_node_items ("
"carpet_node_id INTEGER NOT NULL,"
"item_id INTEGER NOT NULL,"
"chance INTEGER NOT NULL DEFAULT 1 CHECK(chance >= 0),"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"PRIMARY KEY (carpet_node_id, item_id),"
"FOREIGN KEY (carpet_node_id) REFERENCES carpet_nodes(id) ON DELETE CASCADE"
");",
"CREATE TABLE IF NOT EXISTS table_nodes ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"brush_id INTEGER NOT NULL,"
"align TEXT NOT NULL,"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"FOREIGN KEY (brush_id) REFERENCES brushes(id) ON DELETE CASCADE"
");",
"CREATE TABLE IF NOT EXISTS table_node_items ("
"table_node_id INTEGER NOT NULL,"
"item_id INTEGER NOT NULL,"
"chance INTEGER NOT NULL DEFAULT 1 CHECK(chance >= 0),"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"PRIMARY KEY (table_node_id, item_id),"
"FOREIGN KEY (table_node_id) REFERENCES table_nodes(id) ON DELETE CASCADE"
");",
"CREATE TABLE IF NOT EXISTS doodad_alternatives ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"brush_id INTEGER NOT NULL,"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"FOREIGN KEY (brush_id) REFERENCES brushes(id) ON DELETE CASCADE"
");",
"CREATE TABLE IF NOT EXISTS doodad_single_items ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"doodad_alternative_id INTEGER NOT NULL,"
"item_id INTEGER NOT NULL,"
"chance INTEGER NOT NULL DEFAULT 1 CHECK(chance >= 0),"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"FOREIGN KEY (doodad_alternative_id) REFERENCES doodad_alternatives(id) ON DELETE CASCADE"
");",
"CREATE TABLE IF NOT EXISTS doodad_composites ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"doodad_alternative_id INTEGER NOT NULL,"
"chance INTEGER NOT NULL DEFAULT 1 CHECK(chance >= 0),"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"FOREIGN KEY (doodad_alternative_id) REFERENCES doodad_alternatives(id) ON DELETE CASCADE"
");",
"CREATE TABLE IF NOT EXISTS doodad_composite_tiles ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"doodad_composite_id INTEGER NOT NULL,"
"offset_x INTEGER NOT NULL DEFAULT 0,"
"offset_y INTEGER NOT NULL DEFAULT 0,"
"offset_z INTEGER NOT NULL DEFAULT 0,"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"FOREIGN KEY (doodad_composite_id) REFERENCES doodad_composites(id) ON DELETE CASCADE"
");",
"CREATE TABLE IF NOT EXISTS doodad_composite_tile_items ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"doodad_composite_tile_id INTEGER NOT NULL,"
"item_id INTEGER NOT NULL,"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"FOREIGN KEY (doodad_composite_tile_id) REFERENCES doodad_composite_tiles(id) ON DELETE CASCADE"
");",
});
}
bool BrushDatabaseSchemaManager::createVersion2TilesetSchema() {
return executeStatements({
"CREATE TABLE IF NOT EXISTS tilesets ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"name TEXT NOT NULL UNIQUE,"
"source_file TEXT NOT NULL DEFAULT '',"
"created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,"
"updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
");",
"CREATE TABLE IF NOT EXISTS tileset_sections ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"tileset_id INTEGER NOT NULL,"
"section_type TEXT NOT NULL,"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"FOREIGN KEY (tileset_id) REFERENCES tilesets(id) ON DELETE CASCADE,"
"UNIQUE (tileset_id, section_type)"
");",
"CREATE TABLE IF NOT EXISTS tileset_brush_entries ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"tileset_section_id INTEGER NOT NULL,"
"brush_id INTEGER,"
"brush_name TEXT NOT NULL DEFAULT '',"
"after_brush_name TEXT NOT NULL DEFAULT '',"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"FOREIGN KEY (tileset_section_id) REFERENCES tileset_sections(id) ON DELETE CASCADE,"
"FOREIGN KEY (brush_id) REFERENCES brushes(id) ON DELETE SET NULL"
");",
"CREATE INDEX IF NOT EXISTS idx_tileset_brush_entries_section "
"ON tileset_brush_entries(tileset_section_id, sort_order);",
"CREATE INDEX IF NOT EXISTS idx_tileset_brush_entries_name "
"ON tileset_brush_entries(brush_name);",
});
}
bool BrushDatabaseSchemaManager::migrateToVersion2() {
return addVersion2BrushColumns() && createVersion2BorderSchema() && createVersion2BrushDetailSchema() && createVersion2TilesetSchema();
}
template <auto Migration>
bool BrushDatabaseSchemaManager::applySchemaMigrationStep(int &version, int targetVersion) {
if (version >= targetVersion) {
return true;
}
if (!(this->*Migration)()) {
return false;
}
if (!setSchemaVersion(targetVersion)) {
return false;
}
version = targetVersion;
return true;
}
bool BrushDatabaseSchemaManager::initializeSchema() {
if (!isOpen()) {
return setError("SQLite database is not open.");
}
if (readOnly_) {
return setError("SQLite schema initialization requires a read-write connection.");
}
if (!beginTransaction()) {
return false;
}
const auto rollback = [this]() {
rollbackTransaction();
return false;
};
if (!ensureSchemaVersionTable()) {
return rollback();
}
int version = 0;
if (!getCurrentSchemaVersion(version)) {
return rollback();
}
if (version > kBrushDatabaseSchemaVersion) {
rollbackTransaction();
return setError(wxString::Format("SQLite schema version %d is newer than supported version %d.", version, kBrushDatabaseSchemaVersion));
}
if (!applySchemaMigrationStep<&BrushDatabaseSchemaManager::migrateToVersion1>(version, 1) || !applySchemaMigrationStep<&BrushDatabaseSchemaManager::migrateToVersion2>(version, 2) || !applySchemaMigrationStep<&BrushDatabaseSchemaManager::migrateToVersion3>(version, 3) || !applySchemaMigrationStep<&BrushDatabaseSchemaManager::migrateToVersion4>(version, 4) || !applySchemaMigrationStep<&BrushDatabaseSchemaManager::migrateToVersion5>(version, 5) || !applySchemaMigrationStep<&BrushDatabaseSchemaManager::migrateToVersion6>(version, 6)) {
return rollback();
}
if (!commitTransaction()) {
return rollback();
}
spdlog::info("SQLite brush database schema initialized at version {}", version);
return true;
}
bool BrushDatabaseSchemaManager::migrateToVersion3() {
const wxString recreateSql = "DROP TABLE IF EXISTS brush_items;"
"DROP TABLE IF EXISTS wall_part_items;"
"DROP TABLE IF EXISTS carpet_node_items;"
"DROP TABLE IF EXISTS table_node_items;"
"DROP TABLE IF EXISTS doodad_single_items;"
"DROP TABLE IF EXISTS doodad_composites;"
"CREATE TABLE brush_items ("
"brush_id INTEGER NOT NULL,"
"item_id INTEGER NOT NULL,"
"chance INTEGER NOT NULL DEFAULT 1 CHECK(chance >= 0),"
"PRIMARY KEY (brush_id, item_id),"
"FOREIGN KEY (brush_id) REFERENCES brushes(id) ON DELETE CASCADE"
");"
"CREATE INDEX idx_brush_items_item_id ON brush_items(item_id);"
"CREATE TABLE wall_part_items ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"wall_part_id INTEGER NOT NULL,"
"item_id INTEGER NOT NULL,"
"chance INTEGER NOT NULL DEFAULT 1 CHECK(chance >= 0),"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"FOREIGN KEY (wall_part_id) REFERENCES wall_parts(id) ON DELETE CASCADE"
");"
"CREATE INDEX idx_wall_part_items_item ON wall_part_items(item_id);"
"CREATE TABLE carpet_node_items ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"carpet_node_id INTEGER NOT NULL,"
"item_id INTEGER NOT NULL,"
"chance INTEGER NOT NULL DEFAULT 1 CHECK(chance >= 0),"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"FOREIGN KEY (carpet_node_id) REFERENCES carpet_nodes(id) ON DELETE CASCADE"
");"
"CREATE TABLE table_node_items ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"table_node_id INTEGER NOT NULL,"
"item_id INTEGER NOT NULL,"
"chance INTEGER NOT NULL DEFAULT 1 CHECK(chance >= 0),"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"FOREIGN KEY (table_node_id) REFERENCES table_nodes(id) ON DELETE CASCADE"
");"
"CREATE TABLE doodad_single_items ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"doodad_alternative_id INTEGER NOT NULL,"
"item_id INTEGER NOT NULL,"
"chance INTEGER NOT NULL DEFAULT 1 CHECK(chance >= 0),"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"FOREIGN KEY (doodad_alternative_id) REFERENCES doodad_alternatives(id) ON DELETE CASCADE"
");"
"CREATE TABLE doodad_composites ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"doodad_alternative_id INTEGER NOT NULL,"
"chance INTEGER NOT NULL DEFAULT 1 CHECK(chance >= 0),"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"FOREIGN KEY (doodad_alternative_id) REFERENCES doodad_alternatives(id) ON DELETE CASCADE"
");";
if (!execute(recreateSql)) {
return false;
}
return true;
}
bool BrushDatabaseSchemaManager::migrateToVersion4() {
const wxString recreateSql = "DROP TABLE IF EXISTS brush_items;"
"CREATE TABLE brush_items ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"brush_id INTEGER NOT NULL,"
"item_id INTEGER NOT NULL,"
"chance INTEGER NOT NULL DEFAULT 1 CHECK(chance >= 0),"
"sort_order INTEGER NOT NULL DEFAULT 0,"
"FOREIGN KEY (brush_id) REFERENCES brushes(id) ON DELETE CASCADE"
");"
"CREATE INDEX idx_brush_items_item_id ON brush_items(item_id);"
"CREATE INDEX idx_brush_items_brush_sort ON brush_items(brush_id, sort_order);";
return execute(recreateSql);
}
bool BrushDatabaseSchemaManager::migrateToVersion5() {
return execute(kRecreateTilesetTablesSql);
}
bool BrushDatabaseSchemaManager::migrateToVersion6() {
return execute(kRecreateTilesetTablesSql);
}
bool BrushDatabaseSession::testDatabaseConnection() {
if (!isOpen()) {
return setError("SQLite database is not open.");
}
sqlite3_stmt* stmt = nullptr;
if (!prepare("SELECT 1;", &stmt)) {
return false;
}
const int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_ROW) {
return setErrorFromDatabase("SQLite connection test failed");
}
return true;
}
bool BrushDatabaseBrushRepository::testBasicCRUD() {
if (!isOpen()) {
return setError("SQLite database is not open.");
}
BrushRecord brush;
brush.name = "__sqlite_smoke_test__";
brush.type = "ground";
brush.lookId = 100;
brush.zOrder = 3;
int64_t insertedId = 0;
if (!insertBrush(brush, insertedId)) {
return false;
}
std::vector<BrushItemRecord> items = {
{ insertedId, 100, 100 },
{ insertedId, 101, 50 },
};
if (!replaceBrushItems(insertedId, items)) {
deleteBrush(insertedId);
return false;
}
BrushRecord loadedBrush;
if (!getBrushById(insertedId, loadedBrush)) {
deleteBrush(insertedId);
return false;
}
loadedBrush.name = "__sqlite_smoke_test_updated__";
if (!updateBrush(loadedBrush)) {
deleteBrush(insertedId);
return false;
}
std::vector<BrushItemRecord> loadedItems;
if (!getBrushItems(insertedId, loadedItems)) {
deleteBrush(insertedId);
return false;
}
if (loadedItems.size() != 2) {
deleteBrush(insertedId);
return setError("SQLite smoke test failed: unexpected brush item count.");
}
if (!deleteBrush(insertedId)) {
return false;
}
return true;
}
bool BrushDatabaseBrushRepository::insertBrush(const BrushRecord &brush, int64_t &insertedId) {
if (!isOpen()) {
return setError("SQLite database is not open.");
}
sqlite3_stmt* stmt = nullptr;
if (!prepare("INSERT INTO brushes("
"name, type, look_id, z_order, source_file, server_look_id, "
"draggable, on_blocking, on_duplicate, redo_borders, randomize, "
"one_size, solo_optional, thickness, thickness_ceiling, updated_at"
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP);",
&stmt)) {
return false;
}
BindBrushRecordFields(stmt, brush, 1);
const int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
return setErrorFromDatabase("Failed to insert brush");
}
insertedId = sqlite3_last_insert_rowid(connection_);
return true;
}
bool BrushDatabaseBrushRepository::upsertBrush(const BrushRecord &brush, int64_t &brushId) {
BrushRecord existingBrush;
if (findBrushByNameAndType(brush.name, brush.type, existingBrush)) {
BrushRecord updatedBrush = brush;
updatedBrush.id = existingBrush.id;
if (!updateBrush(updatedBrush)) {
return false;
}
brushId = existingBrush.id;
return true;
}
return insertBrush(brush, brushId);
}
bool BrushDatabaseBrushRepository::getBrushById(int64_t brushId, BrushRecord &outBrush) {
if (!isOpen()) {
return setError("SQLite database is not open.");
}
sqlite3_stmt* stmt = nullptr;
if (!prepare(("SELECT " + wxString::FromUTF8(kBrushSelectColumns) + " FROM brushes WHERE id = ?;").utf8_str(), &stmt)) {
return false;
}
sqlite3_bind_int64(stmt, 1, brushId);
const int rc = sqlite3_step(stmt);
if (rc != SQLITE_ROW) {
sqlite3_finalize(stmt);
return setError(wxString::Format("Brush %lld was not found in SQLite.", static_cast<long long>(brushId)));
}
ReadBrushRecordFromStatement(stmt, outBrush);
sqlite3_finalize(stmt);
return true;
}
bool BrushDatabaseBrushRepository::listBrushesByType(const wxString &type, std::vector<BrushRecord> &outBrushes) {
outBrushes.clear();
if (!isOpen()) {
return setError("SQLite database is not open.");
}
sqlite3_stmt* stmt = nullptr;
if (!prepare(("SELECT " + wxString::FromUTF8(kBrushSelectColumns) + " FROM brushes WHERE type = ? ORDER BY name ASC, id ASC;").utf8_str(), &stmt)) {
return false;
}
sqlite3_bind_text(stmt, 1, type.utf8_str(), -1, SQLITE_TRANSIENT);
for (;;) {
const int rc = sqlite3_step(stmt);
if (rc == SQLITE_DONE) {
break;
}
if (rc != SQLITE_ROW) {
sqlite3_finalize(stmt);
return setErrorFromDatabase("Failed to list brushes by type");
}
BrushRecord brush;
ReadBrushRecordFromStatement(stmt, brush);
outBrushes.push_back(brush);
}
sqlite3_finalize(stmt);
return true;
}
bool BrushDatabaseBrushRepository::findBrushByNameAndType(const wxString &name, const wxString &type, BrushRecord &outBrush) {
if (!isOpen()) {
return setError("SQLite database is not open.");
}
sqlite3_stmt* stmt = nullptr;
if (!prepare(("SELECT " + wxString::FromUTF8(kBrushSelectColumns) + " FROM brushes WHERE name = ? AND type = ? LIMIT 1;").utf8_str(), &stmt)) {
return false;
}
sqlite3_bind_text(stmt, 1, name.utf8_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, type.utf8_str(), -1, SQLITE_TRANSIENT);
const int rc = sqlite3_step(stmt);
if (rc == SQLITE_DONE) {
sqlite3_finalize(stmt);
return false;
}
if (rc != SQLITE_ROW) {
sqlite3_finalize(stmt);
setErrorFromDatabase("Failed to query brush by name and type");
return false;
}
ReadBrushRecordFromStatement(stmt, outBrush);
sqlite3_finalize(stmt);
return true;
}
bool BrushDatabaseBrushRepository::getCompleteBrushById(int64_t brushId, BrushStorageRecord &outBrush) {
outBrush = BrushStorageRecord();
if (!getBrushById(brushId, outBrush.brush)) {
return false;
}
if (!getBrushItems(brushId, outBrush.items)) {
return false;
}
if (!getGroundBrushBorders(brushId, outBrush.borders)) {
return false;
}
if (!getBrushLinks(brushId, outBrush.links)) {
return false;
}
if (!getWallParts(brushId, outBrush.wallParts)) {
return false;
}
if (!getCarpetNodes(brushId, outBrush.carpetNodes)) {
return false;
}
if (!getTableNodes(brushId, outBrush.tableNodes)) {
return false;
}
return getDoodadAlternatives(brushId, outBrush.doodadAlternatives);
}
bool BrushDatabaseBrushRepository::updateBrush(const BrushRecord &brush) {
if (!isOpen()) {
return setError("SQLite database is not open.");
}
sqlite3_stmt* stmt = nullptr;
if (!prepare("UPDATE brushes "
"SET name = ?, type = ?, look_id = ?, z_order = ?, source_file = ?, server_look_id = ?, "
"draggable = ?, on_blocking = ?, on_duplicate = ?, redo_borders = ?, randomize = ?, "
"one_size = ?, solo_optional = ?, thickness = ?, thickness_ceiling = ?, updated_at = CURRENT_TIMESTAMP "
"WHERE id = ?;",
&stmt)) {
return false;
}
sqlite3_bind_int64(stmt, BindBrushRecordFields(stmt, brush, 1), brush.id);
const int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
return setErrorFromDatabase("Failed to update brush");
}
return sqlite3_changes(connection_) > 0 || setError(wxString::Format("Brush %lld was not updated.", static_cast<long long>(brush.id)));
}
bool BrushDatabaseBrushRepository::deleteBrush(int64_t brushId) {
if (!isOpen()) {
return setError("SQLite database is not open.");
}
sqlite3_stmt* stmt = nullptr;
if (!prepare("DELETE FROM brushes WHERE id = ?;", &stmt)) {
return false;
}
sqlite3_bind_int64(stmt, 1, brushId);
const int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
return setErrorFromDatabase("Failed to delete brush");
}
return true;
}
bool BrushDatabaseBrushRepository::deleteBrushesByType(const wxString &type) {
if (!isOpen()) {
return setError("SQLite database is not open.");
}
sqlite3_stmt* stmt = nullptr;
if (!prepare("DELETE FROM brushes WHERE type = ?;", &stmt)) {
return false;
}
sqlite3_bind_text(stmt, 1, type.utf8_str(), -1, SQLITE_TRANSIENT);
const int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
return setErrorFromDatabase("Failed to delete brushes by type");
}
return true;
}
bool BrushDatabaseBrushRepository::replaceBrushItems(int64_t brushId, const std::vector<BrushItemRecord> &items) {
if (!isOpen()) {
return setError("SQLite database is not open.");
}
if (!beginTransaction()) {
return false;
}
sqlite3_stmt* deleteStmt = nullptr;
if (!prepare("DELETE FROM brush_items WHERE brush_id = ?;", &deleteStmt)) {
rollbackTransaction();
return false;
}
sqlite3_bind_int64(deleteStmt, 1, brushId);
int rc = sqlite3_step(deleteStmt);
sqlite3_finalize(deleteStmt);
if (rc != SQLITE_DONE) {
rollbackTransaction();
return setErrorFromDatabase("Failed to clear brush items");
}
sqlite3_stmt* insertStmt = nullptr;
if (!prepare("INSERT INTO brush_items(brush_id, item_id, chance, sort_order) VALUES (?, ?, ?, ?);", &insertStmt)) {
rollbackTransaction();
return false;
}
int sortOrder = 0;
for (const BrushItemRecord &item : items) {
sqlite3_reset(insertStmt);
sqlite3_clear_bindings(insertStmt);
sqlite3_bind_int64(insertStmt, 1, brushId);
sqlite3_bind_int(insertStmt, 2, item.itemId);
sqlite3_bind_int(insertStmt, 3, item.chance);
sqlite3_bind_int(insertStmt, 4, item.sortOrder != 0 ? item.sortOrder : sortOrder);
rc = sqlite3_step(insertStmt);
if (rc != SQLITE_DONE) {
sqlite3_finalize(insertStmt);
rollbackTransaction();
return setErrorFromDatabase("Failed to insert brush item");
}
++sortOrder;
}
sqlite3_finalize(insertStmt);
if (!commitTransaction()) {
rollbackTransaction();
return false;
}
return true;
}
bool BrushDatabaseBrushRepository::getBrushItems(int64_t brushId, std::vector<BrushItemRecord> &outItems) {
outItems.clear();
if (!isOpen()) {
return setError("SQLite database is not open.");
}
sqlite3_stmt* stmt = nullptr;
if (!prepare("SELECT brush_id, item_id, chance, sort_order "
"FROM brush_items WHERE brush_id = ? ORDER BY sort_order ASC, id ASC;",
&stmt)) {
return false;
}
sqlite3_bind_int64(stmt, 1, brushId);
for (;;) {
const int rc = sqlite3_step(stmt);
if (rc == SQLITE_DONE) {
break;
}
if (rc != SQLITE_ROW) {
sqlite3_finalize(stmt);
return setErrorFromDatabase("Failed to read brush items");
}
BrushItemRecord item;
item.brushId = sqlite3_column_int64(stmt, 0);
item.itemId = sqlite3_column_int(stmt, 1);
item.chance = sqlite3_column_int(stmt, 2);
item.sortOrder = sqlite3_column_int(stmt, 3);
outItems.push_back(item);
}
sqlite3_finalize(stmt);
return true;
}
bool BrushDatabaseBrushRepository::upsertBorderSet(const BorderSetRecord &borderSet, int64_t &borderSetId) {
if (!isOpen()) {
return setError("SQLite database is not open.");
}
if (borderSet.xmlBorderId > 0) {
BorderSetRecord existing;
if (findBorderSetByXmlBorderId(borderSet.xmlBorderId, existing)) {
sqlite3_stmt* updateStmt = nullptr;
if (!prepare("UPDATE border_sets SET owner_brush_id = ?, border_scope = ?, border_type = ?, "
"border_group = ?, ground_equivalent = ?, source_file = ?, updated_at = CURRENT_TIMESTAMP "
"WHERE id = ?;",
&updateStmt)) {
return false;
}
BindNullableInt64(updateStmt, 1, borderSet.ownerBrushId);
sqlite3_bind_text(updateStmt, 2, borderSet.borderScope.utf8_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(updateStmt, 3, borderSet.borderType.utf8_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(updateStmt, 4, borderSet.borderGroup);
sqlite3_bind_int(updateStmt, 5, borderSet.groundEquivalent);
sqlite3_bind_text(updateStmt, 6, borderSet.sourceFile.utf8_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(updateStmt, 7, existing.id);
const int rc = sqlite3_step(updateStmt);
sqlite3_finalize(updateStmt);
if (rc != SQLITE_DONE) {
return setErrorFromDatabase("Failed to update border set");
}
borderSetId = existing.id;
return true;
}
}
sqlite3_stmt* stmt = nullptr;
if (!prepare("INSERT INTO border_sets(xml_border_id, owner_brush_id, border_scope, border_type, border_group, ground_equivalent, source_file, updated_at) "
"VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP);",
&stmt)) {
return false;
}
if (borderSet.xmlBorderId > 0) {
sqlite3_bind_int(stmt, 1, borderSet.xmlBorderId);
} else {
sqlite3_bind_null(stmt, 1);
}
BindNullableInt64(stmt, 2, borderSet.ownerBrushId);
sqlite3_bind_text(stmt, 3, borderSet.borderScope.utf8_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 4, borderSet.borderType.utf8_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(stmt, 5, borderSet.borderGroup);
sqlite3_bind_int(stmt, 6, borderSet.groundEquivalent);
sqlite3_bind_text(stmt, 7, borderSet.sourceFile.utf8_str(), -1, SQLITE_TRANSIENT);
const int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
return setErrorFromDatabase("Failed to insert border set");
}
borderSetId = sqlite3_last_insert_rowid(connection_);
return true;
}
bool BrushDatabaseBrushRepository::findBorderSetByXmlBorderId(int xmlBorderId, BorderSetRecord &outBorderSet) {
if (!isOpen()) {
return setError("SQLite database is not open.");
}
sqlite3_stmt* stmt = nullptr;
if (!prepare("SELECT id, xml_border_id, owner_brush_id, border_scope, border_type, border_group, ground_equivalent, source_file "
"FROM border_sets WHERE xml_border_id = ? LIMIT 1;",
&stmt)) {
return false;
}
sqlite3_bind_int(stmt, 1, xmlBorderId);
const int rc = sqlite3_step(stmt);
if (rc == SQLITE_DONE) {
sqlite3_finalize(stmt);
return false;
}
if (rc != SQLITE_ROW) {
sqlite3_finalize(stmt);
return setErrorFromDatabase("Failed to query border set by xml id");
}
outBorderSet.id = sqlite3_column_int64(stmt, 0);
outBorderSet.xmlBorderId = sqlite3_column_int(stmt, 1);
outBorderSet.ownerBrushId = ReadNullableInt64(stmt, 2);
outBorderSet.borderScope = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3)));
outBorderSet.borderType = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 4)));
outBorderSet.borderGroup = sqlite3_column_int(stmt, 5);
outBorderSet.groundEquivalent = sqlite3_column_int(stmt, 6);
outBorderSet.sourceFile = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 7)));
sqlite3_finalize(stmt);
return true;
}
bool BrushDatabaseBrushRepository::replaceBorderSetItems(int64_t borderSetId, const std::vector<BorderSetItemRecord> &items) {
if (!isOpen()) {
return setError("SQLite database is not open.");
}
if (!beginTransaction()) {
return false;
}
sqlite3_stmt* deleteStmt = nullptr;
if (!prepare("DELETE FROM border_set_items WHERE border_set_id = ?;", &deleteStmt)) {
rollbackTransaction();
return false;
}
sqlite3_bind_int64(deleteStmt, 1, borderSetId);
int rc = sqlite3_step(deleteStmt);
sqlite3_finalize(deleteStmt);
if (rc != SQLITE_DONE) {
rollbackTransaction();
return setErrorFromDatabase("Failed to clear border set items");
}
sqlite3_stmt* insertStmt = nullptr;
if (!prepare("INSERT INTO border_set_items(border_set_id, edge, item_id, sort_order) VALUES (?, ?, ?, ?);", &insertStmt)) {
rollbackTransaction();
return false;
}
for (const BorderSetItemRecord &item : items) {
sqlite3_reset(insertStmt);
sqlite3_clear_bindings(insertStmt);
sqlite3_bind_int64(insertStmt, 1, borderSetId);
sqlite3_bind_text(insertStmt, 2, item.edge.utf8_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(insertStmt, 3, item.itemId);
sqlite3_bind_int(insertStmt, 4, item.sortOrder);
rc = sqlite3_step(insertStmt);
if (rc != SQLITE_DONE) {
sqlite3_finalize(insertStmt);
rollbackTransaction();
return setErrorFromDatabase("Failed to insert border set item");
}
}
sqlite3_finalize(insertStmt);
if (!commitTransaction()) {
rollbackTransaction();
return false;
}
return true;
}
bool BrushDatabaseBrushRepository::deleteBorderSetsByScope(const wxString &borderScope) {
if (!isOpen()) {
return setError("SQLite database is not open.");
}
sqlite3_stmt* stmt = nullptr;
if (!prepare("DELETE FROM border_sets WHERE border_scope = ?;", &stmt)) {
return false;
}
sqlite3_bind_text(stmt, 1, borderScope.utf8_str(), -1, SQLITE_TRANSIENT);
const int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
return setErrorFromDatabase("Failed to delete border sets by scope");
}
return true;
}
bool BrushDatabaseBrushRepository::deleteOwnedBorderSetsForBrush(int64_t brushId) {
if (!isOpen()) {
return setError("SQLite database is not open.");
}
sqlite3_stmt* stmt = nullptr;
if (!prepare("DELETE FROM border_sets WHERE owner_brush_id = ?;", &stmt)) {
return false;
}
sqlite3_bind_int64(stmt, 1, brushId);
const int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
return setErrorFromDatabase("Failed to delete owned border sets for brush");
}
return true;
}
bool BrushDatabaseBrushRepository::replaceGroundBrushBorders(int64_t brushId, const std::vector<GroundBrushBorderRecord> &borders) {
if (!isOpen()) {
return setError("SQLite database is not open.");
}
if (!beginTransaction()) {
return false;
}
sqlite3_stmt* deleteStmt = nullptr;
if (!prepare("DELETE FROM ground_brush_borders WHERE brush_id = ?;", &deleteStmt)) {
rollbackTransaction();
return false;
}
sqlite3_bind_int64(deleteStmt, 1, brushId);
int rc = sqlite3_step(deleteStmt);
sqlite3_finalize(deleteStmt);
if (rc != SQLITE_DONE) {
rollbackTransaction();
return setErrorFromDatabase("Failed to clear ground brush borders");
}
sqlite3_stmt* insertBorderStmt = nullptr;
if (!prepare("INSERT INTO ground_brush_borders("
"brush_id, border_set_id, border_role, align, target_mode, target_brush_id, target_brush_name, super_border, sort_order"
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);",
&insertBorderStmt)) {
rollbackTransaction();
return false;
}
sqlite3_stmt* insertCaseStmt = nullptr;
if (!prepare("INSERT INTO ground_border_cases(ground_brush_border_id, sort_order) VALUES (?, ?);", &insertCaseStmt)) {
sqlite3_finalize(insertBorderStmt);
rollbackTransaction();
return false;
}
sqlite3_stmt* insertConditionStmt = nullptr;
if (!prepare("INSERT INTO ground_border_case_conditions(ground_border_case_id, condition_type, match_value, edge, sort_order) "
"VALUES (?, ?, ?, ?, ?);",
&insertConditionStmt)) {
sqlite3_finalize(insertBorderStmt);
sqlite3_finalize(insertCaseStmt);
rollbackTransaction();
return false;
}
sqlite3_stmt* insertActionStmt = nullptr;
if (!prepare("INSERT INTO ground_border_case_actions(ground_border_case_id, action_type, target_value, edge, replacement_value, sort_order) "
"VALUES (?, ?, ?, ?, ?, ?);",
&insertActionStmt)) {
sqlite3_finalize(insertBorderStmt);
sqlite3_finalize(insertCaseStmt);
sqlite3_finalize(insertConditionStmt);
rollbackTransaction();
return false;
}
const auto fail = [this, &insertBorderStmt, &insertCaseStmt, &insertConditionStmt, &insertActionStmt](const wxString &message) {
FinalizeStatements({ insertBorderStmt, insertCaseStmt, insertConditionStmt, insertActionStmt });
rollbackTransaction();
return setErrorFromDatabase(message);
};
const auto setDbError = [this](const wxString &message) {
return setErrorFromDatabase(message);
};
for (const GroundBrushBorderRecord &border : borders) {
sqlite3_reset(insertBorderStmt);
sqlite3_clear_bindings(insertBorderStmt);
sqlite3_bind_int64(insertBorderStmt, 1, brushId);
sqlite3_bind_int64(insertBorderStmt, 2, border.borderSetId);
sqlite3_bind_text(insertBorderStmt, 3, border.borderRole.utf8_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(insertBorderStmt, 4, border.align.utf8_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(insertBorderStmt, 5, border.targetMode.utf8_str(), -1, SQLITE_TRANSIENT);
BindNullableInt64(insertBorderStmt, 6, border.targetBrushId);
sqlite3_bind_text(insertBorderStmt, 7, border.targetBrushName.utf8_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(insertBorderStmt, 8, border.superBorder ? 1 : 0);
sqlite3_bind_int(insertBorderStmt, 9, border.sortOrder);
if (sqlite3_step(insertBorderStmt) != SQLITE_DONE) {
return fail("Failed to insert ground brush border");
}
const int64_t groundBrushBorderId = sqlite3_last_insert_rowid(connection_);
if (!WriteGroundBorderCases(
connection_,
groundBrushBorderId,
border.cases,
insertCaseStmt,
insertConditionStmt,
insertActionStmt,
setDbError
)) {
return fail(lastError_);
}
}
FinalizeStatements({ insertBorderStmt, insertCaseStmt, insertConditionStmt, insertActionStmt });
if (!commitTransaction()) {
rollbackTransaction();
return false;
}
return true;
}
bool BrushDatabaseBrushRepository::getGroundBrushBorders(int64_t brushId, std::vector<GroundBrushBorderRecord> &outBorders) {
outBorders.clear();
if (!isOpen()) {
return setError("SQLite database is not open.");
}
sqlite3_stmt* borderStmt = nullptr;
if (!prepare("SELECT id, border_set_id, border_role, align, target_mode, target_brush_id, target_brush_name, super_border, sort_order "
"FROM ground_brush_borders WHERE brush_id = ? ORDER BY sort_order ASC, id ASC;",
&borderStmt)) {
return false;
}
sqlite3_stmt* caseStmt = nullptr;
if (!prepare("SELECT id, sort_order FROM ground_border_cases "
"WHERE ground_brush_border_id = ? ORDER BY sort_order ASC, id ASC;",
&caseStmt)) {
sqlite3_finalize(borderStmt);
return false;
}
sqlite3_stmt* conditionStmt = nullptr;
if (!prepare("SELECT condition_type, match_value, edge, sort_order "
"FROM ground_border_case_conditions WHERE ground_border_case_id = ? "
"ORDER BY sort_order ASC, id ASC;",
&conditionStmt)) {
sqlite3_finalize(borderStmt);
sqlite3_finalize(caseStmt);
return false;
}
sqlite3_stmt* actionStmt = nullptr;
if (!prepare("SELECT action_type, target_value, edge, replacement_value, sort_order "
"FROM ground_border_case_actions WHERE ground_border_case_id = ? "
"ORDER BY sort_order ASC, id ASC;",
&actionStmt)) {
sqlite3_finalize(borderStmt);
sqlite3_finalize(caseStmt);
sqlite3_finalize(conditionStmt);
return false;
}
sqlite3_bind_int64(borderStmt, 1, brushId);
const auto fail = [this, &borderStmt, &caseStmt, &conditionStmt, &actionStmt](const wxString &message) {
FinalizeStatements({ borderStmt, caseStmt, conditionStmt, actionStmt });
return setErrorFromDatabase(message);
};
const auto setDbError = [this](const wxString &message) {
return setErrorFromDatabase(message);
};
for (;;) {
const int borderRc = sqlite3_step(borderStmt);
if (borderRc == SQLITE_DONE) {
break;
}
if (borderRc != SQLITE_ROW) {
return fail("Failed to read ground brush borders");
}
const int64_t groundBrushBorderId = sqlite3_column_int64(borderStmt, 0);
GroundBrushBorderRecord border;
border.borderSetId = sqlite3_column_int64(borderStmt, 1);
border.borderRole = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(borderStmt, 2)));
border.align = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(borderStmt, 3)));
border.targetMode = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(borderStmt, 4)));
border.targetBrushId = ReadNullableInt64(borderStmt, 5);
border.targetBrushName = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(borderStmt, 6)));
border.superBorder = sqlite3_column_int(borderStmt, 7) != 0;
border.sortOrder = sqlite3_column_int(borderStmt, 8);
sqlite3_reset(caseStmt);
sqlite3_clear_bindings(caseStmt);
sqlite3_bind_int64(caseStmt, 1, groundBrushBorderId);
if (!ReadGroundBorderCases(caseStmt, conditionStmt, actionStmt, border.cases, setDbError)) {
return fail(lastError_);
}
outBorders.push_back(border);
}
FinalizeStatements({ borderStmt, caseStmt, conditionStmt, actionStmt });
return true;
}
bool BrushDatabaseBrushRepository::replaceBrushLinks(int64_t brushId, const std::vector<BrushLinkRecord> &links) {
if (!isOpen()) {
return setError("SQLite database is not open.");
}
if (!beginTransaction()) {
return false;
}
sqlite3_stmt* deleteStmt = nullptr;
if (!prepare("DELETE FROM brush_links WHERE brush_id = ?;", &deleteStmt)) {
rollbackTransaction();
return false;
}
sqlite3_bind_int64(deleteStmt, 1, brushId);
int rc = sqlite3_step(deleteStmt);
sqlite3_finalize(deleteStmt);
if (rc != SQLITE_DONE) {
rollbackTransaction();
return setErrorFromDatabase("Failed to clear brush links");
}
sqlite3_stmt* insertStmt = nullptr;
if (!prepare("INSERT INTO brush_links(brush_id, target_brush_id, target_brush_name, relation_type, sort_order) VALUES (?, ?, ?, ?, ?);", &insertStmt)) {
rollbackTransaction();
return false;
}
for (const BrushLinkRecord &link : links) {
sqlite3_reset(insertStmt);
sqlite3_clear_bindings(insertStmt);
sqlite3_bind_int64(insertStmt, 1, brushId);
BindNullableInt64(insertStmt, 2, link.targetBrushId);
sqlite3_bind_text(insertStmt, 3, link.targetBrushName.utf8_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(insertStmt, 4, link.relationType.utf8_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(insertStmt, 5, link.sortOrder);
rc = sqlite3_step(insertStmt);
if (rc != SQLITE_DONE) {
sqlite3_finalize(insertStmt);
rollbackTransaction();
return setErrorFromDatabase("Failed to insert brush link");
}
}
sqlite3_finalize(insertStmt);
if (!commitTransaction()) {
rollbackTransaction();
return false;
}
return true;
}
bool BrushDatabaseBrushRepository::getBrushLinks(int64_t brushId, std::vector<BrushLinkRecord> &outLinks) {
outLinks.clear();
if (!isOpen()) {
return setError("SQLite database is not open.");
}
sqlite3_stmt* stmt = nullptr;
if (!prepare("SELECT brush_id, target_brush_id, target_brush_name, relation_type, sort_order "
"FROM brush_links WHERE brush_id = ? ORDER BY sort_order ASC, id ASC;",
&stmt)) {
return false;
}
sqlite3_bind_int64(stmt, 1, brushId);
for (;;) {
const int rc = sqlite3_step(stmt);
if (rc == SQLITE_DONE) {
break;
}
if (rc != SQLITE_ROW) {
sqlite3_finalize(stmt);
return setErrorFromDatabase("Failed to read brush links");
}
BrushLinkRecord link;
link.brushId = sqlite3_column_int64(stmt, 0);
link.targetBrushId = ReadNullableInt64(stmt, 1);
link.targetBrushName = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2)));
link.relationType = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3)));
link.sortOrder = sqlite3_column_int(stmt, 4);
outLinks.push_back(link);
}
sqlite3_finalize(stmt);
return true;
}
bool BrushDatabaseBrushRepository::replaceWallParts(int64_t brushId, const std::vector<WallPartRecord> &parts) {
if (!isOpen()) {
return setError("SQLite database is not open.");
}
if (!beginTransaction()) {
return false;
}
sqlite3_stmt* deleteStmt = nullptr;
if (!prepare("DELETE FROM wall_parts WHERE brush_id = ?;", &deleteStmt)) {
rollbackTransaction();
return false;
}
sqlite3_bind_int64(deleteStmt, 1, brushId);
int rc = sqlite3_step(deleteStmt);
sqlite3_finalize(deleteStmt);
if (rc != SQLITE_DONE) {
rollbackTransaction();
return setErrorFromDatabase("Failed to clear wall parts");
}
sqlite3_stmt* insertPartStmt = nullptr;
if (!prepare("INSERT INTO wall_parts(brush_id, part_type, sort_order) VALUES (?, ?, ?);", &insertPartStmt)) {
rollbackTransaction();
return false;
}
sqlite3_stmt* insertItemStmt = nullptr;
if (!prepare("INSERT INTO wall_part_items(wall_part_id, item_id, chance, sort_order) VALUES (?, ?, ?, ?);", &insertItemStmt)) {
sqlite3_finalize(insertPartStmt);
rollbackTransaction();
return false;
}
sqlite3_stmt* insertDoorStmt = nullptr;
if (!prepare("INSERT INTO wall_part_doors(wall_part_id, item_id, door_type, is_open, wall_hate_me, sort_order) "
"VALUES (?, ?, ?, ?, ?, ?);",
&insertDoorStmt)) {
sqlite3_finalize(insertPartStmt);
sqlite3_finalize(insertItemStmt);
rollbackTransaction();
return false;
}
const auto failWithDatabaseError = [this, &insertPartStmt, &insertItemStmt, &insertDoorStmt](const wxString &message) {
FinalizeStatements({ insertPartStmt, insertItemStmt, insertDoorStmt });
rollbackTransaction();
return setErrorFromDatabase(message);
};
const auto fail = [this, &insertPartStmt, &insertItemStmt, &insertDoorStmt]() {
FinalizeStatements({ insertPartStmt, insertItemStmt, insertDoorStmt });
rollbackTransaction();
return false;
};
const auto setDbError = [this](const wxString &message) {
return setErrorFromDatabase(message);
};
for (const WallPartRecord &part : parts) {
sqlite3_reset(insertPartStmt);
sqlite3_clear_bindings(insertPartStmt);
sqlite3_bind_int64(insertPartStmt, 1, brushId);
sqlite3_bind_text(insertPartStmt, 2, part.partType.utf8_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(insertPartStmt, 3, part.sortOrder);
if (sqlite3_step(insertPartStmt) != SQLITE_DONE) {
return failWithDatabaseError("Failed to insert wall part");
}
const int64_t wallPartId = sqlite3_last_insert_rowid(connection_);
if (!WriteWallPartItems(wallPartId, part.items, insertItemStmt, setDbError)) {
return fail();
}
if (!WriteWallPartDoors(wallPartId, part.doors, insertDoorStmt, setDbError)) {
return fail();
}
}
FinalizeStatements({ insertPartStmt, insertItemStmt, insertDoorStmt });
if (!commitTransaction()) {
rollbackTransaction();
return false;
}
return true;
}
bool BrushDatabaseBrushRepository::getWallParts(int64_t brushId, std::vector<WallPartRecord> &outParts) {
outParts.clear();
if (!isOpen()) {
return setError("SQLite database is not open.");
}
sqlite3_stmt* partStmt = nullptr;
if (!prepare("SELECT id, part_type, sort_order FROM wall_parts "
"WHERE brush_id = ? ORDER BY sort_order ASC, id ASC;",
&partStmt)) {
return false;
}
sqlite3_stmt* itemStmt = nullptr;
if (!prepare("SELECT item_id, chance, sort_order FROM wall_part_items "
"WHERE wall_part_id = ? ORDER BY sort_order ASC, id ASC;",
&itemStmt)) {
sqlite3_finalize(partStmt);
return false;
}
sqlite3_stmt* doorStmt = nullptr;
if (!prepare("SELECT item_id, door_type, is_open, wall_hate_me, sort_order FROM wall_part_doors "
"WHERE wall_part_id = ? ORDER BY sort_order ASC, id ASC;",
&doorStmt)) {
sqlite3_finalize(partStmt);
sqlite3_finalize(itemStmt);
return false;
}
sqlite3_bind_int64(partStmt, 1, brushId);
for (;;) {
const int partRc = sqlite3_step(partStmt);
if (partRc == SQLITE_DONE) {
break;
}
if (partRc != SQLITE_ROW) {
sqlite3_finalize(partStmt);
sqlite3_finalize(itemStmt);
sqlite3_finalize(doorStmt);
return setErrorFromDatabase("Failed to read wall parts");
}
const int64_t wallPartId = sqlite3_column_int64(partStmt, 0);
WallPartRecord part;
part.partType = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(partStmt, 1)));
part.sortOrder = sqlite3_column_int(partStmt, 2);
sqlite3_reset(itemStmt);
sqlite3_clear_bindings(itemStmt);
sqlite3_bind_int64(itemStmt, 1, wallPartId);
for (;;) {
const int itemRc = sqlite3_step(itemStmt);
if (itemRc == SQLITE_DONE) {
break;
}
if (itemRc != SQLITE_ROW) {
sqlite3_finalize(partStmt);
sqlite3_finalize(itemStmt);
sqlite3_finalize(doorStmt);
return setErrorFromDatabase("Failed to read wall part items");
}
WallPartItemRecord item;
item.itemId = sqlite3_column_int(itemStmt, 0);
item.chance = sqlite3_column_int(itemStmt, 1);
item.sortOrder = sqlite3_column_int(itemStmt, 2);
part.items.push_back(item);
}
sqlite3_reset(doorStmt);
sqlite3_clear_bindings(doorStmt);
sqlite3_bind_int64(doorStmt, 1, wallPartId);
for (;;) {
const int doorRc = sqlite3_step(doorStmt);
if (doorRc == SQLITE_DONE) {
break;
}
if (doorRc != SQLITE_ROW) {
sqlite3_finalize(partStmt);
sqlite3_finalize(itemStmt);
sqlite3_finalize(doorStmt);
return setErrorFromDatabase("Failed to read wall part doors");
}
WallPartDoorRecord door;
door.itemId = sqlite3_column_int(doorStmt, 0);
door.doorType = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(doorStmt, 1)));
door.isOpen = sqlite3_column_int(doorStmt, 2) != 0;
door.wallHateMe = sqlite3_column_int(doorStmt, 3) != 0;
door.sortOrder = sqlite3_column_int(doorStmt, 4);
part.doors.push_back(door);
}
outParts.push_back(part);
}
sqlite3_finalize(partStmt);
sqlite3_finalize(itemStmt);
sqlite3_finalize(doorStmt);
return true;
}
bool BrushDatabaseBrushRepository::replaceCarpetNodes(int64_t brushId, const std::vector<CarpetNodeRecord> &nodes) {
if (!isOpen()) {
return setError("SQLite database is not open.");
}
if (!beginTransaction()) {
return false;
}
sqlite3_stmt* deleteStmt = nullptr;
if (!prepare("DELETE FROM carpet_nodes WHERE brush_id = ?;", &deleteStmt)) {
rollbackTransaction();
return false;
}
sqlite3_bind_int64(deleteStmt, 1, brushId);
int rc = sqlite3_step(deleteStmt);
sqlite3_finalize(deleteStmt);
if (rc != SQLITE_DONE) {
rollbackTransaction();
return setErrorFromDatabase("Failed to clear carpet nodes");
}
sqlite3_stmt* insertNodeStmt = nullptr;
if (!prepare("INSERT INTO carpet_nodes(brush_id, align, sort_order) VALUES (?, ?, ?);", &insertNodeStmt)) {
rollbackTransaction();
return false;
}
sqlite3_stmt* insertItemStmt = nullptr;
if (!prepare("INSERT INTO carpet_node_items(carpet_node_id, item_id, chance, sort_order) VALUES (?, ?, ?, ?);", &insertItemStmt)) {
sqlite3_finalize(insertNodeStmt);
rollbackTransaction();
return false;
}
if (!WriteAlignedNodesWithItems<CarpetNodeRecord, CarpetNodeItemRecord>(
connection_,
brushId,
nodes,
{ insertNodeStmt, insertItemStmt, "Failed to insert carpet node", "Failed to insert carpet node item" },
[this](const wxString &message) { return setErrorFromDatabase(message); }
)) {
FinalizeStatements({ insertNodeStmt, insertItemStmt });
rollbackTransaction();
return false;
}
FinalizeStatements({ insertNodeStmt, insertItemStmt });
if (!commitTransaction()) {
rollbackTransaction();
return false;
}
return true;
}
bool BrushDatabaseBrushRepository::getCarpetNodes(int64_t brushId, std::vector<CarpetNodeRecord> &outNodes) {
outNodes.clear();
if (!isOpen()) {
return setError("SQLite database is not open.");
}
sqlite3_stmt* nodeStmt = nullptr;
if (!prepare("SELECT id, align, sort_order FROM carpet_nodes "
"WHERE brush_id = ? ORDER BY sort_order ASC, id ASC;",
&nodeStmt)) {
return false;
}
sqlite3_stmt* itemStmt = nullptr;
if (!prepare("SELECT item_id, chance, sort_order FROM carpet_node_items "
"WHERE carpet_node_id = ? ORDER BY sort_order ASC, id ASC;",
&itemStmt)) {
sqlite3_finalize(nodeStmt);
return false;
}
sqlite3_bind_int64(nodeStmt, 1, brushId);
if (!ReadAlignedNodesWithItems<CarpetNodeRecord, CarpetNodeItemRecord>(
nodeStmt,
itemStmt,
outNodes,
"Failed to read carpet nodes",
"Failed to read carpet node items",
[this](const wxString &message) { return setErrorFromDatabase(message); }
)) {
FinalizeStatements({ nodeStmt, itemStmt });
return false;
}
FinalizeStatements({ nodeStmt, itemStmt });
return true;
}
bool BrushDatabaseBrushRepository::replaceTableNodes(int64_t brushId, const std::vector<TableNodeRecord> &nodes) {
if (!isOpen()) {
return setError("SQLite database is not open.");
}
if (!beginTransaction()) {
return false;
}
sqlite3_stmt* deleteStmt = nullptr;
if (!prepare("DELETE FROM table_nodes WHERE brush_id = ?;", &deleteStmt)) {
rollbackTransaction();
return false;
}
sqlite3_bind_int64(deleteStmt, 1, brushId);
int rc = sqlite3_step(deleteStmt);
sqlite3_finalize(deleteStmt);
if (rc != SQLITE_DONE) {
rollbackTransaction();
return setErrorFromDatabase("Failed to clear table nodes");
}
sqlite3_stmt* insertNodeStmt = nullptr;
if (!prepare("INSERT INTO table_nodes(brush_id, align, sort_order) VALUES (?, ?, ?);", &insertNodeStmt)) {
rollbackTransaction();
return false;
}
sqlite3_stmt* insertItemStmt = nullptr;
if (!prepare("INSERT INTO table_node_items(table_node_id, item_id, chance, sort_order) VALUES (?, ?, ?, ?);", &insertItemStmt)) {
sqlite3_finalize(insertNodeStmt);
rollbackTransaction();
return false;
}
if (!WriteAlignedNodesWithItems<TableNodeRecord, TableNodeItemRecord>(
connection_,
brushId,
nodes,
{ insertNodeStmt, insertItemStmt, "Failed to insert table node", "Failed to insert table node item" },
[this](const wxString &message) { return setErrorFromDatabase(message); }
)) {
FinalizeStatements({ insertNodeStmt, insertItemStmt });
rollbackTransaction();
return false;
}
FinalizeStatements({ insertNodeStmt, insertItemStmt });
if (!commitTransaction()) {
rollbackTransaction();
return false;
}
return true;
}
bool BrushDatabaseBrushRepository::getTableNodes(int64_t brushId, std::vector<TableNodeRecord> &outNodes) {
outNodes.clear();
if (!isOpen()) {
return setError("SQLite database is not open.");
}
sqlite3_stmt* nodeStmt = nullptr;
if (!prepare("SELECT id, align, sort_order FROM table_nodes "
"WHERE brush_id = ? ORDER BY sort_order ASC, id ASC;",
&nodeStmt)) {
return false;
}
sqlite3_stmt* itemStmt = nullptr;
if (!prepare("SELECT item_id, chance, sort_order FROM table_node_items "
"WHERE table_node_id = ? ORDER BY sort_order ASC, id ASC;",
&itemStmt)) {
sqlite3_finalize(nodeStmt);
return false;
}
sqlite3_bind_int64(nodeStmt, 1, brushId);
if (!ReadAlignedNodesWithItems<TableNodeRecord, TableNodeItemRecord>(
nodeStmt,
itemStmt,
outNodes,
"Failed to read table nodes",
"Failed to read table node items",
[this](const wxString &message) { return setErrorFromDatabase(message); }
)) {
FinalizeStatements({ nodeStmt, itemStmt });
return false;
}
FinalizeStatements({ nodeStmt, itemStmt });
return true;
}
bool BrushDatabaseBrushRepository::replaceDoodadAlternatives(int64_t brushId, const std::vector<DoodadAlternativeRecord> &alternatives) {
if (!isOpen()) {
return setError("SQLite database is not open.");
}
if (!beginTransaction()) {
return false;
}
sqlite3_stmt* deleteStmt = nullptr;
if (!prepare("DELETE FROM doodad_alternatives WHERE brush_id = ?;", &deleteStmt)) {
rollbackTransaction();
return false;
}
sqlite3_bind_int64(deleteStmt, 1, brushId);
int rc = sqlite3_step(deleteStmt);
sqlite3_finalize(deleteStmt);
if (rc != SQLITE_DONE) {
rollbackTransaction();
return setErrorFromDatabase("Failed to clear doodad alternatives");
}
sqlite3_stmt* insertAltStmt = nullptr;
if (!prepare("INSERT INTO doodad_alternatives(brush_id, sort_order) VALUES (?, ?);", &insertAltStmt)) {
rollbackTransaction();
return false;
}
sqlite3_stmt* insertSingleStmt = nullptr;
if (!prepare("INSERT INTO doodad_single_items(doodad_alternative_id, item_id, chance, sort_order) VALUES (?, ?, ?, ?);", &insertSingleStmt)) {
sqlite3_finalize(insertAltStmt);
rollbackTransaction();
return false;
}
sqlite3_stmt* insertCompositeStmt = nullptr;
if (!prepare("INSERT INTO doodad_composites(doodad_alternative_id, chance, sort_order) VALUES (?, ?, ?);", &insertCompositeStmt)) {
sqlite3_finalize(insertAltStmt);
sqlite3_finalize(insertSingleStmt);
rollbackTransaction();
return false;
}
sqlite3_stmt* insertTileStmt = nullptr;
if (!prepare("INSERT INTO doodad_composite_tiles(doodad_composite_id, offset_x, offset_y, offset_z, sort_order) VALUES (?, ?, ?, ?, ?);", &insertTileStmt)) {
sqlite3_finalize(insertAltStmt);
sqlite3_finalize(insertSingleStmt);
sqlite3_finalize(insertCompositeStmt);
rollbackTransaction();
return false;
}
sqlite3_stmt* insertTileItemStmt = nullptr;
if (!prepare("INSERT INTO doodad_composite_tile_items(doodad_composite_tile_id, item_id, sort_order) VALUES (?, ?, ?);", &insertTileItemStmt)) {
sqlite3_finalize(insertAltStmt);
sqlite3_finalize(insertSingleStmt);
sqlite3_finalize(insertCompositeStmt);
sqlite3_finalize(insertTileStmt);
rollbackTransaction();
return false;
}
const auto fail = [this, &insertAltStmt, &insertSingleStmt, &insertCompositeStmt, &insertTileStmt, &insertTileItemStmt](const wxString &message) {
FinalizeStatements({ insertAltStmt, insertSingleStmt, insertCompositeStmt, insertTileStmt, insertTileItemStmt });
rollbackTransaction();
return setErrorFromDatabase(message);
};
const auto setDbError = [this](const wxString &message) {
return setErrorFromDatabase(message);
};
for (const DoodadAlternativeRecord &alternative : alternatives) {
sqlite3_reset(insertAltStmt);
sqlite3_clear_bindings(insertAltStmt);
sqlite3_bind_int64(insertAltStmt, 1, brushId);
sqlite3_bind_int(insertAltStmt, 2, alternative.sortOrder);
if (sqlite3_step(insertAltStmt) != SQLITE_DONE) {
return fail("Failed to insert doodad alternative");
}
const int64_t alternativeId = sqlite3_last_insert_rowid(connection_);
if (!WriteDoodadSingleItems(alternativeId, alternative.singleItems, insertSingleStmt, setDbError)) {
return fail(lastError_);
}
if (!WriteDoodadComposites(
connection_,
alternativeId,
alternative.composites,
insertCompositeStmt,
insertTileStmt,
insertTileItemStmt,
setDbError
)) {
return fail(lastError_);
}
}
FinalizeStatements({ insertAltStmt, insertSingleStmt, insertCompositeStmt, insertTileStmt, insertTileItemStmt });
if (!commitTransaction()) {
rollbackTransaction();
return false;
}
return true;
}
bool BrushDatabaseBrushRepository::getDoodadAlternatives(int64_t brushId, std::vector<DoodadAlternativeRecord> &outAlternatives) {
outAlternatives.clear();
if (!isOpen()) {
return setError("SQLite database is not open.");
}
sqlite3_stmt* altStmt = nullptr;
if (!prepare("SELECT id, sort_order FROM doodad_alternatives "
"WHERE brush_id = ? ORDER BY sort_order ASC, id ASC;",
&altStmt)) {
return false;
}
sqlite3_stmt* singleStmt = nullptr;
if (!prepare("SELECT item_id, chance, sort_order FROM doodad_single_items "
"WHERE doodad_alternative_id = ? ORDER BY sort_order ASC, id ASC;",
&singleStmt)) {
sqlite3_finalize(altStmt);
return false;
}
sqlite3_stmt* compositeStmt = nullptr;
if (!prepare("SELECT id, chance, sort_order FROM doodad_composites "
"WHERE doodad_alternative_id = ? ORDER BY sort_order ASC, id ASC;",
&compositeStmt)) {
sqlite3_finalize(altStmt);
sqlite3_finalize(singleStmt);
return false;
}
sqlite3_stmt* tileStmt = nullptr;
if (!prepare("SELECT id, offset_x, offset_y, offset_z, sort_order FROM doodad_composite_tiles "
"WHERE doodad_composite_id = ? ORDER BY sort_order ASC, id ASC;",
&tileStmt)) {
sqlite3_finalize(altStmt);
sqlite3_finalize(singleStmt);
sqlite3_finalize(compositeStmt);
return false;
}
sqlite3_stmt* tileItemStmt = nullptr;
if (!prepare("SELECT item_id, sort_order FROM doodad_composite_tile_items "
"WHERE doodad_composite_tile_id = ? ORDER BY sort_order ASC, id ASC;",
&tileItemStmt)) {
sqlite3_finalize(altStmt);
sqlite3_finalize(singleStmt);
sqlite3_finalize(compositeStmt);
sqlite3_finalize(tileStmt);
return false;
}
sqlite3_bind_int64(altStmt, 1, brushId);
const auto fail = [this, &altStmt, &singleStmt, &compositeStmt, &tileStmt, &tileItemStmt](const wxString &message) {
FinalizeStatements({ altStmt, singleStmt, compositeStmt, tileStmt, tileItemStmt });
return setErrorFromDatabase(message);
};
const auto setDbError = [this](const wxString &message) {
return setErrorFromDatabase(message);
};
for (;;) {
const int altRc = sqlite3_step(altStmt);
if (altRc == SQLITE_DONE) {
break;
}
if (altRc != SQLITE_ROW) {
return fail("Failed to read doodad alternatives");
}
const int64_t alternativeId = sqlite3_column_int64(altStmt, 0);
DoodadAlternativeRecord alternative;
alternative.sortOrder = sqlite3_column_int(altStmt, 1);
if (!ReadDoodadSingleItems(alternativeId, singleStmt, alternative.singleItems, setDbError)) {
return fail(lastError_);
}
if (!ReadDoodadComposites(
alternativeId,
compositeStmt,
tileStmt,
tileItemStmt,
alternative.composites,
setDbError
)) {
return fail(lastError_);
}
outAlternatives.push_back(alternative);
}
FinalizeStatements({ altStmt, singleStmt, compositeStmt, tileStmt, tileItemStmt });
return true;
}
bool BrushDatabaseCatalogRepository::replaceAllTilesets(const std::vector<TilesetStorageRecord> &tilesets) {
if (!isOpen()) {
return setError("SQLite database is not open.");
}
if (!beginTransaction()) {
return false;
}
if (!execute("DELETE FROM tilesets;")) {
rollbackTransaction();
return false;
}
sqlite3_stmt* insertTilesetStmt = nullptr;
if (!prepare("INSERT INTO tilesets(name, source_file) VALUES (?, ?);", &insertTilesetStmt)) {
rollbackTransaction();
return false;
}
sqlite3_stmt* insertSectionStmt = nullptr;
if (!prepare("INSERT INTO tileset_sections(tileset_id, section_type, sort_order) VALUES (?, ?, ?);", &insertSectionStmt)) {
sqlite3_finalize(insertTilesetStmt);
rollbackTransaction();
return false;
}
sqlite3_stmt* insertEntryStmt = nullptr;
if (!prepare("INSERT INTO tileset_brush_entries(tileset_section_id, entry_kind, brush_id, brush_name, item_id, from_item_id, to_item_id, after_brush_name, after_item_id, sort_order) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
&insertEntryStmt)) {
sqlite3_finalize(insertTilesetStmt);
sqlite3_finalize(insertSectionStmt);
rollbackTransaction();
return false;
}
sqlite3_stmt* findBrushStmt = nullptr;
if (!prepare("SELECT id FROM brushes WHERE name = ? LIMIT 2;", &findBrushStmt)) {
sqlite3_finalize(insertTilesetStmt);
sqlite3_finalize(insertSectionStmt);
sqlite3_finalize(insertEntryStmt);
rollbackTransaction();
return false;
}
const auto fail = [this, &insertTilesetStmt, &insertSectionStmt, &insertEntryStmt, &findBrushStmt](const wxString &message) {
FinalizeStatements({ insertTilesetStmt, insertSectionStmt, insertEntryStmt, findBrushStmt });
rollbackTransaction();
return setErrorFromDatabase(message);
};
const auto setDbError = [this](const wxString &message) {
return setErrorFromDatabase(message);
};
for (const TilesetStorageRecord &tileset : tilesets) {
sqlite3_reset(insertTilesetStmt);
sqlite3_clear_bindings(insertTilesetStmt);
sqlite3_bind_text(insertTilesetStmt, 1, tileset.name.utf8_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(insertTilesetStmt, 2, tileset.sourceFile.utf8_str(), -1, SQLITE_TRANSIENT);
if (sqlite3_step(insertTilesetStmt) != SQLITE_DONE) {
return fail("Failed to insert tileset");
}
const int64_t tilesetId = sqlite3_last_insert_rowid(connection_);
for (const TilesetSectionRecord &section : tileset.sections) {
sqlite3_reset(insertSectionStmt);
sqlite3_clear_bindings(insertSectionStmt);
sqlite3_bind_int64(insertSectionStmt, 1, tilesetId);
sqlite3_bind_text(insertSectionStmt, 2, section.sectionType.utf8_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(insertSectionStmt, 3, section.sortOrder);
if (sqlite3_step(insertSectionStmt) != SQLITE_DONE) {
return fail("Failed to insert tileset section");
}
const int64_t sectionId = sqlite3_last_insert_rowid(connection_);
if (!WriteTilesetEntries(sectionId, section.entries, insertEntryStmt, findBrushStmt, setDbError)) {
return fail(lastError_);
}
}
}
FinalizeStatements({ insertTilesetStmt, insertSectionStmt, insertEntryStmt, findBrushStmt });
if (!commitTransaction()) {
rollbackTransaction();
return false;
}
return true;
}
bool BrushDatabaseCatalogRepository::getTilesetByName(const wxString &name, TilesetStorageRecord &outTileset) {
outTileset = TilesetStorageRecord();
if (!isOpen()) {
return setError("SQLite database is not open.");
}
sqlite3_stmt* tilesetStmt = nullptr;
if (!prepare("SELECT id, name, source_file FROM tilesets WHERE name = ? LIMIT 1;", &tilesetStmt)) {
return false;
}
sqlite3_bind_text(tilesetStmt, 1, name.utf8_str(), -1, SQLITE_TRANSIENT);
const int rc = sqlite3_step(tilesetStmt);
if (rc == SQLITE_DONE) {
sqlite3_finalize(tilesetStmt);
return setError("Tileset '" + name + "' was not found in SQLite.");
}
if (rc != SQLITE_ROW) {
sqlite3_finalize(tilesetStmt);
return setErrorFromDatabase("Failed to query tileset by name");
}
const int64_t tilesetId = sqlite3_column_int64(tilesetStmt, 0);
outTileset.name = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(tilesetStmt, 1)));
outTileset.sourceFile = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(tilesetStmt, 2)));
sqlite3_finalize(tilesetStmt);
sqlite3_stmt* sectionStmt = nullptr;
if (!prepare("SELECT id, section_type, sort_order FROM tileset_sections "
"WHERE tileset_id = ? ORDER BY sort_order ASC, id ASC;",
&sectionStmt)) {
return false;
}
sqlite3_stmt* entryStmt = nullptr;
if (!prepare("SELECT entry_kind, brush_id, brush_name, item_id, from_item_id, to_item_id, after_brush_name, after_item_id, sort_order "
"FROM tileset_brush_entries WHERE tileset_section_id = ? ORDER BY sort_order ASC, id ASC;",
&entryStmt)) {
sqlite3_finalize(sectionStmt);
return false;
}
sqlite3_bind_int64(sectionStmt, 1, tilesetId);
for (;;) {
const int sectionRc = sqlite3_step(sectionStmt);
if (sectionRc == SQLITE_DONE) {
break;
}
if (sectionRc != SQLITE_ROW) {
sqlite3_finalize(sectionStmt);
sqlite3_finalize(entryStmt);
return setErrorFromDatabase("Failed to read tileset sections");
}
const int64_t sectionId = sqlite3_column_int64(sectionStmt, 0);
TilesetSectionRecord section;
section.sectionType = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(sectionStmt, 1)));
section.sortOrder = sqlite3_column_int(sectionStmt, 2);
sqlite3_reset(entryStmt);
sqlite3_clear_bindings(entryStmt);
sqlite3_bind_int64(entryStmt, 1, sectionId);
for (;;) {
const int entryRc = sqlite3_step(entryStmt);
if (entryRc == SQLITE_DONE) {
break;
}
if (entryRc != SQLITE_ROW) {
sqlite3_finalize(sectionStmt);
sqlite3_finalize(entryStmt);
return setErrorFromDatabase("Failed to read tileset entries");
}
TilesetEntryRecord entry;
entry.entryKind = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(entryStmt, 0)));
entry.brushId = ReadNullableInt64(entryStmt, 1);
entry.brushName = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(entryStmt, 2)));
entry.itemId = sqlite3_column_int(entryStmt, 3);
entry.fromItemId = sqlite3_column_int(entryStmt, 4);
entry.toItemId = sqlite3_column_int(entryStmt, 5);
entry.afterBrushName = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(entryStmt, 6)));
entry.afterItemId = sqlite3_column_int(entryStmt, 7);
entry.sortOrder = sqlite3_column_int(entryStmt, 8);
section.entries.push_back(entry);
}
outTileset.sections.push_back(section);
}
sqlite3_finalize(sectionStmt);
sqlite3_finalize(entryStmt);
return true;
}
bool BrushDatabaseCatalogRepository::getAllTilesets(std::vector<TilesetStorageRecord> &outTilesets) {
outTilesets.clear();
if (!isOpen()) {
return setError("SQLite database is not open.");
}
sqlite3_stmt* stmt = nullptr;
if (!prepare("SELECT name FROM tilesets ORDER BY name ASC, id ASC;", &stmt)) {
return false;
}
for (;;) {
const int rc = sqlite3_step(stmt);
if (rc == SQLITE_DONE) {
break;
}
if (rc != SQLITE_ROW) {
sqlite3_finalize(stmt);
return setErrorFromDatabase("Failed to list tilesets");
}
TilesetStorageRecord tileset;
if (!getTilesetByName(ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0))), tileset)) {
sqlite3_finalize(stmt);
return false;
}
outTilesets.push_back(tileset);
}
sqlite3_finalize(stmt);
return true;
}
bool BrushDatabaseCatalogRepository::generateAuditReport(MaterialsDatabaseAuditReport &outReport) {
outReport = MaterialsDatabaseAuditReport();
if (!isOpen()) {
return setError("SQLite database is not open.");
}
sqlite3_stmt* countStmt = nullptr;
if (!prepare("SELECT "
"(SELECT COUNT(*) FROM brushes), "
"(SELECT COUNT(*) FROM border_sets), "
"(SELECT COUNT(*) FROM tilesets), "
"(SELECT COUNT(*) FROM tileset_sections), "
"(SELECT COUNT(*) FROM tileset_brush_entries), "
"(SELECT COUNT(*) FROM ground_brush_borders WHERE target_mode = 'brush' AND target_brush_name <> '' AND target_brush_id IS NULL), "
"(SELECT COUNT(*) FROM brush_links WHERE target_brush_name <> '' AND target_brush_name <> 'all' AND target_brush_id IS NULL), "
"(SELECT COUNT(*) FROM tileset_brush_entries WHERE brush_name <> '' AND brush_id IS NULL);",
&countStmt)) {
return false;
}
const int rc = sqlite3_step(countStmt);
if (rc != SQLITE_ROW) {
sqlite3_finalize(countStmt);
return setErrorFromDatabase("Failed to build SQLite audit report");
}
outReport.brushCount = sqlite3_column_int(countStmt, 0);
outReport.borderSetCount = sqlite3_column_int(countStmt, 1);
outReport.tilesetCount = sqlite3_column_int(countStmt, 2);
outReport.tilesetSectionCount = sqlite3_column_int(countStmt, 3);
outReport.tilesetEntryCount = sqlite3_column_int(countStmt, 4);
outReport.unresolvedGroundTargets = sqlite3_column_int(countStmt, 5);
outReport.unresolvedBrushLinks = sqlite3_column_int(countStmt, 6);
outReport.unresolvedTilesetEntries = sqlite3_column_int(countStmt, 7);
sqlite3_finalize(countStmt);
sqlite3_stmt* groupedStmt = nullptr;
if (!prepare("SELECT type, COUNT(*) FROM brushes GROUP BY type ORDER BY type ASC;", &groupedStmt)) {
return false;
}
for (;;) {
const int groupedRc = sqlite3_step(groupedStmt);
if (groupedRc == SQLITE_DONE) {
break;
}
if (groupedRc != SQLITE_ROW) {
sqlite3_finalize(groupedStmt);
return setErrorFromDatabase("Failed to group brushes by type");
}
BrushTypeCountRecord typeCount;
typeCount.type = ToWxString(reinterpret_cast<const char*>(sqlite3_column_text(groupedStmt, 0)));
typeCount.count = sqlite3_column_int(groupedStmt, 1);
outReport.brushTypeCounts.push_back(typeCount);
}
sqlite3_finalize(groupedStmt);
return true;
}
bool BrushDatabaseCatalogRepository::hasCompleteImportForCurrentSchema(bool &outReady) {
outReady = false;
if (!isOpen()) {
return setError("SQLite database is not open.");
}
int version = 0;
if (!schemaManager_.getCurrentSchemaVersion(version)) {
return false;
}
if (version != kBrushDatabaseSchemaVersion) {
return true;
}
MaterialsDatabaseAuditReport report;
if (!generateAuditReport(report)) {
return false;
}
bool hasGround = false;
bool hasWall = false;
bool hasDoodad = false;
bool hasCarpet = false;
bool hasTable = false;
for (const BrushTypeCountRecord &typeCount : report.brushTypeCounts) {
if (typeCount.type == "ground" && typeCount.count > 0) {
hasGround = true;
} else if (typeCount.type == "wall" && typeCount.count > 0) {
hasWall = true;
} else if (typeCount.type == "doodad" && typeCount.count > 0) {
hasDoodad = true;
} else if (typeCount.type == "carpet" && typeCount.count > 0) {
hasCarpet = true;
} else if (typeCount.type == "table" && typeCount.count > 0) {
hasTable = true;
}
}
outReady = hasGround && hasWall && hasDoodad && hasCarpet && hasTable && report.borderSetCount > 0 && report.tilesetCount > 0;
return true;
}
int BrushDatabaseCatalogRepository::getExpectedSchemaVersion() const {
return kBrushDatabaseSchemaVersion;
}
bool BrushDatabaseBrushRepository::resolveGroundReferenceNames() {
if (!isOpen()) {
return setError("SQLite database is not open.");
}
if (!execute("UPDATE ground_brush_borders "
"SET target_brush_id = ("
"SELECT id FROM brushes b "
"WHERE b.name = ground_brush_borders.target_brush_name AND b.type = 'ground' "
"LIMIT 1"
") "
"WHERE target_brush_name <> '' AND target_mode = 'brush';")) {
return false;
}
return execute("UPDATE brush_links "
"SET target_brush_id = ("
"SELECT id FROM brushes b "
"WHERE b.name = brush_links.target_brush_name "
"LIMIT 1"
") "
"WHERE target_brush_name <> '' AND target_brush_name <> 'all';");
}
bool BrushDatabaseSession::execute(const wxString &sql) {
char* errorMessage = nullptr;
const int rc = sqlite3_exec(connection_, sql.utf8_str(), nullptr, nullptr, &errorMessage);
if (rc != SQLITE_OK) {
const wxString detail = errorMessage ? wxString::FromUTF8(errorMessage) : wxString();
sqlite3_free(errorMessage);
return setError("SQLite exec failed: " + detail);
}
return true;
}
bool BrushDatabaseSession::prepare(const char* sql, sqlite3_stmt** stmt) {
const int rc = sqlite3_prepare_v2(connection_, sql, -1, stmt, nullptr);
if (rc != SQLITE_OK) {
return setErrorFromDatabase("Failed to prepare SQLite statement");
}
return true;
}
bool BrushDatabaseSession::beginTransaction() {
if (readOnly_) {
return setError("SQLite transaction requires a read-write connection.");
}
if (transactionDepth_ == 0) {
if (!execute("BEGIN IMMEDIATE TRANSACTION;")) {
return false;
}
transactionDepth_ = 1;
savepointIds_.clear();
return true;
}
const int savepointId = nextSavepointId_++;
const wxString savepointName = MakeTransactionSavepointName(savepointId);
if (!execute("SAVEPOINT " + savepointName + ";")) {
return false;
}
savepointIds_.push_back(savepointId);
++transactionDepth_;
return true;
}
bool BrushDatabaseSession::commitTransaction() {
if (readOnly_) {
return setError("SQLite transaction requires a read-write connection.");
}
if (transactionDepth_ <= 0) {
return setError("SQLite commit requested without an active transaction.");
}
if (transactionDepth_ == 1) {
if (!execute("COMMIT;")) {
return false;
}
transactionDepth_ = 0;
savepointIds_.clear();
return true;
}
const int savepointId = savepointIds_.back();
const wxString savepointName = MakeTransactionSavepointName(savepointId);
if (!execute("RELEASE SAVEPOINT " + savepointName + ";")) {
return false;
}
savepointIds_.pop_back();
--transactionDepth_;
return true;
}
bool BrushDatabaseSession::rollbackTransaction() {
if (readOnly_) {
return setError("SQLite transaction requires a read-write connection.");
}
if (transactionDepth_ <= 0) {
return setError("SQLite rollback requested without an active transaction.");
}
if (transactionDepth_ == 1) {
if (!execute("ROLLBACK;")) {
return false;
}
transactionDepth_ = 0;
savepointIds_.clear();
return true;
}
const int savepointId = savepointIds_.back();
const wxString savepointName = MakeTransactionSavepointName(savepointId);
if (!execute("ROLLBACK TO SAVEPOINT " + savepointName + ";")) {
return false;
}
if (!execute("RELEASE SAVEPOINT " + savepointName + ";")) {
return false;
}
savepointIds_.pop_back();
--transactionDepth_;
return true;
}
bool BrushDatabaseSession::setError(const wxString &message) {
lastError_ = message;
spdlog::error("[BrushDatabase] {}", lastError_.ToStdString());
return false;
}
bool BrushDatabaseSession::setErrorFromDatabase(const wxString &prefix) {
const wxString dbMessage = connection_ ? ToWxString(sqlite3_errmsg(connection_)) : wxString("No SQLite connection");
return setError(prefix + ": " + dbMessage);
}