diff --git a/cockatrice/src/interface/deck_sync/deck_sync_manager.cpp b/cockatrice/src/interface/deck_sync/deck_sync_manager.cpp index e32beada8..eb4987ba9 100644 --- a/cockatrice/src/interface/deck_sync/deck_sync_manager.cpp +++ b/cockatrice/src/interface/deck_sync/deck_sync_manager.cpp @@ -135,6 +135,9 @@ void DeckSyncManager::collectServerFiles(const ServerInfo_DeckStorage_Folder &fo { for (const auto &item : folder.items()) { const QString name = QString::fromStdString(item.name()); + if (name.isEmpty() || name == "." || name == ".." || name.contains('/') || name.contains('\\')) { + continue; + } if (item.has_folder()) { const QString path = prefix.isEmpty() ? name : prefix + "/" + name; dirs.insert(path); @@ -173,8 +176,37 @@ void DeckSyncManager::onListFinished(const Response &response, const CommandCont sidecar.load(); const qint64 nowSecs = QDateTime::currentDateTime().toSecsSinceEpoch(); + QMap serverPathById; + QMap serverPathByGuid; + for (auto sit = serverFiles.begin(); sit != serverFiles.end(); ++sit) { + serverPathById.insert(sit.value().id, sit.key()); + if (!sit.value().guid.isEmpty()) { + serverPathByGuid.insert(sit.value().guid, sit.key()); + } + } + QSet matchedServerPaths; + for (auto it = localFiles.begin(); it != localFiles.end(); ++it) { - const QString serverPath = serverPathFor(it.key()); + const DeckSyncRecord sidecarRecord = sidecar.get(it.key()); + QString serverPath; + if (sidecarRecord.remoteId != -1 && serverPathById.contains(sidecarRecord.remoteId)) { + serverPath = serverPathById.value(sidecarRecord.remoteId); + } else if (!sidecarRecord.guid.isEmpty() && serverPathByGuid.contains(sidecarRecord.guid)) { + serverPath = serverPathByGuid.value(sidecarRecord.guid); + } else { + serverPath = serverPathFor(it.key()); + if (!serverFiles.contains(serverPath)) { + UploadOp op; + op.relativePath = it.key(); + op.serverPath = serverPath; + op.deckId = -1; + op.guid = sidecar.contains(it.key()) && !sidecar.get(it.key()).guid.isEmpty() + ? sidecar.get(it.key()).guid + : QUuid::createUuid().toString(QUuid::WithoutBraces); + pendingUploads.append(op); + continue; + } + } auto serverIt = serverFiles.find(serverPath); if (serverIt == serverFiles.end()) { UploadOp op; @@ -187,6 +219,7 @@ void DeckSyncManager::onListFinished(const Response &response, const CommandCont pendingUploads.append(op); continue; } + matchedServerPaths.insert(serverIt.key()); if (it.value().hash == serverIt.value().hash) { DeckSyncRecord record = sidecar.get(it.key()); if (record.guid.isEmpty()) { @@ -222,6 +255,9 @@ void DeckSyncManager::onListFinished(const Response &response, const CommandCont } for (auto it = serverFiles.begin(); it != serverFiles.end(); ++it) { + if (matchedServerPaths.contains(it.key())) { + continue; + } const QString wantedPath = it.key().endsWith(".cod", Qt::CaseInsensitive) ? it.key() : it.key() + ".cod"; if (localFiles.contains(wantedPath) || QFileInfo(deckPath + "/" + wantedPath).exists()) { continue; @@ -455,7 +491,13 @@ void DeckSyncManager::onDownloadFinished(const Response &response, return; } - const QString absolutePath = deckPath + "/" + relativePath; + const QString cleanDeckPath = QDir::cleanPath(deckPath); + const QString absolutePath = QDir::cleanPath(deckPath + "/" + relativePath); + if (absolutePath != cleanDeckPath && !absolutePath.startsWith(cleanDeckPath + "/")) { + ++skippedCount; + processNext(); + return; + } if (localFiles.contains(relativePath)) { const QString backupPath = absolutePath + BACKUP_SUFFIX + QString::number(QDateTime::currentDateTime().toSecsSinceEpoch()); diff --git a/cockatrice/src/interface/deck_sync/deck_sync_sidecar.cpp b/cockatrice/src/interface/deck_sync/deck_sync_sidecar.cpp index 861149464..2e3a5ba49 100644 --- a/cockatrice/src/interface/deck_sync/deck_sync_sidecar.cpp +++ b/cockatrice/src/interface/deck_sync/deck_sync_sidecar.cpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace { @@ -64,12 +65,12 @@ bool DeckSyncSidecar::save() const root.insert("records", records); QDir().mkpath(QFileInfo(filePath).absolutePath()); - QFile file(filePath); - if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + QSaveFile file(filePath); + if (!file.open(QIODevice::WriteOnly)) { return false; } file.write(QJsonDocument(root).toJson(QJsonDocument::Compact)); - return true; + return file.commit(); } bool DeckSyncSidecar::contains(const QString &relativePath) const diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_diff.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_diff.cpp index 31a6b622b..b918dd9ee 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_diff.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_diff.cpp @@ -13,12 +13,36 @@ namespace { using ZoneCardKey = QPair; +struct ZoneCardInfo +{ + int count = 0; + QString cardSetShortName; + QString cardSetNumber; + QString cardProviderId; +}; + +QMap zoneCardInfos(const DeckList &deck) +{ + QMap infos; + deck.forEachCard([&infos](InnerDecklistNode *zone, DecklistCardNode *card) { + ZoneCardInfo &info = infos[{zone->getName(), card->getName()}]; + if (info.count == 0) { + info.cardSetShortName = card->getCardSetShortName(); + info.cardSetNumber = card->getCardCollectorNumber(); + info.cardProviderId = card->getCardProviderId(); + } + info.count += card->getNumber(); + }); + return infos; +} + QMap zoneCardCounts(const DeckList &deck) { QMap counts; - deck.forEachCard([&counts](InnerDecklistNode *zone, DecklistCardNode *card) { - counts[{zone->getName(), card->getName()}] += card->getNumber(); - }); + const QMap infos = zoneCardInfos(deck); + for (auto it = infos.begin(); it != infos.end(); ++it) { + counts.insert(it.key(), it.value().count); + } return counts; } @@ -34,17 +58,25 @@ bool changeLessThan(const DeckCardChange &a, const DeckCardChange &b) DeckDiffResult DeckDiff::diff(const DeckList &oldDeck, const DeckList &newDeck) { const QMap oldCounts = zoneCardCounts(oldDeck); - const QMap newCounts = zoneCardCounts(newDeck); + const QMap newInfos = zoneCardInfos(newDeck); QSet keys(oldCounts.keyBegin(), oldCounts.keyEnd()); - keys.unite(QSet(newCounts.keyBegin(), newCounts.keyEnd())); + keys.unite(QSet(newInfos.keyBegin(), newInfos.keyEnd())); DeckDiffResult result; for (const ZoneCardKey &key : keys) { const int oldCount = oldCounts.value(key, 0); - const int newCount = newCounts.value(key, 0); - if (oldCount != newCount) { - result.changes.append({key.first, key.second, oldCount, newCount}); + const ZoneCardInfo newInfo = newInfos.value(key, ZoneCardInfo()); + if (oldCount != newInfo.count) { + DeckCardChange change; + change.zone = key.first; + change.cardName = key.second; + change.oldCount = oldCount; + change.newCount = newInfo.count; + change.cardSetShortName = newInfo.cardSetShortName; + change.cardSetNumber = newInfo.cardSetNumber; + change.cardProviderId = newInfo.cardProviderId; + result.changes.append(change); } } std::sort(result.changes.begin(), result.changes.end(), changeLessThan); @@ -69,21 +101,41 @@ DeckList DeckDiff::merge(const DeckList &baseDeck, const DeckDiffResult &localChanges, const DeckDiffResult &remoteChanges) { QMap counts = zoneCardCounts(baseDeck); + QMap printings; for (const DeckCardChange &change : localChanges.changes) { counts[{change.zone, change.cardName}] = change.newCount; + printings[{change.zone, change.cardName}] = change; } for (const DeckCardChange &change : remoteChanges.changes) { counts[{change.zone, change.cardName}] = change.newCount; + printings[{change.zone, change.cardName}] = change; } - DeckList merged(baseDeck.getMetadata(), DecklistNodeTree(), baseDeck.getSideboardPlans()); - QList keys = counts.keys(); - std::sort(keys.begin(), keys.end()); - for (const ZoneCardKey &key : keys) { - const int count = qMax(0, counts.value(key)); - for (int i = 0; i < count; ++i) { - merged.addCard(key.second, key.first); + DeckList merged = baseDeck; + QMap> nodesByKey; + merged.forEachCard([&nodesByKey](InnerDecklistNode *zone, DecklistCardNode *card) { + nodesByKey[{zone->getName(), card->getName()}].append(card); + }); + + for (auto it = counts.begin(); it != counts.end(); ++it) { + const ZoneCardKey &key = it.key(); + const int target = qMax(0, it.value()); + QList nodes = nodesByKey.value(key); + if (target == 0) { + for (DecklistCardNode *node : nodes) { + merged.getTree()->deleteNode(node); + } + } else if (nodes.isEmpty()) { + const DeckCardChange printing = printings.value(key); + merged.getTree()->addCard(printing.cardName, target, printing.zone, -1, printing.cardSetShortName, + printing.cardSetNumber, printing.cardProviderId, true); + } else { + nodes.first()->setNumber(target); + for (int i = 1; i < nodes.size(); ++i) { + merged.getTree()->deleteNode(nodes.at(i)); + } } } + merged.refreshDeckHash(); return merged; } diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_diff.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_diff.h index 2b4fb8732..563b54b70 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_diff.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_diff.h @@ -14,10 +14,13 @@ class DeckList; /** @brief A single card-count change within one zone. */ struct DeckCardChange { - QString zone; ///< Zone name, e.g. "main". - QString cardName; ///< Card name. - int oldCount = 0; ///< Copies in the old deck. - int newCount = 0; ///< Copies in the new deck. + QString zone; ///< Zone name, e.g. "main". + QString cardName; ///< Card name. + int oldCount = 0; ///< Copies in the old deck. + int newCount = 0; ///< Copies in the new deck. + QString cardSetShortName; ///< Printing set carried from the new side, empty when unchanged. + QString cardSetNumber; ///< Printing collector number carried from the new side. + QString cardProviderId; ///< Printing provider id carried from the new side. /** @brief Copies added (positive) or removed (negative). */ [[nodiscard]] int delta() const { @@ -40,9 +43,9 @@ struct DeckDiffResult * @class DeckDiff * @brief Pure card-count diff and three-way merge for deck sync. * - * Compares card names and copy counts per zone only; printing information - * (set, collector number, provider) is not tracked, so a merged deck keeps - * base printings with merged counts. + * Compares card names and copy counts per zone only; a merged deck keeps + * base printings for existing cards and carries the changed side's printing + * for newly added cards. */ class DeckDiff {