Document avatar manager jobs and Lights Out behavior

This commit is contained in:
evgeny committed 2026-10-27 13:29:54 +01:00
1 parent 8691608924
commit 2d255afa6c
3 files changed
+106 -43

No files matched your search

+83 -27
View File
@@ -1,5 +1,7 @@
/* Один менеджер на аккаунт: подписанные ссылки, общий кеш и ограниченная очередь передач.
* Методы/сигналы — GUI-поток; DbManager заимствуется и должен переживать менеджер. */
/* Один активный менеджер на аккаунт: ссылки из таблиц участников, кеш и не более двух активных Job.
* Методы/сигналы — GUI-поток, файловые операции — последовательный worker; запросы ядру идут через gui_bridge.
* DbManager заимствуется и должен переживать менеджер. После открытия БД вызвать initialize,
* передавать события участников/member_files/avatar_result и реагировать на сигналы обновления. */
#pragma once
#include "avatarcache.h"
#include <QObject>
@@ -15,79 +17,133 @@ class DbManager;
class AvatarManager : public QObject {
Q_OBJECT
public:
/* Регистрирует instance() и запускает таймер; db должен оставаться доступным до разрушения менеджера. */
explicit AvatarManager(DbManager* db, QObject* parent = nullptr);
/* Останавливает таймер, ждёт worker и при готовом ядре запрашивает отмену известных m_jobs. */
~AvatarManager() override;
/* Заимствованный указатель на активный менеджер, nullptr вне его времени жизни. */
static AvatarManager* instance();
/* Одноразово готовит кеш аккаунта и читает сохранённый default; до открытия БД/получения myNodeId ничего не делает.
* ready становится true только после успешного worker-результата; ошибка сообщает statusChanged. */
void initialize();
/* При открытой БД и готовом ядре запрашивает полные таблицы участников всех локальных каналов. */
void refreshMembers();
/* channel — десятичный ID; data — count записей CHAT_MEMBER_DISPLAY_SIZE внутри length байтов, заимствуется на вызов.
* full заменяет таблицу группы; false обновляет только переданные записи. Изменения запускают own-upload/pump. */
void onMembers(const QString& channel, int count, const uint8_t* data, int length, bool full = true);
/* Удаляет ссылку/ожидание изображения и роль сервера участника, сообщает changed. */
void removeMember(const QString& channel, quint64 owner);
/* Принимают заимствованные на вызов payload событий gui_bridge: ответ member_files, завершение передачи
* (51 байт, фаза 2) и результат публикации ссылки (17 байт). Чужие/устаревшие ID не обрабатываются. */
void onResponse(const uint8_t* data, int length);
void onTransfer(const uint8_t* data, int length);
void onPublished(const uint8_t* data, int length);
/* Возвращает готовое превью или пустое QImage и планирует чтение/скачивание; готовность сообщает changed.
* channel — десятичный ID; пустой/нечисловой выбирает ссылку owner из группы с наименьшим ID. */
QImage image(const QString& channel, quint64 owner);
/* Объединённая по всем группам карта node_id -> имя участников с storage_role, без проверки их доступности. */
QMap<quint64, QString> servers() const;
/* Начинает worker-подготовку локального source; требует ready, ненулевой server и готовое ядро.
* crop — нормализованный после EXIF квадрат из selectAvatarCrop, пустой — центральный квадрат.
* Пока текущая подготовка занята, повтор игнорируется; clearDefault отменяет поколение и позволяет новый upload.
* Устаревший worker не меняет default/preparing. Из обработчиков сигналов подготовки можно удалить manager. */
void uploadDefault(const QString& source, quint64 server, const QRectF& crop = {});
/* Открывает контекст истории owner, сбрасывая прежний; channel выбирается так же, как в image.
* Сначала выдаёт текущий хеш, затем каталог по убыванию времени файлов (при равенстве — по хешу).
* Не создаёт окно; отсутствие ссылки сообщает через galleryError. */
void openGallery(const QString& channel, quint64 owner);
/* Закрывает каталог и отменяет задания только галереи; загрузки превью, нужные image(), сохраняются. */
void closeGallery();
/* Снимает задержку повтора для изображений галереи и повторяет каталог, если нет активного LIST. */
void retryGallery();
/* Хеши манифестов без .json; текущая ссылка участника присутствует даже при отсутствии в каталоге. */
QStringList gallery() const { return m_gallery; }
/* Хеш ссылки участника при openGallery, а не выбранный пользователем элемент окна. */
QString galleryCurrent() const { return m_galleryRef.hash; }
/* Как image(), но только для хеша из открытой галереи; original запрашивает исходник (декодируется до 2048). */
QImage galleryImage(const QString& hash, bool original = false);
/* При ready и готовом ядре сохраняет отказ от default и планирует очистку своих ссылок в группах.
* Инвалидирует подготовку/старые upload, но не удаляет объекты кеша и серверную историю. */
void clearDefault();
/* Только готовое превью default из RAM; само чтение/скачивание не запускает. */
QImage defaultImage() const;
/* Выбранный сервер default, 0 — default не установлен. */
quint64 defaultServer() const { return m_defaultServer; }
/* Успешная инициализация дискового кеша, независимо от готовности сети/ядра. */
bool ready() const { return m_ready; }
signals:
/* Изменилась ссылка либо доступность её пикселей; получатель повторно вызывает image. */
void changed(const QString& channel, quint64 owner);
/* Обновились серверы либо изображения/default, используемые UI выбора сервера. */
void serversChanged();
/* Текст состояния подготовки, загрузки или ошибки default. */
void statusChanged(const QString& status);
/* Обновился каталог/кеш галереи; получатель повторно читает gallery/galleryImage. */
void galleryChanged();
/* Ошибка каталога/скачивания в открытой галерее; повтор доступен через retryGallery. */
void galleryError(const QString& error);
private:
struct Ref { quint64 group = 0, owner = 0, server = 0; QString hash; };
struct Job {
Ref ref; quint64 id = 0, generation = 0; bool upload = false, original = false, gallery = false;
int stage = 0, object = 0; QString local, file; QStringList files;
struct Ref {
quint64 group = 0, owner = 0, server = 0; /* Группа, участник, storage-сервер (0 — ссылка отсутствует). */
QString hash; /* SHA-256 манифеста без .json. */
};
struct Job {
Ref ref; /* Снимок ссылки операции. */
quint64 id = 0, generation = 0; /* ID текущего шага и поколение default для upload. */
bool upload = false, original = false, gallery = false; /* Направление, исходник вместо превью, контекст галереи. */
int stage = 0, object = 0; /* MF-команда либо 99 transfer/100 publish/101 worker; индекс files. */
QString local, file; /* Локальный путь текущего шага и имя объекта на сервере. */
QStringList files; /* Upload: исходник, превью, манифест в этом порядке. */
};
/* Ключ контекста «десятичная группа:десятичный owner». */
static QString key(quint64 group, quint64 owner);
/* Дедуплицированное чтение кеша в worker с возвратом результата в GUI-поток. */
void loadCache(const QString& hash, bool original = false);
/* Возвращает пиксели из RAM или планирует диск/сеть с учётом backoff и storage_role сервера. */
QImage fetch(const Ref& ref, bool original, bool gallery);
/* Переходит к STAT следующего объекта upload, после последнего публикует ссылку. */
void nextUpload(Job& job);
/* Запрашивает страницу avatars; cursor=0 начинает каталог, version продолжает тот же снимок. */
void listGallery(quint64 cursor = 0, const QByteArray& version = {});
/* Уведомляет UI, использующий hash, включая открытый каталог. */
void notify(const QString& hash);
/* Планирует приведение своей ссылки в группе к выбранному default, если контекст и кеш готовы. */
void scheduleOwn(quint64 group);
/* Убирает дубликаты; upload/исходники ставит в начало очереди, превью — в конец. */
void enqueue(const Job& job);
/* Отбрасывает устаревшие задания и запускает очередь до лимита двух активных Job. */
void pump();
/* Посылает MF-команду с новым request_id; инвалидирует ссылку job в m_jobs. false завершает Job с ошибкой. */
bool post(Job& job, int command);
/* Начинает загрузку/скачивание текущего объекта с ограничением размера. */
void transfer(Job& job);
/* Передаёт свою ссылку (или очистку) chat_core только при актуальном поколении default. */
void publish(Job& job);
/* Удаляет Job, назначает backoff 5..160 секунд при ошибке и откладывает следующий pump в event loop. */
void finish(quint64 id, int status);
/* Сохраняет default в ui_state через ядро: server_hex/hash.json либо «-» для явного отказа. */
void saveDefault();
/* Убирает queued upload и запрашивает отмену активных, кроме уже переданной в chat_core публикации. */
void cancelUploads();
DbManager* m_db;
QString m_directory, m_defaultHash;
quint64 m_defaultServer = 0, m_generation = 1;
bool m_ready = false, m_preparing = false, m_defaultConfigured = false;
QHash<QString, Ref> m_refs;
QHash<quint64, QMap<quint64, QString>> m_servers;
QCache<QString, QImage> m_images{32 * 1024};
QSet<QString> m_loading, m_missing, m_wanted;
QHash<QString, qint64> m_retry;
QHash<QString, unsigned> m_failures;
QQueue<Job> m_queue;
QHash<quint64, Job> m_jobs;
QHash<QString, AvatarCache::Record> m_records;
Ref m_galleryRef;
QStringList m_gallery;
QMap<QString, quint64> m_catalog;
quint64 m_listId = 0;
quint64 m_galleryEpoch = 0, m_listCursor = 0;
unsigned m_listRetries = 0;
QThreadPool m_pool;
QTimer m_timer;
DbManager* m_db; /* Заимствованная БД аккаунта. */
QString m_directory, m_defaultHash; /* Каталог аккаунта и хеш default без суффикса. */
quint64 m_defaultServer = 0, m_generation = 1; /* Сервер default и счётчик его замен/очисток. */
bool m_ready = false, m_preparing = false, m_defaultConfigured = false; /* Кеш готов, worker занят, выбор задан явно. */
QHash<QString, Ref> m_refs; /* Контекст группы/участника -> последняя ссылка. */
QHash<quint64, QMap<quint64, QString>> m_servers; /* Группа -> storage-участники и их имена. */
QCache<QString, QImage> m_images{32 * 1024}; /* Владеет QImage, бюджет 32 MiB с учётом стоимости в KiB. */
QSet<QString> m_loading, m_missing, m_wanted; /* Первые два — ключи изображений; wanted — контексты вызовов image. */
QHash<QString, qint64> m_retry; /* Контекст upload или hash[:original] -> срок повтора, epoch ms. */
QHash<QString, unsigned> m_failures; /* Контекст -> число ошибок, ограниченное шестью. */
QQueue<Job> m_queue; /* Ожидание свободного слота, без отдельного лимита длины. */
QHash<quint64, Job> m_jobs; /* request_id -> активное задание, включая ожидание worker. */
QHash<QString, AvatarCache::Record> m_records; /* Хеш манифеста -> проверенное описание объектов. */
Ref m_galleryRef; /* Контекст и текущая ссылка участника при открытии галереи. */
QStringList m_gallery; /* Хеши истории в порядке показа. */
QMap<QString, quint64> m_catalog; /* Собираемый LIST: хеш -> время файла. */
quint64 m_listId = 0; /* Активный LIST, 0 — запроса нет; не входит в m_jobs. */
quint64 m_galleryEpoch = 0, m_listCursor = 0; /* Поколение контекста и курсор последнего LIST. */
unsigned m_listRetries = 0; /* Повторы каталога после MF_STALE/MF_BUSY, максимум три. */
QThreadPool m_pool; /* Собственный worker, maxThreadCount=1. */
QTimer m_timer; /* Каждые 2 секунды проверяет own-upload, wanted и очередь. */
};
+14 -12
View File
@@ -1,18 +1,20 @@
/* Игровое поле Lights Out, все вызовы — GUI-поток. После создания вызвать newGame(3..8).
* Хранит real-биты и показывает XOR клетки с существующими ортогональными соседями.
* Клик меняет один real-бит и тем самым отображаемый крест; игра не блокируется после победы. */
#ifndef LIGHTSOUT_BOARDWIDGET_H
#define LIGHTSOUT_BOARDWIDGET_H
#include <QWidget>
#include <QVector>
// Виджет игрового поля: хранит истинные состояния клеток (real, 0/1),
// отображает XOR каждой клетки с четырьмя соседями, опционально показывает
// подсказку (точку) на клетках, чьё истинное значение равно 0.
class BoardWidget : public QWidget {
Q_OBJECT
public:
// Создаёт пустой виджет без партии; parent управляет временем жизни.
explicit BoardWidget(QWidget *parent = nullptr);
// Создаёт новую игру: поле size x size, случайная решаемая позиция.
// Начинает size x size, сбрасывает ходы, сохраняет флаг подсказки и испускает stateChanged.
// size должен быть 3..8 (метод не проверяет); позиция решаемая, но может сразу оказаться погашенной.
void newGame(int size);
// Включает/выключает показ подсказки (точка на клетках real == 0).
@@ -25,7 +27,7 @@ public:
int moves() const { return m_moves; }
signals:
// Испускается после каждого хода; solved == true, если все клетки погашены.
// После newGame и каждого хода; solved проверяет отображаемые клетки, а не равенство всех real нулю.
void stateChanged(bool solved);
protected:
@@ -33,22 +35,22 @@ protected:
void mousePressEvent(QMouseEvent *event) override;
private:
// Прямоугольник, занимаемый сеткой клеток (центрированный квадрат).
// Центрированный квадрат: клетки 36 px, промежутки 4 px; не масштабируется под размер виджета.
QRectF boardRect() const;
// Отображаемое значение клетки: XOR её и четырёх соседей.
// XOR клетки row/col и существующих соседей; индексы должны быть внутри поля.
int displayValue(int row, int col) const;
// Переключить клетку (обрабатывает клик), пересчитать и проверить победу.
// Меняет real[row,col], увеличивает moves, обновляет рисунок и испускает stateChanged; индексы внутри поля.
void toggleCell(int row, int col);
// Проверяет, все ли отображаемые клетки погашены.
bool isSolved() const;
int m_size = 0; // размер поля (3..8)
QVector<quint8> m_real; // истинное состояние клеток
bool m_hintEnabled = false; // показывать подсказку
int m_moves = 0; // число ходов
int m_size = 0; // 0 до newGame; затем размер поля, заданный вызывающим кодом (3..8).
QVector<quint8> m_real; // size*size битов 0/1, индекс row*size+col.
bool m_hintEnabled = false; // Зелёные точки на real == 0, сохраняется между партиями.
int m_moves = 0; // Число переключений после newGame, генерация поля не учитывается.
};
#endif // LIGHTSOUT_BOARDWIDGET_H
+9 -4
View File
@@ -1,3 +1,5 @@
/* Главное окно Lights Out: доска, новая партия, подсказка и счётчик ходов.
* Создавать/использовать в GUI-потоке; размер начальной партии задаёт вызывающий код. */
#ifndef LIGHTSOUT_MAINWINDOW_H
#define LIGHTSOUT_MAINWINDOW_H
@@ -6,21 +8,24 @@
class BoardWidget;
class QLabel;
// Главное окно игры: доска, кнопки «Новая игра»/«Подсказка», счётчик ходов.
class MainWindow : public QMainWindow {
Q_OBJECT
public:
// Создаёт дочерние виджеты и начальную партию size x size; требуется size=3..8, проверки здесь нет.
explicit MainWindow(int size, QWidget *parent = nullptr);
private slots:
// Спрашивает размер 3..8; отмена оставляет текущую партию. Новая партия сохраняет настройку подсказки.
void newGame();
// Инвертирует флаг подсказки доски при переключении checkable-кнопки.
void toggleHint();
// Обновляет счётчик; при solved показывает победу, Reset открывает диалог выбора размера новой игры.
void onBoardStateChanged(bool solved);
private:
int m_size;
BoardWidget *m_board;
QLabel *m_movesLabel;
int m_size; // Размер начальной/последней принятой партии.
BoardWidget *m_board; // Доска принадлежит centralWidget окна.
QLabel *m_movesLabel; // Дочерняя метка числа ходов.
};
#endif // LIGHTSOUT_MAINWINDOW_H