From 2d255afa6ce8f556926b64c6e7ce24bc370b9948 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 13:29:54 +0100 Subject: [PATCH] Document avatar manager jobs and Lights Out behavior --- tools/chatgui/src/avatars/avatarmanager.h | 110 ++++++++++++++++------ tools/lightsout/src/boardwidget.h | 26 ++--- tools/lightsout/src/mainwindow.h | 13 ++- 3 files changed, 106 insertions(+), 43 deletions(-) diff --git a/tools/chatgui/src/avatars/avatarmanager.h b/tools/chatgui/src/avatars/avatarmanager.h index 62a80ab0..75516f22 100644 --- a/tools/chatgui/src/avatars/avatarmanager.h +++ b/tools/chatgui/src/avatars/avatarmanager.h @@ -1,5 +1,7 @@ -/* Один менеджер на аккаунт: подписанные ссылки, общий кеш и ограниченная очередь передач. - * Методы/сигналы — GUI-поток; DbManager заимствуется и должен переживать менеджер. */ +/* Один активный менеджер на аккаунт: ссылки из таблиц участников, кеш и не более двух активных Job. + * Методы/сигналы — GUI-поток, файловые операции — последовательный worker; запросы ядру идут через gui_bridge. + * DbManager заимствуется и должен переживать менеджер. После открытия БД вызвать initialize, + * передавать события участников/member_files/avatar_result и реагировать на сигналы обновления. */ #pragma once #include "avatarcache.h" #include @@ -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 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 m_refs; - QHash> m_servers; - QCache m_images{32 * 1024}; - QSet m_loading, m_missing, m_wanted; - QHash m_retry; - QHash m_failures; - QQueue m_queue; - QHash m_jobs; - QHash m_records; - Ref m_galleryRef; - QStringList m_gallery; - QMap 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 m_refs; /* Контекст группы/участника -> последняя ссылка. */ + QHash> m_servers; /* Группа -> storage-участники и их имена. */ + QCache m_images{32 * 1024}; /* Владеет QImage, бюджет 32 MiB с учётом стоимости в KiB. */ + QSet m_loading, m_missing, m_wanted; /* Первые два — ключи изображений; wanted — контексты вызовов image. */ + QHash m_retry; /* Контекст upload или hash[:original] -> срок повтора, epoch ms. */ + QHash m_failures; /* Контекст -> число ошибок, ограниченное шестью. */ + QQueue m_queue; /* Ожидание свободного слота, без отдельного лимита длины. */ + QHash m_jobs; /* request_id -> активное задание, включая ожидание worker. */ + QHash m_records; /* Хеш манифеста -> проверенное описание объектов. */ + Ref m_galleryRef; /* Контекст и текущая ссылка участника при открытии галереи. */ + QStringList m_gallery; /* Хеши истории в порядке показа. */ + QMap 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 и очередь. */ }; diff --git a/tools/lightsout/src/boardwidget.h b/tools/lightsout/src/boardwidget.h index 220b0fd0..d5736d6f 100644 --- a/tools/lightsout/src/boardwidget.h +++ b/tools/lightsout/src/boardwidget.h @@ -1,18 +1,20 @@ +/* Игровое поле Lights Out, все вызовы — GUI-поток. После создания вызвать newGame(3..8). + * Хранит real-биты и показывает XOR клетки с существующими ортогональными соседями. + * Клик меняет один real-бит и тем самым отображаемый крест; игра не блокируется после победы. */ #ifndef LIGHTSOUT_BOARDWIDGET_H #define LIGHTSOUT_BOARDWIDGET_H #include #include -// Виджет игрового поля: хранит истинные состояния клеток (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 m_real; // истинное состояние клеток - bool m_hintEnabled = false; // показывать подсказку - int m_moves = 0; // число ходов + int m_size = 0; // 0 до newGame; затем размер поля, заданный вызывающим кодом (3..8). + QVector m_real; // size*size битов 0/1, индекс row*size+col. + bool m_hintEnabled = false; // Зелёные точки на real == 0, сохраняется между партиями. + int m_moves = 0; // Число переключений после newGame, генерация поля не учитывается. }; #endif // LIGHTSOUT_BOARDWIDGET_H diff --git a/tools/lightsout/src/mainwindow.h b/tools/lightsout/src/mainwindow.h index 3bd7832a..e2514476 100644 --- a/tools/lightsout/src/mainwindow.h +++ b/tools/lightsout/src/mainwindow.h @@ -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