mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-08 19:13:25 -08:00
Address review findings
This commit is contained in:
parent
997eee835e
commit
6eef7a6849
5 changed files with 95 additions and 22 deletions
|
|
@ -116,6 +116,7 @@ void DeckSyncManager::syncNow()
|
||||||
skippedCount = 0;
|
skippedCount = 0;
|
||||||
pendingUploads.clear();
|
pendingUploads.clear();
|
||||||
pendingDownloads.clear();
|
pendingDownloads.clear();
|
||||||
|
plannedLocalPaths.clear();
|
||||||
ensuredDirs.clear();
|
ensuredDirs.clear();
|
||||||
ensuredDirs.insert(QString());
|
ensuredDirs.insert(QString());
|
||||||
localFiles.clear();
|
localFiles.clear();
|
||||||
|
|
@ -188,6 +189,10 @@ void DeckSyncManager::onListFinished(const Response &response, const CommandCont
|
||||||
if (!syncing) {
|
if (!syncing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (response.response_code() == Response::RespNotConnected) {
|
||||||
|
failSync(tr("Server connection lost."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (response.response_code() != Response::RespOk) {
|
if (response.response_code() != Response::RespOk) {
|
||||||
failSync(tr("Could not read server decks."));
|
failSync(tr("Could not read server decks."));
|
||||||
return;
|
return;
|
||||||
|
|
@ -219,6 +224,24 @@ void DeckSyncManager::onListFinished(const Response &response, const CommandCont
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
QSet<int> matchedServerIds;
|
QSet<int> matchedServerIds;
|
||||||
|
QMap<QString, int> identityByLocal;
|
||||||
|
|
||||||
|
// Pass 1: claim every row a sidecar names before any guess runs. In a single
|
||||||
|
// pass a content or path match could take the row another local file still
|
||||||
|
// identifies by id or guid, turning an unchanged deck into a duplicate.
|
||||||
|
for (auto it = localFiles.begin(); it != localFiles.end(); ++it) {
|
||||||
|
const DeckSyncRecord sidecarRecord = sidecar.get(it.key());
|
||||||
|
int serverId = -1;
|
||||||
|
if (sidecarRecord.remoteId != -1 && serverFiles.contains(sidecarRecord.remoteId)) {
|
||||||
|
serverId = sidecarRecord.remoteId;
|
||||||
|
} else if (!sidecarRecord.guid.isEmpty() && serverIdByGuid.contains(sidecarRecord.guid)) {
|
||||||
|
serverId = serverIdByGuid.value(sidecarRecord.guid);
|
||||||
|
}
|
||||||
|
if (serverId != -1 && !matchedServerIds.contains(serverId)) {
|
||||||
|
matchedServerIds.insert(serverId);
|
||||||
|
identityByLocal.insert(it.key(), serverId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// A local file without a usable sidecar record still usually has a server
|
// A local file without a usable sidecar record still usually has a server
|
||||||
// counterpart: rows are named after the deck, so compare content rather than
|
// counterpart: rows are named after the deck, so compare content rather than
|
||||||
|
|
@ -235,14 +258,10 @@ void DeckSyncManager::onListFinished(const Response &response, const CommandCont
|
||||||
return -1;
|
return -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Pass 2: resolve the remaining files by content, then path, and queue work.
|
||||||
for (auto it = localFiles.begin(); it != localFiles.end(); ++it) {
|
for (auto it = localFiles.begin(); it != localFiles.end(); ++it) {
|
||||||
const DeckSyncRecord sidecarRecord = sidecar.get(it.key());
|
int serverId = identityByLocal.value(it.key(), -1);
|
||||||
int serverId = -1;
|
if (serverId == -1) {
|
||||||
if (sidecarRecord.remoteId != -1 && serverFiles.contains(sidecarRecord.remoteId)) {
|
|
||||||
serverId = sidecarRecord.remoteId;
|
|
||||||
} else if (!sidecarRecord.guid.isEmpty() && serverIdByGuid.contains(sidecarRecord.guid)) {
|
|
||||||
serverId = serverIdByGuid.value(sidecarRecord.guid);
|
|
||||||
} else {
|
|
||||||
// Content first: a row with the same deck is unambiguous, while a
|
// Content first: a row with the same deck is unambiguous, while a
|
||||||
// matching file name may belong to a different deck entirely.
|
// matching file name may belong to a different deck entirely.
|
||||||
serverId = unmatchedByHash(it.value().hash);
|
serverId = unmatchedByHash(it.value().hash);
|
||||||
|
|
@ -310,6 +329,7 @@ void DeckSyncManager::onListFinished(const Response &response, const CommandCont
|
||||||
++skippedCount;
|
++skippedCount;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
plannedLocalPaths.insert(relativePath);
|
||||||
pendingDownloads.append(DownloadOp{.relativePath = relativePath,
|
pendingDownloads.append(DownloadOp{.relativePath = relativePath,
|
||||||
.deckId = it.value().id,
|
.deckId = it.value().id,
|
||||||
.guid = it.value().guid,
|
.guid = it.value().guid,
|
||||||
|
|
@ -381,6 +401,10 @@ void DeckSyncManager::onEnsureDirFinished(const Response &response,
|
||||||
if (!syncing) {
|
if (!syncing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (response.response_code() == Response::RespNotConnected) {
|
||||||
|
failSync(tr("Server connection lost."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (response.response_code() != Response::RespOk) {
|
if (response.response_code() != Response::RespOk) {
|
||||||
failSync(tr("Could not create server folder."));
|
failSync(tr("Could not create server folder."));
|
||||||
return;
|
return;
|
||||||
|
|
@ -408,7 +432,9 @@ void DeckSyncManager::sendUpload(const UploadOp &op)
|
||||||
processNext();
|
processNext();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const QString deckString = loaded->deckList.writeToString_Native();
|
// Collection sync compares and uploads the same timestamp-free string, so the
|
||||||
|
// server's contentHash() of what it stores matches getContentHash() locally.
|
||||||
|
const QString deckString = loaded->deckList.writeToString_Native(false);
|
||||||
if (deckString.length() > MAX_FILE_LENGTH) {
|
if (deckString.length() > MAX_FILE_LENGTH) {
|
||||||
++skippedCount;
|
++skippedCount;
|
||||||
processNext();
|
processNext();
|
||||||
|
|
@ -439,6 +465,10 @@ void DeckSyncManager::onUploadFinished(const Response &response,
|
||||||
if (!syncing) {
|
if (!syncing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (response.response_code() == Response::RespNotConnected) {
|
||||||
|
failSync(tr("Server connection lost."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const QString relativePath = extraData.toString();
|
const QString relativePath = extraData.toString();
|
||||||
if (response.response_code() != Response::RespOk) {
|
if (response.response_code() != Response::RespOk) {
|
||||||
++skippedCount;
|
++skippedCount;
|
||||||
|
|
@ -477,7 +507,12 @@ void DeckSyncManager::onUploadFinished(const Response &response,
|
||||||
|
|
||||||
QString DeckSyncManager::uniqueLocalPath(const QString &relativePath) const
|
QString DeckSyncManager::uniqueLocalPath(const QString &relativePath) const
|
||||||
{
|
{
|
||||||
if (!QFileInfo(deckPath + "/" + relativePath).exists()) {
|
// Planned paths are not on disk yet, so the set keeps two downloads from
|
||||||
|
// being queued into the same file during one run.
|
||||||
|
auto isTaken = [this](const QString &path) {
|
||||||
|
return plannedLocalPaths.contains(path) || QFileInfo(deckPath + "/" + path).exists();
|
||||||
|
};
|
||||||
|
if (!isTaken(relativePath)) {
|
||||||
return relativePath;
|
return relativePath;
|
||||||
}
|
}
|
||||||
QString base = relativePath;
|
QString base = relativePath;
|
||||||
|
|
@ -486,7 +521,7 @@ QString DeckSyncManager::uniqueLocalPath(const QString &relativePath) const
|
||||||
}
|
}
|
||||||
for (int i = 2; i < 1000; ++i) {
|
for (int i = 2; i < 1000; ++i) {
|
||||||
const QString candidate = base + " (" + QString::number(i) + ").cod";
|
const QString candidate = base + " (" + QString::number(i) + ").cod";
|
||||||
if (!QFileInfo(deckPath + "/" + candidate).exists()) {
|
if (!isTaken(candidate)) {
|
||||||
return candidate;
|
return candidate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -512,12 +547,18 @@ void DeckSyncManager::onDownloadFinished(const Response &response,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const QString relativePath = extraData.toString();
|
const QString relativePath = extraData.toString();
|
||||||
|
int deckId = -1;
|
||||||
for (int i = 0; i < pendingDownloads.size(); ++i) {
|
for (int i = 0; i < pendingDownloads.size(); ++i) {
|
||||||
if (pendingDownloads.at(i).relativePath == relativePath) {
|
if (pendingDownloads.at(i).relativePath == relativePath) {
|
||||||
|
deckId = pendingDownloads.at(i).deckId;
|
||||||
pendingDownloads.removeAt(i);
|
pendingDownloads.removeAt(i);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (response.response_code() == Response::RespNotConnected) {
|
||||||
|
failSync(tr("Server connection lost."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (response.response_code() != Response::RespOk) {
|
if (response.response_code() != Response::RespOk) {
|
||||||
++skippedCount;
|
++skippedCount;
|
||||||
processNext();
|
processNext();
|
||||||
|
|
@ -558,6 +599,9 @@ void DeckSyncManager::onDownloadFinished(const Response &response,
|
||||||
DeckSyncSidecar sidecar(sidecarPath);
|
DeckSyncSidecar sidecar(sidecarPath);
|
||||||
sidecar.load();
|
sidecar.load();
|
||||||
DeckSyncRecord record = sidecar.get(relativePath);
|
DeckSyncRecord record = sidecar.get(relativePath);
|
||||||
|
if (deckId != -1) {
|
||||||
|
record.remoteId = deckId;
|
||||||
|
}
|
||||||
if (!download.guid().empty()) {
|
if (!download.guid().empty()) {
|
||||||
record.guid = QString::fromStdString(download.guid());
|
record.guid = QString::fromStdString(download.guid());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,7 @@ private:
|
||||||
QString sidecarPath;
|
QString sidecarPath;
|
||||||
QMap<QString, LocalFile> localFiles;
|
QMap<QString, LocalFile> localFiles;
|
||||||
QSet<QString> ensuredDirs;
|
QSet<QString> ensuredDirs;
|
||||||
|
QSet<QString> plannedLocalPaths; ///< Download targets reserved during this run, not yet on disk.
|
||||||
QList<UploadOp> pendingUploads;
|
QList<UploadOp> pendingUploads;
|
||||||
QList<DownloadOp> pendingDownloads;
|
QList<DownloadOp> pendingDownloads;
|
||||||
int uploadedCount = 0;
|
int uploadedCount = 0;
|
||||||
|
|
|
||||||
|
|
@ -123,9 +123,11 @@ bool DeckList::Metadata::readElement(QXmlStreamReader *xml, const QString &child
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeckList::Metadata::write(QXmlStreamWriter *xml) const
|
void DeckList::Metadata::write(QXmlStreamWriter *xml, bool includeLastLoadedTimestamp) const
|
||||||
{
|
{
|
||||||
xml->writeTextElement("lastLoadedTimestamp", lastLoadedTimestamp);
|
if (includeLastLoadedTimestamp) {
|
||||||
|
xml->writeTextElement("lastLoadedTimestamp", lastLoadedTimestamp);
|
||||||
|
}
|
||||||
xml->writeTextElement("deckname", name);
|
xml->writeTextElement("deckname", name);
|
||||||
xml->writeTextElement("format", gameFormat);
|
xml->writeTextElement("format", gameFormat);
|
||||||
xml->writeStartElement("bannerCard");
|
xml->writeStartElement("bannerCard");
|
||||||
|
|
@ -203,12 +205,12 @@ bool DeckList::readElement(QXmlStreamReader *xml)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeckList::write(QXmlStreamWriter *xml) const
|
void DeckList::write(QXmlStreamWriter *xml, bool includeLastLoadedTimestamp) const
|
||||||
{
|
{
|
||||||
xml->writeStartElement("cockatrice_deck");
|
xml->writeStartElement("cockatrice_deck");
|
||||||
xml->writeAttribute("version", "1");
|
xml->writeAttribute("version", "1");
|
||||||
|
|
||||||
metadata.write(xml);
|
metadata.write(xml, includeLastLoadedTimestamp);
|
||||||
|
|
||||||
// Write zones
|
// Write zones
|
||||||
tree.write(xml);
|
tree.write(xml);
|
||||||
|
|
@ -270,12 +272,12 @@ bool DeckList::loadFromString_Native(const QString &nativeString)
|
||||||
return loadFromXml(&xml);
|
return loadFromXml(&xml);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString DeckList::writeToString_Native() const
|
QString DeckList::writeToString_Native(bool includeLastLoadedTimestamp) const
|
||||||
{
|
{
|
||||||
QString result;
|
QString result;
|
||||||
QXmlStreamWriter xml(&result);
|
QXmlStreamWriter xml(&result);
|
||||||
xml.writeStartDocument();
|
xml.writeStartDocument();
|
||||||
write(&xml);
|
write(&xml, includeLastLoadedTimestamp);
|
||||||
xml.writeEndDocument();
|
xml.writeEndDocument();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
@ -486,7 +488,7 @@ QString DeckList::contentHash(const QString &nativeDeck)
|
||||||
|
|
||||||
QString DeckList::getContentHash() const
|
QString DeckList::getContentHash() const
|
||||||
{
|
{
|
||||||
return contentHash(writeToString_Native());
|
return contentHash(writeToString_Native(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -95,8 +95,10 @@ public:
|
||||||
* @brief Writes the deck metadata section of a Cockatrice deck XML file.
|
* @brief Writes the deck metadata section of a Cockatrice deck XML file.
|
||||||
*
|
*
|
||||||
* @param xml Writer to append the metadata elements to.
|
* @param xml Writer to append the metadata elements to.
|
||||||
|
* @param includeLastLoadedTimestamp Whether to write lastLoadedTimestamp. Collection sync
|
||||||
|
* excludes it, since it changes every time a deck is opened.
|
||||||
*/
|
*/
|
||||||
void write(QXmlStreamWriter *xml) const;
|
void write(QXmlStreamWriter *xml, bool includeLastLoadedTimestamp = true) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
@ -244,10 +246,15 @@ public:
|
||||||
/** @name Serialization (XML) */
|
/** @name Serialization (XML) */
|
||||||
///@{
|
///@{
|
||||||
bool readElement(QXmlStreamReader *xml);
|
bool readElement(QXmlStreamReader *xml);
|
||||||
void write(QXmlStreamWriter *xml) const;
|
void write(QXmlStreamWriter *xml, bool includeLastLoadedTimestamp = true) const;
|
||||||
bool loadFromXml(QXmlStreamReader *xml);
|
bool loadFromXml(QXmlStreamReader *xml);
|
||||||
bool loadFromString_Native(const QString &nativeString);
|
bool loadFromString_Native(const QString &nativeString);
|
||||||
QString writeToString_Native() const;
|
/**
|
||||||
|
* @brief Serialises the deck to a Cockatrice native XML string.
|
||||||
|
* @param includeLastLoadedTimestamp Whether to write lastLoadedTimestamp. Pass false for
|
||||||
|
* content comparisons, so opening a deck does not change its identity.
|
||||||
|
*/
|
||||||
|
QString writeToString_Native(bool includeLastLoadedTimestamp = true) const;
|
||||||
bool loadFromFile_Native(QIODevice *device);
|
bool loadFromFile_Native(QIODevice *device);
|
||||||
bool saveToFile_Native(QIODevice *device) const;
|
bool saveToFile_Native(QIODevice *device) const;
|
||||||
///@}
|
///@}
|
||||||
|
|
@ -305,7 +312,12 @@ public:
|
||||||
* peers compare the same bytes.
|
* peers compare the same bytes.
|
||||||
*/
|
*/
|
||||||
static QString contentHash(const QString &nativeDeck);
|
static QString contentHash(const QString &nativeDeck);
|
||||||
/** @brief contentHash() of this deck's native serialisation. */
|
/**
|
||||||
|
* @brief contentHash() of this deck's native serialisation, without lastLoadedTimestamp.
|
||||||
|
*
|
||||||
|
* Opening a deck rewrites that timestamp, so hashing it would make every played deck
|
||||||
|
* look modified to the server.
|
||||||
|
*/
|
||||||
QString getContentHash() const;
|
QString getContentHash() const;
|
||||||
void refreshDeckHash();
|
void refreshDeckHash();
|
||||||
///@}
|
///@}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,21 @@ TEST(DeckContentHashTest, MatchesNativeSerialisation)
|
||||||
{
|
{
|
||||||
DeckList deck;
|
DeckList deck;
|
||||||
ASSERT_TRUE(deck.loadFromString_Native(deckWithName(QStringLiteral("Burn"))));
|
ASSERT_TRUE(deck.loadFromString_Native(deckWithName(QStringLiteral("Burn"))));
|
||||||
EXPECT_EQ(deck.getContentHash(), DeckList::contentHash(deck.writeToString_Native()));
|
// Content identity skips lastLoadedTimestamp: opening a deck rewrites it.
|
||||||
|
EXPECT_EQ(deck.getContentHash(), DeckList::contentHash(deck.writeToString_Native(false)));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DeckContentHashTest, IgnoresLastLoadedTimestamp)
|
||||||
|
{
|
||||||
|
DeckList first;
|
||||||
|
DeckList second;
|
||||||
|
ASSERT_TRUE(first.loadFromString_Native(deckWithName(QStringLiteral("Burn"))));
|
||||||
|
ASSERT_TRUE(second.loadFromString_Native(deckWithName(QStringLiteral("Burn"))));
|
||||||
|
first.setLastLoadedTimestamp(QStringLiteral("2026-01-01T00:00:00"));
|
||||||
|
second.setLastLoadedTimestamp(QStringLiteral("2026-11-08T12:34:56"));
|
||||||
|
|
||||||
|
EXPECT_NE(first.writeToString_Native(), second.writeToString_Native());
|
||||||
|
EXPECT_EQ(first.getContentHash(), second.getContentHash());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(DeckContentHashTest, IsStableAcrossLoads)
|
TEST(DeckContentHashTest, IsStableAcrossLoads)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue