////////////////////////////////////////////////////////////////////// // This file is part of Remere's Map Editor ////////////////////////////////////////////////////////////////////// // Remere's Map Editor is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Remere's Map Editor is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . ////////////////////////////////////////////////////////////////////// #include "main.h" #include "editor.h" #include "items.h" #include "monsters.h" #include #include "gui.h" #include "materials.h" #include "brush_database.h" #include "brush.h" #include "monster_brush.h" #include "npc_brush.h" #include "raw_brush.h" #include Materials g_materials; namespace { bool ExtractSimpleBrushNode(pugi::xml_node brushNode, BrushRecord &outBrush, std::vector &outItems, wxString &error, wxArrayString &warnings) { pugi::xml_attribute nameAttr = brushNode.attribute("name"); pugi::xml_attribute typeAttr = brushNode.attribute("type"); if (!nameAttr || !typeAttr) { error = "Brush node is missing required name/type attributes."; return false; } outBrush.name = wxString(nameAttr.as_string(), wxConvUTF8); outBrush.type = wxString(typeAttr.as_string(), wxConvUTF8); outBrush.lookId = brushNode.attribute("server_lookid") ? brushNode.attribute("server_lookid").as_int() : brushNode.attribute("lookid").as_int(); outBrush.zOrder = brushNode.attribute("z-order").as_int(); outItems.clear(); for (pugi::xml_node childNode = brushNode.first_child(); childNode; childNode = childNode.next_sibling()) { const std::string childName = as_lower_str(childNode.name()); if (childName == "item") { pugi::xml_attribute itemIdAttr = childNode.attribute("id"); if (!itemIdAttr) { continue; } BrushItemRecord item; item.itemId = itemIdAttr.as_int(); item.chance = childNode.attribute("chance").as_int(1); outItems.push_back(item); } else if (childName == "composite") { error = "Only simple brushes with direct nodes are supported by the SQLite validation import."; return false; } } if (outItems.empty()) { warnings.push_back("SQLite validation import found brush \"" + outBrush.name + "\" without direct item nodes."); } return true; } bool FindAndExtractSimpleBrushRecursive(const FileName &filename, const wxString &brushName, BrushRecord &outBrush, std::vector &outItems, wxString &error, wxArrayString &warnings, std::set &visited) { const wxString normalizedPath = filename.GetFullPath(); if (visited.find(normalizedPath) != visited.end()) { return false; } visited.insert(normalizedPath); pugi::xml_document doc; pugi::xml_parse_result result = doc.load_file(filename.GetFullPath().mb_str()); if (!result) { warnings.push_back("SQLite validation import could not read " + filename.GetFullName()); return false; } pugi::xml_node materialsNode = doc.child("materials"); if (!materialsNode) { warnings.push_back("SQLite validation import found invalid root in " + filename.GetFullName()); return false; } for (pugi::xml_node childNode = materialsNode.first_child(); childNode; childNode = childNode.next_sibling()) { const std::string childName = as_lower_str(childNode.name()); if (childName == "brush") { pugi::xml_attribute nameAttr = childNode.attribute("name"); if (nameAttr && wxString(nameAttr.as_string(), wxConvUTF8) == brushName) { return ExtractSimpleBrushNode(childNode, outBrush, outItems, error, warnings); } } else if (childName == "include") { pugi::xml_attribute includeAttr = childNode.attribute("file"); if (!includeAttr) { continue; } FileName includeName; includeName.SetPath(filename.GetPath()); includeName.SetName(wxString(includeAttr.as_string(), wxConvUTF8)); if (FindAndExtractSimpleBrushRecursive(includeName, brushName, outBrush, outItems, error, warnings, visited)) { return true; } } } return false; } struct PendingBorderSetImport { BorderSetRecord borderSet; std::vector items; }; struct PendingGroundBrushBorderImport { int xmlBorderId = 0; int inlineBorderIndex = -1; GroundBrushBorderRecord border; }; struct PendingGroundBrushImport { BrushRecord brush; std::vector items; std::vector optionalInlineBorders; std::vector optionalBorders; std::vector normalInlineBorders; std::vector normalBorders; std::vector links; }; wxString MaterialSourcePath(const FileName &filename) { return filename.GetFullPath(); } bool LoadMaterialsDocumentRoot(const FileName &filename, const wxString &context, pugi::xml_document &doc, pugi::xml_node &materialsNode, wxArrayString &warnings) { pugi::xml_parse_result result = doc.load_file(filename.GetFullPath().mb_str()); if (!result) { warnings.push_back(context + " could not read " + filename.GetFullName()); return false; } materialsNode = doc.child("materials"); if (!materialsNode) { warnings.push_back(context + " found invalid root in " + filename.GetFullName()); return false; } return true; } FileName ResolveMaterialInclude(const FileName &baseFile, const wxString &includePath) { return FileName(baseFile.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR) + includePath); } bool ImportGroundBrushesRecursive(const FileName &groundsFile, wxArrayString &warnings, std::set &visited); bool ImportWallBrushesRecursive(const FileName &wallsFile, wxArrayString &warnings, std::set &visited); wxString ParseBorderTargetMode(pugi::xml_node borderNode, wxString &targetBrushName) { const wxString toValue = wxString(borderNode.attribute("to").as_string(), wxConvUTF8); if (toValue.IsEmpty() || toValue == "all") { targetBrushName.clear(); return "all"; } if (toValue == "none") { targetBrushName.clear(); return "none"; } targetBrushName = toValue; return "brush"; } wxString ParseBorderAlign(pugi::xml_node borderNode) { const wxString align = wxString(borderNode.attribute("align").as_string(), wxConvUTF8); return align.IsEmpty() ? wxString("outer") : align; } void ParseThicknessString(const wxString &thicknessString, int &thickness, int &thicknessCeiling) { thickness = 0; thicknessCeiling = 0; if (thicknessString.IsEmpty()) { return; } wxString beforeSlash = thicknessString.BeforeFirst('/'); wxString afterSlash = thicknessString.AfterFirst('/'); long parsedValue = 0; if (!beforeSlash.IsEmpty() && beforeSlash.ToLong(&parsedValue)) { thickness = static_cast(parsedValue); } if (!afterSlash.IsEmpty() && afterSlash != thicknessString && afterSlash.ToLong(&parsedValue)) { thicknessCeiling = static_cast(parsedValue); } } void CollectBorderSetItems(pugi::xml_node borderNode, std::vector &outItems) { outItems.clear(); int sortOrder = 0; for (pugi::xml_node childNode = borderNode.first_child(); childNode; childNode = childNode.next_sibling()) { if (as_lower_str(childNode.name()) != "borderitem") { continue; } const wxString edge = wxString(childNode.attribute("edge").as_string(), wxConvUTF8); const int itemId = childNode.attribute("item").as_int(); if (edge.IsEmpty() || itemId <= 0) { continue; } BorderSetItemRecord item; item.edge = edge; item.itemId = itemId; item.sortOrder = sortOrder++; outItems.push_back(item); } } bool TryParseGroundCaseCondition(pugi::xml_node conditionNode, int sortOrder, GroundBorderCaseConditionRecord &outCondition) { const std::string conditionName = as_lower_str(conditionNode.name()); if (conditionName != "match_border" && conditionName != "match_group" && conditionName != "match_item") { return false; } outCondition = GroundBorderCaseConditionRecord(); outCondition.conditionType = wxString::FromUTF8(conditionName.c_str()); outCondition.sortOrder = sortOrder; outCondition.edge = wxString(conditionNode.attribute("edge").as_string(), wxConvUTF8); if (conditionName == "match_group") { outCondition.matchValue = conditionNode.attribute("group").as_int(); } else { outCondition.matchValue = conditionNode.attribute("id").as_int(); } return true; } void CollectGroundCaseConditions(pugi::xml_node conditionsNode, std::vector &outConditions) { auto sortOrder = static_cast(outConditions.size()); for (pugi::xml_node conditionNode = conditionsNode.first_child(); conditionNode; conditionNode = conditionNode.next_sibling()) { GroundBorderCaseConditionRecord condition; if (!TryParseGroundCaseCondition(conditionNode, sortOrder, condition)) { continue; } outConditions.push_back(condition); ++sortOrder; } } bool TryParseGroundCaseAction(pugi::xml_node actionNode, int sortOrder, GroundBorderCaseActionRecord &outAction) { const std::string actionName = as_lower_str(actionNode.name()); if (actionName != "replace_border" && actionName != "replace_item" && actionName != "delete_borders") { return false; } outAction = GroundBorderCaseActionRecord(); outAction.actionType = wxString::FromUTF8(actionName.c_str()); outAction.sortOrder = sortOrder; outAction.targetValue = actionNode.attribute("id").as_int(); outAction.edge = wxString(actionNode.attribute("edge").as_string(), wxConvUTF8); outAction.replacementValue = actionNode.attribute("with").as_int(); return true; } void CollectGroundCaseActions(pugi::xml_node actionsNode, std::vector &outActions) { auto sortOrder = static_cast(outActions.size()); for (pugi::xml_node actionNode = actionsNode.first_child(); actionNode; actionNode = actionNode.next_sibling()) { GroundBorderCaseActionRecord action; if (!TryParseGroundCaseAction(actionNode, sortOrder, action)) { continue; } outActions.push_back(action); ++sortOrder; } } void CollectGroundSpecificCaseBranches(pugi::xml_node specificNode, GroundBorderCaseRecord &caseRecord) { for (pugi::xml_node branchNode = specificNode.first_child(); branchNode; branchNode = branchNode.next_sibling()) { const std::string branchName = as_lower_str(branchNode.name()); if (branchName == "conditions") { CollectGroundCaseConditions(branchNode, caseRecord.conditions); continue; } if (branchName == "actions") { CollectGroundCaseActions(branchNode, caseRecord.actions); } } } bool ImportGlobalBordersRecursive(const FileName &filename, wxArrayString &warnings, std::set &visited) { const wxString normalizedPath = filename.GetFullPath(); if (visited.find(normalizedPath) != visited.end()) { return true; } visited.insert(normalizedPath); pugi::xml_document doc; pugi::xml_node materialsNode; if (!LoadMaterialsDocumentRoot(filename, "SQLite border import", doc, materialsNode, warnings)) { return false; } for (pugi::xml_node childNode = materialsNode.first_child(); childNode; childNode = childNode.next_sibling()) { const std::string childName = as_lower_str(childNode.name()); if (childName == "include") { const wxString includePath = wxString(childNode.attribute("file").as_string(), wxConvUTF8); if (!includePath.empty() && !ImportGlobalBordersRecursive(ResolveMaterialInclude(filename, includePath), warnings, visited)) { return false; } } else if (childName == "border") { const int xmlBorderId = childNode.attribute("id").as_int(); if (xmlBorderId <= 0) { continue; } BorderSetRecord borderSet; borderSet.xmlBorderId = xmlBorderId; borderSet.borderScope = "global"; borderSet.borderType = wxString(childNode.attribute("type").as_string(), wxConvUTF8); if (borderSet.borderType.IsEmpty()) { borderSet.borderType = "normal"; } borderSet.borderGroup = childNode.attribute("group").as_int(); borderSet.sourceFile = MaterialSourcePath(filename); int64_t borderSetId = 0; if (!g_brush_database.upsertBorderSet(borderSet, borderSetId)) { warnings.push_back(wxString::FromUTF8(std::format("SQLite border import failed for border id {}: {}", xmlBorderId, g_brush_database.getLastError().ToStdString()))); return false; } std::vector items; CollectBorderSetItems(childNode, items); if (!g_brush_database.replaceBorderSetItems(borderSetId, items)) { warnings.push_back(wxString::FromUTF8(std::format("SQLite border item import failed for border id {}: {}", xmlBorderId, g_brush_database.getLastError().ToStdString()))); return false; } } } return true; } bool ParseGroundSpecificCases(pugi::xml_node borderNode, std::vector &outCases) { outCases.clear(); int caseSortOrder = 0; for (pugi::xml_node specificNode = borderNode.first_child(); specificNode; specificNode = specificNode.next_sibling()) { if (as_lower_str(specificNode.name()) != "specific") { continue; } GroundBorderCaseRecord caseRecord; caseRecord.sortOrder = caseSortOrder++; CollectGroundSpecificCaseBranches(specificNode, caseRecord); outCases.push_back(caseRecord); } return true; } PendingBorderSetImport BuildInlineBorderSet(pugi::xml_node node, const wxString &borderType, const FileName &sourceFile) { PendingBorderSetImport pending; pending.borderSet.borderScope = "inline"; pending.borderSet.borderType = borderType; pending.borderSet.groundEquivalent = node.attribute("ground_equivalent").as_int(); pending.borderSet.sourceFile = MaterialSourcePath(sourceFile); CollectBorderSetItems(node, pending.items); return pending; } bool ParseGroundBrushNode(const FileName &sourceFile, pugi::xml_node brushNode, PendingGroundBrushImport &outBrush, wxArrayString &warnings) { if (wxString(brushNode.attribute("type").as_string(), wxConvUTF8) != "ground") { return false; } outBrush = PendingGroundBrushImport(); outBrush.brush.name = wxString(brushNode.attribute("name").as_string(), wxConvUTF8); outBrush.brush.type = "ground"; outBrush.brush.lookId = brushNode.attribute("lookid").as_int(); outBrush.brush.serverLookId = brushNode.attribute("server_lookid").as_int(); outBrush.brush.zOrder = brushNode.attribute("z-order").as_int(); outBrush.brush.randomize = brushNode.attribute("randomize").as_bool(); outBrush.brush.soloOptional = brushNode.attribute("solo_optional").as_bool(); outBrush.brush.sourceFile = MaterialSourcePath(sourceFile); if (outBrush.brush.name.IsEmpty()) { warnings.push_back("SQLite ground import found ground brush without name in " + sourceFile.GetFullName()); return false; } int linkSortOrder = 0; int optionalSortOrder = 0; int borderSortOrder = 0; for (pugi::xml_node childNode = brushNode.first_child(); childNode; childNode = childNode.next_sibling()) { const std::string childName = as_lower_str(childNode.name()); if (childName == "item") { const int itemId = childNode.attribute("id").as_int(); if (itemId <= 0) { continue; } BrushItemRecord item; item.itemId = itemId; item.chance = childNode.attribute("chance").as_int(1); outBrush.items.push_back(item); } else if (childName == "optional") { PendingGroundBrushBorderImport borderImport; borderImport.border.borderRole = "optional"; borderImport.border.align = "optional"; borderImport.border.targetMode = "none"; borderImport.border.sortOrder = optionalSortOrder++; if (childNode.attribute("id")) { borderImport.xmlBorderId = childNode.attribute("id").as_int(); } else if (childNode.attribute("ground_equivalent")) { borderImport.inlineBorderIndex = static_cast(outBrush.optionalInlineBorders.size()); outBrush.optionalInlineBorders.push_back(BuildInlineBorderSet(childNode, "optional", sourceFile)); } outBrush.optionalBorders.push_back(borderImport); } else if (childName == "border") { PendingGroundBrushBorderImport borderImport; borderImport.border.borderRole = "normal"; borderImport.border.align = ParseBorderAlign(childNode); borderImport.border.targetMode = ParseBorderTargetMode(childNode, borderImport.border.targetBrushName); borderImport.border.superBorder = childNode.attribute("super").as_bool(); borderImport.border.sortOrder = borderSortOrder++; if (childNode.attribute("id")) { borderImport.xmlBorderId = childNode.attribute("id").as_int(); } else if (childNode.attribute("ground_equivalent")) { borderImport.inlineBorderIndex = static_cast(outBrush.normalInlineBorders.size()); outBrush.normalInlineBorders.push_back(BuildInlineBorderSet(childNode, "normal", sourceFile)); } ParseGroundSpecificCases(childNode, borderImport.border.cases); outBrush.normalBorders.push_back(borderImport); } else if (childName == "friend" || childName == "enemy") { BrushLinkRecord link; link.relationType = wxString::FromUTF8(childName.c_str()); link.targetBrushName = wxString(childNode.attribute("name").as_string(), wxConvUTF8); link.sortOrder = linkSortOrder++; outBrush.links.push_back(link); } else if (childName == "clear_borders") { outBrush.normalInlineBorders.clear(); outBrush.normalBorders.clear(); borderSortOrder = 0; } else if (childName == "clear_friends") { outBrush.links.clear(); linkSortOrder = 0; } } return true; } void AssignBrushIdToItems(std::vector &items, int64_t brushId) { for (BrushItemRecord &item : items) { item.brushId = brushId; } } void AssignBrushIdToLinks(std::vector &links, int64_t brushId) { for (BrushLinkRecord &link : links) { link.brushId = brushId; } } bool ImportPendingGroundInlineBorders( std::vector &pendingInlineBorders, int64_t brushId, const wxString &brushName, const wxString &importFailureContext, const wxString &itemImportFailureContext, std::vector &outBorderSetIds, wxArrayString &warnings ) { outBorderSetIds.assign(pendingInlineBorders.size(), 0); for (size_t i = 0; i < pendingInlineBorders.size(); ++i) { pendingInlineBorders[i].borderSet.ownerBrushId = brushId; int64_t borderSetId = 0; if (!g_brush_database.upsertBorderSet(pendingInlineBorders[i].borderSet, borderSetId)) { warnings.push_back(importFailureContext + " for \"" + brushName + "\": " + g_brush_database.getLastError()); return false; } outBorderSetIds[i] = borderSetId; if (!g_brush_database.replaceBorderSetItems(borderSetId, pendingInlineBorders[i].items)) { warnings.push_back(itemImportFailureContext + " for \"" + brushName + "\": " + g_brush_database.getLastError()); return false; } } return true; } bool ResolveGroundBorderSetId( const PendingGroundBrushBorderImport &pendingBorder, const std::vector &inlineBorderIds, const wxString &brushName, const wxString &missingBorderContext, int64_t &outBorderSetId, wxArrayString &warnings ) { outBorderSetId = 0; if (pendingBorder.xmlBorderId > 0) { BorderSetRecord borderSet; if (!g_brush_database.findBorderSetByXmlBorderId(pendingBorder.xmlBorderId, borderSet)) { warnings.push_back(missingBorderContext + " " + std::to_string(pendingBorder.xmlBorderId) + " not found for \"" + brushName + "\""); return false; } outBorderSetId = borderSet.id; return true; } if (pendingBorder.inlineBorderIndex >= 0 && pendingBorder.inlineBorderIndex < static_cast(inlineBorderIds.size())) { outBorderSetId = inlineBorderIds[pendingBorder.inlineBorderIndex]; } return true; } bool AppendResolvedGroundBorders( const std::vector &pendingBorders, const std::vector &inlineBorderIds, const wxString &brushName, const wxString &missingBorderContext, std::vector &outBorders, wxArrayString &warnings ) { for (const PendingGroundBrushBorderImport &pendingBorder : pendingBorders) { int64_t borderSetId = 0; if (!ResolveGroundBorderSetId(pendingBorder, inlineBorderIds, brushName, missingBorderContext, borderSetId, warnings)) { return false; } if (borderSetId <= 0) { continue; } GroundBrushBorderRecord border = pendingBorder.border; border.borderSetId = borderSetId; outBorders.push_back(border); } return true; } bool ImportPendingGroundBrush(PendingGroundBrushImport pending, wxArrayString &warnings) { int64_t brushId = 0; if (!g_brush_database.upsertBrush(pending.brush, brushId)) { warnings.push_back("SQLite ground import failed for brush \"" + pending.brush.name + "\": " + g_brush_database.getLastError()); return false; } if (!g_brush_database.deleteOwnedBorderSetsForBrush(brushId)) { warnings.push_back("SQLite ground import failed cleaning inline borders for \"" + pending.brush.name + "\": " + g_brush_database.getLastError()); return false; } AssignBrushIdToItems(pending.items, brushId); if (!g_brush_database.replaceBrushItems(brushId, pending.items)) { warnings.push_back("SQLite ground import failed writing items for \"" + pending.brush.name + "\": " + g_brush_database.getLastError()); return false; } std::vector optionalInlineIds; if (!ImportPendingGroundInlineBorders( pending.optionalInlineBorders, brushId, pending.brush.name, "SQLite optional border import failed", "SQLite optional border item import failed", optionalInlineIds, warnings )) { return false; } std::vector normalInlineIds; if (!ImportPendingGroundInlineBorders( pending.normalInlineBorders, brushId, pending.brush.name, "SQLite inline border import failed", "SQLite inline border item import failed", normalInlineIds, warnings )) { return false; } std::vector borders; if (!AppendResolvedGroundBorders( pending.optionalBorders, optionalInlineIds, pending.brush.name, "SQLite optional border id", borders, warnings )) { return false; } if (!AppendResolvedGroundBorders( pending.normalBorders, normalInlineIds, pending.brush.name, "SQLite border id", borders, warnings )) { return false; } if (!g_brush_database.replaceGroundBrushBorders(brushId, borders)) { warnings.push_back("SQLite ground border import failed for \"" + pending.brush.name + "\": " + g_brush_database.getLastError()); return false; } AssignBrushIdToLinks(pending.links, brushId); if (!g_brush_database.replaceBrushLinks(brushId, pending.links)) { warnings.push_back("SQLite brush link import failed for \"" + pending.brush.name + "\": " + g_brush_database.getLastError()); return false; } return true; } bool ImportGroundBrushesFile(const FileName &groundsFile, wxArrayString &warnings) { std::set visited; return ImportGroundBrushesRecursive(groundsFile, warnings, visited); } bool ImportGroundBrushesRecursive(const FileName &groundsFile, wxArrayString &warnings, std::set &visited) { const wxString normalizedPath = groundsFile.GetFullPath(); if (visited.find(normalizedPath) != visited.end()) { return true; } visited.insert(normalizedPath); pugi::xml_document doc; pugi::xml_node materialsNode; if (!LoadMaterialsDocumentRoot(groundsFile, "SQLite ground import", doc, materialsNode, warnings)) { return false; } for (pugi::xml_node brushNode = materialsNode.first_child(); brushNode; brushNode = brushNode.next_sibling()) { const std::string childName = as_lower_str(brushNode.name()); if (childName == "include") { const wxString includePath = wxString(brushNode.attribute("file").as_string(), wxConvUTF8); if (!includePath.empty() && !ImportGroundBrushesRecursive(ResolveMaterialInclude(groundsFile, includePath), warnings, visited)) { return false; } continue; } if (childName != "brush") { continue; } PendingGroundBrushImport pending; if (!ParseGroundBrushNode(groundsFile, brushNode, pending, warnings)) { continue; } if (!ImportPendingGroundBrush(pending, warnings)) { return false; } } return true; } void CollectWallItemNodes(pugi::xml_node parentNode, std::vector &outItems) { auto sortOrder = static_cast(outItems.size()); for (pugi::xml_node childNode = parentNode.first_child(); childNode; childNode = childNode.next_sibling()) { if (as_lower_str(childNode.name()) != "item") { continue; } const int itemId = childNode.attribute("id").as_int(); if (itemId <= 0) { continue; } WallPartItemRecord item; item.itemId = itemId; item.chance = childNode.attribute("chance").as_int(); item.sortOrder = sortOrder++; outItems.push_back(item); } } void CollectWallDoorNodes(pugi::xml_node parentNode, std::vector &outDoors) { auto sortOrder = static_cast(outDoors.size()); for (pugi::xml_node childNode = parentNode.first_child(); childNode; childNode = childNode.next_sibling()) { if (as_lower_str(childNode.name()) != "door") { continue; } const int itemId = childNode.attribute("id").as_int(); if (itemId <= 0) { continue; } WallPartDoorRecord door; door.itemId = itemId; door.doorType = wxString(childNode.attribute("type").as_string(), wxConvUTF8); door.isOpen = childNode.attribute("open").as_bool(); door.wallHateMe = childNode.attribute("hate").as_bool(); door.sortOrder = sortOrder++; outDoors.push_back(door); } } bool WallPartHasContent(const WallPartRecord &part) { return !part.items.empty() || !part.doors.empty(); } WallPartRecord &GetOrCreateWallPart(std::vector &outParts, const wxString &partType, int sortOrder) { for (auto &existingPart : outParts) { if (existingPart.partType == partType) { return existingPart; } } WallPartRecord part; part.partType = partType; part.sortOrder = sortOrder; outParts.push_back(part); return outParts.back(); } bool PopulateWallPartContent(pugi::xml_node sourceNode, WallPartRecord &part) { const bool wasEmpty = !WallPartHasContent(part); CollectWallItemNodes(sourceNode, part.items); CollectWallDoorNodes(sourceNode, part.doors); return wasEmpty && WallPartHasContent(part); } void CollectNestedWallAlternates(pugi::xml_node wallNode, const wxString &basePartType, std::vector &outParts, int &partSortOrder) { int localAlternateIndex = 0; for (pugi::xml_node subChild = wallNode.first_child(); subChild; subChild = subChild.next_sibling()) { if (as_lower_str(subChild.name()) != "alternate") { continue; } const wxString alternatePartType = basePartType + wxString::Format("/alternate/%d", localAlternateIndex++); auto &alternatePart = GetOrCreateWallPart(outParts, alternatePartType, partSortOrder); if (PopulateWallPartContent(subChild, alternatePart)) { ++partSortOrder; } } } void CollectWallPartNode(pugi::xml_node childNode, std::vector &outParts, int &partSortOrder) { const wxString partType = wxString(childNode.attribute("type").as_string(), wxConvUTF8); if (partType.IsEmpty()) { return; } auto &part = GetOrCreateWallPart(outParts, partType, partSortOrder); if (PopulateWallPartContent(childNode, part)) { ++partSortOrder; } CollectNestedWallAlternates(childNode, part.partType, outParts, partSortOrder); } void CollectStandaloneWallAlternateNode(pugi::xml_node childNode, std::vector &outParts, int &partSortOrder, int &alternateIndex) { WallPartRecord alternatePart; alternatePart.partType = wxString::Format("alternate/%d", alternateIndex++); alternatePart.sortOrder = partSortOrder; if (!PopulateWallPartContent(childNode, alternatePart)) { return; } ++partSortOrder; outParts.push_back(alternatePart); } void AppendWallFriendLinks(pugi::xml_node childNode, std::vector &outLinks, int &linkSortOrder) { const wxString targetName = wxString(childNode.attribute("name").as_string(), wxConvUTF8); if (targetName.IsEmpty()) { return; } BrushLinkRecord friendLink; friendLink.targetBrushName = targetName; friendLink.relationType = "friend"; friendLink.sortOrder = linkSortOrder++; outLinks.push_back(friendLink); if (!childNode.attribute("redirect").as_bool()) { return; } BrushLinkRecord redirectLink; redirectLink.targetBrushName = targetName; redirectLink.relationType = "redirect"; redirectLink.sortOrder = linkSortOrder++; outLinks.push_back(redirectLink); } bool TryAppendDoodadSingleItem(pugi::xml_node childNode, int &singleSortOrder, DoodadAlternativeRecord &alternative) { if (as_lower_str(childNode.name()) != "item") { return false; } const int itemId = childNode.attribute("id").as_int(); if (itemId <= 0) { return true; } DoodadSingleItemRecord item; item.itemId = itemId; item.chance = childNode.attribute("chance").as_int(); item.sortOrder = singleSortOrder++; alternative.singleItems.push_back(item); return true; } bool TryAppendDoodadCompositeTileItem(pugi::xml_node itemNode, int &tileItemSortOrder, DoodadCompositeTileRecord &tile) { if (as_lower_str(itemNode.name()) != "item") { return false; } const int itemId = itemNode.attribute("id").as_int(); if (itemId <= 0) { return true; } DoodadCompositeTileItemRecord item; item.itemId = itemId; item.sortOrder = tileItemSortOrder++; tile.items.push_back(item); return true; } void CollectDoodadCompositeTileItems(pugi::xml_node tileNode, DoodadCompositeTileRecord &tile) { int tileItemSortOrder = 0; for (pugi::xml_node itemNode = tileNode.first_child(); itemNode; itemNode = itemNode.next_sibling()) { TryAppendDoodadCompositeTileItem(itemNode, tileItemSortOrder, tile); } } bool TryAppendDoodadCompositeTile(pugi::xml_node tileNode, int &tileSortOrder, DoodadCompositeRecord &composite) { if (as_lower_str(tileNode.name()) != "tile") { return false; } DoodadCompositeTileRecord tile; tile.offsetX = tileNode.attribute("x").as_int(); tile.offsetY = tileNode.attribute("y").as_int(); tile.offsetZ = tileNode.attribute("z").as_int(); tile.sortOrder = tileSortOrder++; CollectDoodadCompositeTileItems(tileNode, tile); if (!tile.items.empty()) { composite.tiles.push_back(tile); } return true; } void CollectDoodadCompositeTiles(pugi::xml_node compositeNode, DoodadCompositeRecord &composite) { int tileSortOrder = 0; for (pugi::xml_node tileNode = compositeNode.first_child(); tileNode; tileNode = tileNode.next_sibling()) { TryAppendDoodadCompositeTile(tileNode, tileSortOrder, composite); } } bool TryAppendDoodadComposite(pugi::xml_node childNode, int &compositeSortOrder, DoodadAlternativeRecord &alternative) { if (as_lower_str(childNode.name()) != "composite") { return false; } DoodadCompositeRecord composite; composite.chance = childNode.attribute("chance").as_int(); composite.sortOrder = compositeSortOrder++; CollectDoodadCompositeTiles(childNode, composite); if (!composite.tiles.empty()) { alternative.composites.push_back(composite); } return true; } bool ParseWallBrushNode(const FileName &sourceFile, pugi::xml_node brushNode, BrushRecord &outBrush, std::vector &outParts, std::vector &outLinks, wxArrayString &warnings) { if (wxString(brushNode.attribute("type").as_string(), wxConvUTF8) != "wall") { return false; } outBrush = BrushRecord(); outBrush.name = wxString(brushNode.attribute("name").as_string(), wxConvUTF8); outBrush.type = "wall"; outBrush.lookId = brushNode.attribute("lookid").as_int(); outBrush.serverLookId = brushNode.attribute("server_lookid").as_int(); outBrush.draggable = brushNode.attribute("draggable").as_bool(); outBrush.onBlocking = brushNode.attribute("on_blocking").as_bool(); outBrush.onDuplicate = brushNode.attribute("on_duplicate").as_bool(); outBrush.redoBorders = brushNode.attribute("redo_borders").as_bool() || brushNode.attribute("reborder").as_bool(); outBrush.oneSize = brushNode.attribute("one_size").as_bool(); outBrush.sourceFile = MaterialSourcePath(sourceFile); ParseThicknessString(wxString(brushNode.attribute("thickness").as_string(), wxConvUTF8), outBrush.thickness, outBrush.thicknessCeiling); if (outBrush.name.IsEmpty()) { warnings.push_back("SQLite wall import found wall brush without name in " + sourceFile.GetFullName()); return false; } outParts.clear(); outLinks.clear(); int partSortOrder = 0; int alternateIndex = 0; int linkSortOrder = 0; for (pugi::xml_node childNode = brushNode.first_child(); childNode; childNode = childNode.next_sibling()) { const std::string childName = as_lower_str(childNode.name()); if (childName == "wall") { CollectWallPartNode(childNode, outParts, partSortOrder); continue; } if (childName == "alternate") { CollectStandaloneWallAlternateNode(childNode, outParts, partSortOrder, alternateIndex); continue; } if (childName == "friend") { AppendWallFriendLinks(childNode, outLinks, linkSortOrder); } } return true; } bool ImportWallBrushesFile(const FileName &wallsFile, wxArrayString &warnings) { std::set visited; return ImportWallBrushesRecursive(wallsFile, warnings, visited); } bool ImportWallBrushesRecursive(const FileName &wallsFile, wxArrayString &warnings, std::set &visited) { const wxString normalizedPath = wallsFile.GetFullPath(); if (visited.find(normalizedPath) != visited.end()) { return true; } visited.insert(normalizedPath); pugi::xml_document doc; pugi::xml_node materialsNode; if (!LoadMaterialsDocumentRoot(wallsFile, "SQLite wall import", doc, materialsNode, warnings)) { return false; } for (pugi::xml_node brushNode = materialsNode.first_child(); brushNode; brushNode = brushNode.next_sibling()) { const std::string childName = as_lower_str(brushNode.name()); if (childName == "include") { const wxString includePath = wxString(brushNode.attribute("file").as_string(), wxConvUTF8); if (!includePath.empty() && !ImportWallBrushesRecursive(ResolveMaterialInclude(wallsFile, includePath), warnings, visited)) { return false; } continue; } if (childName != "brush") { continue; } BrushRecord brush; std::vector parts; std::vector links; if (!ParseWallBrushNode(wallsFile, brushNode, brush, parts, links, warnings)) { continue; } int64_t brushId = 0; if (!g_brush_database.upsertBrush(brush, brushId)) { warnings.push_back("SQLite wall import failed for brush \"" + brush.name + "\": " + g_brush_database.getLastError()); return false; } if (!g_brush_database.replaceWallParts(brushId, parts)) { warnings.push_back("SQLite wall parts import failed for brush \"" + brush.name + "\": " + g_brush_database.getLastError()); return false; } for (BrushLinkRecord &link : links) { link.brushId = brushId; } if (!g_brush_database.replaceBrushLinks(brushId, links)) { warnings.push_back("SQLite wall links import failed for brush \"" + brush.name + "\": " + g_brush_database.getLastError()); return false; } } return true; } void CollectDoodadAlternativeContent(pugi::xml_node sourceNode, DoodadAlternativeRecord &alternative) { auto singleSortOrder = static_cast(alternative.singleItems.size()); auto compositeSortOrder = static_cast(alternative.composites.size()); for (pugi::xml_node childNode = sourceNode.first_child(); childNode; childNode = childNode.next_sibling()) { if (TryAppendDoodadSingleItem(childNode, singleSortOrder, alternative)) { continue; } TryAppendDoodadComposite(childNode, compositeSortOrder, alternative); } } bool NodeHasDoodadContent(pugi::xml_node node) { for (pugi::xml_node childNode = node.first_child(); childNode; childNode = childNode.next_sibling()) { const std::string childName = as_lower_str(childNode.name()); if (childName == "item" || childName == "composite") { return true; } } return false; } bool ParseDoodadBrushNode(const FileName &sourceFile, pugi::xml_node brushNode, BrushRecord &outBrush, std::vector &outAlternatives, wxArrayString &warnings) { if (wxString(brushNode.attribute("type").as_string(), wxConvUTF8) != "doodad") { return false; } outBrush = BrushRecord(); outBrush.name = wxString(brushNode.attribute("name").as_string(), wxConvUTF8); outBrush.type = "doodad"; outBrush.lookId = brushNode.attribute("lookid").as_int(); outBrush.serverLookId = brushNode.attribute("server_lookid").as_int(); outBrush.draggable = brushNode.attribute("draggable").as_bool(); outBrush.onBlocking = brushNode.attribute("on_blocking").as_bool(); outBrush.onDuplicate = brushNode.attribute("on_duplicate").as_bool(); outBrush.redoBorders = brushNode.attribute("redo_borders").as_bool() || brushNode.attribute("reborder").as_bool(); outBrush.oneSize = brushNode.attribute("one_size").as_bool(); outBrush.sourceFile = MaterialSourcePath(sourceFile); ParseThicknessString(wxString(brushNode.attribute("thickness").as_string(), wxConvUTF8), outBrush.thickness, outBrush.thicknessCeiling); if (outBrush.name.IsEmpty()) { warnings.push_back("SQLite doodad import found brush without name in " + sourceFile.GetFullName()); return false; } outAlternatives.clear(); int sortOrder = 0; for (pugi::xml_node childNode = brushNode.first_child(); childNode; childNode = childNode.next_sibling()) { if (as_lower_str(childNode.name()) != "alternate") { continue; } DoodadAlternativeRecord alternative; alternative.sortOrder = sortOrder++; CollectDoodadAlternativeContent(childNode, alternative); outAlternatives.push_back(alternative); } if (NodeHasDoodadContent(brushNode)) { if (outAlternatives.empty()) { DoodadAlternativeRecord alternative; alternative.sortOrder = sortOrder++; CollectDoodadAlternativeContent(brushNode, alternative); outAlternatives.push_back(alternative); } else { CollectDoodadAlternativeContent(brushNode, outAlternatives.back()); } } return true; } struct AlignedBrushParseOptions { wxString expectedBrushType; const char* containerNodeName = nullptr; bool allowDirectIdFallback = false; }; template bool ParseAlignedBrushNode( const FileName &sourceFile, pugi::xml_node brushNode, const AlignedBrushParseOptions &options, BrushRecord &outBrush, std::vector &outNodes, wxArrayString &warnings ) { if (wxString(brushNode.attribute("type").as_string(), wxConvUTF8) != options.expectedBrushType) { return false; } outBrush = BrushRecord(); outBrush.name = wxString(brushNode.attribute("name").as_string(), wxConvUTF8); outBrush.type = options.expectedBrushType; outBrush.lookId = brushNode.attribute("lookid").as_int(); outBrush.serverLookId = brushNode.attribute("server_lookid").as_int(); outBrush.sourceFile = MaterialSourcePath(sourceFile); if (outBrush.name.IsEmpty()) { warnings.push_back("SQLite " + options.expectedBrushType + " import found brush without name in " + sourceFile.GetFullName()); return false; } outNodes.clear(); int nodeSortOrder = 0; for (pugi::xml_node childNode = brushNode.first_child(); childNode; childNode = childNode.next_sibling()) { if (as_lower_str(childNode.name()) != options.containerNodeName) { continue; } NodeRecord node; node.align = wxString(childNode.attribute("align").as_string(), wxConvUTF8); node.sortOrder = nodeSortOrder++; int itemSortOrder = 0; bool hasNestedItems = false; for (pugi::xml_node itemNode = childNode.first_child(); itemNode; itemNode = itemNode.next_sibling()) { if (as_lower_str(itemNode.name()) != "item") { continue; } hasNestedItems = true; const int itemId = itemNode.attribute("id").as_int(); if (itemId <= 0) { continue; } ItemRecord item; item.itemId = itemId; item.chance = itemNode.attribute("chance").as_int(); item.sortOrder = itemSortOrder++; node.items.push_back(item); } if (options.allowDirectIdFallback && !hasNestedItems) { const int itemId = childNode.attribute("id").as_int(); if (itemId > 0) { ItemRecord item; item.itemId = itemId; item.chance = 1; item.sortOrder = 0; node.items.push_back(item); } } if (!node.items.empty()) { outNodes.push_back(node); } } return true; } bool ParseCarpetBrushNode(const FileName &sourceFile, pugi::xml_node brushNode, BrushRecord &outBrush, std::vector &outNodes, wxArrayString &warnings) { static const AlignedBrushParseOptions options = { "carpet", "carpet", true }; return ParseAlignedBrushNode( sourceFile, brushNode, options, outBrush, outNodes, warnings ); } bool ParseTableBrushNode(const FileName &sourceFile, pugi::xml_node brushNode, BrushRecord &outBrush, std::vector &outNodes, wxArrayString &warnings) { static const AlignedBrushParseOptions options = { "table", "table", false }; return ParseAlignedBrushNode( sourceFile, brushNode, options, outBrush, outNodes, warnings ); } bool NormalizeTilesetSectionType(const std::string &nodeName, wxString §ionType) { if (nodeName == "terrain" || nodeName == "terrain_and_raw") { sectionType = "terrain"; return true; } if (nodeName == "doodad" || nodeName == "doodad_and_raw") { sectionType = "doodad"; return true; } if (nodeName == "items" || nodeName == "items_and_raw") { sectionType = "items"; return true; } return false; } void CollectTilesetSectionEntries(pugi::xml_node sectionNode, TilesetSectionRecord §ion) { int sortOrder = 0; for (pugi::xml_node entryNode = sectionNode.first_child(); entryNode; entryNode = entryNode.next_sibling()) { const std::string entryNodeName = as_lower_str(entryNode.name()); if (entryNodeName != "brush" && entryNodeName != "item") { continue; } TilesetEntryRecord entry; entry.entryKind = wxString::FromUTF8(entryNodeName.c_str()); entry.brushName = wxString(entryNode.attribute("name").as_string(), wxConvUTF8); entry.itemId = entryNode.attribute("id").as_int(); entry.fromItemId = entryNode.attribute("fromid").as_int(); entry.toItemId = entryNode.attribute("toid").as_int(); entry.afterBrushName = wxString(entryNode.attribute("after").as_string(), wxConvUTF8); entry.afterItemId = entryNode.attribute("afteritem").as_int(); if (entry.toItemId == 0) { entry.toItemId = entry.fromItemId != 0 ? entry.fromItemId : entry.itemId; } entry.sortOrder = sortOrder++; if (entry.entryKind == "brush" && !entry.brushName.IsEmpty()) { section.entries.push_back(entry); } else if (entry.entryKind == "item" && (entry.itemId > 0 || entry.fromItemId > 0)) { section.entries.push_back(entry); } } } bool ParseTilesetNode(const FileName &sourceFile, pugi::xml_node tilesetNode, TilesetStorageRecord &outTileset, wxArrayString &warnings) { outTileset = TilesetStorageRecord(); outTileset.name = wxString(tilesetNode.attribute("name").as_string(), wxConvUTF8); outTileset.sourceFile = MaterialSourcePath(sourceFile); if (outTileset.name.IsEmpty()) { warnings.push_back("SQLite tileset import found tileset without name in " + sourceFile.GetFullName()); return false; } int sectionSortOrder = 0; for (pugi::xml_node childNode = tilesetNode.first_child(); childNode; childNode = childNode.next_sibling()) { wxString sectionType; if (!NormalizeTilesetSectionType(as_lower_str(childNode.name()), sectionType)) { continue; } TilesetSectionRecord section; section.sectionType = sectionType; section.sortOrder = sectionSortOrder++; CollectTilesetSectionEntries(childNode, section); outTileset.sections.push_back(section); } return true; } bool ImportTilesetsRecursive(const FileName &filename, wxArrayString &warnings, std::set &visited, std::vector &outTilesets) { const wxString normalizedPath = filename.GetFullPath(); if (visited.find(normalizedPath) != visited.end()) { return true; } visited.insert(normalizedPath); pugi::xml_document doc; pugi::xml_node materialsNode; if (!LoadMaterialsDocumentRoot(filename, "SQLite tileset import", doc, materialsNode, warnings)) { return false; } for (pugi::xml_node childNode = materialsNode.first_child(); childNode; childNode = childNode.next_sibling()) { const std::string childName = as_lower_str(childNode.name()); if (childName == "include") { const wxString includePath = wxString(childNode.attribute("file").as_string(), wxConvUTF8); if (!includePath.empty() && !ImportTilesetsRecursive(ResolveMaterialInclude(filename, includePath), warnings, visited, outTilesets)) { return false; } continue; } if (childName != "tileset") { continue; } TilesetStorageRecord tileset; if (ParseTilesetNode(filename, childNode, tileset, warnings)) { outTilesets.push_back(tileset); } } return true; } enum class DecorativeBrushKind { None, Doodad, Carpet, Table, }; struct ParsedDecorativeBrush { DecorativeBrushKind kind = DecorativeBrushKind::None; BrushRecord brush; std::vector doodadAlternatives; std::vector carpetNodes; std::vector tableNodes; }; bool ParseDecorativeBrushNode(const FileName &filename, pugi::xml_node brushNode, ParsedDecorativeBrush &outParsed, wxArrayString &warnings) { using enum DecorativeBrushKind; outParsed = ParsedDecorativeBrush(); if (ParseDoodadBrushNode(filename, brushNode, outParsed.brush, outParsed.doodadAlternatives, warnings)) { outParsed.kind = Doodad; return true; } if (ParseCarpetBrushNode(filename, brushNode, outParsed.brush, outParsed.carpetNodes, warnings)) { outParsed.kind = Carpet; return true; } if (ParseTableBrushNode(filename, brushNode, outParsed.brush, outParsed.tableNodes, warnings)) { outParsed.kind = Table; return true; } return false; } bool StoreDecorativeBrushContent(const ParsedDecorativeBrush &parsed, int64_t brushId, wxArrayString &warnings) { using enum DecorativeBrushKind; const wxString &brushName = parsed.brush.name; switch (parsed.kind) { case Doodad: if (g_brush_database.replaceDoodadAlternatives(brushId, parsed.doodadAlternatives)) { return true; } warnings.push_back("SQLite doodad import failed for brush \"" + brushName + "\": " + g_brush_database.getLastError()); return false; case Carpet: if (g_brush_database.replaceCarpetNodes(brushId, parsed.carpetNodes)) { return true; } warnings.push_back("SQLite carpet import failed for brush \"" + brushName + "\": " + g_brush_database.getLastError()); return false; case Table: if (g_brush_database.replaceTableNodes(brushId, parsed.tableNodes)) { return true; } warnings.push_back("SQLite table import failed for brush \"" + brushName + "\": " + g_brush_database.getLastError()); return false; case None: return false; } return false; } bool ImportDecorativeBrushesRecursive(const FileName &filename, wxArrayString &warnings, std::set &visited) { const wxString normalizedPath = filename.GetFullPath(); if (visited.find(normalizedPath) != visited.end()) { return true; } visited.insert(normalizedPath); pugi::xml_document doc; pugi::xml_node materialsNode; if (!LoadMaterialsDocumentRoot(filename, "SQLite decorative import", doc, materialsNode, warnings)) { return false; } for (pugi::xml_node childNode = materialsNode.first_child(); childNode; childNode = childNode.next_sibling()) { const std::string childName = as_lower_str(childNode.name()); if (childName == "include") { const wxString includePath = wxString(childNode.attribute("file").as_string(), wxConvUTF8); if (!includePath.empty() && !ImportDecorativeBrushesRecursive(ResolveMaterialInclude(filename, includePath), warnings, visited)) { return false; } continue; } if (childName != "brush") { continue; } ParsedDecorativeBrush parsed; if (!ParseDecorativeBrushNode(filename, childNode, parsed, warnings)) { continue; } int64_t brushId = 0; if (!g_brush_database.upsertBrush(parsed.brush, brushId)) { warnings.push_back("SQLite decorative import failed for brush \"" + parsed.brush.name + "\": " + g_brush_database.getLastError()); return false; } if (!StoreDecorativeBrushContent(parsed, brushId, warnings)) { return false; } if (!g_brush_database.replaceBrushLinks(brushId, {})) { warnings.push_back("SQLite decorative link cleanup failed for brush \"" + parsed.brush.name + "\": " + g_brush_database.getLastError()); return false; } } return true; } } // namespace Materials::Materials() { //// } Materials::~Materials() { clear(); } void Materials::clear() { for (TilesetContainer::iterator iter = tilesets.begin(); iter != tilesets.end(); ++iter) { delete iter->second; } tilesets.clear(); } bool Materials::initializeBrushDatabase(wxArrayString &warnings) { if (!g_settings.getBoolean(Config::USE_SQLITE_MATERIALS)) { return true; } const wxString databasePath = GUI::GetDataDirectory() + "materials/materials.db"; if (!g_brush_database.initialize(databasePath)) { warnings.push_back("SQLite brush database initialization failed: " + g_brush_database.getLastError()); return false; } if (!g_brush_database.testDatabaseConnection()) { warnings.push_back("SQLite brush database connection test failed: " + g_brush_database.getLastError()); return false; } spdlog::info("SQLite brush backend is enabled at {}", databasePath.ToStdString()); return true; } bool Materials::shouldSkipSqliteBootstrapImports(bool &outSkip, wxString &reason) { outSkip = false; reason.clear(); if (!g_settings.getBoolean(Config::USE_SQLITE_MATERIALS)) { return true; } if (!g_brush_database.isOpen()) { reason = "SQLite brush database is not open."; return false; } if (!g_brush_database.hasCompleteImportForCurrentSchema(outSkip)) { reason = g_brush_database.getLastError(); return false; } if (outSkip) { reason = wxString::Format("SQLite materials import skipped: materials.db already matches schema version %d and contains imported data.", g_brush_database.getExpectedSchemaVersion()); } else { reason = wxString::Format("SQLite materials import required: materials.db is missing imported data for schema version %d.", g_brush_database.getExpectedSchemaVersion()); } return true; } bool Materials::bootstrapSqliteDatabase(wxString &error, wxArrayString &warnings) { if (!g_settings.getBoolean(Config::USE_SQLITE_MATERIALS)) { return true; } if (!g_brush_database.isOpen()) { error = "SQLite brush database is not open."; return false; } const bool imported = g_brush_database.runInTransaction([&]() { if (!migrateGroundsToSQLite(error, warnings)) { return false; } if (!migrateWallsToSQLite(error, warnings)) { return false; } if (!migrateDecorativeBrushesToSQLite(error, warnings)) { return false; } return migrateTilesetsToSQLite(error, warnings); }); if (!imported && error.IsEmpty()) { error = g_brush_database.getLastError(); } return imported; } bool Materials::migrateSingleBrushToSQLite(const FileName &identifier, const wxString &brushName, wxString &error, wxArrayString &warnings) { if (!g_settings.getBoolean(Config::USE_SQLITE_MATERIALS)) { return true; } if (!g_brush_database.isOpen()) { error = "SQLite brush database is not open."; return false; } BrushRecord brush; std::vector items; std::set visited; if (!FindAndExtractSimpleBrushRecursive(identifier, brushName, brush, items, error, warnings, visited)) { if (error.IsEmpty()) { error = "Brush \"" + brushName + "\" was not found in materials XML includes."; } return false; } int64_t brushId = 0; if (!g_brush_database.upsertBrush(brush, brushId)) { error = g_brush_database.getLastError(); return false; } for (BrushItemRecord &item : items) { item.brushId = brushId; } if (!g_brush_database.replaceBrushItems(brushId, items)) { error = g_brush_database.getLastError(); return false; } spdlog::info("SQLite validation import migrated brush '{}' with {} items", brush.name.ToStdString(), items.size()); return true; } bool Materials::migrateGroundsToSQLite(wxString &error, wxArrayString &warnings) { if (!g_settings.getBoolean(Config::USE_SQLITE_MATERIALS)) { return true; } if (!g_brush_database.isOpen()) { error = "SQLite brush database is not open."; return false; } const FileName bordersFile(g_gui.getFoundDataDirectory() + "materials/borders.xml"); // Ground brushes are not confined to grounds.xml in the current dataset, so import // every ground brush reachable from the shared brushs.xml include tree. const FileName brushsRoot(g_gui.getFoundDataDirectory() + "materials/brushs.xml"); if (!g_brush_database.deleteBrushesByType("ground")) { error = g_brush_database.getLastError(); return false; } if (!g_brush_database.deleteBorderSetsByScope("global")) { error = g_brush_database.getLastError(); return false; } std::set visited; if (!ImportGlobalBordersRecursive(bordersFile, warnings, visited)) { error = "Failed to import border sets into SQLite."; return false; } if (!ImportGroundBrushesFile(brushsRoot, warnings)) { error = "Failed to import ground brushes into SQLite."; return false; } if (!g_brush_database.resolveGroundReferenceNames()) { error = g_brush_database.getLastError(); return false; } spdlog::info("SQLite ground import completed from {}", brushsRoot.GetFullPath().ToStdString()); return true; } bool Materials::migrateWallsToSQLite(wxString &error, wxArrayString &warnings) { if (!g_settings.getBoolean(Config::USE_SQLITE_MATERIALS)) { return true; } if (!g_brush_database.isOpen()) { error = "SQLite brush database is not open."; return false; } // Wall brushes are not confined to walls.xml in the current dataset, so import // every wall brush reachable from the shared brushs.xml include tree. const FileName brushsRoot(g_gui.getFoundDataDirectory() + "materials/brushs.xml"); if (!g_brush_database.deleteBrushesByType("wall")) { error = g_brush_database.getLastError(); return false; } if (!ImportWallBrushesFile(brushsRoot, warnings)) { error = "Failed to import wall brushes into SQLite."; return false; } if (!g_brush_database.resolveGroundReferenceNames()) { error = g_brush_database.getLastError(); return false; } spdlog::info("SQLite wall import completed from {}", brushsRoot.GetFullPath().ToStdString()); return true; } bool Materials::migrateDecorativeBrushesToSQLite(wxString &error, wxArrayString &warnings) { if (!g_settings.getBoolean(Config::USE_SQLITE_MATERIALS)) { return true; } if (!g_brush_database.isOpen()) { error = "SQLite brush database is not open."; return false; } const FileName brushsRoot(g_gui.getFoundDataDirectory() + "materials/brushs.xml"); if (!g_brush_database.deleteBrushesByType("doodad")) { error = g_brush_database.getLastError(); return false; } if (!g_brush_database.deleteBrushesByType("carpet")) { error = g_brush_database.getLastError(); return false; } if (!g_brush_database.deleteBrushesByType("table")) { error = g_brush_database.getLastError(); return false; } std::set visited; if (!ImportDecorativeBrushesRecursive(brushsRoot, warnings, visited)) { error = "Failed to import doodad/carpet/table brushes into SQLite."; return false; } spdlog::info("SQLite decorative brush import completed from {}", brushsRoot.GetFullPath().ToStdString()); return true; } bool Materials::migrateTilesetsToSQLite(wxString &error, wxArrayString &warnings) { if (!g_settings.getBoolean(Config::USE_SQLITE_MATERIALS)) { return true; } if (!g_brush_database.isOpen()) { error = "SQLite brush database is not open."; return false; } const FileName tilesetsRoot(g_gui.getFoundDataDirectory() + "materials/tilesets.xml"); std::vector tilesetsToStore; std::set visited; if (!ImportTilesetsRecursive(tilesetsRoot, warnings, visited, tilesetsToStore)) { error = "Failed to import tilesets into SQLite."; return false; } if (!g_brush_database.replaceAllTilesets(tilesetsToStore)) { error = g_brush_database.getLastError(); return false; } spdlog::info("SQLite tileset import completed from {}", tilesetsRoot.GetFullPath().ToStdString()); return true; } bool Materials::loadMaterials(const FileName &identifier, wxString &error, wxArrayString &warnings) { pugi::xml_document doc; pugi::xml_parse_result result = doc.load_file(identifier.GetFullPath().mb_str()); if (!result) { warnings.push_back("Could not open " + identifier.GetFullName() + " (file not found or syntax error)"); return false; } pugi::xml_node node = doc.child("materials"); if (!node) { warnings.push_back(identifier.GetFullName() + ": Invalid rootheader."); return false; } unserializeMaterials(identifier, node, error, warnings); return true; } bool Materials::unserializeMaterials(const FileName &filename, pugi::xml_node node, wxString &error, wxArrayString &warnings) { wxString warning; pugi::xml_attribute attribute; for (pugi::xml_node childNode = node.first_child(); childNode; childNode = childNode.next_sibling()) { const std::string &childName = as_lower_str(childNode.name()); if (childName == "include") { if (!(attribute = childNode.attribute("file"))) { continue; } FileName includeName; includeName.SetPath(filename.GetPath()); includeName.SetName(wxString(attribute.as_string(), wxConvUTF8)); wxString subError; if (!loadMaterials(includeName, subError, warnings)) { warnings.push_back("Error while loading file \"" + includeName.GetFullName() + "\": " + subError); } } else if (childName == "metaitem") { g_items.loadMetaItem(childNode); } else if (childName == "border") { g_brushes.unserializeBorder(childNode, warnings); if (warning.size()) { warnings.push_back("materials.xml: " + warning); } } else if (childName == "brush") { g_brushes.unserializeBrush(childNode, warnings); if (warning.size()) { warnings.push_back("materials.xml: " + warning); } } else if (childName == "tileset") { unserializeTileset(childNode, warnings); } } return true; } void Materials::populateRawBrushes(Tileset* others) { for (int32_t id = 0; id <= g_items.getMaxID(); ++id) { auto type = g_items.getRawItemType(id); if (!type) { continue; } if (!type->isMetaItem()) { Brush* brush; if (type->in_other_tileset) { others->getCategory(TILESET_RAW)->brushlist.push_back(type->raw_brush); continue; } else if (!type->raw_brush) { brush = type->raw_brush = newd RAWBrush(type->id); type->has_raw = true; g_brushes.addBrush(type->raw_brush); } else if (!type->has_raw) { brush = type->raw_brush; } else { continue; } brush->flagAsVisible(); others->getCategory(TILESET_RAW)->brushlist.push_back(type->raw_brush); type->in_other_tileset = true; } } } void Materials::createOtherTileset() { Tileset* others; if (tilesets["Others"] != nullptr) { others = tilesets["Others"]; others->clear(); } else { others = newd Tileset(g_brushes, "Others"); tilesets["Others"] = others; } populateRawBrushes(others); // Clear TILESET_MONSTER categories from previous folder tilesets for (auto &[name, ts] : tilesets) { if (name != "Others" && name != "Monsters") { TilesetCategory* cat = ts->getCategory(TILESET_MONSTER); if (cat) { cat->brushlist.clear(); } } } for (MonsterMap::iterator iter = g_monsters.begin(); iter != g_monsters.end(); ++iter) { MonsterType* type = iter->second; if (type->brush == nullptr) { type->brush = newd MonsterBrush(type); g_brushes.addBrush(type->brush); type->brush->flagAsVisible(); type->in_other_tileset = true; } // Always adds in Others others->getCategory(TILESET_MONSTER)->brushlist.push_back(type->brush); // Adds to the folder tileset if it exists if (!type->folder.empty()) { std::string tsName = type->folder; std::replace(tsName.begin(), tsName.end(), '_', ' '); tsName[0] = std::toupper(static_cast(tsName[0])); if (tsName == "Others") { continue; } Tileset* folderTs; auto it = tilesets.find(tsName); if (it != tilesets.end()) { folderTs = it->second; } else { folderTs = newd Tileset(g_brushes, tsName); tilesets[tsName] = folderTs; } folderTs->getCategory(TILESET_MONSTER)->brushlist.push_back(type->brush); } } } void Materials::createNpcTileset() { Tileset* npcTileset; if (tilesets["NPCs"] != nullptr) { npcTileset = tilesets["NPCs"]; npcTileset->clear(); } else { npcTileset = newd Tileset(g_brushes, "NPCs"); tilesets["NPCs"] = npcTileset; } for (NpcMap::iterator iter = g_npcs.begin(); iter != g_npcs.end(); ++iter) { NpcType* type = iter->second; if (type->in_other_tileset) { npcTileset->getCategory(TILESET_NPC)->brushlist.push_back(type->brush); } else if (type->brush == nullptr) { type->brush = newd NpcBrush(type); g_brushes.addBrush(type->brush); type->brush->flagAsVisible(); type->in_other_tileset = true; npcTileset->getCategory(TILESET_NPC)->brushlist.push_back(type->brush); } } } bool Materials::unserializeTileset(pugi::xml_node node, wxArrayString &warnings) { pugi::xml_attribute attribute; if (!(attribute = node.attribute("name"))) { warnings.push_back("Couldn't read tileset name"); return false; } const std::string &name = attribute.as_string(); Tileset* tileset; auto it = tilesets.find(name); if (it != tilesets.end()) { tileset = it->second; } else { tileset = newd Tileset(g_brushes, name); tilesets.insert(std::make_pair(name, tileset)); } for (pugi::xml_node childNode = node.first_child(); childNode; childNode = childNode.next_sibling()) { tileset->loadCategory(childNode, warnings); } return true; } void Materials::addToTileset(std::string tilesetName, int itemId, TilesetCategoryType categoryType) { ItemType &it = g_items[itemId]; if (it.id == 0) { return; } Tileset* tileset; auto _it = tilesets.find(tilesetName); if (_it != tilesets.end()) { tileset = _it->second; } else { tileset = newd Tileset(g_brushes, tilesetName); tilesets.insert(std::make_pair(tilesetName, tileset)); } TilesetCategory* category = tileset->getCategory(categoryType); if (!it.isMetaItem()) { Brush* brush; if (it.in_other_tileset) { category->brushlist.push_back(it.raw_brush); return; } else if (it.raw_brush == nullptr) { brush = it.raw_brush = newd RAWBrush(it.id); it.has_raw = true; g_brushes.addBrush(it.raw_brush); } else { brush = it.raw_brush; } brush->flagAsVisible(); category->brushlist.push_back(it.raw_brush); it.in_other_tileset = true; } } bool Materials::isInTileset(Item* item, std::string tilesetName) const { const ItemType &type = g_items.getItemType(item->getID()); return type.id != 0 && (isInTileset(type.brush, tilesetName) || isInTileset(type.doodad_brush, tilesetName) || isInTileset(type.raw_brush, tilesetName)); } bool Materials::isInTileset(Brush* brush, std::string tilesetName) const { if (!brush) { return false; } TilesetContainer::const_iterator tilesetiter = tilesets.find(tilesetName); if (tilesetiter == tilesets.end()) { return false; } Tileset* tileset = tilesetiter->second; return tileset->containsBrush(brush); }