From adf32f47275a2bac02034b848215d4413e0f9b37 Mon Sep 17 00:00:00 2001 From: in1tiate <32779090+in1tiate@users.noreply.github.com> Date: Mon, 20 May 2024 08:47:27 -0500 Subject: [PATCH 01/16] Initial slide rebase commit --- src/aoapplication.h | 8 +- src/aolayer.cpp | 52 ++++- src/aolayer.h | 44 ++++- src/courtroom.cpp | 335 +++++++++++++++++++++++++------- src/courtroom.h | 36 +++- src/datatypes.h | 1 + src/options.cpp | 10 + src/options.h | 5 + src/path_functions.cpp | 37 +++- src/text_file_functions.cpp | 19 ++ src/widgets/aooptionsdialog.cpp | 2 + src/widgets/aooptionsdialog.h | 1 + 12 files changed, 450 insertions(+), 100 deletions(-) diff --git a/src/aoapplication.h b/src/aoapplication.h index c2a019148..ccc509d85 100644 --- a/src/aoapplication.h +++ b/src/aoapplication.h @@ -126,7 +126,7 @@ class AOApplication : public QApplication QString get_asset(QString p_element, QString p_theme = QString(), QString p_subtheme = QString(), QString p_default_theme = QString(), QString p_misc = QString(), QString p_character = QString(), QString p_placeholder = QString()); QString get_image(QString p_element, QString p_theme = QString(), QString p_subtheme = QString(), QString p_default_theme = QString(), QString p_misc = QString(), QString p_character = QString(), QString p_placeholder = QString(), bool static_image = false); QString get_sfx(QString p_sfx, QString p_misc = QString(), QString p_character = QString()); - QString get_pos_path(const QString &pos, bool desk = false); + QPair get_pos_path(const QString &pos, bool desk = false); QString get_case_sensitive_path(QString p_file); QString get_real_path(const VPath &vpath, const QStringList &suffixes = {""}); @@ -250,6 +250,12 @@ class AOApplication : public QApplication // Returns whether the given pos is a judge position bool get_pos_is_judge(const QString &p_pos); + /** + * @brief Returns the duration of the transition animation between the two + * given positions, if it exists + */ + int get_pos_transition_duration(const QString &old_pos, const QString &new_pos); + // Returns the total amount of emotes of p_char int get_emote_number(QString p_char); diff --git a/src/aolayer.cpp b/src/aolayer.cpp index 8284230e9..4ac7d4864 100644 --- a/src/aolayer.cpp +++ b/src/aolayer.cpp @@ -116,16 +116,45 @@ void AOLayer::set_frame(QPixmap f_pixmap) void AOLayer::center_pixmap(QPixmap f_pixmap) { - QLabel::move(x + (f_w - f_pixmap.width()) / 2, - y + (f_h - f_pixmap.height())); // Always center horizontally, always put - // at the bottom vertically + if (g_center == -1) + { + centered_offset = (f_w - f_pixmap.width()) / 2; + QLabel::move(x + (f_w - f_pixmap.width()) / 2, + y + (f_h - f_pixmap.height())); // Always center horizontally, always + // put at the bottom vertically + } + else + { + QLabel::move(get_pos_from_center(g_center), y + (f_h - f_pixmap.height())); + } if (masked) { - this->setMask(QRegion((f_pixmap.width() - f_w) / 2, (f_pixmap.height() - f_h) / 2, f_w, - f_h)); // make sure we don't escape the area we've been given + if (g_center == -1) + { + this->setMask(QRegion((f_pixmap.width() - f_w) / 2, (f_pixmap.height() - f_h) / 2, f_w, + f_h)); // make sure we don't escape the area we've been given + } + else + { + int center_scaled = int(float(g_center) * scaling_factor); + this->setMask(QRegion((f_pixmap.width() - center_scaled) / 2, (f_pixmap.height() - f_h) / 2, f_w, f_h)); + } } } +int AOLayer::get_centered_offset() +{ + return centered_offset; +} + +int AOLayer::get_pos_from_center(int f_center) +{ + int center_scaled = int(float(f_center) * scaling_factor); + int f_pos = x + (center_scaled - (f_w / 2)) * -1; + qDebug() << "centering image at center" << f_center << "final position" << f_pos; + return f_pos; +} + void AOLayer::combo_resize(int w, int h) { QSize f_size(w, h); @@ -160,8 +189,14 @@ void AOLayer::move_and_center(int ax, int ay) } } -void BackgroundLayer::load_image(QString p_filename) +float AOLayer::get_scaling_factor() +{ + return scaling_factor; +} + +void BackgroundLayer::load_image(QString p_filename, int p_center) { + g_center = p_center; play_once = false; cull_image = false; VPath design_path = ao_app->get_background_path("design.ini"); @@ -172,7 +207,7 @@ void BackgroundLayer::load_image(QString p_filename) #endif QString final_path = ao_app->get_image_suffix(ao_app->get_background_path(p_filename)); - if (final_path == last_path) + if (final_path == last_path && g_center == last_center) { // Don't restart background if background is unchanged return; @@ -326,7 +361,7 @@ void AOLayer::start_playback(QString p_image) force_continuous = true; } - if (((last_path == p_image) && (!force_continuous)) || p_image == "") + if (((last_path == p_image) && (!force_continuous) && (g_center == last_center)) || p_image == "") { return; } @@ -370,6 +405,7 @@ void AOLayer::start_playback(QString p_image) frame_loader = QtConcurrent::run(thread_pool, &AOLayer::populate_vectors, this); #endif last_path = p_image; + last_center = g_center; while (movie_frames.size() <= frame) // if we haven't loaded the frame we need yet { frameAdded.wait(&mutex); // wait for the frame loader to add another frame, then check again diff --git a/src/aolayer.h b/src/aolayer.h index 0a44d8f5f..b5591e8a5 100644 --- a/src/aolayer.h +++ b/src/aolayer.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -81,6 +82,9 @@ class AOLayer : public QLabel // Move the label and center it void move_and_center(int ax, int ay); + // Returns the factor by which the image is scaled + float get_scaling_factor(); + // This is somewhat pointless now as there's no "QMovie" object to resize, aka // no "combo" to speak of void combo_resize(int w, int h); @@ -88,10 +92,26 @@ class AOLayer : public QLabel // Return the frame delay adjusted for speed int get_frame_delay(int delay); + /** + * @brief Returns the x offset to use to ensure proper centering in the + * viewport. This is used by courtroom transition code to know exactly where + * to put the characters at the start and end of the animation. + * @return The offset to center the pixmap in the viewport + */ + int get_centered_offset(); + // iterate through a list of paths and return the first entry that exists. if // none exist, return NULL (safe because we check again for existence later) QString find_image(QStringList p_list); + QPropertyAnimation *slide(int newcenter, int duration); + + // Start playback of the movie (if animated). + void play(); + + // Freeze the movie at the current frame. + void freeze(); + protected: AOApplication *ao_app; QVector movie_frames; @@ -114,6 +134,8 @@ class AOLayer : public QLabel int f_w = 0; int f_h = 0; + float scaling_factor = 0.0; + int frame = 0; int max_frames = 0; int last_max_frames = 0; @@ -124,21 +146,29 @@ class AOLayer : public QLabel int duration = 0; - // Start playback of the movie (if animated). - void play(); - - // Freeze the movie at the current frame. - void freeze(); - // Retreive a pixmap adjused for mirroring/aspect ratio shenanigans from a // provided QImage QPixmap get_pixmap(QImage image); // Set the movie's frame to provided pixmap void set_frame(QPixmap f_pixmap); + + // If set to anything other than -1, overrides center_pixmap to use it as a + // pixel position to center at. Currently only used by background layers + int g_center = -1; + int last_center = -1; // g_center from the last image. + int centered_offset = 0; + // Center the QLabel in the viewport based on the dimensions of f_pixmap void center_pixmap(QPixmap f_pixmap); + /*! + @brief Get the position to move us to, given the pixel X position of the + point in the original image that we'd like to be centered. + @return The position to move to. + */ + int get_pos_from_center(int f_center); + private: // Populates the frame and delay vectors. void populate_vectors(); @@ -164,7 +194,7 @@ class BackgroundLayer : public AOLayer Q_OBJECT public: BackgroundLayer(AOApplication *p_ao_app, QWidget *p_parent); - void load_image(QString p_filename); + void load_image(QString p_filename, int center = -1); }; class CharLayer : public AOLayer diff --git a/src/courtroom.cpp b/src/courtroom.cpp index 89c5c75c5..5a1ac537b 100644 --- a/src/courtroom.cpp +++ b/src/courtroom.cpp @@ -47,6 +47,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) ui_viewport->setObjectName("ui_viewport"); ui_vp_background = new BackgroundLayer(ao_app, ui_viewport); ui_vp_background->setObjectName("ui_vp_background"); + ui_vp_background->masked = false; ui_vp_speedlines = new SplashLayer(ao_app, ui_viewport); ui_vp_speedlines->setObjectName("ui_vp_speedlines"); ui_vp_speedlines->stretch = true; @@ -57,8 +58,15 @@ Courtroom::Courtroom(AOApplication *p_ao_app) ui_vp_sideplayer_char->setObjectName("ui_vp_sideplayer_char"); ui_vp_sideplayer_char->masked = false; ui_vp_sideplayer_char->hide(); + ui_vp_dummy_char = new CharLayer(ao_app, ui_viewport); + ui_vp_dummy_char->masked = false; + ui_vp_dummy_char->hide(); + ui_vp_sidedummy_char = new CharLayer(ao_app, ui_viewport); + ui_vp_sidedummy_char->masked = false; + ui_vp_sidedummy_char->hide(); ui_vp_desk = new BackgroundLayer(ao_app, ui_viewport); ui_vp_desk->setObjectName("ui_vp_desk"); + ui_vp_desk->masked = false; ui_vp_effect = new EffectLayer(ao_app, this); ui_vp_effect->setAttribute(Qt::WA_TransparentForMouseEvents); @@ -315,6 +323,11 @@ Courtroom::Courtroom(AOApplication *p_ao_app) ui_showname_enable->setText(tr("Shownames")); ui_showname_enable->setObjectName("ui_showname_enable"); + ui_slide_enable = new QCheckBox(this); + ui_slide_enable->setChecked(false); + ui_slide_enable->setText(tr("Slide")); + ui_slide_enable->setObjectName("ui_slide_enable"); + ui_immediate = new QCheckBox(this); ui_immediate->setText(tr("Immediate")); ui_immediate->hide(); @@ -487,10 +500,11 @@ Courtroom::Courtroom(AOApplication *p_ao_app) connect(ui_settings, &AOButton::clicked, this, &Courtroom::on_settings_clicked); connect(ui_switch_area_music, &AOButton::clicked, this, &Courtroom::on_switch_area_music_clicked); - connect(ui_pre, &AOButton::clicked, this, &Courtroom::on_pre_clicked); - connect(ui_flip, &AOButton::clicked, this, &Courtroom::on_flip_clicked); + connect(ui_pre, &AOButton::clicked, this, &Courtroom::focus_ic_input); + connect(ui_flip, &AOButton::clicked, this, &Courtroom::focus_ic_input); connect(ui_additive, &AOButton::clicked, this, &Courtroom::on_additive_clicked); - connect(ui_guard, &AOButton::clicked, this, &Courtroom::on_guard_clicked); + connect(ui_guard, &AOButton::clicked, this, &Courtroom::focus_ic_input); + connect(ui_slide_enable, &AOButton::clicked, this, &Courtroom::focus_ic_input); connect(ui_showname_enable, &AOButton::clicked, this, &Courtroom::on_showname_enable_clicked); @@ -507,6 +521,8 @@ Courtroom::Courtroom(AOApplication *p_ao_app) connect(ui_vp_evidence_display, &AOEvidenceDisplay::show_evidence_details, this, &Courtroom::show_evidence); + connect(transition_animation_group, &QParallelAnimationGroup::finished, this, &Courtroom::on_transition_finish); + set_widgets(); set_char_select(); @@ -762,6 +778,13 @@ void Courtroom::set_widgets() ui_vp_sideplayer_char->move_and_center(0, 0); ui_vp_sideplayer_char->combo_resize(ui_viewport->width(), ui_viewport->height()); + ui_vp_dummy_char->move_and_center(0, 0); + ui_vp_dummy_char->combo_resize(ui_viewport->width(), + ui_viewport->height()); + ui_vp_sidedummy_char->move_and_center(0, 0); + ui_vp_sidedummy_char->combo_resize(ui_viewport->width(), + ui_viewport->height()); + // the AO2 desk element ui_vp_desk->move_and_center(0, 0); ui_vp_desk->combo_resize(ui_viewport->width(), ui_viewport->height()); @@ -1077,6 +1100,9 @@ void Courtroom::set_widgets() set_size_and_pos(ui_showname_enable, "showname_enable"); ui_showname_enable->setToolTip(tr("Display customized shownames for all users when checked.")); + set_size_and_pos(ui_slide_enable, "slide_enable"); + ui_slide_enable->setToolTip(tr("Allow your messages to trigger slide animations when checked.")); + set_size_and_pos(ui_custom_objection, "custom_objection"); ui_custom_objection->setText(tr("Custom Shout!")); ui_custom_objection->setImage("custom"); @@ -1148,6 +1174,7 @@ void Courtroom::set_widgets() truncate_label_text(ui_pre, "pre"); truncate_label_text(ui_flip, "flip"); truncate_label_text(ui_showname_enable, "showname_enable"); + truncate_label_text(ui_slide_enable, "slide_enable"); // QLabel truncate_label_text(ui_music_label, "music_label"); @@ -1392,8 +1419,12 @@ void Courtroom::set_background(QString p_background, bool display) } for (const QString &pos : ao_app->read_design_ini("positions", ao_app->get_background_path("design.ini")).split(",")) { - if (file_exists(ao_app->get_image_suffix(ao_app->get_background_path(pos)))) - { + QString real_pos = pos.split(":")[0]; + QStringList overrides = {"def", "wit", "pro"}; + if ((file_exists(ao_app->get_image_suffix(ao_app->get_background_path(real_pos)))) || // Normal check, OR + (overrides.contains(pos) && // It's one of our subpos overrides, AND + file_exists(ao_app->get_image_suffix(ao_app->get_background_path("court"))) && // the "court" default image exists, AND + !ao_app->read_design_ini("court:" + pos + "/pos_center", ao_app->get_background_path("design.ini")).isEmpty())) { // config exists for this pos pos_list.append(pos); } } @@ -1489,7 +1520,7 @@ void Courtroom::set_pos_dropdown(QStringList pos_dropdowns) { QString pos = pos_dropdown_list.at(n); ui_pos_dropdown->addItem(pos); - QPixmap image = QPixmap(ao_app->get_image_suffix(ao_app->get_background_path(ao_app->get_pos_path(pos)))); + QPixmap image = QPixmap(ao_app->get_image_suffix(ao_app->get_background_path(ao_app->get_pos_path(pos).first))); if (!image.isNull()) { image = image.scaledToHeight(ui_pos_dropdown->iconSize().height()); @@ -1637,7 +1668,7 @@ void Courtroom::update_character(int p_cid, QString char_name, bool reset_emote) } ui_char_select_background->hide(); ui_ic_chat_message->setEnabled(m_cid != -1); - ui_ic_chat_message->setFocus(); + focus_ic_input(); update_audio_volume(); } @@ -2310,6 +2341,9 @@ void Courtroom::reset_ui() // Turn off our Preanim checkbox ui_pre->setChecked(false); } + + // Slides can't be sticky for nausea reasons. + ui_slide_enable->setChecked(false); } void Courtroom::chatmessage_enqueue(QStringList p_contents) @@ -2735,8 +2769,6 @@ void Courtroom::display_character() // Determine if we should flip the character or not ui_vp_player_char->set_flipped(m_chatmessage[FLIP].toInt() == 1); - // Move the character on the viewport according to the offsets - set_self_offset(m_chatmessage[SELF_OFFSET]); } void Courtroom::display_pair_character(QString other_charid, QString other_offset) @@ -2870,32 +2902,7 @@ void Courtroom::handle_ic_message() // Update the chatbox information initialize_chatbox(); - int emote_mod = m_chatmessage[EMOTE_MOD].toInt(); - bool immediate = m_chatmessage[IMMEDIATE].toInt() == 1; - if (m_chatmessage[EMOTE] != "") - { - // Display our own character - display_character(); - - // Reset the pair character - ui_vp_sideplayer_char->stop(); - ui_vp_sideplayer_char->move(0, 0); - - // If the emote_mod is not zooming - if (emote_mod != ZOOM && emote_mod != PREANIM_ZOOM) - { - // Display the pair character - display_pair_character(m_chatmessage[OTHER_CHARID], m_chatmessage[OTHER_OFFSET]); - } - - // Parse the emote_mod part of the chat message - handle_emote_mod(emote_mod, immediate); - } - else - { - play_sfx(); - start_chat_ticking(); - } + do_transition(m_chatmessage[DESK_MOD], last_side, m_chatmessage[SIDE]); // if we have instant objections disabled, and queue is not empty, check if next message after this is an objection. if (!Options::getInstance().objectionSkipQueueEnabled() && chatmessage_queue.size() > 0) @@ -2956,6 +2963,196 @@ void Courtroom::do_screenshake() screenshake_animation_group->start(); } +void Courtroom::do_transition(QString p_desk_mod, QString old_pos, QString new_pos) { + + if (m_chatmessage[EMOTE] != "") + display_character(); + + const QStringList legacy_pos = {"def", "wit", "pro"}; + QString t_old_pos = old_pos; + QString t_new_pos = new_pos; + if (file_exists(ao_app->get_image_suffix(ao_app->get_background_path("court")))) { + if (legacy_pos.contains(old_pos)) { + t_old_pos = "court:" + old_pos; + } + if (legacy_pos.contains(new_pos)) { + t_new_pos = "court:" + new_pos; + } + } + + QPair old_pos_pair = ao_app->get_pos_path(t_old_pos); + QPair new_pos_pair = ao_app->get_pos_path(t_new_pos); + + int duration = ao_app->get_pos_transition_duration(t_old_pos, t_new_pos); + + // conditions to stop slide + if (old_pos == new_pos || + old_pos_pair.first != new_pos_pair.first || + new_pos_pair.second == -1 || + !Options::getInstance().slidesEnabled() || + m_chatmessage[SLIDE] != "1" || + duration == -1 || + m_chatmessage[EMOTE_MOD].toInt() == ZOOM || + m_chatmessage[EMOTE_MOD].toInt() == PREANIM_ZOOM) { +#ifdef DEBUG_TRANSITION + qDebug() << "skipping transition - not applicable"; +#endif + post_transition_cleanup(); + return; + } +#ifdef DEBUG_TRANSITION + // for debugging animation + ui_vp_sideplayer_char->setStyleSheet("background-color:rgba(0, 0, 255, 128);"); + ui_vp_dummy_char->setStyleSheet("background-color:rgba(255, 0, 0, 128);"); + ui_vp_sidedummy_char->setStyleSheet("background-color:rgba(0, 255, 0, 128);"); + + qDebug() << "STARTING TRANSITION, CURRENT TIME:" << transition_animation_group->currentTime(); +#endif + + set_scene(p_desk_mod.toInt(), old_pos); + + QList affected_list = {ui_vp_background, ui_vp_desk, ui_vp_player_char}; + + bool paired = false; + if (!ui_vp_sideplayer_char->isHidden()) { + affected_list.append(ui_vp_sideplayer_char); + paired = true; + } + + // Set up the background, desk, and player objects' animations + + float scaling_factor = ui_vp_background->get_scaling_factor(); + int offset = (float(old_pos_pair.second) * scaling_factor) - (float(new_pos_pair.second) * scaling_factor); + + for (AOLayer *ui_element : affected_list) { + QPropertyAnimation *transition_animation = new QPropertyAnimation(ui_element, "pos", this); + transition_animation->setStartValue(ui_element->pos()); + transition_animation->setDuration(duration); + transition_animation->setEndValue(QPoint(ui_element->pos().x() + offset, ui_element->pos().y())); + transition_animation->setEasingCurve(QEasingCurve::InOutCubic); + transition_animation_group->addAnimation(transition_animation); + } + + // Setting up the dummy characters to work for us as our stand-in for the next characters + // This should be easy. But it isn't + + QString slide_emote; + if (m_chatmessage[OBJECTION_MOD].contains("4") || m_chatmessage[OBJECTION_MOD].toInt() == 2) { + slide_emote = "(a)" + ao_app->read_char_ini(m_chatmessage[CHAR_NAME], "objection_pose", "Options"); + if (slide_emote == "(a)") { + slide_emote = "(a)" + m_chatmessage[EMOTE]; + } + } + else + slide_emote = "(a)" + m_chatmessage[EMOTE]; + + QString other_slide_emote = "(a)" + m_chatmessage[OTHER_EMOTE]; + + // Load the image we're going to use to get scaling information, and move it into the final position for animation data + ui_vp_dummy_char->set_flipped(m_chatmessage[FLIP].toInt()); + ui_vp_dummy_char->load_image(slide_emote, m_chatmessage[CHAR_NAME], 0, false); + set_self_offset(m_chatmessage[SELF_OFFSET], ui_vp_dummy_char); + + QPoint starting_position = QPoint(ui_vp_player_char->pos().x() - offset, ui_vp_player_char->pos().y()); + QPropertyAnimation *ui_vp_dummy_animation = new QPropertyAnimation(ui_vp_dummy_char, "pos", this); + + ui_vp_dummy_animation->setDuration(duration); + ui_vp_dummy_animation->setStartValue(starting_position); + ui_vp_dummy_animation->setEndValue(ui_vp_dummy_char->pos()); + ui_vp_dummy_animation->setEasingCurve(QEasingCurve::InOutCubic); + transition_animation_group->addAnimation(ui_vp_dummy_animation); + + ui_vp_dummy_char->move(starting_position.x(), starting_position.y()); + + // If the new message is paired, do it all again for the pair character. Yippee! + if (m_chatmessage[OTHER_CHARID].toInt() != -1 && !m_chatmessage[OTHER_NAME].isEmpty()) { + ui_vp_sidedummy_char->set_flipped(m_chatmessage[OTHER_FLIP].toInt()); + ui_vp_sidedummy_char->load_image(other_slide_emote, m_chatmessage[OTHER_NAME], 0, false); + set_self_offset(m_chatmessage[OTHER_OFFSET], ui_vp_sidedummy_char); + QStringList args = m_chatmessage[OTHER_CHARID].split("^"); + if (args.size() > 1) + { + // Change the order of appearance based on the pair order variable + int order = args.at(1).toInt(); + switch (order) { + case 0: // Our character is in front + ui_vp_sidedummy_char->stackUnder(ui_vp_dummy_char); + break; + case 1: // Our character is behind + ui_vp_dummy_char->stackUnder(ui_vp_sidedummy_char); + break; + default: + break; + } + } + + QPoint other_starting_position = QPoint(ui_vp_sideplayer_char->pos().x() - offset, ui_vp_sideplayer_char->pos().y()); + QPropertyAnimation *ui_vp_sidedummy_animation = new QPropertyAnimation(ui_vp_sidedummy_char, "pos", this); + + ui_vp_sidedummy_animation->setDuration(duration); + ui_vp_sidedummy_animation->setStartValue(starting_position); + ui_vp_sidedummy_animation->setEndValue(ui_vp_sidedummy_char->pos()); + ui_vp_sidedummy_animation->setEasingCurve(QEasingCurve::InOutCubic); + transition_animation_group->addAnimation(ui_vp_sidedummy_animation); + + ui_vp_sidedummy_char->move(starting_position.x(), starting_position.y()); + } + else { + ui_vp_sidedummy_char->stop(); + } + + ui_vp_player_char->freeze(); + ui_vp_player_char->show(); + if (paired) { + ui_vp_sideplayer_char->freeze(); + ui_vp_sideplayer_char->show(); + } + else { + ui_vp_sideplayer_char->stop(); + } + ui_vp_dummy_char->freeze(); + ui_vp_sidedummy_char->freeze(); + QTimer::singleShot(TRANSITION_BOOKEND_DELAY, transition_animation_group, SLOT(start())); +} + + +void Courtroom::on_transition_finish() { + transition_animation_group->clear(); + transition_animation_group->setCurrentTime(0); + QTimer::singleShot(TRANSITION_BOOKEND_DELAY, this, SLOT(post_transition_cleanup())); +} + +void Courtroom::post_transition_cleanup() { + + set_scene(m_chatmessage[DESK_MOD].toInt(), m_chatmessage[SIDE]); + + // Move the character on the viewport according to the offsets + set_self_offset(m_chatmessage[SELF_OFFSET], ui_vp_player_char); + + int emote_mod = m_chatmessage[EMOTE_MOD].toInt(); + bool immediate = m_chatmessage[IMMEDIATE].toInt() == 1; + + + // Reset the pair character + ui_vp_sideplayer_char->stop(); + ui_vp_sideplayer_char->move_and_center(0, 0); + + // If the emote_mod is not zooming + if (emote_mod != ZOOM && emote_rows != PREANIM_ZOOM) { + // Display the pair character + display_pair_character(m_chatmessage[OTHER_CHARID], m_chatmessage[OTHER_OFFSET]); + } + + // Reset tweedle dee and tweedle dummy + ui_vp_dummy_char->stop(); + ui_vp_dummy_char->move_and_center(0,0); + ui_vp_sidedummy_char->stop(); + ui_vp_sidedummy_char->move_and_center(0,0); + + // Parse the emote_mod part of the chat message + handle_emote_mod(emote_mod, immediate); +} + void Courtroom::do_flash() { if (!Options::getInstance().effectsEnabled()) @@ -3878,7 +4075,7 @@ void Courtroom::start_chat_ticking() switch (m_chatmessage[DESK_MOD].toInt()) { case DESK_EMOTE_ONLY_EX: - set_self_offset(m_chatmessage[SELF_OFFSET]); + set_self_offset(m_chatmessage[SELF_OFFSET], ui_vp_player_char); [[fallthrough]]; case DESK_EMOTE_ONLY: case DESK_SHOW: @@ -4331,8 +4528,11 @@ void Courtroom::play_sfx() void Courtroom::set_scene(bool show_desk, const QString f_side) { - ui_vp_background->load_image(ao_app->get_pos_path(f_side)); - ui_vp_desk->load_image(ao_app->get_pos_path(f_side, true)); + QPair bg_pair = ao_app->get_pos_path(f_side); + QPair desk_pair = ao_app->get_pos_path(f_side, true); + ui_vp_background->load_image(bg_pair.first, bg_pair.second); + ui_vp_desk->load_image(desk_pair.first, desk_pair.second); + last_side = f_side; if (show_desk) { @@ -4344,7 +4544,7 @@ void Courtroom::set_scene(bool show_desk, const QString f_side) } } -void Courtroom::set_self_offset(const QString &p_list) +void Courtroom::set_self_offset(const QString& p_list, AOLayer* p_layer) { { QStringList self_offsets = p_list.split("&"); int self_offset = self_offsets[0].toInt(); @@ -4357,7 +4557,7 @@ void Courtroom::set_self_offset(const QString &p_list) { self_offset_v = self_offsets[1].toInt(); } - ui_vp_player_char->move_and_center(ui_viewport->width() * self_offset / 100, ui_viewport->height() * self_offset_v / 100); + p_layer->move_and_center(ui_viewport->width() * self_offset / 100, ui_viewport->height() * self_offset_v / 100); } void Courtroom::set_ip_list(QString p_list) @@ -4381,7 +4581,7 @@ void Courtroom::set_mute(bool p_muted, int p_cid) else { ui_muted->hide(); - ui_ic_chat_message->setFocus(); + focus_ic_input(); } ui_muted->resize(ui_ic_chat_message->width(), ui_ic_chat_message->height()); @@ -5005,7 +5205,7 @@ void Courtroom::on_pos_remove_clicked() ui_pos_dropdown->blockSignals(false); current_side = ""; ui_pos_remove->hide(); - ui_ic_chat_message->setFocus(); + focus_ic_input(); } void Courtroom::set_iniswap_dropdown() @@ -5052,7 +5252,7 @@ void Courtroom::set_iniswap_dropdown() void Courtroom::on_iniswap_dropdown_changed(int p_index) { - ui_ic_chat_message->setFocus(); + focus_ic_input(); QString iniswap = ui_iniswap_dropdown->itemText(p_index); QStringList swaplist; @@ -5190,7 +5390,7 @@ void Courtroom::set_sfx_dropdown() void Courtroom::on_sfx_dropdown_changed(int p_index) { custom_sfx = ""; - ui_ic_chat_message->setFocus(); + focus_ic_input(); if (p_index == 0) { ui_sfx_remove->hide(); @@ -5345,7 +5545,7 @@ void Courtroom::on_character_effects_edit_requested() void Courtroom::on_effects_dropdown_changed(int p_index) { effect = ui_effects_dropdown->itemText(p_index); - ui_ic_chat_message->setFocus(); + focus_ic_input(); } bool Courtroom::effects_dropdown_find_and_set(QString effect) @@ -5701,7 +5901,7 @@ void Courtroom::on_hold_it_clicked() objection_state = 1; } - ui_ic_chat_message->setFocus(); + focus_ic_input(); } void Courtroom::on_objection_clicked() @@ -5721,7 +5921,7 @@ void Courtroom::on_objection_clicked() objection_state = 2; } - ui_ic_chat_message->setFocus(); + focus_ic_input(); } void Courtroom::on_take_that_clicked() @@ -5741,7 +5941,7 @@ void Courtroom::on_take_that_clicked() objection_state = 3; } - ui_ic_chat_message->setFocus(); + focus_ic_input(); } void Courtroom::on_custom_objection_clicked() @@ -5761,7 +5961,7 @@ void Courtroom::on_custom_objection_clicked() objection_state = 4; } - ui_ic_chat_message->setFocus(); + focus_ic_input(); } void Courtroom::show_custom_objection_menu(const QPoint &pos) @@ -5814,7 +6014,7 @@ void Courtroom::on_realization_clicked() ui_realization->setImage("realization"); } - ui_ic_chat_message->setFocus(); + focus_ic_input(); } void Courtroom::on_screenshake_clicked() @@ -5830,7 +6030,7 @@ void Courtroom::on_screenshake_clicked() ui_screenshake->setImage("screenshake"); } - ui_ic_chat_message->setFocus(); + focus_ic_input(); } void Courtroom::on_mute_clicked() @@ -6033,13 +6233,13 @@ void Courtroom::on_text_color_changed(int p_color) text_color = 0; } } - ui_ic_chat_message->setFocus(); + focus_ic_input(); } void Courtroom::on_music_slider_moved(int p_value) { music_player->setStreamVolume(p_value, 0); // Set volume on music layer - ui_ic_chat_message->setFocus(); + focus_ic_input(); } void Courtroom::on_sfx_slider_moved(int p_value) @@ -6051,13 +6251,13 @@ void Courtroom::on_sfx_slider_moved(int p_value) music_player->setStreamVolume(p_value, i); } objection_player->setVolume(p_value); - ui_ic_chat_message->setFocus(); + focus_ic_input(); } void Courtroom::on_blip_slider_moved(int p_value) { blip_player->setVolume(p_value); - ui_ic_chat_message->setFocus(); + focus_ic_input(); } void Courtroom::on_log_limit_changed(int value) @@ -6084,7 +6284,7 @@ void Courtroom::on_witness_testimony_clicked() ao_app->send_server_packet(AOPacket("RT", {"testimony1"})); - ui_ic_chat_message->setFocus(); + focus_ic_input(); } void Courtroom::on_cross_examination_clicked() @@ -6096,7 +6296,7 @@ void Courtroom::on_cross_examination_clicked() ao_app->send_server_packet(AOPacket("RT", {"testimony2"})); - ui_ic_chat_message->setFocus(); + focus_ic_input(); } void Courtroom::on_not_guilty_clicked() @@ -6108,7 +6308,7 @@ void Courtroom::on_not_guilty_clicked() ao_app->send_server_packet(AOPacket("RT", {"judgeruling", "0"})); - ui_ic_chat_message->setFocus(); + focus_ic_input(); } void Courtroom::on_guilty_clicked() @@ -6120,7 +6320,7 @@ void Courtroom::on_guilty_clicked() ao_app->send_server_packet(AOPacket("RT", {"judgeruling", "1"})); - ui_ic_chat_message->setFocus(); + focus_ic_input(); } void Courtroom::on_change_character_clicked() @@ -6207,7 +6407,7 @@ void Courtroom::on_call_mod_clicked() ao_app->send_server_packet(AOPacket("ZZ")); } - ui_ic_chat_message->setFocus(); + focus_ic_input(); } void Courtroom::on_settings_clicked() @@ -6215,15 +6415,6 @@ void Courtroom::on_settings_clicked() ao_app->call_settings_menu(); } -void Courtroom::on_pre_clicked() -{ - ui_ic_chat_message->setFocus(); -} - -void Courtroom::on_flip_clicked() -{ - ui_ic_chat_message->setFocus(); -} void Courtroom::on_additive_clicked() { @@ -6234,10 +6425,10 @@ void Courtroom::on_additive_clicked() ui_ic_chat_message->end(false); // move cursor to the end of the message // without selecting anything } - ui_ic_chat_message->setFocus(); + focus_ic_input(); } -void Courtroom::on_guard_clicked() +void Courtroom::focus_ic_input() { ui_ic_chat_message->setFocus(); } @@ -6245,7 +6436,7 @@ void Courtroom::on_guard_clicked() void Courtroom::on_showname_enable_clicked() { regenerate_ic_chatlog(); - ui_ic_chat_message->setFocus(); + focus_ic_input(); } void Courtroom::regenerate_ic_chatlog() diff --git a/src/courtroom.h b/src/courtroom.h index 0f49839c5..0370892b9 100644 --- a/src/courtroom.h +++ b/src/courtroom.h @@ -150,8 +150,9 @@ class Courtroom : public QMainWindow // sets desk and bg based on pos in chatmessage void set_scene(bool show_desk, QString f_side); - // sets ui_vp_player_char according to SELF_OFFSET, only a function bc it's used with desk_mod 4 and 5 - void set_self_offset(const QString &p_list); + // sets p_layer according to SELF_OFFSET, only a function bc it's used with + // desk_mod 4 and 5 + void set_self_offset(const QString &p_list, AOLayer *p_layer); // takes in serverD-formatted IP list as prints a converted version to server // OOC admittedly poorly named @@ -215,6 +216,9 @@ class Courtroom : public QMainWindow // Handle the stuff that comes when the character appears on screen and starts animating (preanims etc.) void handle_ic_message(); + // Start the logic for doing a courtroom pan slide + void do_transition(QString desk_mod, QString old_pos, QString new_pos); + // Display the character. void display_character(); @@ -308,6 +312,8 @@ class Courtroom : public QMainWindow QParallelAnimationGroup *screenshake_animation_group = new QParallelAnimationGroup; + QParallelAnimationGroup *transition_animation_group = new QParallelAnimationGroup; + bool next_character_is_not_special = false; // If true, write the // next character as it is. @@ -436,9 +442,15 @@ class Courtroom : public QMainWindow // Minumum and maximum number of parameters in the MS packet static const int MS_MINIMUM = 15; - static const int MS_MAXIMUM = 31; + static const int MS_MAXIMUM = 32; QString m_chatmessage[MS_MAXIMUM]; + /** + * @brief The amount of time to wait at the start and end of slide + * animations + */ + static const int TRANSITION_BOOKEND_DELAY = 300; + QString previous_ic_message; QString additive_previous; @@ -570,6 +582,11 @@ class Courtroom : public QMainWindow QString current_background = "default"; QString current_side; + // used for courtroom slide logic + QString last_side = ""; + int last_offset = 0; + int last_v_offset = 0; + QString last_music_search; QString last_area_search; @@ -595,6 +612,8 @@ class Courtroom : public QMainWindow SplashLayer *ui_vp_speedlines; CharLayer *ui_vp_player_char; CharLayer *ui_vp_sideplayer_char; + CharLayer *ui_vp_dummy_char; + CharLayer *ui_vp_sidedummy_char; BackgroundLayer *ui_vp_desk; AOEvidenceDisplay *ui_vp_evidence_display; AOImage *ui_vp_chatbox; @@ -693,6 +712,8 @@ class Courtroom : public QMainWindow QCheckBox *ui_immediate; QCheckBox *ui_showname_enable; + QCheckBox *ui_slide_enable; + AOButton *ui_custom_objection; QMenu *custom_obj_menu; AOButton *ui_realization; @@ -784,6 +805,7 @@ public Q_SLOTS: void objection_done(); void preanim_done(); void do_screenshake(); + void on_transition_finish(); void do_flash(); void do_effect(QString fx_path, QString fx_sound, QString p_char, QString p_folder); void play_char_sfx(QString sfx_name); @@ -912,10 +934,8 @@ private Q_SLOTS: void on_call_mod_clicked(); void on_settings_clicked(); - void on_pre_clicked(); - void on_flip_clicked(); + void focus_ic_input(); void on_additive_clicked(); - void on_guard_clicked(); void on_showname_enable_clicked(); @@ -960,4 +980,8 @@ private Q_SLOTS: void preview_emote(QString emote); void update_emote_preview(); + + // After attempting to play a transition animation, clean up the viewport + // objects for everyone else and continue the IC processing callstack + void post_transition_cleanup(); }; diff --git a/src/datatypes.h b/src/datatypes.h index 9463a471a..ac9c64627 100644 --- a/src/datatypes.h +++ b/src/datatypes.h @@ -78,6 +78,7 @@ enum CHAT_MESSAGE ADDITIVE, EFFECTS, BLIPNAME, + SLIDE, }; enum EMOTE_MOD_TYPE diff --git a/src/options.cpp b/src/options.cpp index 80060764f..d06c5e463 100644 --- a/src/options.cpp +++ b/src/options.cpp @@ -401,6 +401,16 @@ void Options::setNetworkedFrameSfxEnabled(bool value) config.setValue("framenetwork", value); } +bool Options::slidesEnabled() const +{ + return config.value("slides", true).toBool(); +} + +void Options::setSlidesEnabled(bool value) +{ + config.setValue("slides", value); +} + bool Options::colorLogEnabled() const { return config.value("colorlog", true).toBool(); diff --git a/src/options.h b/src/options.h index 088e34ba5..1ea76216d 100644 --- a/src/options.h +++ b/src/options.h @@ -95,6 +95,11 @@ class Options bool networkedFrameSfxEnabled() const; void setNetworkedFrameSfxEnabled(bool value); + // Returns the value of whether courtroom slide animations should be played + // on this client. + bool slidesEnabled() const; + void setSlidesEnabled(bool value); + // Returns the value of whether colored ic log should be a thing. // from the config.ini. bool colorLogEnabled() const; diff --git a/src/path_functions.cpp b/src/path_functions.cpp index d68535c4a..186024539 100644 --- a/src/path_functions.cpp +++ b/src/path_functions.cpp @@ -78,9 +78,34 @@ VPath AOApplication::get_default_background_path(QString p_file) return VPath("background/default/" + p_file); } -QString AOApplication::get_pos_path(const QString &pos, const bool desk) +QPair AOApplication::get_pos_path(const QString &pos, const bool desk) { // witness is default if pos is invalid + QString f_pos = pos; + // legacy overrides for new format if found + if (pos == "def" && file_exists(get_image_suffix(get_background_path("court")))) + { + f_pos = "court:def"; + } + else if (pos == "pro" && file_exists(get_image_suffix(get_background_path("court")))) + { + f_pos = "court:pro"; + } + else if (pos == "wit" && file_exists(get_image_suffix(get_background_path("court")))) + { + f_pos = "court:wit"; + } + QStringList f_pos_split = f_pos.split(":"); + int f_center = -1; + if (f_pos_split.size() > 1) + { // Subposition, get center info + bool bOk; + int subpos_center = read_design_ini(f_pos + "/pos_center", get_background_path("design.ini")).toInt(&bOk); + if (bOk) + { + f_center = subpos_center; + } + } QString f_background; QString f_desk_image; if (file_exists(get_image_suffix(get_background_path("witnessempty")))) @@ -130,10 +155,10 @@ QString AOApplication::get_pos_path(const QString &pos, const bool desk) f_desk_image = "seancedesk"; } - if (file_exists(get_image_suffix(get_background_path(pos)))) // Unique pos path + if (file_exists(get_image_suffix(get_background_path(f_pos_split[0])))) // Unique pos path { - f_background = pos; - f_desk_image = pos + "_overlay"; + f_background = f_pos_split[0]; + f_desk_image = f_pos_split[0] + "_overlay"; } QString desk_override = read_design_ini("overlays/" + f_background, get_background_path("design.ini")); @@ -143,9 +168,9 @@ QString AOApplication::get_pos_path(const QString &pos, const bool desk) } if (desk) { - return f_desk_image; + return {f_desk_image, f_center}; } - return f_background; + return {f_background, f_center}; } VPath AOApplication::get_evidence_path(QString p_file) diff --git a/src/text_file_functions.cpp b/src/text_file_functions.cpp index e7c4511b5..b123da370 100644 --- a/src/text_file_functions.cpp +++ b/src/text_file_functions.cpp @@ -912,3 +912,22 @@ bool AOApplication::get_pos_is_judge(const QString &p_pos) } return positions.contains(p_pos.trimmed()); } + +int AOApplication::get_pos_transition_duration(const QString &old_pos, const QString &new_pos) +{ + if (old_pos.split(":").size() < 2 || new_pos.split(":").size() < 2) + { + return -1; // no subpositions + } + + QString new_subpos = new_pos.split(":")[1]; + + bool ok; + int duration = read_design_ini(old_pos + "/slide_ms_" + new_subpos, get_background_path("design.ini")).toInt(&ok); + if (ok) + { + return duration; + } + else + return -1; // invalid +} diff --git a/src/widgets/aooptionsdialog.cpp b/src/widgets/aooptionsdialog.cpp index beb7e55ea..a0b7d7ce5 100644 --- a/src/widgets/aooptionsdialog.cpp +++ b/src/widgets/aooptionsdialog.cpp @@ -358,6 +358,7 @@ void AOOptionsDialog::setupUI() FROM_UI(QCheckBox, category_stop_cb); FROM_UI(QCheckBox, sfx_on_idle_cb); FROM_UI(QCheckBox, evidence_double_click_cb); + FROM_UI(QCheckBox, slides_cb); registerOption("theme_scaling_factor_sb", &Options::themeScalingFactor, &Options::setThemeScalingFactor); registerOption("animated_theme_cb", &Options::animatedThemeEnabled, &Options::setAnimatedThemeEnabled); @@ -400,6 +401,7 @@ void AOOptionsDialog::setupUI() registerOption("category_stop_cb", &Options::stopMusicOnCategoryEnabled, &Options::setStopMusicOnCategoryEnabled); registerOption("sfx_on_idle_cb", &Options::playSelectedSFXOnIdle, &Options::setPlaySelectedSFXOnIdle); registerOption("evidence_double_click_cb", &Options::evidenceDoubleClickEdit, &Options::setEvidenceDoubleClickEdit); + registerOption("slides_cb", &Options::slidesEnabled, &Options::setSlidesEnabled); // Callwords tab. This could just be a QLineEdit, but no, we decided to allow // people to put a billion entries in. diff --git a/src/widgets/aooptionsdialog.h b/src/widgets/aooptionsdialog.h index bccec5812..b16be3313 100644 --- a/src/widgets/aooptionsdialog.h +++ b/src/widgets/aooptionsdialog.h @@ -47,6 +47,7 @@ class AOOptionsDialog : public QDialog QPushButton *ui_theme_reload_button; QPushButton *ui_theme_folder_button; QCheckBox *ui_evidence_double_click_cb; + QCheckBox *ui_slides_cb; QCheckBox *ui_animated_theme_cb; QSpinBox *ui_stay_time_spinbox; QCheckBox *ui_instant_objection_cb; From 48aab621fb363920dc70e0b9569fb6b85b68a46b Mon Sep 17 00:00:00 2001 From: in1tiate <32779090+in1tiate@users.noreply.github.com> Date: Mon, 20 May 2024 08:48:17 -0500 Subject: [PATCH 02/16] Add tickbox to options UI --- data/ui/options_dialog.ui | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/data/ui/options_dialog.ui b/data/ui/options_dialog.ui index db6db7252..19100cde6 100644 --- a/data/ui/options_dialog.ui +++ b/data/ui/options_dialog.ui @@ -39,9 +39,9 @@ 0 - 0 + -511 394 - 858 + 850 @@ -556,6 +556,23 @@ + + + + If ticked, slide animations will play when requested. + + + Slide Animations: + + + + + + + + + + From 6ff71de06b829f46eede5d697dae1b637e328795 Mon Sep 17 00:00:00 2001 From: in1tiate <32779090+in1tiate@users.noreply.github.com> Date: Mon, 20 May 2024 08:49:26 -0500 Subject: [PATCH 03/16] fix opening bracket --- src/courtroom.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/courtroom.cpp b/src/courtroom.cpp index 5a1ac537b..0d7dd6d77 100644 --- a/src/courtroom.cpp +++ b/src/courtroom.cpp @@ -4544,7 +4544,7 @@ void Courtroom::set_scene(bool show_desk, const QString f_side) } } -void Courtroom::set_self_offset(const QString& p_list, AOLayer* p_layer) { +void Courtroom::set_self_offset(const QString& p_list, AOLayer* p_layer) { QStringList self_offsets = p_list.split("&"); int self_offset = self_offsets[0].toInt(); From a23614059b67f57d7a2d66ad67aec6770dd3788d Mon Sep 17 00:00:00 2001 From: in1tiate <32779090+in1tiate@users.noreply.github.com> Date: Mon, 20 May 2024 09:03:16 -0500 Subject: [PATCH 04/16] add missing code from rebase --- src/aolayer.cpp | 9 +++------ src/courtroom.cpp | 5 ++++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/aolayer.cpp b/src/aolayer.cpp index 4ac7d4864..a70e7b829 100644 --- a/src/aolayer.cpp +++ b/src/aolayer.cpp @@ -91,6 +91,7 @@ QPixmap AOLayer::get_pixmap(QImage image) // auto aspect_ratio = Qt::KeepAspectRatio; if (!f_pixmap.isNull()) { + scaling_factor = float(f_h) / float(f_pixmap.height()); if (f_pixmap.height() > f_h) // We are downscaling, use anti-aliasing. { transform_mode = Qt::SmoothTransformation; @@ -118,7 +119,6 @@ void AOLayer::center_pixmap(QPixmap f_pixmap) { if (g_center == -1) { - centered_offset = (f_w - f_pixmap.width()) / 2; QLabel::move(x + (f_w - f_pixmap.width()) / 2, y + (f_h - f_pixmap.height())); // Always center horizontally, always // put at the bottom vertically @@ -142,16 +142,13 @@ void AOLayer::center_pixmap(QPixmap f_pixmap) } } -int AOLayer::get_centered_offset() -{ - return centered_offset; -} - int AOLayer::get_pos_from_center(int f_center) { int center_scaled = int(float(f_center) * scaling_factor); int f_pos = x + (center_scaled - (f_w / 2)) * -1; +#ifdef DEBUG_MOVIE qDebug() << "centering image at center" << f_center << "final position" << f_pos; +#endif return f_pos; } diff --git a/src/courtroom.cpp b/src/courtroom.cpp index 0d7dd6d77..2f7e3ed5f 100644 --- a/src/courtroom.cpp +++ b/src/courtroom.cpp @@ -2303,7 +2303,10 @@ void Courtroom::on_chat_return_pressed() } } - packet_contents.append(ao_app->get_blipname(current_char, current_emote)); + if (ao_app->m_serverdata.get_feature(server::BASE_FEATURE_SET::CUSTOM_BLIPS)) { + packet_contents.append(ao_app->get_blipname(current_char, current_emote)); + } + packet_contents.append(ui_slide_enable->isChecked() ? "1" : "0"); // just let the server figure out what to do with this ao_app->send_server_packet(AOPacket("MS", packet_contents)); } From 5d90088b5c01c3933e5d1ccf497e7733b03195f2 Mon Sep 17 00:00:00 2001 From: in1tiate <32779090+in1tiate@users.noreply.github.com> Date: Mon, 20 May 2024 09:12:47 -0500 Subject: [PATCH 05/16] clang-format pass --- src/courtroom.cpp | 323 +++++++++++++++++++++++----------------------- 1 file changed, 164 insertions(+), 159 deletions(-) diff --git a/src/courtroom.cpp b/src/courtroom.cpp index 2f7e3ed5f..ba76604e7 100644 --- a/src/courtroom.cpp +++ b/src/courtroom.cpp @@ -779,11 +779,9 @@ void Courtroom::set_widgets() ui_vp_sideplayer_char->combo_resize(ui_viewport->width(), ui_viewport->height()); ui_vp_dummy_char->move_and_center(0, 0); - ui_vp_dummy_char->combo_resize(ui_viewport->width(), - ui_viewport->height()); + ui_vp_dummy_char->combo_resize(ui_viewport->width(), ui_viewport->height()); ui_vp_sidedummy_char->move_and_center(0, 0); - ui_vp_sidedummy_char->combo_resize(ui_viewport->width(), - ui_viewport->height()); + ui_vp_sidedummy_char->combo_resize(ui_viewport->width(), ui_viewport->height()); // the AO2 desk element ui_vp_desk->move_and_center(0, 0); @@ -1422,9 +1420,10 @@ void Courtroom::set_background(QString p_background, bool display) QString real_pos = pos.split(":")[0]; QStringList overrides = {"def", "wit", "pro"}; if ((file_exists(ao_app->get_image_suffix(ao_app->get_background_path(real_pos)))) || // Normal check, OR - (overrides.contains(pos) && // It's one of our subpos overrides, AND - file_exists(ao_app->get_image_suffix(ao_app->get_background_path("court"))) && // the "court" default image exists, AND - !ao_app->read_design_ini("court:" + pos + "/pos_center", ao_app->get_background_path("design.ini")).isEmpty())) { // config exists for this pos + (overrides.contains(pos) && // It's one of our subpos overrides, AND + file_exists(ao_app->get_image_suffix(ao_app->get_background_path("court"))) && // the "court" default image exists, AND + !ao_app->read_design_ini("court:" + pos + "/pos_center", ao_app->get_background_path("design.ini")).isEmpty())) + { // config exists for this pos pos_list.append(pos); } } @@ -2303,7 +2302,8 @@ void Courtroom::on_chat_return_pressed() } } - if (ao_app->m_serverdata.get_feature(server::BASE_FEATURE_SET::CUSTOM_BLIPS)) { + if (ao_app->m_serverdata.get_feature(server::BASE_FEATURE_SET::CUSTOM_BLIPS)) + { packet_contents.append(ao_app->get_blipname(current_char, current_emote)); } packet_contents.append(ui_slide_enable->isChecked() ? "1" : "0"); // just let the server figure out what to do with this @@ -2966,194 +2966,200 @@ void Courtroom::do_screenshake() screenshake_animation_group->start(); } -void Courtroom::do_transition(QString p_desk_mod, QString old_pos, QString new_pos) { +void Courtroom::do_transition(QString p_desk_mod, QString old_pos, QString new_pos) +{ + if (m_chatmessage[EMOTE] != "") + display_character(); - if (m_chatmessage[EMOTE] != "") - display_character(); - - const QStringList legacy_pos = {"def", "wit", "pro"}; - QString t_old_pos = old_pos; - QString t_new_pos = new_pos; - if (file_exists(ao_app->get_image_suffix(ao_app->get_background_path("court")))) { - if (legacy_pos.contains(old_pos)) { - t_old_pos = "court:" + old_pos; - } - if (legacy_pos.contains(new_pos)) { - t_new_pos = "court:" + new_pos; - } + const QStringList legacy_pos = {"def", "wit", "pro"}; + QString t_old_pos = old_pos; + QString t_new_pos = new_pos; + if (file_exists(ao_app->get_image_suffix(ao_app->get_background_path("court")))) + { + if (legacy_pos.contains(old_pos)) + { + t_old_pos = "court:" + old_pos; } + if (legacy_pos.contains(new_pos)) + { + t_new_pos = "court:" + new_pos; + } + } - QPair old_pos_pair = ao_app->get_pos_path(t_old_pos); - QPair new_pos_pair = ao_app->get_pos_path(t_new_pos); + QPair old_pos_pair = ao_app->get_pos_path(t_old_pos); + QPair new_pos_pair = ao_app->get_pos_path(t_new_pos); - int duration = ao_app->get_pos_transition_duration(t_old_pos, t_new_pos); + int duration = ao_app->get_pos_transition_duration(t_old_pos, t_new_pos); - // conditions to stop slide - if (old_pos == new_pos || - old_pos_pair.first != new_pos_pair.first || - new_pos_pair.second == -1 || - !Options::getInstance().slidesEnabled() || - m_chatmessage[SLIDE] != "1" || - duration == -1 || - m_chatmessage[EMOTE_MOD].toInt() == ZOOM || - m_chatmessage[EMOTE_MOD].toInt() == PREANIM_ZOOM) { + // conditions to stop slide + if (old_pos == new_pos || old_pos_pair.first != new_pos_pair.first || new_pos_pair.second == -1 || !Options::getInstance().slidesEnabled() || m_chatmessage[SLIDE] != "1" || duration == -1 || m_chatmessage[EMOTE_MOD].toInt() == ZOOM || m_chatmessage[EMOTE_MOD].toInt() == PREANIM_ZOOM) + { #ifdef DEBUG_TRANSITION - qDebug() << "skipping transition - not applicable"; + qDebug() << "skipping transition - not applicable"; #endif - post_transition_cleanup(); - return; - } + post_transition_cleanup(); + return; + } #ifdef DEBUG_TRANSITION - // for debugging animation - ui_vp_sideplayer_char->setStyleSheet("background-color:rgba(0, 0, 255, 128);"); - ui_vp_dummy_char->setStyleSheet("background-color:rgba(255, 0, 0, 128);"); - ui_vp_sidedummy_char->setStyleSheet("background-color:rgba(0, 255, 0, 128);"); + // for debugging animation + ui_vp_sideplayer_char->setStyleSheet("background-color:rgba(0, 0, 255, 128);"); + ui_vp_dummy_char->setStyleSheet("background-color:rgba(255, 0, 0, 128);"); + ui_vp_sidedummy_char->setStyleSheet("background-color:rgba(0, 255, 0, 128);"); - qDebug() << "STARTING TRANSITION, CURRENT TIME:" << transition_animation_group->currentTime(); + qDebug() << "STARTING TRANSITION, CURRENT TIME:" << transition_animation_group->currentTime(); #endif - set_scene(p_desk_mod.toInt(), old_pos); + set_scene(p_desk_mod.toInt(), old_pos); - QList affected_list = {ui_vp_background, ui_vp_desk, ui_vp_player_char}; + QList affected_list = {ui_vp_background, ui_vp_desk, ui_vp_player_char}; - bool paired = false; - if (!ui_vp_sideplayer_char->isHidden()) { - affected_list.append(ui_vp_sideplayer_char); - paired = true; - } + bool paired = false; + if (!ui_vp_sideplayer_char->isHidden()) + { + affected_list.append(ui_vp_sideplayer_char); + paired = true; + } - // Set up the background, desk, and player objects' animations + // Set up the background, desk, and player objects' animations - float scaling_factor = ui_vp_background->get_scaling_factor(); - int offset = (float(old_pos_pair.second) * scaling_factor) - (float(new_pos_pair.second) * scaling_factor); + float scaling_factor = ui_vp_background->get_scaling_factor(); + int offset = (float(old_pos_pair.second) * scaling_factor) - (float(new_pos_pair.second) * scaling_factor); - for (AOLayer *ui_element : affected_list) { - QPropertyAnimation *transition_animation = new QPropertyAnimation(ui_element, "pos", this); - transition_animation->setStartValue(ui_element->pos()); - transition_animation->setDuration(duration); - transition_animation->setEndValue(QPoint(ui_element->pos().x() + offset, ui_element->pos().y())); - transition_animation->setEasingCurve(QEasingCurve::InOutCubic); - transition_animation_group->addAnimation(transition_animation); - } + for (AOLayer *ui_element : affected_list) + { + QPropertyAnimation *transition_animation = new QPropertyAnimation(ui_element, "pos", this); + transition_animation->setStartValue(ui_element->pos()); + transition_animation->setDuration(duration); + transition_animation->setEndValue(QPoint(ui_element->pos().x() + offset, ui_element->pos().y())); + transition_animation->setEasingCurve(QEasingCurve::InOutCubic); + transition_animation_group->addAnimation(transition_animation); + } - // Setting up the dummy characters to work for us as our stand-in for the next characters - // This should be easy. But it isn't + // Setting up the dummy characters to work for us as our stand-in for the next characters + // This should be easy. But it isn't - QString slide_emote; - if (m_chatmessage[OBJECTION_MOD].contains("4") || m_chatmessage[OBJECTION_MOD].toInt() == 2) { - slide_emote = "(a)" + ao_app->read_char_ini(m_chatmessage[CHAR_NAME], "objection_pose", "Options"); - if (slide_emote == "(a)") { - slide_emote = "(a)" + m_chatmessage[EMOTE]; - } + QString slide_emote; + if (m_chatmessage[OBJECTION_MOD].contains("4") || m_chatmessage[OBJECTION_MOD].toInt() == 2) + { + slide_emote = "(a)" + ao_app->read_char_ini(m_chatmessage[CHAR_NAME], "objection_pose", "Options"); + if (slide_emote == "(a)") + { + slide_emote = "(a)" + m_chatmessage[EMOTE]; } - else - slide_emote = "(a)" + m_chatmessage[EMOTE]; + } + else + slide_emote = "(a)" + m_chatmessage[EMOTE]; - QString other_slide_emote = "(a)" + m_chatmessage[OTHER_EMOTE]; + QString other_slide_emote = "(a)" + m_chatmessage[OTHER_EMOTE]; - // Load the image we're going to use to get scaling information, and move it into the final position for animation data - ui_vp_dummy_char->set_flipped(m_chatmessage[FLIP].toInt()); - ui_vp_dummy_char->load_image(slide_emote, m_chatmessage[CHAR_NAME], 0, false); - set_self_offset(m_chatmessage[SELF_OFFSET], ui_vp_dummy_char); + // Load the image we're going to use to get scaling information, and move it into the final position for animation data + ui_vp_dummy_char->set_flipped(m_chatmessage[FLIP].toInt()); + ui_vp_dummy_char->load_image(slide_emote, m_chatmessage[CHAR_NAME], 0, false); + set_self_offset(m_chatmessage[SELF_OFFSET], ui_vp_dummy_char); - QPoint starting_position = QPoint(ui_vp_player_char->pos().x() - offset, ui_vp_player_char->pos().y()); - QPropertyAnimation *ui_vp_dummy_animation = new QPropertyAnimation(ui_vp_dummy_char, "pos", this); + QPoint starting_position = QPoint(ui_vp_player_char->pos().x() - offset, ui_vp_player_char->pos().y()); + QPropertyAnimation *ui_vp_dummy_animation = new QPropertyAnimation(ui_vp_dummy_char, "pos", this); - ui_vp_dummy_animation->setDuration(duration); - ui_vp_dummy_animation->setStartValue(starting_position); - ui_vp_dummy_animation->setEndValue(ui_vp_dummy_char->pos()); - ui_vp_dummy_animation->setEasingCurve(QEasingCurve::InOutCubic); - transition_animation_group->addAnimation(ui_vp_dummy_animation); + ui_vp_dummy_animation->setDuration(duration); + ui_vp_dummy_animation->setStartValue(starting_position); + ui_vp_dummy_animation->setEndValue(ui_vp_dummy_char->pos()); + ui_vp_dummy_animation->setEasingCurve(QEasingCurve::InOutCubic); + transition_animation_group->addAnimation(ui_vp_dummy_animation); - ui_vp_dummy_char->move(starting_position.x(), starting_position.y()); + ui_vp_dummy_char->move(starting_position.x(), starting_position.y()); - // If the new message is paired, do it all again for the pair character. Yippee! - if (m_chatmessage[OTHER_CHARID].toInt() != -1 && !m_chatmessage[OTHER_NAME].isEmpty()) { - ui_vp_sidedummy_char->set_flipped(m_chatmessage[OTHER_FLIP].toInt()); - ui_vp_sidedummy_char->load_image(other_slide_emote, m_chatmessage[OTHER_NAME], 0, false); - set_self_offset(m_chatmessage[OTHER_OFFSET], ui_vp_sidedummy_char); - QStringList args = m_chatmessage[OTHER_CHARID].split("^"); - if (args.size() > 1) - { - // Change the order of appearance based on the pair order variable - int order = args.at(1).toInt(); - switch (order) { - case 0: // Our character is in front - ui_vp_sidedummy_char->stackUnder(ui_vp_dummy_char); - break; - case 1: // Our character is behind - ui_vp_dummy_char->stackUnder(ui_vp_sidedummy_char); - break; - default: - break; - } - } + // If the new message is paired, do it all again for the pair character. Yippee! + if (m_chatmessage[OTHER_CHARID].toInt() != -1 && !m_chatmessage[OTHER_NAME].isEmpty()) + { + ui_vp_sidedummy_char->set_flipped(m_chatmessage[OTHER_FLIP].toInt()); + ui_vp_sidedummy_char->load_image(other_slide_emote, m_chatmessage[OTHER_NAME], 0, false); + set_self_offset(m_chatmessage[OTHER_OFFSET], ui_vp_sidedummy_char); + QStringList args = m_chatmessage[OTHER_CHARID].split("^"); + if (args.size() > 1) + { + // Change the order of appearance based on the pair order variable + int order = args.at(1).toInt(); + switch (order) + { + case 0: // Our character is in front + ui_vp_sidedummy_char->stackUnder(ui_vp_dummy_char); + break; + case 1: // Our character is behind + ui_vp_dummy_char->stackUnder(ui_vp_sidedummy_char); + break; + default: + break; + } + } - QPoint other_starting_position = QPoint(ui_vp_sideplayer_char->pos().x() - offset, ui_vp_sideplayer_char->pos().y()); - QPropertyAnimation *ui_vp_sidedummy_animation = new QPropertyAnimation(ui_vp_sidedummy_char, "pos", this); + QPoint other_starting_position = QPoint(ui_vp_sideplayer_char->pos().x() - offset, ui_vp_sideplayer_char->pos().y()); + QPropertyAnimation *ui_vp_sidedummy_animation = new QPropertyAnimation(ui_vp_sidedummy_char, "pos", this); - ui_vp_sidedummy_animation->setDuration(duration); - ui_vp_sidedummy_animation->setStartValue(starting_position); - ui_vp_sidedummy_animation->setEndValue(ui_vp_sidedummy_char->pos()); - ui_vp_sidedummy_animation->setEasingCurve(QEasingCurve::InOutCubic); - transition_animation_group->addAnimation(ui_vp_sidedummy_animation); + ui_vp_sidedummy_animation->setDuration(duration); + ui_vp_sidedummy_animation->setStartValue(starting_position); + ui_vp_sidedummy_animation->setEndValue(ui_vp_sidedummy_char->pos()); + ui_vp_sidedummy_animation->setEasingCurve(QEasingCurve::InOutCubic); + transition_animation_group->addAnimation(ui_vp_sidedummy_animation); - ui_vp_sidedummy_char->move(starting_position.x(), starting_position.y()); - } - else { - ui_vp_sidedummy_char->stop(); - } + ui_vp_sidedummy_char->move(starting_position.x(), starting_position.y()); + } + else + { + ui_vp_sidedummy_char->stop(); + } - ui_vp_player_char->freeze(); - ui_vp_player_char->show(); - if (paired) { - ui_vp_sideplayer_char->freeze(); - ui_vp_sideplayer_char->show(); - } - else { - ui_vp_sideplayer_char->stop(); - } - ui_vp_dummy_char->freeze(); - ui_vp_sidedummy_char->freeze(); - QTimer::singleShot(TRANSITION_BOOKEND_DELAY, transition_animation_group, SLOT(start())); + ui_vp_player_char->freeze(); + ui_vp_player_char->show(); + if (paired) + { + ui_vp_sideplayer_char->freeze(); + ui_vp_sideplayer_char->show(); + } + else + { + ui_vp_sideplayer_char->stop(); + } + ui_vp_dummy_char->freeze(); + ui_vp_sidedummy_char->freeze(); + QTimer::singleShot(TRANSITION_BOOKEND_DELAY, transition_animation_group, SLOT(start())); } - -void Courtroom::on_transition_finish() { - transition_animation_group->clear(); - transition_animation_group->setCurrentTime(0); - QTimer::singleShot(TRANSITION_BOOKEND_DELAY, this, SLOT(post_transition_cleanup())); +void Courtroom::on_transition_finish() +{ + transition_animation_group->clear(); + transition_animation_group->setCurrentTime(0); + QTimer::singleShot(TRANSITION_BOOKEND_DELAY, this, SLOT(post_transition_cleanup())); } -void Courtroom::post_transition_cleanup() { - - set_scene(m_chatmessage[DESK_MOD].toInt(), m_chatmessage[SIDE]); - - // Move the character on the viewport according to the offsets - set_self_offset(m_chatmessage[SELF_OFFSET], ui_vp_player_char); +void Courtroom::post_transition_cleanup() +{ + set_scene(m_chatmessage[DESK_MOD].toInt(), m_chatmessage[SIDE]); - int emote_mod = m_chatmessage[EMOTE_MOD].toInt(); - bool immediate = m_chatmessage[IMMEDIATE].toInt() == 1; + // Move the character on the viewport according to the offsets + set_self_offset(m_chatmessage[SELF_OFFSET], ui_vp_player_char); + int emote_mod = m_chatmessage[EMOTE_MOD].toInt(); + bool immediate = m_chatmessage[IMMEDIATE].toInt() == 1; - // Reset the pair character - ui_vp_sideplayer_char->stop(); - ui_vp_sideplayer_char->move_and_center(0, 0); + // Reset the pair character + ui_vp_sideplayer_char->stop(); + ui_vp_sideplayer_char->move_and_center(0, 0); - // If the emote_mod is not zooming - if (emote_mod != ZOOM && emote_rows != PREANIM_ZOOM) { - // Display the pair character - display_pair_character(m_chatmessage[OTHER_CHARID], m_chatmessage[OTHER_OFFSET]); - } + // If the emote_mod is not zooming + if (emote_mod != ZOOM && emote_rows != PREANIM_ZOOM) + { + // Display the pair character + display_pair_character(m_chatmessage[OTHER_CHARID], m_chatmessage[OTHER_OFFSET]); + } - // Reset tweedle dee and tweedle dummy - ui_vp_dummy_char->stop(); - ui_vp_dummy_char->move_and_center(0,0); - ui_vp_sidedummy_char->stop(); - ui_vp_sidedummy_char->move_and_center(0,0); + // Reset tweedle dee and tweedle dummy + ui_vp_dummy_char->stop(); + ui_vp_dummy_char->move_and_center(0, 0); + ui_vp_sidedummy_char->stop(); + ui_vp_sidedummy_char->move_and_center(0, 0); - // Parse the emote_mod part of the chat message - handle_emote_mod(emote_mod, immediate); + // Parse the emote_mod part of the chat message + handle_emote_mod(emote_mod, immediate); } void Courtroom::do_flash() @@ -4547,7 +4553,7 @@ void Courtroom::set_scene(bool show_desk, const QString f_side) } } -void Courtroom::set_self_offset(const QString& p_list, AOLayer* p_layer) +void Courtroom::set_self_offset(const QString &p_list, AOLayer *p_layer) { QStringList self_offsets = p_list.split("&"); int self_offset = self_offsets[0].toInt(); @@ -6418,7 +6424,6 @@ void Courtroom::on_settings_clicked() ao_app->call_settings_menu(); } - void Courtroom::on_additive_clicked() { if (ui_additive->isChecked()) From e2b47afd912e80f784bc6a25bf03e65eb405e0a6 Mon Sep 17 00:00:00 2001 From: in1tiate <32779090+in1tiate@users.noreply.github.com> Date: Mon, 20 May 2024 12:03:36 -0500 Subject: [PATCH 06/16] stop filling the log with raw net traffic --- src/courtroom.cpp | 19 +++++++++++++------ src/networkmanager.cpp | 5 ++++- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/courtroom.cpp b/src/courtroom.cpp index ba76604e7..ab8d2c4e2 100644 --- a/src/courtroom.cpp +++ b/src/courtroom.cpp @@ -1415,15 +1415,22 @@ void Courtroom::set_background(QString p_background, bool display) pos_list.append(default_pos[key]); } } + if (file_exists(ao_app->get_image_suffix(ao_app->get_background_path("court")))) + { + const QStringList overrides = {"def", "wit", "pro"}; + for (const QString &override_pos : overrides) + { + if (!ao_app->read_design_ini("court:" + override_pos + "/pos_center", ao_app->get_background_path("design.ini")).isEmpty()) + { + pos_list.append(override_pos); + } + } + } for (const QString &pos : ao_app->read_design_ini("positions", ao_app->get_background_path("design.ini")).split(",")) { QString real_pos = pos.split(":")[0]; - QStringList overrides = {"def", "wit", "pro"}; - if ((file_exists(ao_app->get_image_suffix(ao_app->get_background_path(real_pos)))) || // Normal check, OR - (overrides.contains(pos) && // It's one of our subpos overrides, AND - file_exists(ao_app->get_image_suffix(ao_app->get_background_path("court"))) && // the "court" default image exists, AND - !ao_app->read_design_ini("court:" + pos + "/pos_center", ao_app->get_background_path("design.ini")).isEmpty())) - { // config exists for this pos + if ((file_exists(ao_app->get_image_suffix(ao_app->get_background_path(real_pos))))) + { pos_list.append(pos); } } diff --git a/src/networkmanager.cpp b/src/networkmanager.cpp index 69ff839c5..b574b6386 100644 --- a/src/networkmanager.cpp +++ b/src/networkmanager.cpp @@ -197,8 +197,9 @@ void NetworkManager::ship_server_packet(AOPacket packet) qCritical() << "Failed to ship packet; not connected."; return; } - +#ifdef NETWORK_DEBUG qInfo().noquote() << "Sending packet:" << packet.toString(); +#endif m_connection->sendPacket(packet); } @@ -209,6 +210,8 @@ void NetworkManager::join_to_server() void NetworkManager::handle_server_packet(AOPacket packet) { +#ifdef NETWORK_DEBUG qInfo().noquote() << "Received packet:" << packet.toString(); +#endif ao_app->server_packet_received(packet); } From 2c696f2cd1c8ecb806f12d0224d5e8beceb973cd Mon Sep 17 00:00:00 2001 From: in1tiate <32779090+in1tiate@users.noreply.github.com> Date: Mon, 20 May 2024 12:32:35 -0500 Subject: [PATCH 07/16] other random fixes fix evidence presenting sfx playing at literally 100x intended volume fix "def2" etc wrong evidence icon position, remove inaccurate evidence positions --- src/aosfxplayer.cpp | 2 +- src/courtroom.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/aosfxplayer.cpp b/src/aosfxplayer.cpp index 0775eb99c..ac3b8513d 100644 --- a/src/aosfxplayer.cpp +++ b/src/aosfxplayer.cpp @@ -8,7 +8,7 @@ AOSfxPlayer::AOSfxPlayer(AOApplication *ao_app) int AOSfxPlayer::volume() { - return m_volume * 100; + return m_volume; } void AOSfxPlayer::setVolume(int value) diff --git a/src/courtroom.cpp b/src/courtroom.cpp index ab8d2c4e2..15fc62ed5 100644 --- a/src/courtroom.cpp +++ b/src/courtroom.cpp @@ -3435,7 +3435,7 @@ void Courtroom::display_evidence_image() QString f_image = local_evidence_list.at(f_evi_id - 1).image; // QString f_evi_name = local_evidence_list.at(f_evi_id - 1).name; // def jud and hlp should display the evidence icon on the RIGHT side - bool is_left_side = !(side == "def" || side == "hlp" || side == "jud" || side == "jur"); + bool is_left_side = !(side.startsWith("def") || side == "hlp"); ui_vp_evidence_display->show_evidence(f_evi_id, f_image, is_left_side, sfx_player->volume()); } } From 137a2d3a04bb0381d1923b1a9530d1cdd9872e88 Mon Sep 17 00:00:00 2001 From: in1tiate <32779090+in1tiate@users.noreply.github.com> Date: Mon, 20 May 2024 12:46:35 -0500 Subject: [PATCH 08/16] speedline thing --- src/courtroom.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/courtroom.cpp b/src/courtroom.cpp index 15fc62ed5..b5f62f5a8 100644 --- a/src/courtroom.cpp +++ b/src/courtroom.cpp @@ -3453,7 +3453,7 @@ void Courtroom::handle_ic_speaking() // Obtain character information for our character QString filename; // I still hate this hardcoding. If we're on pos pro, hlp and wit, use prosecution_speedlines. Otherwise, defense_speedlines. - if (side == "pro" || side == "hlp" || side == "wit") + if (side.startsWith("pro") || side == "hlp" || side.startsWith("wit")) { filename = "prosecution_speedlines"; } From 695d51dbfe858d877408de78b424c1af8fc30e3a Mon Sep 17 00:00:00 2001 From: TrickyLeifa Date: Wed, 22 May 2024 22:10:29 +0200 Subject: [PATCH 09/16] Complete AOLayer reimplementation, ... * Complete AOLayer reimplementation * Reimplemented sliding as well. --- CMakeLists.txt | 6 +- src/animationlayer.cpp | 662 +++++++++++++++++++++++++++++++ src/animationlayer.h | 261 +++++++++++++ src/animationloader.cpp | 103 +++++ src/animationloader.h | 55 +++ src/aoapplication.cpp | 14 + src/aoapplication.h | 4 +- src/aoemotepreview.cpp | 23 +- src/aoemotepreview.h | 12 +- src/aoevidencedisplay.cpp | 16 +- src/aoevidencedisplay.h | 4 +- src/aolayer.cpp | 749 ------------------------------------ src/aolayer.h | 297 -------------- src/courtroom.cpp | 531 ++++++++++++------------- src/courtroom.h | 40 +- src/emotes.cpp | 21 +- src/path_functions.cpp | 21 +- src/text_file_functions.cpp | 2 + 18 files changed, 1424 insertions(+), 1397 deletions(-) create mode 100644 src/animationlayer.cpp create mode 100644 src/animationlayer.h create mode 100644 src/animationloader.cpp create mode 100644 src/animationloader.h delete mode 100644 src/aolayer.cpp delete mode 100644 src/aolayer.h diff --git a/CMakeLists.txt b/CMakeLists.txt index cbd32fa98..b1f25290b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,8 +38,10 @@ add_executable(Attorney_Online src/aoevidencedisplay.h src/aoimage.cpp src/aoimage.h - src/aolayer.cpp - src/aolayer.h + src/animationlayer.cpp + src/animationlayer.h + src/animationloader.h + src/animationloader.cpp src/aomusicplayer.cpp src/aomusicplayer.h src/aopacket.cpp diff --git a/src/animationlayer.cpp b/src/animationlayer.cpp new file mode 100644 index 000000000..310f97078 --- /dev/null +++ b/src/animationlayer.cpp @@ -0,0 +1,662 @@ +#include "animationlayer.h" + +#include "aoapplication.h" +#include "options.h" + +#include + +static QThreadPool *thread_pool; + +namespace kal +{ +AnimationLayer::AnimationLayer(QWidget *parent) + : QLabel(parent) +{ + setAlignment(Qt::AlignCenter); + + m_ticker = new QTimer(this); + m_ticker->setSingleShot(true); + connect(m_ticker, &QTimer::timeout, this, &AnimationLayer::frameTicker); + + if (!thread_pool) + { + thread_pool = new QThreadPool(qApp); + thread_pool->setMaxThreadCount(8); + } + + createLoader(); +} + +AnimationLayer::~AnimationLayer() +{ + deleteLoader(); +} + +QString AnimationLayer::fileName() +{ + return m_file_name; +} + +void AnimationLayer::setFileName(QString fileName) +{ + stopPlayback(); + m_file_name = fileName; + if (m_file_name.trimmed().isEmpty()) + { +#ifdef DEBUG_MOVIE + qWarning() << "AnimationLayer::setFileName called with empty string"; +#endif + m_file_name = QObject::tr("Invalid File"); + } + resetData(); +} + +void AnimationLayer::startPlayback() +{ + if (m_processing) + { +#ifdef DEBUG_MOVIE + qWarning() << "AnimationLayer::startPlayback called while already processing"; +#endif + return; + } + resetData(); + m_processing = true; + Q_EMIT startedPlayback(); + frameTicker(); +} + +void AnimationLayer::stopPlayback() +{ + if (m_ticker->isActive()) + { + m_ticker->stop(); + } + m_processing = false; + if (m_reset_cache_when_stopped) + { + createLoader(); + } + Q_EMIT stoppedPlayback(); +} + +void AnimationLayer::restartPlayback() +{ + stopPlayback(); + startPlayback(); +} + +void AnimationLayer::pausePlayback(bool enabled) +{ + if (m_pause == enabled) + { +#ifdef DEBUG_MOVIE + qWarning() << "AnimationLayer::pausePlayback called with identical state"; +#endif + return; + } + m_pause = enabled; +} + +QSize AnimationLayer::frameSize() +{ + return m_frame_size; +} + +int AnimationLayer::frameCount() +{ + return m_frame_count; +} + +int AnimationLayer::currentFrameNumber() +{ + return m_frame_number; +} + +/** + * @brief AnimationLayer::jumpToFrame + * @param number The frame number to jump to. Must be in valid range. If the number is out of range, the method does nothing. + * @details If frame number is valid and playback is processing, the frame will immediately be displayed. + */ +void AnimationLayer::jumpToFrame(int number) +{ + if (number < 0 || number >= m_frame_count) + { +#ifdef DEBUG_MOVIE + qWarning() << "AnimationLayer::jumpToFrame failed to jump to frame" << number << "(file:" << m_file_name << ", frame count:" << m_frame_count << ")"; +#endif + return; + } + + if (m_ticker->isActive()) + { + m_ticker->stop(); + } + m_frame_number = number; + frameTicker(); +} + +void AnimationLayer::setPlayOnce(bool enabled) +{ + m_play_once = enabled; +} + +void AnimationLayer::setStretchToFit(bool enabled) +{ + m_stretch_to_fit = enabled; +} + +void AnimationLayer::setResetCacheWhenStopped(bool enabled) +{ + m_reset_cache_when_stopped = enabled; +} + +void AnimationLayer::setFlipped(bool enabled) +{ + m_flipped = enabled; +} + +void AnimationLayer::setTransformationMode(Qt::TransformationMode mode) +{ + m_transformation_mode = mode; +} + +void AnimationLayer::setMinimumDurationPerFrame(int duration) +{ + return; + m_minimum_duration = duration; +} + +void AnimationLayer::setMaximumDurationPerFrame(int duration) +{ + return; + m_maximum_duration = duration; +} + +void AnimationLayer::setMaskingRect(QRect rect) +{ + return; // TODO re-enable + + m_mask_rect_hint = rect; + calculateFrameGeometry(); +} + +void AnimationLayer::resizeEvent(QResizeEvent *event) +{ + QLabel::resizeEvent(event); + calculateFrameGeometry(); +} + +void AnimationLayer::createLoader() +{ + deleteLoader(); + m_loader = new AnimationLoader(thread_pool); +} + +void AnimationLayer::deleteLoader() +{ + if (m_loader) + { + delete m_loader; + m_loader = nullptr; + } +} + +void AnimationLayer::resetData() +{ + m_first_frame = true; + m_frame_number = 0; + if (m_file_name != m_loader->loadedFileName()) + { + m_loader->load(m_file_name); + } + m_frame_count = m_loader->frameCount(); + m_frame_size = m_loader->size(); + m_frame_rect = QRect(QPoint(0, 0), m_frame_size); + m_ticker->stop(); + calculateFrameGeometry(); +} + +void AnimationLayer::calculateFrameGeometry() +{ + m_mask_rect = QRect(); + m_display_rect = QRect(); + m_scaled_frame_size = QSize(); + + QSize widget_size = size(); + if (!widget_size.isValid() || !m_frame_size.isValid()) + { + return; + } + + if (m_stretch_to_fit) + { + m_scaled_frame_size = widget_size; + } + else + { + QSize target_frame_size = m_frame_size; + if (m_frame_rect.contains(m_mask_rect_hint)) + { + m_mask_rect = m_mask_rect_hint; + target_frame_size = m_mask_rect_hint.size(); + } + + double scale = double(widget_size.height()) / double(target_frame_size.height()); + m_scaled_frame_size = target_frame_size * scale; + + // display the frame in its center + int x = (m_scaled_frame_size.width() - widget_size.width()) / 2; + m_display_rect = QRect(x, 0, widget_size.width(), m_scaled_frame_size.height()); + } + + displayCurrentFrame(); +} + +void AnimationLayer::finishPlayback() +{ + stopPlayback(); + Q_EMIT finishedPlayback(); +} + +void AnimationLayer::prepareNextTick() +{ + int duration = m_current_frame.duration; + + duration = (m_minimum_duration > 0) ? qMax(m_minimum_duration, duration) : duration; + duration = (m_maximum_duration > 0) ? qMin(m_maximum_duration, duration) : duration; + + m_ticker->start(duration); +} + +void AnimationLayer::displayCurrentFrame() +{ + QPixmap image = m_current_frame.texture; + + if (m_frame_size.isValid()) + { + if (m_mask_rect.isValid()) + { + image = image.copy(m_mask_rect); + } + + if (!image.isNull()) + { + image = image.scaled(m_scaled_frame_size, Qt::IgnoreAspectRatio, m_transformation_mode); + + if (m_display_rect.isValid()) + { + image = image.copy(m_display_rect); + } + + if (m_flipped) + { + image = image.transformed(QTransform().scale(-1.0, 1.0)); + } + } + } + else + { + image = QPixmap(1, 1); + image.fill(Qt::transparent); + } + + setPixmap(image); +} + +void AnimationLayer::frameTicker() +{ + if (!m_processing) + { + return; + } + + if (m_frame_count < 1) + { + if (m_play_once) + { + finishPlayback(); + return; + } + } + + if (m_pause && !m_first_frame) + { + return; + } + + if (m_frame_number == m_frame_count) + { + if (m_play_once) + { + finishPlayback(); + return; + } + + if (m_frame_count > 1) + { + m_frame_number = 0; + } + else + { + return; + } + } + + m_first_frame = false; + m_current_frame = m_loader->frame(m_frame_number); + displayCurrentFrame(); + Q_EMIT frameNumberChanged(m_frame_number); + ++m_frame_number; + + if (!m_pause) + { + m_ticker->start(m_current_frame.duration); + } +} + +CharacterAnimationLayer::CharacterAnimationLayer(AOApplication *ao_app, QWidget *parent) + : AnimationLayer(parent) + , ao_app(ao_app) +{ + m_duration_timer = new QTimer(this); + m_duration_timer->setSingleShot(true); + connect(m_duration_timer, &QTimer::timeout, this, &CharacterAnimationLayer::onDurationLimitReached); + + connect(this, &CharacterAnimationLayer::stoppedPlayback, this, &CharacterAnimationLayer::onPlaybackStopped); + connect(this, &CharacterAnimationLayer::frameNumberChanged, this, &CharacterAnimationLayer::notifyFrameEffect); + connect(this, &CharacterAnimationLayer::finishedPlayback, this, &CharacterAnimationLayer::notifyEmotePlaybackFinished); +} + +void CharacterAnimationLayer::loadCharacterEmote(QString character, QString fileName, EmoteType emoteType, int durationLimit) +{ + auto is_dialog_emote = [](EmoteType emoteType) { + return emoteType == IdleEmote || emoteType == TalkEmote; + }; + + bool synchronize_frame = false; + const int previous_frame_count = frameCount(); + const int previous_frame_number = currentFrameNumber(); + if (m_character == character && m_emote == fileName && is_dialog_emote(m_emote_type) && is_dialog_emote(emoteType)) + { + synchronize_frame = true; + } + + m_character = character; + m_emote = fileName; + m_emote_type = emoteType; + + QStringList prefixes; + bool placeholder_fallback = false; + bool play_once = false; + switch (emoteType) + { + default: + break; + + case PreEmote: + play_once = true; + break; + + case IdleEmote: + prefixes << QStringLiteral("(a)") << QStringLiteral("(a)/"); + placeholder_fallback = true; + break; + + case TalkEmote: + prefixes << QStringLiteral("(b)") << QStringLiteral("(b)/"); + placeholder_fallback = true; + break; + + case PostEmote: + prefixes << QStringLiteral("(c)") << QStringLiteral("(c)/"); + break; + } + + QVector path_list; + for (const QString &prefix : qAsConst(prefixes)) + { + path_list << ao_app->get_character_path(character, prefix + m_emote); + } + path_list << ao_app->get_character_path(character, m_emote); + + if (placeholder_fallback) + { + path_list << ao_app->get_character_path(character, QStringLiteral("placeholder")); + path_list << ao_app->get_theme_path("placeholder", ao_app->default_theme); + } + + setFileName(ao_app->get_image_path(path_list)); + setPlayOnce(play_once); + setTransformationMode(ao_app->get_scaling(ao_app->get_emote_property(character, fileName, "scaling"))); + setStretchToFit(ao_app->get_emote_property(character, fileName, "stretch").startsWith("true")); + if (synchronize_frame && previous_frame_count == frameCount()) + { + jumpToFrame(previous_frame_number); + } + m_duration = durationLimit; +} + +void CharacterAnimationLayer::setFrameEffects(QStringList data) +{ + m_effects.clear(); + + static const QList EFFECT_TYPE_LIST{ShakeEffect, FlashEffect, SfxEffect}; + for (int i = 0; i < data.length(); ++i) + { + const EffectType effect_type = EFFECT_TYPE_LIST.at(i); + + QStringList emotes = data.at(i).split("^"); + for (const QString &emote : qAsConst(emotes)) + { + QStringList emote_effects = emote.split("|"); + + const QString emote_name = emote_effects.takeFirst(); + + for (const QString &raw_effect : qAsConst(emote_effects)) + { + QStringList frame_data = raw_effect.split("="); + + const int frame_number = frame_data.at(0).toInt(); + + FrameEffect effect; + effect.emote_name = emote_name; + effect.type = effect_type; + if (effect_type == EffectType::SfxEffect) + { + effect.file_name = frame_data.at(1); + } + + m_effects[frame_number].append(effect); + } + } + } +} + +void CharacterAnimationLayer::startTimeLimit() +{ + if (m_duration > 0) + { + m_duration_timer->start(m_duration); + } +} + +void CharacterAnimationLayer::onPlaybackStopped() +{ + if (m_duration_timer->isActive()) + { + m_duration_timer->stop(); + } +} + +void CharacterAnimationLayer::notifyEmotePlaybackFinished() +{ + if (m_emote_type == PreEmote || m_emote_type == PostEmote) + { + Q_EMIT finishedPreOrPostEmotePlayback(); + } +} + +void CharacterAnimationLayer::onPlaybackFinished() +{ + if (m_emote_type == PreEmote || m_emote_type == PostEmote) + { + if (m_duration_timer->isActive()) + { + m_duration_timer->stop(); + } + + notifyEmotePlaybackFinished(); + } +} + +void CharacterAnimationLayer::onDurationLimitReached() +{ + stopPlayback(); + notifyEmotePlaybackFinished(); +} + +void CharacterAnimationLayer::notifyFrameEffect(int frameNumber) +{ + auto it = m_effects.constFind(frameNumber); + if (it != m_effects.constEnd()) + { + for (const FrameEffect &effect : qAsConst(*it)) + { + if (effect.emote_name == m_emote) + { + switch (effect.type) + { + default: + break; + + case EffectType::SfxEffect: + Q_EMIT soundEffect(effect.file_name); + break; + + case EffectType::ShakeEffect: + Q_EMIT shakeEffect(); + break; + + case EffectType::FlashEffect: + Q_EMIT flashEffect(); + break; + } + } + } + } +} + +BackgroundAnimationLayer::BackgroundAnimationLayer(AOApplication *ao_app, QWidget *parent) + : AnimationLayer(parent) + , ao_app(ao_app) +{} + +void BackgroundAnimationLayer::loadAndPlayAnimation(QString fileName) +{ + QString file_path = ao_app->get_image_suffix(ao_app->get_background_path(fileName)); +#ifdef DEBUG_MOVIE + if (file_path.isEmpty()) + { + qWarning() << "[BackgroundLayer] Failed to load background:" << fileName; + } + else if (file_path == this->fileName()) + { + return; + } + else + { + qInfo() << "[BackgroundLayer] Loading background:" << file_path; + } +#endif + + setFileName(file_path); + VPath design_path = ao_app->get_background_path("design.ini"); + setTransformationMode(ao_app->get_scaling(ao_app->read_design_ini("scaling", design_path))); + setStretchToFit(ao_app->read_design_ini("stretch", design_path).startsWith("true")); + startPlayback(); +} + +SplashAnimationLayer::SplashAnimationLayer(AOApplication *ao_app, QWidget *parent) + : AnimationLayer(parent) + , ao_app(ao_app) +{ + connect(this, &SplashAnimationLayer::startedPlayback, this, &SplashAnimationLayer::show); + connect(this, &SplashAnimationLayer::stoppedPlayback, this, &SplashAnimationLayer::hide); +} + +void SplashAnimationLayer::loadAndPlayAnimation(QString p_filename, QString p_charname, QString p_miscname) +{ + QString file_path = ao_app->get_image(p_filename, Options::getInstance().theme(), Options::getInstance().subTheme(), ao_app->default_theme, p_miscname, p_charname, "placeholder"); + setFileName(file_path); + setTransformationMode(ao_app->get_misc_scaling(p_miscname)); + startPlayback(); +} + +EffectAnimationLayer::EffectAnimationLayer(AOApplication *ao_app, QWidget *parent) + : AnimationLayer(parent) + , ao_app(ao_app) +{ + connect(this, &EffectAnimationLayer::startedPlayback, this, &EffectAnimationLayer::show); + connect(this, &EffectAnimationLayer::stoppedPlayback, this, &EffectAnimationLayer::maybeHide); +} + +void EffectAnimationLayer::loadAndPlayAnimation(QString p_filename, bool repeat) +{ + setFileName(p_filename); + setPlayOnce(!repeat); + startPlayback(); +} + +void EffectAnimationLayer::setHideWhenStopped(bool enabled) +{ + m_hide_when_stopped = enabled; +} + +void EffectAnimationLayer::maybeHide() +{ + if (m_hide_when_stopped) + { + hide(); + } +} + +InterfaceAnimationLayer::InterfaceAnimationLayer(AOApplication *ao_app, QWidget *parent) + : AnimationLayer(parent) + , ao_app(ao_app) +{ + setStretchToFit(true); + + connect(this, &InterfaceAnimationLayer::startedPlayback, this, &InterfaceAnimationLayer::show); + connect(this, &InterfaceAnimationLayer::stoppedPlayback, this, &InterfaceAnimationLayer::hide); +} + +void InterfaceAnimationLayer::loadAndPlayAnimation(QString fileName, QString miscName) +{ + QString file_path = ao_app->get_image(fileName, Options::getInstance().theme(), Options::getInstance().subTheme(), ao_app->default_theme, miscName); + setFileName(file_path); + startPlayback(); +} + +StickerAnimationLayer::StickerAnimationLayer(AOApplication *ao_app, QWidget *parent) + : AnimationLayer(parent) + , ao_app(ao_app) +{ + connect(this, &StickerAnimationLayer::startedPlayback, this, &StickerAnimationLayer::show); + connect(this, &StickerAnimationLayer::stoppedPlayback, this, &StickerAnimationLayer::hide); +} + +void StickerAnimationLayer::loadAndPlayAnimation(QString fileName) +{ + QString misc_file; // FIXME this is a bad name + if (Options::getInstance().customChatboxEnabled()) + { + misc_file = ao_app->get_chat(fileName); + } + + QString file_path = ao_app->get_image("sticker/" + fileName, Options::getInstance().theme(), Options::getInstance().subTheme(), ao_app->default_theme, misc_file); + setFileName(file_path); + setTransformationMode(ao_app->get_misc_scaling(misc_file)); + startPlayback(); +} +} // namespace kal diff --git a/src/animationlayer.h b/src/animationlayer.h new file mode 100644 index 000000000..da64354aa --- /dev/null +++ b/src/animationlayer.h @@ -0,0 +1,261 @@ +#pragma once + +#include "animationloader.h" + +#include +#include +#include +#include +#include + +// #define DEBUG_MOVIE + +#ifdef DEBUG_MOVIE +#include +#endif + +class AOApplication; +class VPath; + +// "Brief" explanation of what the hell this is: +// +// AOLayer handles all animations both inside and outside +// the viewport. It was originally devised as a layering +// system, but turned into a full refactor of the existing +// animation code. +// +// AOLayer has six subclasses, all of which differ mainly in +// how they handle path resolution. +// +// - BackgroundLayer: self-explanatory, handles files found in base/background +// - CharLayer: handles all the "wonderful" quirks of character path resolution +// - SplashLayer: handles elements that can either be provided by a misc/ directory +// or by the theme - speedlines, shouts, WT/CE, et cetera +// - EffectLayer: this is basically a dummy layer since effects do their own wonky +// path resolution in a different file +// - InterfaceLayer: handles UI elements like the chat arrow and the music display +// - StickerLayer: Crystalwarrior really wanted this. Handles "stickers," whatever those are. +// +// For questions comments or concerns, bother someone else + +namespace kal +{ +class AnimationLayer : public QLabel +{ + Q_OBJECT + +public: + explicit AnimationLayer(QWidget *parent = nullptr); + virtual ~AnimationLayer(); + + QString fileName(); + void setFileName(QString fileName); + + void startPlayback(); + void stopPlayback(); + void restartPlayback(); + void pausePlayback(bool enabled); + + QSize frameSize(); + + int frameCount(); + int currentFrameNumber(); + void jumpToFrame(int number); + + void setPlayOnce(bool enabled); + void setStretchToFit(bool enabled); + void setResetCacheWhenStopped(bool enabled); + void setFlipped(bool enabled); + void setTransformationMode(Qt::TransformationMode mode); + void setMinimumDurationPerFrame(int duration); + void setMaximumDurationPerFrame(int duration); + +public Q_SLOTS: + void setMaskingRect(QRect rect); + +Q_SIGNALS: + void startedPlayback(); + void stoppedPlayback(); /* Is emitted whenever playback is stopped, whether by user or by reaching the end */ + void finishedPlayback(); /* Is emitted only when playback reaches the end */ + void frameNumberChanged(int frameNumber); + +protected: + void resizeEvent(QResizeEvent *event) override; + +private: + QString m_file_name; + bool m_play_once = false; + bool m_stretch_to_fit = false; + bool m_reset_cache_when_stopped = false; + bool m_flipped = false; + int m_minimum_duration = 0; + int m_maximum_duration = 0; + Qt::TransformationMode m_transformation_mode = Qt::FastTransformation; + AnimationLoader *m_loader = nullptr; + QSize m_frame_size; + QRect m_frame_rect; + QRect m_mask_rect_hint; + QRect m_mask_rect; + QRect m_display_rect; + QSize m_scaled_frame_size; + bool m_processing = false; + bool m_pause = false; + QTimer *m_ticker = nullptr; + bool m_first_frame = false; + int m_frame_number = 0; + int m_frame_count = 0; + AnimationFrame m_current_frame; + + void createLoader(); + void deleteLoader(); + + void resetData(); + + void calculateFrameGeometry(); + + void finishPlayback(); + + void prepareNextTick(); + + void displayCurrentFrame(); + +private Q_SLOTS: + void frameTicker(); +}; + +class CharacterAnimationLayer : public AnimationLayer +{ + Q_OBJECT + +public: + enum EmoteType + { + NoEmoteType, + PreEmote, + IdleEmote, + TalkEmote, + PostEmote, + }; + + enum EffectType + { + SfxEffect, + ShakeEffect, + FlashEffect, + }; + + class FrameEffect + { + public: + QString emote_name; + EffectType type = SfxEffect; + QString file_name; + }; + + CharacterAnimationLayer(AOApplication *ao_app, QWidget *parent = nullptr); + + void loadCharacterEmote(QString character, QString fileName, EmoteType emoteType, int durationLimit = 0); + + void setFrameEffects(QStringList data); + +Q_SIGNALS: + void finishedPreOrPostEmotePlayback(); + + void soundEffect(QString sfx); + void shakeEffect(); + void flashEffect(); + +private: + AOApplication *ao_app; + + QString m_character; + QString m_emote; + EmoteType m_emote_type = NoEmoteType; + QTimer *m_duration_timer = nullptr; + int m_duration = 0; + + QMap> m_effects; + + void startTimeLimit(); + +private Q_SLOTS: + void onPlaybackStopped(); + void onPlaybackFinished(); + void onDurationLimitReached(); + + void notifyFrameEffect(int frame); + void notifyEmotePlaybackFinished(); +}; + +class BackgroundAnimationLayer : public AnimationLayer +{ + Q_OBJECT + +public: + BackgroundAnimationLayer(AOApplication *ao_app, QWidget *parent = nullptr); + + void loadAndPlayAnimation(QString fileName); + +private: + AOApplication *ao_app; +}; + +class SplashAnimationLayer : public AnimationLayer +{ + Q_OBJECT + +public: + SplashAnimationLayer(AOApplication *ao_app, QWidget *parent = nullptr); + + void loadAndPlayAnimation(QString fileName, QString character, QString miscellaneous); + +private: + AOApplication *ao_app; +}; + +class EffectAnimationLayer : public AnimationLayer +{ + Q_OBJECT + +public: + EffectAnimationLayer(AOApplication *ao_app, QWidget *parent = nullptr); + + void loadAndPlayAnimation(QString fileName, bool repeat = false); + + void setHideWhenStopped(bool enabled); + +private: + AOApplication *ao_app; + + bool m_hide_when_stopped = false; + +private Q_SLOTS: + void maybeHide(); +}; + +class InterfaceAnimationLayer : public AnimationLayer +{ + Q_OBJECT + +public: + InterfaceAnimationLayer(AOApplication *ao_app, QWidget *parent = nullptr); + + void loadAndPlayAnimation(QString fileName, QString miscName); + +private: + AOApplication *ao_app; +}; + +class StickerAnimationLayer : public AnimationLayer +{ + Q_OBJECT + +public: + StickerAnimationLayer(AOApplication *ao_app, QWidget *parent = nullptr); + + void loadAndPlayAnimation(QString fileName); + +private: + AOApplication *ao_app; +}; +} // namespace kal diff --git a/src/animationloader.cpp b/src/animationloader.cpp new file mode 100644 index 000000000..d35744833 --- /dev/null +++ b/src/animationloader.cpp @@ -0,0 +1,103 @@ +#include "animationloader.h" + +#include +#include + +namespace kal +{ +AnimationLoader::AnimationLoader(QThreadPool *threadPool) + : m_thread_pool(threadPool) +{} + +AnimationLoader::~AnimationLoader() +{ + stopLoading(); +} + +QString AnimationLoader::loadedFileName() const +{ + return m_file_name; +} + +void AnimationLoader::load(const QString &fileName) +{ + if (m_file_name == fileName) + { + return; + } + stopLoading(); + m_file_name = fileName; + QImageReader *reader = new QImageReader; + reader->setFileName(fileName); + m_size = reader->size(); + m_frames.clear(); + m_frame_count = reader->imageCount(); + m_loop_count = reader->loopCount(); + m_exit_task = false; + m_task = QtConcurrent::run(m_thread_pool, [this, reader]() { populateVector(reader); }); +} + +void AnimationLoader::stopLoading() +{ + m_exit_task = true; + if (m_task.isRunning()) + { + m_task.waitForFinished(); + } +} + +QSize AnimationLoader::size() +{ + return m_size; +} + +int AnimationLoader::frameCount() +{ + return m_frame_count; +} + +AnimationFrame AnimationLoader::frame(int frameNumber) +{ + if (m_frame_count <= 0) + { + return AnimationFrame(); + } + + m_task_lock.lock(); + while (m_frames.size() < frameNumber + 1) + { + qDebug().noquote() << "Waiting for frame" << frameNumber << QString("(file: %1, frame count: %2)").arg(m_file_name).arg(m_frame_count); + m_task_signal.wait(&m_task_lock); + } + + AnimationFrame frame = qAsConst(m_frames)[frameNumber]; + m_task_lock.unlock(); + + return frame; +} + +int AnimationLoader::loopCount() +{ + return m_loop_count; +} + +void AnimationLoader::populateVector(QImageReader *reader) +{ + int loaded_frame_count = 0; + int frame_count = reader->imageCount(); + while (!m_exit_task && loaded_frame_count < frame_count) + { + { + QMutexLocker locker(&m_task_lock); + AnimationFrame frame; + frame.texture = QPixmap::fromImage(reader->read()); + frame.duration = reader->nextImageDelay(); + m_frames.append(frame); + ++loaded_frame_count; + } + m_task_signal.wakeAll(); + } + + delete reader; +} +} // namespace kal diff --git a/src/animationloader.h b/src/animationloader.h new file mode 100644 index 000000000..a3a103670 --- /dev/null +++ b/src/animationloader.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace kal +{ +class AnimationFrame +{ +public: + QPixmap texture; + int duration = 0; +}; + +class AnimationLoader +{ + Q_DISABLE_COPY_MOVE(AnimationLoader) + +public: + explicit AnimationLoader(QThreadPool *threadPool); + virtual ~AnimationLoader(); + + QString loadedFileName() const; + void load(const QString &fileName); + void stopLoading(); + + QSize size(); + + int frameCount(); + AnimationFrame frame(int frameNumber); + + int loopCount(); + +private: + QThreadPool *m_thread_pool; + QString m_file_name; + QSize m_size; + int m_frame_count = 0; + int m_loop_count = -1; + QList m_frames; + QFuture m_task; + std::atomic_bool m_exit_task = false; + QMutex m_task_lock; + QWaitCondition m_task_signal; + + void populateVector(QImageReader *reader); +}; +} // namespace kal diff --git a/src/aoapplication.cpp b/src/aoapplication.cpp index a3ed6b953..f3260ed23 100644 --- a/src/aoapplication.cpp +++ b/src/aoapplication.cpp @@ -132,6 +132,20 @@ QString AOApplication::get_version_string() return QString::number(RELEASE) + "." + QString::number(MAJOR_VERSION) + "." + QString::number(MINOR_VERSION); } +QString AOApplication::find_image(QStringList p_list) +{ + QString image_path; + for (const QString &path : p_list) + { + if (file_exists(path)) + { + image_path = path; + break; + } + } + return image_path; +} + void AOApplication::server_disconnected() { if (is_courtroom_constructed()) diff --git a/src/aoapplication.h b/src/aoapplication.h index ccc509d85..6d2b55a3a 100644 --- a/src/aoapplication.h +++ b/src/aoapplication.h @@ -126,10 +126,12 @@ class AOApplication : public QApplication QString get_asset(QString p_element, QString p_theme = QString(), QString p_subtheme = QString(), QString p_default_theme = QString(), QString p_misc = QString(), QString p_character = QString(), QString p_placeholder = QString()); QString get_image(QString p_element, QString p_theme = QString(), QString p_subtheme = QString(), QString p_default_theme = QString(), QString p_misc = QString(), QString p_character = QString(), QString p_placeholder = QString(), bool static_image = false); QString get_sfx(QString p_sfx, QString p_misc = QString(), QString p_character = QString()); - QPair get_pos_path(const QString &pos, bool desk = false); + QPair get_pos_path(const QString &pos, bool desk = false); QString get_case_sensitive_path(QString p_file); QString get_real_path(const VPath &vpath, const QStringList &suffixes = {""}); + QString find_image(QStringList p_list); + ////// Functions for reading and writing files ////// // Implementations file_functions.cpp diff --git a/src/aoemotepreview.cpp b/src/aoemotepreview.cpp index e427cb894..a5a6a2717 100644 --- a/src/aoemotepreview.cpp +++ b/src/aoemotepreview.cpp @@ -8,9 +8,8 @@ AOEmotePreview::AOEmotePreview(AOApplication *ao_app, QWidget *parent) setWindowFlag(Qt::WindowMinMaxButtonsHint, false); ui_viewport = new QWidget(this); - ui_vp_player_char = new CharLayer(ao_app, ui_viewport); + ui_vp_player_char = new kal::CharacterAnimationLayer(ao_app, ui_viewport); ui_vp_player_char->setObjectName("ui_vp_player_char"); - ui_vp_player_char->masked = false; ui_size_label = new QLabel(this); ui_size_label->setObjectName("ui_size_label"); } @@ -19,21 +18,22 @@ void AOEmotePreview::updateViewportGeometry() { ui_viewport->resize(size()); - ui_vp_player_char->move_and_center(0, 0); - ui_vp_player_char->combo_resize(ui_viewport->width(), ui_viewport->height()); + ui_vp_player_char->move(0, 0); + ui_vp_player_char->resize(ui_viewport->width(), ui_viewport->height()); - ui_size_label->setText(QString::number(width()) + "x" + QString::number(height())); + ui_size_label->setText(QString::number(ui_viewport->width()) + "x" + QString::number(ui_viewport->height())); } -void AOEmotePreview::display(QString character, QString emote, bool flipped, int xOffset, int yOffset) +void AOEmotePreview::display(QString character, QString emote, kal::CharacterAnimationLayer::EmoteType emoteType, bool flipped, int xOffset, int yOffset) { m_character = character; m_emote = emote; - ui_vp_player_char->stop(); - ui_vp_player_char->set_flipped(flipped); - ui_vp_player_char->move_and_center(ui_viewport->width() * xOffset / 100, ui_viewport->height() * yOffset / 100); - ui_vp_player_char->load_image(emote, character, 0, false); - ui_vp_player_char->set_play_once(false); + ui_vp_player_char->stopPlayback(); + ui_vp_player_char->move(ui_viewport->width() * xOffset / 100, ui_viewport->height() * yOffset / 100); + ui_vp_player_char->loadCharacterEmote(character, emote, emoteType); + ui_vp_player_char->setPlayOnce(false); + ui_vp_player_char->setFlipped(flipped); + ui_vp_player_char->startPlayback(); setWindowTitle(character + ": " + emote); } @@ -41,5 +41,4 @@ void AOEmotePreview::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); updateViewportGeometry(); - ui_vp_player_char->load_image(m_emote, m_character, 0, false); } diff --git a/src/aoemotepreview.h b/src/aoemotepreview.h index 0cf88c9e3..a83a0d960 100644 --- a/src/aoemotepreview.h +++ b/src/aoemotepreview.h @@ -1,6 +1,6 @@ #pragma once -#include "aolayer.h" +#include "animationlayer.h" #include class AOEmotePreview : public QWidget @@ -10,7 +10,7 @@ class AOEmotePreview : public QWidget public: AOEmotePreview(AOApplication *ao_app, QWidget *parent = nullptr); - void display(QString character, QString emote, bool flipped = false, int xOffset = 0, int yOffset = 0); + void display(QString character, QString emote, kal::CharacterAnimationLayer::EmoteType emoteType, bool flipped = false, int xOffset = 0, int yOffset = 0); void updateViewportGeometry(); @@ -24,9 +24,9 @@ class AOEmotePreview : public QWidget QString m_emote; QWidget *ui_viewport; - BackgroundLayer *ui_vp_background; - SplashLayer *ui_vp_speedlines; - CharLayer *ui_vp_player_char; - BackgroundLayer *ui_vp_desk; + kal::BackgroundAnimationLayer *ui_vp_background; + kal::SplashAnimationLayer *ui_vp_speedlines; + kal::CharacterAnimationLayer *ui_vp_player_char; + kal::BackgroundAnimationLayer *ui_vp_desk; QLabel *ui_size_label; }; diff --git a/src/aoevidencedisplay.cpp b/src/aoevidencedisplay.cpp index 489699ec3..27afdb65b 100644 --- a/src/aoevidencedisplay.cpp +++ b/src/aoevidencedisplay.cpp @@ -12,9 +12,9 @@ AOEvidenceDisplay::AOEvidenceDisplay(AOApplication *p_ao_app, QWidget *p_parent) m_sfx_player = new AOSfxPlayer(ao_app); - m_evidence_movie = new InterfaceLayer(ao_app, this); + m_evidence_movie = new kal::InterfaceAnimationLayer(ao_app, this); - connect(m_evidence_movie, &InterfaceLayer::done, this, &AOEvidenceDisplay::show_done); + connect(m_evidence_movie, &kal::InterfaceAnimationLayer::finishedPlayback, this, &AOEvidenceDisplay::show_done); connect(ui_prompt_details, &QPushButton::clicked, this, &AOEvidenceDisplay::icon_clicked); } @@ -52,17 +52,17 @@ void AOEvidenceDisplay::show_evidence(int p_index, QString p_evidence_image, boo ui_prompt_details->setIconSize(f_pixmap.rect().size()); ui_prompt_details->resize(f_pixmap.rect().size()); ui_prompt_details->move(icon_dimensions.x, icon_dimensions.y); - m_evidence_movie->static_duration = 320; - m_evidence_movie->max_duration = 1000; - m_evidence_movie->set_play_once(true); - m_evidence_movie->load_image(gif_name, ""); + m_evidence_movie->setMinimumDurationPerFrame(320); + m_evidence_movie->setMaximumDurationPerFrame(1000); + m_evidence_movie->setPlayOnce(true); + m_evidence_movie->loadAndPlayAnimation(gif_name, ""); m_sfx_player->findAndPlaySfx(ao_app->get_court_sfx("evidence_present")); } void AOEvidenceDisplay::reset() { m_sfx_player->stop(); - m_evidence_movie->kill(); + m_evidence_movie->stopPlayback(); ui_prompt_details->hide(); this->clear(); } @@ -84,5 +84,5 @@ void AOEvidenceDisplay::combo_resize(int w, int h) { QSize f_size(w, h); this->resize(f_size); - m_evidence_movie->combo_resize(w, h); + m_evidence_movie->resize(w, h); } diff --git a/src/aoevidencedisplay.h b/src/aoevidencedisplay.h index b23bc0f78..a2590738b 100644 --- a/src/aoevidencedisplay.h +++ b/src/aoevidencedisplay.h @@ -1,7 +1,7 @@ #pragma once +#include "animationlayer.h" #include "aoapplication.h" -#include "aolayer.h" #include "aosfxplayer.h" #include @@ -28,7 +28,7 @@ class AOEvidenceDisplay : public QLabel int m_last_evidence_index = -1; AOSfxPlayer *m_sfx_player; - InterfaceLayer *m_evidence_movie; + kal::InterfaceAnimationLayer *m_evidence_movie; QPushButton *ui_prompt_details; private Q_SLOTS: diff --git a/src/aolayer.cpp b/src/aolayer.cpp deleted file mode 100644 index a70e7b829..000000000 --- a/src/aolayer.cpp +++ /dev/null @@ -1,749 +0,0 @@ -#include "aolayer.h" - -#include "aoapplication.h" -#include "file_functions.h" -#include "options.h" - -static QThreadPool *thread_pool; - -AOLayer::AOLayer(AOApplication *p_ao_app, QWidget *p_parent) - : QLabel(p_parent) -{ - ao_app = p_ao_app; - - // used for culling images when their max_duration is exceeded - shfx_timer = new QTimer(this); - shfx_timer->setTimerType(Qt::PreciseTimer); - shfx_timer->setSingleShot(true); - connect(shfx_timer, &QTimer::timeout, this, &AOLayer::shfx_timer_done); - - ticker = new QTimer(this); - ticker->setTimerType(Qt::PreciseTimer); - ticker->setSingleShot(false); - connect(ticker, &QTimer::timeout, this, &AOLayer::movie_ticker); - - preanim_timer = new QTimer(this); - preanim_timer->setSingleShot(true); - connect(preanim_timer, &QTimer::timeout, this, &AOLayer::preanim_done); - - if (!thread_pool) - { - thread_pool = new QThreadPool(p_ao_app); - thread_pool->setMaxThreadCount(8); - } -} - -BackgroundLayer::BackgroundLayer(AOApplication *p_ao_app, QWidget *p_parent) - : AOLayer(p_ao_app, p_parent) -{} - -CharLayer::CharLayer(AOApplication *p_ao_app, QWidget *p_parent) - : AOLayer(p_ao_app, p_parent) -{} - -EffectLayer::EffectLayer(AOApplication *p_ao_app, QWidget *p_parent) - : AOLayer(p_ao_app, p_parent) -{} - -SplashLayer::SplashLayer(AOApplication *p_ao_app, QWidget *p_parent) - : AOLayer(p_ao_app, p_parent) -{} - -InterfaceLayer::InterfaceLayer(AOApplication *p_ao_app, QWidget *p_parent) - : AOLayer(p_ao_app, p_parent) -{} - -StickerLayer::StickerLayer(AOApplication *p_ao_app, QWidget *p_parent) - : AOLayer(p_ao_app, p_parent) -{} - -QString AOLayer::find_image(QStringList p_list) -{ - QString image_path; - for (const QString &path : p_list) - { -#ifdef DEBUG_MOVIE - qDebug() << "checking path " << path; -#endif - if (file_exists(path)) - { - image_path = path; -#ifdef DEBUG_MOVIE - qDebug() << "found path " << path; -#endif - break; - } - } - return image_path; -} - -QPixmap AOLayer::get_pixmap(QImage image) -{ - QPixmap f_pixmap; - if (m_flipped) - { - f_pixmap = QPixmap::fromImage(image.mirrored(true, false)); - } - else - { - f_pixmap = QPixmap::fromImage(image); - } - // auto aspect_ratio = Qt::KeepAspectRatio; - if (!f_pixmap.isNull()) - { - scaling_factor = float(f_h) / float(f_pixmap.height()); - if (f_pixmap.height() > f_h) // We are downscaling, use anti-aliasing. - { - transform_mode = Qt::SmoothTransformation; - } - if (stretch) - { - f_pixmap = f_pixmap.scaled(f_w, f_h); - } - else - { - f_pixmap = f_pixmap.scaledToHeight(f_h, transform_mode); - } - this->resize(f_pixmap.size()); - } - return f_pixmap; -} - -void AOLayer::set_frame(QPixmap f_pixmap) -{ - this->setPixmap(f_pixmap); - this->center_pixmap(f_pixmap); -} - -void AOLayer::center_pixmap(QPixmap f_pixmap) -{ - if (g_center == -1) - { - QLabel::move(x + (f_w - f_pixmap.width()) / 2, - y + (f_h - f_pixmap.height())); // Always center horizontally, always - // put at the bottom vertically - } - else - { - QLabel::move(get_pos_from_center(g_center), y + (f_h - f_pixmap.height())); - } - if (masked) - { - if (g_center == -1) - { - this->setMask(QRegion((f_pixmap.width() - f_w) / 2, (f_pixmap.height() - f_h) / 2, f_w, - f_h)); // make sure we don't escape the area we've been given - } - else - { - int center_scaled = int(float(g_center) * scaling_factor); - this->setMask(QRegion((f_pixmap.width() - center_scaled) / 2, (f_pixmap.height() - f_h) / 2, f_w, f_h)); - } - } -} - -int AOLayer::get_pos_from_center(int f_center) -{ - int center_scaled = int(float(f_center) * scaling_factor); - int f_pos = x + (center_scaled - (f_w / 2)) * -1; -#ifdef DEBUG_MOVIE - qDebug() << "centering image at center" << f_center << "final position" << f_pos; -#endif - return f_pos; -} - -void AOLayer::combo_resize(int w, int h) -{ - QSize f_size(w, h); - f_w = w; - f_h = h; - this->resize(f_size); -} - -int AOLayer::get_frame_delay(int delay) -{ - return static_cast(double(delay) * double(speed / 100)); -} - -void AOLayer::move(int ax, int ay) -{ - x = ax; - y = ay; - QLabel::move(x, y); -} - -void AOLayer::move_and_center(int ax, int ay) -{ - x = ax; - y = ay; - if (movie_frames.isEmpty()) // safeguard - { - QLabel::move(x, y); - } - else - { - center_pixmap(movie_frames[0]); // just use the first frame since dimensions are all that matter - } -} - -float AOLayer::get_scaling_factor() -{ - return scaling_factor; -} - -void BackgroundLayer::load_image(QString p_filename, int p_center) -{ - g_center = p_center; - play_once = false; - cull_image = false; - VPath design_path = ao_app->get_background_path("design.ini"); - transform_mode = ao_app->get_scaling(ao_app->read_design_ini("scaling", design_path)); - stretch = ao_app->read_design_ini("stretch", design_path).startsWith("true"); -#ifdef DEBUG_MOVIE - qDebug() << "[BackgroundLayer] BG loaded: " << p_filename; -#endif - QString final_path = ao_app->get_image_suffix(ao_app->get_background_path(p_filename)); - - if (final_path == last_path && g_center == last_center) - { - // Don't restart background if background is unchanged - return; - } - - start_playback(final_path); - play(); -} - -void CharLayer::load_image(QString p_filename, QString p_charname, int p_duration, bool p_is_preanim) -{ - duration = p_duration; - cull_image = false; - force_continuous = false; - transform_mode = ao_app->get_scaling(ao_app->get_emote_property(p_charname, p_filename, "scaling")); - stretch = ao_app->get_emote_property(p_charname, p_filename, "stretch").startsWith("true"); - if ((p_charname == last_char) && ((p_filename == last_emote) || (p_filename.mid(3, -1) == last_emote.mid(3, -1))) && (!is_preanim) && (!was_preanim)) - { - continuous = true; - force_continuous = true; - } - else - { - continuous = false; - force_continuous = true; - } - prefix = ""; - current_emote = p_filename; - was_preanim = is_preanim; - m_char = p_charname; - m_emote = current_emote; - last_char = p_charname; - last_emote = current_emote; - last_prefix = prefix; - is_preanim = p_is_preanim; - if ((p_filename.left(3) == "(a)") || (p_filename.left(3) == "(b)")) - { // if we are playing an idle or talking animation - prefix = p_filename.left(3); // separate the prefix from the emote name - current_emote = p_filename.mid(3, -1); - } - else if ((duration > 0) || (p_filename.left(3) == "(c)")) - { // else if we are playing a preanim or postanim - if (p_filename.left(3) == "(c)") - { // if we are playing a postanim - prefix = "(c)"; // separate the prefix from the emote name - current_emote = p_filename.mid(3, -1); - } - // pre/postanim specific flags - is_preanim = true; - play_once = true; - preanim_timer->start(duration); - } -#ifdef DEBUG_MOVIE - qDebug() << "[CharLayer] anim loaded: prefix " << prefix << " filename " << current_emote << " from character: " << p_charname << " continuous: " << continuous; -#endif - QVector pathlist{ // cursed character path resolution vector - ao_app->get_character_path(p_charname, prefix + current_emote), // Default path - ao_app->get_character_path(p_charname, - prefix + "/" + current_emote), // Path check if it's categorized - // into a folder - ao_app->get_character_path(p_charname, - current_emote), // Just use the non-prefixed image, animated or not - VPath(current_emote), // The path by itself after the above fail - ao_app->get_theme_path("placeholder"), // Theme placeholder path - ao_app->get_theme_path("placeholder", ao_app->default_theme)}; // Default theme placeholder path - start_playback(ao_app->get_image_path(pathlist)); -} - -void SplashLayer::load_image(QString p_filename, QString p_charname, QString p_miscname) -{ - transform_mode = ao_app->get_misc_scaling(p_miscname); - QString final_image = ao_app->get_image(p_filename, Options::getInstance().theme(), Options::getInstance().subTheme(), ao_app->default_theme, p_miscname, p_charname, "placeholder"); - start_playback(final_image); - play(); -} - -void EffectLayer::load_image(QString p_filename, bool p_looping) -{ - if (p_looping) - { - play_once = false; - } - else - { - play_once = true; - } - continuous = false; - force_continuous = true; - cull_image = false; - - start_playback(p_filename); // path resolution is handled by the caller for EffectLayer objects - play(); -} - -void InterfaceLayer::load_image(QString p_filename, QString p_miscname) -{ - last_path = ""; - stretch = true; - QString final_image = ao_app->get_image(p_filename, Options::getInstance().theme(), Options::getInstance().subTheme(), ao_app->default_theme, p_miscname); - start_playback(final_image); - play(); -} - -void StickerLayer::load_image(QString p_charname) -{ - QString p_miscname; - if (Options::getInstance().customChatboxEnabled()) - { - p_miscname = ao_app->get_chat(p_charname); - } - transform_mode = ao_app->get_misc_scaling(p_miscname); - QString final_image = ao_app->get_image("sticker/" + p_charname, Options::getInstance().theme(), Options::getInstance().subTheme(), ao_app->default_theme, p_miscname); - start_playback(final_image); - play(); -} - -void CharLayer::start_playback(QString p_image) -{ - movie_effects.clear(); - AOLayer::start_playback(p_image); - if (m_network_strings.size() > 0) // our FX overwritten by networked ones - { - load_network_effects(); - } - else // Use default ini FX - { - load_effects(); - } - play(); -} - -void AOLayer::start_playback(QString p_image) -{ - if (p_image == "") - { // image wasn't found by the path resolution function - this->kill(); - return; - } - - if (frame_loader.isRunning()) - { - exit_loop = true; // tell the loader to stop, we have a new image to load - } - - QMutexLocker locker(&mutex); - this->show(); - - if (!Options::getInstance().continuousPlaybackEnabled()) - { - continuous = false; - force_continuous = true; - } - - if (((last_path == p_image) && (!force_continuous) && (g_center == last_center)) || p_image == "") - { - return; - } - -#ifdef DEBUG_MOVIE - actual_time.restart(); -#endif - this->clear(); - this->freeze(); - movie_frames.clear(); - movie_delays.clear(); - QString scaling_override = ao_app->read_design_ini("scaling", p_image + ".ini"); - if (scaling_override != "") - { - transform_mode = ao_app->get_scaling(scaling_override); - } - QString stretch_override = ao_app->read_design_ini("stretch", p_image + ".ini"); - if (stretch_override != "") - { - stretch = stretch_override.startsWith("true"); - } - -#ifdef DEBUG_MOVIE - qDebug() << "[AOLayer::start_playback] Stretch:" << stretch << "Filename:" << p_image; -#endif - m_reader.setFileName(p_image); - last_max_frames = max_frames; - max_frames = m_reader.imageCount(); - if (m_reader.loopCount() == 0 && max_frames > 1) - { - play_once = true; - } - if (!continuous || ((continuous) && (max_frames != last_max_frames)) || max_frames == 0 || frame >= max_frames) - { - frame = 0; - continuous = false; - } -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - frame_loader = QtConcurrent::run(thread_pool, this, &AOLayer::populate_vectors); -#else - frame_loader = QtConcurrent::run(thread_pool, &AOLayer::populate_vectors, this); -#endif - last_path = p_image; - last_center = g_center; - while (movie_frames.size() <= frame) // if we haven't loaded the frame we need yet - { - frameAdded.wait(&mutex); // wait for the frame loader to add another frame, then check again - } - this->set_frame(movie_frames[frame]); - - if (max_frames <= 1) - { - duration = static_duration; -#ifdef DEBUG_MOVIE - qDebug() << "[AOLayer::start_playback] max_frames is <= 1, using static duration"; -#endif - } - if (duration > 0 && cull_image == true) - { - shfx_timer->start(duration); - } -#ifdef DEBUG_MOVIE - qDebug() << "[AOLayer::start_playback] Max frames:" << max_frames << "Setting image to " << p_image << "Time taken to process image:" << actual_time.elapsed(); - - actual_time.restart(); -#endif -} - -void CharLayer::play() -{ - if (max_frames <= 1) - { - if (play_once) - { - preanim_timer->start(qMax(0, duration)); - } - return; - } - play_frame_effect(frame); - AOLayer::play(); -} - -void AOLayer::play() -{ - if (max_frames <= 1) - { - if (play_once) - { - if (duration > 0) - { - ticker->start(duration); - } - else - { - preanim_done(); - } - } - else - { - this->freeze(); - } - } - else - { - while (movie_delays.size() <= frame) - { - frameAdded.wait(&mutex); - } - ticker->start(this->get_frame_delay(movie_delays[frame])); - } -} - -void AOLayer::set_play_once(bool p_play_once) -{ - play_once = p_play_once; -} -void AOLayer::set_cull_image(bool p_cull_image) -{ - cull_image = p_cull_image; -} -void AOLayer::set_static_duration(int p_static_duration) -{ - static_duration = p_static_duration; -} -void AOLayer::set_max_duration(int p_max_duration) -{ - max_duration = p_max_duration; -} - -void CharLayer::load_effects() -{ - movie_effects.clear(); - if (max_frames <= 1) - { - return; - } - movie_effects.resize(max_frames); - for (int e_frame = 0; e_frame < max_frames; ++e_frame) - { - QString effect = ao_app->get_screenshake_frame(m_char, m_emote, e_frame); - if (effect != "") - { - movie_effects[e_frame].append("shake"); - } - - effect = ao_app->get_flash_frame(m_char, m_emote, e_frame); - if (effect != "") - { - movie_effects[e_frame].append("flash"); - } - - effect = ao_app->get_sfx_frame(m_char, m_emote, e_frame); - if (effect != "") - { - movie_effects[e_frame].append("sfx^" + effect); - } - } -} - -void CharLayer::load_network_effects() -{ - movie_effects.clear(); - if (max_frames <= 1) - { - return; - } - movie_effects.resize(max_frames); - // Order is important!!! - QStringList effects_list = {"shake", "flash", "sfx^"}; - - // Determines which list is smaller - effects_list or m_network_strings - and - // uses it as basis for the loop. This way, incomplete m_network_strings would - // still be parsed, and excess/unaccounted for networked information is - // omitted. - int effects_size = qMin(effects_list.size(), m_network_strings.size()); - - for (int i = 0; i < effects_size; ++i) - { - QString netstring = m_network_strings.at(i); - QStringList emote_splits = netstring.split("^"); - for (const QString &emote : emote_splits) - { - QStringList parsed = emote.split("|"); - if (parsed.size() <= 0 || parsed.at(0) != m_emote) - { - continue; - } - foreach (QString frame_data, parsed) - { - QStringList frame_split = frame_data.split("="); - if (frame_split.size() <= 1) // We might still be hanging at the emote itself (entry 0). - { - continue; - } - int f_frame = frame_split.at(0).toInt(); - if (f_frame >= max_frames || f_frame < 0) - { - qWarning() << "out of bounds" << effects_list[i] << "frame" << f_frame << "out of" << max_frames << "for" << m_emote; - continue; - } - QString f_data = frame_split.at(1); - if (f_data != "") - { - QString effect = effects_list[i]; - if (effect == "sfx^") // Currently the only frame result that feeds us - // data, let's yank it in. - { - effect += f_data; - } -#ifdef DEBUG_MOVIE - qDebug() << "[CharLayer::load_network_effects]" << effect << f_data << "frame" << f_frame << "for" << m_emote; -#endif - movie_effects[f_frame].append(effect); - } - } - } - } -} - -void CharLayer::play_frame_effect(int p_frame) -{ - if (p_frame >= movie_effects.size()) - { - qWarning() << "Attempted to play a frame effect bigger than the size of movie_effects"; - return; - } - if (p_frame < max_frames) - { - foreach (QString effect, movie_effects[p_frame]) - { - if (effect == "shake") - { - Q_EMIT shake(); -#ifdef DEBUG_MOVIE - qDebug() << "[CharLayer::play_frame_effect] Attempting to play shake on frame" << frame; -#endif - } - - if (effect == "flash") - { - Q_EMIT flash(); -#ifdef DEBUG_MOVIE - qDebug() << "[CharLayer::play_frame_effect] Attempting to play flash on frame" << frame; -#endif - } - - if (effect.startsWith("sfx^")) - { - QString sfx = effect.section("^", 1); - Q_EMIT play_sfx(sfx); -#ifdef DEBUG_MOVIE - qDebug() << "[CharLayer::play_frame_effect] Attempting to play sfx" << sfx << "on frame" << frame; -#endif - } - } - } -} - -void AOLayer::stop() -{ - // for all intents and purposes, stopping is the same as hiding. at no point - // do we want a frozen gif to display - this->freeze(); - this->hide(); - shfx_timer->stop(); -} - -void AOLayer::freeze() -{ - // aT nO pOiNt Do We WaNt A fRoZeN gIf To DiSpLaY - ticker->stop(); - preanim_timer->stop(); -} - -void AOLayer::kill() -{ - // used for when we want to ensure a file is loaded anew - this->stop(); - this->clear(); - movie_frames.clear(); - movie_delays.clear(); - last_max_frames = max_frames; - max_frames = 0; - last_path = ""; -} - -void CharLayer::movie_ticker() -{ - AOLayer::movie_ticker(); - play_frame_effect(frame); -} - -void AOLayer::movie_ticker() -{ - ++frame; - if (frame >= max_frames) - { - if (play_once) - { - if (cull_image) - { - this->stop(); - } - else - { - this->freeze(); - } - preanim_done(); - return; - } - else - { - frame = 0; - } - } - { - QMutexLocker locker(&mutex); - while (frame >= movie_frames.size() && frame < max_frames) // oops! our frame isn't ready yet - { - frameAdded.wait(&mutex); // wait for a new frame to be added, then check again - } - } -#ifdef DEBUG_MOVIE - qDebug() << "[AOLayer::movie_ticker] Frame:" << frame << "Delay:" << movie_delays[frame] << "Actual time taken from last frame:" << actual_time.restart(); -#endif - this->set_frame(movie_frames[frame]); - ticker->setInterval(this->get_frame_delay(movie_delays[frame])); -} - -void AOLayer::populate_vectors() -{ -#ifdef DEBUG_MOVIE - qDebug() << "[AOLayer::populate_vectors] Started thread"; -#endif - while (!exit_loop && movie_frames.size() < max_frames) - { - load_next_frame(); -#ifdef DEBUG_MOVIE - qDebug() << "[AOLayer::populate_vectors] Loaded frame" << movie_frames.size(); -#endif - } -#ifdef DEBUG_MOVIE - if (exit_loop) - { - qDebug() << "[AOLayer::populate_vectors] Exit requested"; - } -#endif - exit_loop = false; -} - -void AOLayer::load_next_frame() -{ - { - QMutexLocker locker(&mutex); - movie_frames.append(this->get_pixmap(m_reader.read())); - movie_delays.append(m_reader.nextImageDelay()); - } - frameAdded.wakeAll(); -} - -void CharLayer::preanim_done() -{ - if (is_preanim) - { - AOLayer::preanim_done(); - } - else - { - return; - } -} - -void AOLayer::preanim_done() -{ - ticker->stop(); - preanim_timer->stop(); - Q_EMIT done(); -} - -void AOLayer::shfx_timer_done() -{ - this->stop(); -#ifdef DEBUG_MOVIE - qDebug() << "shfx timer signaled done"; -#endif - // signal connected to courtroom object, let it figure out what to do - Q_EMIT done(); -} diff --git a/src/aolayer.h b/src/aolayer.h deleted file mode 100644 index b5591e8a5..000000000 --- a/src/aolayer.h +++ /dev/null @@ -1,297 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class AOApplication; -class VPath; - -// "Brief" explanation of what the hell this is: -// -// AOLayer handles all animations both inside and outside -// the viewport. It was originally devised as a layering -// system, but turned into a full refactor of the existing -// animation code. -// -// AOLayer has six subclasses, all of which differ mainly in -// how they handle path resolution. -// -// - BackgroundLayer: self-explanatory, handles files found in base/background -// - CharLayer: handles all the "wonderful" quirks of character path resolution -// - SplashLayer: handles elements that can either be provided by a misc/ directory -// or by the theme - speedlines, shouts, WT/CE, et cetera -// - EffectLayer: this is basically a dummy layer since effects do their own wonky -// path resolution in a different file -// - InterfaceLayer: handles UI elements like the chat arrow and the music display -// - StickerLayer: Crystalwarrior really wanted this. Handles "stickers," whatever those are. -// -// For questions comments or concerns, bother someone else - -class AOLayer : public QLabel -{ - Q_OBJECT - -public: - AOLayer(AOApplication *p_ao_app, QWidget *p_parent = nullptr); - - QString filename; // file name without extension, i.e. "witnesstestimony" - int static_duration; // time in ms for static images to be displayed, if - // applicable. set to 0 for infinite - int max_duration; // maximum duration in ms, image will be culled if it is - // exceeded. set this to 0 for infinite duration - bool play_once = false; // Whether to loop this animation or not - bool cull_image = true; // if we're done playing this animation, should we - // hide it? also controls durational culling - // Are we loading this from the same frame we left off on? - bool continuous = false; - // Whether or not to forcibly bypass the simple check done by start_playback - // and use the existent value of continuous instead - bool force_continuous = false; - Qt::TransformationMode transform_mode = Qt::FastTransformation; // transformation mode to use for this image - bool stretch = false; // Should we stretch/squash this image to fill the screen? - bool masked = true; // Set a mask to the dimensions of the widget? - - // Set the movie's image to provided paths, preparing for playback. - void start_playback(QString p_image); - - void set_play_once(bool p_play_once); - void set_cull_image(bool p_cull_image); - void set_static_duration(int p_static_duration); - void set_max_duration(int p_max_duration); - - // Stop the movie, clearing the image - void stop(); - - // Stop the movie and clear all vectors - void kill(); - - // Set the m_flipped variable to true/false - void set_flipped(bool p_flipped) { m_flipped = p_flipped; } - - // Move the label itself around - void move(int ax, int ay); - - // Move the label and center it - void move_and_center(int ax, int ay); - - // Returns the factor by which the image is scaled - float get_scaling_factor(); - - // This is somewhat pointless now as there's no "QMovie" object to resize, aka - // no "combo" to speak of - void combo_resize(int w, int h); - - // Return the frame delay adjusted for speed - int get_frame_delay(int delay); - - /** - * @brief Returns the x offset to use to ensure proper centering in the - * viewport. This is used by courtroom transition code to know exactly where - * to put the characters at the start and end of the animation. - * @return The offset to center the pixmap in the viewport - */ - int get_centered_offset(); - - // iterate through a list of paths and return the first entry that exists. if - // none exist, return NULL (safe because we check again for existence later) - QString find_image(QStringList p_list); - - QPropertyAnimation *slide(int newcenter, int duration); - - // Start playback of the movie (if animated). - void play(); - - // Freeze the movie at the current frame. - void freeze(); - -protected: - AOApplication *ao_app; - QVector movie_frames; - QVector movie_delays; - - QTimer *preanim_timer; - QTimer *shfx_timer; - QTimer *ticker; - QString last_path; - QImageReader m_reader; - - QElapsedTimer actual_time; - - // These are the X and Y values before they are fixed based on the sprite's - // width. - int x = 0; - int y = 0; - // These are the width and height values before they are fixed based on the - // sprite's width. - int f_w = 0; - int f_h = 0; - - float scaling_factor = 0.0; - - int frame = 0; - int max_frames = 0; - int last_max_frames = 0; - - int speed = 100; - - bool m_flipped = false; - - int duration = 0; - - // Retreive a pixmap adjused for mirroring/aspect ratio shenanigans from a - // provided QImage - QPixmap get_pixmap(QImage image); - - // Set the movie's frame to provided pixmap - void set_frame(QPixmap f_pixmap); - - // If set to anything other than -1, overrides center_pixmap to use it as a - // pixel position to center at. Currently only used by background layers - int g_center = -1; - int last_center = -1; // g_center from the last image. - int centered_offset = 0; - - // Center the QLabel in the viewport based on the dimensions of f_pixmap - void center_pixmap(QPixmap f_pixmap); - - /*! - @brief Get the position to move us to, given the pixel X position of the - point in the original image that we'd like to be centered. - @return The position to move to. - */ - int get_pos_from_center(int f_center); - -private: - // Populates the frame and delay vectors. - void populate_vectors(); - - // used in populate_vectors - void load_next_frame(); - std::atomic_bool exit_loop{false}; // awful solution but i'm not fucking using QThread - QFuture frame_loader; - QMutex mutex; - QWaitCondition frameAdded; - -Q_SIGNALS: - void done(); - -protected Q_SLOTS: - virtual void preanim_done(); - void shfx_timer_done(); - virtual void movie_ticker(); -}; - -class BackgroundLayer : public AOLayer -{ - Q_OBJECT -public: - BackgroundLayer(AOApplication *p_ao_app, QWidget *p_parent); - void load_image(QString p_filename, int center = -1); -}; - -class CharLayer : public AOLayer -{ - Q_OBJECT - -public: - CharLayer(AOApplication *p_ao_app, QWidget *p_parent = nullptr); - - QStringList &network_strings2() { return m_network_strings; } - void set_network_string(QStringList list) { m_network_strings = list; } - - void load_image(QString p_filename, QString p_charname, int p_duration, bool p_is_preanim); - - void play(); // overloaded so we can play effects - -private: - QString current_emote; // name of the emote we're using - bool is_preanim; // equivalent to the old play_once, if true we don't want - // to loop this - QString prefix; // prefix, left blank if it's a preanim - QStringList m_network_strings; - - QString last_char; // name of the last character we used - QString last_emote; // name of the last animation we used - QString last_prefix; // prefix of the last animation we played - bool was_preanim = false; // whether is_preanim was true last time - - // Effects such as sfx, screenshakes and realization flashes are stored in - // here. QString entry format: "sfx^[sfx_name]", "shake", "flash". The program - // uses the QVector index as reference. - QVector> movie_effects; - - // used for effect loading - QString m_char; - QString m_emote; - - // overloaded for effects reasons - void start_playback(QString p_image); - - // Initialize the frame-specific effects from the char.ini - void load_effects(); - - // Initialize the frame-specific effects from the provided network_strings, - // this is only initialized if network_strings has size more than 0. - void load_network_effects(); - - // Play a frame-specific effect, if there's any defined for that specific - // frame. - void play_frame_effect(int p_frame); - -private Q_SLOTS: - void preanim_done() override; // overridden so we don't accidentally cull characters - void movie_ticker() override; // overridden so we can play effects - -Q_SIGNALS: - void shake(); - void flash(); - void play_sfx(QString sfx); -}; - -class SplashLayer : public AOLayer -{ - Q_OBJECT - -public: - SplashLayer(AOApplication *p_ao_app, QWidget *p_parent = nullptr); - - void load_image(QString p_filename, QString p_charname, QString p_miscname); -}; - -class EffectLayer : public AOLayer -{ - Q_OBJECT - -public: - EffectLayer(AOApplication *p_ao_app, QWidget *p_parent = nullptr); - - void load_image(QString p_filename, bool p_looping); -}; - -class InterfaceLayer : public AOLayer -{ - Q_OBJECT - -public: - InterfaceLayer(AOApplication *p_ao_app, QWidget *p_parent = nullptr); - - void load_image(QString p_filename, QString p_miscname); -}; - -class StickerLayer : public AOLayer -{ - Q_OBJECT - -public: - StickerLayer(AOApplication *p_ao_app, QWidget *p_parent = nullptr); - - void load_image(QString p_charname); -}; diff --git a/src/courtroom.cpp b/src/courtroom.cpp index b5f62f5a8..bf49fdc1c 100644 --- a/src/courtroom.cpp +++ b/src/courtroom.cpp @@ -1,6 +1,11 @@ #include "courtroom.h" + #include "options.h" +#include + +// #define DEBUG_TRANSITION + Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() { @@ -45,30 +50,29 @@ Courtroom::Courtroom(AOApplication *p_ao_app) ui_viewport = new QWidget(this); ui_viewport->setObjectName("ui_viewport"); - ui_vp_background = new BackgroundLayer(ao_app, ui_viewport); + ui_vp_background = new kal::BackgroundAnimationLayer(ao_app, ui_viewport); ui_vp_background->setObjectName("ui_vp_background"); - ui_vp_background->masked = false; - ui_vp_speedlines = new SplashLayer(ao_app, ui_viewport); + ui_vp_speedlines = new kal::SplashAnimationLayer(ao_app, ui_viewport); ui_vp_speedlines->setObjectName("ui_vp_speedlines"); - ui_vp_speedlines->stretch = true; - ui_vp_player_char = new CharLayer(ao_app, ui_viewport); + ui_vp_speedlines->setStretchToFit(true); + ui_vp_player_char = new kal::CharacterAnimationLayer(ao_app, ui_viewport); ui_vp_player_char->setObjectName("ui_vp_player_char"); - ui_vp_player_char->masked = false; - ui_vp_sideplayer_char = new CharLayer(ao_app, ui_viewport); + ui_vp_sideplayer_char = new kal::CharacterAnimationLayer(ao_app, ui_viewport); ui_vp_sideplayer_char->setObjectName("ui_vp_sideplayer_char"); - ui_vp_sideplayer_char->masked = false; ui_vp_sideplayer_char->hide(); - ui_vp_dummy_char = new CharLayer(ao_app, ui_viewport); - ui_vp_dummy_char->masked = false; + ui_vp_dummy_char = new kal::CharacterAnimationLayer(ao_app, ui_viewport); + ui_vp_dummy_char->setObjectName("ui_vp_dummy_char"); + ui_vp_dummy_char->setResetCacheWhenStopped(true); ui_vp_dummy_char->hide(); - ui_vp_sidedummy_char = new CharLayer(ao_app, ui_viewport); - ui_vp_sidedummy_char->masked = false; + ui_vp_sidedummy_char = new kal::CharacterAnimationLayer(ao_app, ui_viewport); + ui_vp_sidedummy_char->setObjectName("ui_vp_sidedummy_char"); + ui_vp_sidedummy_char->setResetCacheWhenStopped(true); ui_vp_sidedummy_char->hide(); - ui_vp_desk = new BackgroundLayer(ao_app, ui_viewport); + ui_vp_char_list = QList{ui_vp_player_char, ui_vp_sideplayer_char, ui_vp_dummy_char, ui_vp_sidedummy_char}; + ui_vp_desk = new kal::BackgroundAnimationLayer(ao_app, ui_viewport); ui_vp_desk->setObjectName("ui_vp_desk"); - ui_vp_desk->masked = false; - ui_vp_effect = new EffectLayer(ao_app, this); + ui_vp_effect = new kal::EffectAnimationLayer(ao_app, this); ui_vp_effect->setAttribute(Qt::WA_TransparentForMouseEvents); ui_vp_effect->setObjectName("ui_vp_effect"); @@ -78,18 +82,14 @@ Courtroom::Courtroom(AOApplication *p_ao_app) ui_vp_chatbox = new AOImage(ao_app, this); ui_vp_chatbox->setObjectName("ui_vp_chatbox"); - ui_vp_sticker = new StickerLayer(ao_app, this); - ui_vp_sticker->set_play_once(false); - ui_vp_sticker->set_cull_image(false); + ui_vp_sticker = new kal::StickerAnimationLayer(ao_app, this); ui_vp_sticker->setAttribute(Qt::WA_TransparentForMouseEvents); ui_vp_sticker->setObjectName("ui_vp_sticker"); ui_vp_showname = new AOChatboxLabel(ui_vp_chatbox); ui_vp_showname->setObjectName("ui_vp_showname"); ui_vp_showname->setAlignment(Qt::AlignLeft); - ui_vp_chat_arrow = new InterfaceLayer(ao_app, this); - ui_vp_chat_arrow->set_play_once(false); - ui_vp_chat_arrow->set_cull_image(false); + ui_vp_chat_arrow = new kal::InterfaceAnimationLayer(ao_app, this); ui_vp_chat_arrow->setObjectName("ui_vp_chat_arrow"); ui_vp_message = new QTextEdit(this); @@ -99,20 +99,15 @@ Courtroom::Courtroom(AOApplication *p_ao_app) ui_vp_message->setReadOnly(true); ui_vp_message->setObjectName("ui_vp_message"); - ui_vp_testimony = new SplashLayer(ao_app, this); - ui_vp_testimony->set_play_once(false); + ui_vp_testimony = new kal::SplashAnimationLayer(ao_app, this); ui_vp_testimony->setAttribute(Qt::WA_TransparentForMouseEvents); ui_vp_testimony->setObjectName("ui_vp_testimony"); - ui_vp_wtce = new SplashLayer(ao_app, this); - ui_vp_wtce->set_play_once(true); - ui_vp_wtce->continuous = false; - ui_vp_wtce->force_continuous = true; + ui_vp_wtce = new kal::SplashAnimationLayer(ao_app, this); ui_vp_wtce->setAttribute(Qt::WA_TransparentForMouseEvents); ui_vp_wtce->setObjectName("ui_vp_wtce"); - ui_vp_objection = new SplashLayer(ao_app, this); - ui_vp_objection->set_play_once(true); - ui_vp_objection->continuous = false; - ui_vp_objection->force_continuous = true; + ui_vp_wtce->setPlayOnce(true); + ui_vp_objection = new kal::SplashAnimationLayer(ao_app, this); + ui_vp_objection->setPlayOnce(true); ui_vp_objection->setAttribute(Qt::WA_TransparentForMouseEvents); ui_vp_objection->setObjectName("ui_vp_objection"); @@ -159,10 +154,8 @@ Courtroom::Courtroom(AOApplication *p_ao_app) ui_music_list->setUniformRowHeights(true); ui_music_list->setObjectName("ui_music_list"); - ui_music_display = new InterfaceLayer(ao_app, this); - ui_music_display->set_play_once(false); - ui_music_display->set_cull_image(false); - ui_music_display->transform_mode = Qt::SmoothTransformation; + ui_music_display = new kal::InterfaceAnimationLayer(ao_app, this); + ui_music_display->setTransformationMode(Qt::SmoothTransformation); ui_music_display->setAttribute(Qt::WA_TransparentForMouseEvents); ui_music_display->setObjectName("ui_music_display"); @@ -419,11 +412,11 @@ Courtroom::Courtroom(AOApplication *p_ao_app) connect(keepalive_timer, &QTimer::timeout, this, &Courtroom::ping_server); - connect(ui_vp_objection, &SplashLayer::done, this, &Courtroom::objection_done); - connect(ui_vp_player_char, &CharLayer::done, this, &Courtroom::preanim_done); - connect(ui_vp_player_char, &CharLayer::shake, this, &Courtroom::do_screenshake); - connect(ui_vp_player_char, &CharLayer::flash, this, &Courtroom::do_flash); - connect(ui_vp_player_char, &CharLayer::play_sfx, this, &Courtroom::play_char_sfx); + connect(ui_vp_objection, &kal::SplashAnimationLayer::finishedPlayback, this, &Courtroom::objection_done); + connect(ui_vp_player_char, &kal::CharacterAnimationLayer::finishedPreOrPostEmotePlayback, this, &Courtroom::preanim_done); + connect(ui_vp_player_char, &kal::CharacterAnimationLayer::shakeEffect, this, &Courtroom::do_screenshake); + connect(ui_vp_player_char, &kal::CharacterAnimationLayer::flashEffect, this, &Courtroom::do_flash); + connect(ui_vp_player_char, &kal::CharacterAnimationLayer::soundEffect, this, &Courtroom::play_char_sfx); connect(text_delay_timer, &QTimer::timeout, this, &Courtroom::start_chat_ticking); @@ -521,7 +514,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) connect(ui_vp_evidence_display, &AOEvidenceDisplay::show_evidence_details, this, &Courtroom::show_evidence); - connect(transition_animation_group, &QParallelAnimationGroup::finished, this, &Courtroom::on_transition_finish); + connect(transition_animation_group, &QParallelAnimationGroup::finished, this, &Courtroom::post_transition_cleanup); set_widgets(); @@ -763,29 +756,29 @@ void Courtroom::set_widgets() ui_settings->show(); // make the BG's reload - ui_vp_background->kill(); - ui_vp_desk->kill(); + ui_vp_background->restartPlayback(); + ui_vp_desk->restartPlayback(); - ui_vp_background->move_and_center(0, 0); - ui_vp_background->combo_resize(ui_viewport->width(), ui_viewport->height()); + ui_vp_background->move(0, 0); + ui_vp_background->resize(ui_viewport->width(), ui_viewport->height()); - ui_vp_speedlines->move_and_center(0, 0); - ui_vp_speedlines->combo_resize(ui_viewport->width(), ui_viewport->height()); + ui_vp_speedlines->move(0, 0); + ui_vp_speedlines->resize(ui_viewport->width(), ui_viewport->height()); - ui_vp_player_char->move_and_center(0, 0); - ui_vp_player_char->combo_resize(ui_viewport->width(), ui_viewport->height()); + ui_vp_player_char->move(0, 0); + ui_vp_player_char->resize(ui_viewport->width(), ui_viewport->height()); - ui_vp_sideplayer_char->move_and_center(0, 0); - ui_vp_sideplayer_char->combo_resize(ui_viewport->width(), ui_viewport->height()); + ui_vp_sideplayer_char->move(0, 0); + ui_vp_sideplayer_char->resize(ui_viewport->width(), ui_viewport->height()); - ui_vp_dummy_char->move_and_center(0, 0); - ui_vp_dummy_char->combo_resize(ui_viewport->width(), ui_viewport->height()); - ui_vp_sidedummy_char->move_and_center(0, 0); - ui_vp_sidedummy_char->combo_resize(ui_viewport->width(), ui_viewport->height()); + ui_vp_dummy_char->move(0, 0); + ui_vp_dummy_char->resize(ui_viewport->width(), ui_viewport->height()); + ui_vp_sidedummy_char->move(0, 0); + ui_vp_sidedummy_char->resize(ui_viewport->width(), ui_viewport->height()); // the AO2 desk element - ui_vp_desk->move_and_center(0, 0); - ui_vp_desk->combo_resize(ui_viewport->width(), ui_viewport->height()); + ui_vp_desk->move(0, 0); + ui_vp_desk->resize(ui_viewport->width(), ui_viewport->height()); ui_vp_evidence_display->move(0, 0); ui_vp_evidence_display->combo_resize(ui_viewport->width(), ui_viewport->height()); @@ -794,17 +787,17 @@ void Courtroom::set_widgets() // thing, which is to parent these to ui_viewport. instead, AOLayer handles // masking so we don't overlap parts of the UI, and they become free floating // widgets. - ui_vp_testimony->move_and_center(ui_viewport->x(), ui_viewport->y()); - ui_vp_testimony->combo_resize(ui_viewport->width(), ui_viewport->height()); + ui_vp_testimony->move(ui_viewport->x(), ui_viewport->y()); + ui_vp_testimony->resize(ui_viewport->width(), ui_viewport->height()); - ui_vp_effect->move_and_center(ui_viewport->x(), ui_viewport->y()); - ui_vp_effect->combo_resize(ui_viewport->width(), ui_viewport->height()); + ui_vp_effect->move(ui_viewport->x(), ui_viewport->y()); + ui_vp_effect->resize(ui_viewport->width(), ui_viewport->height()); - ui_vp_wtce->move_and_center(ui_viewport->x(), ui_viewport->y()); - ui_vp_wtce->combo_resize(ui_viewport->width(), ui_viewport->height()); + ui_vp_wtce->move(ui_viewport->x(), ui_viewport->y()); + ui_vp_wtce->resize(ui_viewport->width(), ui_viewport->height()); - ui_vp_objection->move_and_center(ui_viewport->x(), ui_viewport->y()); - ui_vp_objection->combo_resize(ui_viewport->width(), ui_viewport->height()); + ui_vp_objection->move(ui_viewport->x(), ui_viewport->y()); + ui_vp_objection->resize(ui_viewport->width(), ui_viewport->height()); log_maximum_blocks = Options::getInstance().maxLogSize(); @@ -888,9 +881,9 @@ void Courtroom::set_widgets() else { ui_music_display->move(design_ini_result.x, design_ini_result.y); - ui_music_display->combo_resize(design_ini_result.width, design_ini_result.height); + ui_music_display->resize(design_ini_result.width, design_ini_result.height); } - ui_music_display->load_image("music_display", ""); + ui_music_display->loadAndPlayAnimation("music_display", ""); for (int i = 0; i < max_clocks; i++) { @@ -902,7 +895,7 @@ void Courtroom::set_widgets() initialize_chatbox(); ui_vp_sticker->move(ui_viewport->x(), ui_viewport->y()); - ui_vp_sticker->combo_resize(ui_viewport->width(), ui_viewport->height()); + ui_vp_sticker->resize(ui_viewport->width(), ui_viewport->height()); ui_muted->resize(ui_ic_chat_message->width(), ui_ic_chat_message->height()); ui_muted->setImage("muted"); @@ -1390,7 +1383,7 @@ void Courtroom::done_received() void Courtroom::set_background(QString p_background, bool display) { - ui_vp_testimony->stop(); + ui_vp_testimony->stopPlayback(); current_background = p_background; // welcome to hardcode central may I take your order of regularly scheduled @@ -1440,21 +1433,20 @@ void Courtroom::set_background(QString p_background, bool display) if (display) { ui_vp_speedlines->hide(); - ui_vp_player_char->stop(); - - ui_vp_sideplayer_char->stop(); - ui_vp_effect->stop(); + ui_vp_player_char->stopPlayback(); + ui_vp_sideplayer_char->stopPlayback(); + ui_vp_effect->stopPlayback(); ui_vp_message->hide(); ui_vp_chatbox->setVisible(chatbox_always_show); // Show it if chatbox always shows if (Options::getInstance().characterStickerEnabled() && chatbox_always_show) { - ui_vp_sticker->load_image(m_chatmessage[CHAR_NAME]); + ui_vp_sticker->loadAndPlayAnimation(m_chatmessage[CHAR_NAME]); } // Hide the face sticker else { - ui_vp_sticker->stop(); + ui_vp_sticker->stopPlayback(); } // Stop the chat arrow from animating ui_vp_chat_arrow->hide(); @@ -1465,7 +1457,7 @@ void Courtroom::set_background(QString p_background, bool display) text_state = 2; anim_state = 3; - ui_vp_objection->stop(); + ui_vp_objection->stopPlayback(); chat_tick_timer->stop(); ui_vp_evidence_display->reset(); QString f_side = current_side; @@ -1730,7 +1722,7 @@ void Courtroom::enter_courtroom() // Update the audio sliders update_audio_volume(); - ui_vp_testimony->stop(); + ui_vp_testimony->stopPlayback(); // ui_server_chatlog->setHtml(ui_server_chatlog->toHtml()); } @@ -1894,6 +1886,9 @@ void Courtroom::list_areas() void Courtroom::debug_message_handler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { +#ifdef QT_DEBUG + return; +#endif Q_UNUSED(context); const QMap colors = {{QtDebugMsg, "debug"}, {QtInfoMsg, "info"}, {QtWarningMsg, "warn"}, {QtCriticalMsg, "critical"}, {QtFatalMsg, "fatal"}}; const QString color_id = QString("debug_log_%1_color").arg(colors.value(type, "info")); @@ -2312,8 +2307,9 @@ void Courtroom::on_chat_return_pressed() if (ao_app->m_serverdata.get_feature(server::BASE_FEATURE_SET::CUSTOM_BLIPS)) { packet_contents.append(ao_app->get_blipname(current_char, current_emote)); + + packet_contents.append(ui_slide_enable->isChecked() ? "1" : "0"); // just let the server figure out what to do with this } - packet_contents.append(ui_slide_enable->isChecked() ? "1" : "0"); // just let the server figure out what to do with this ao_app->send_server_packet(AOPacket("MS", packet_contents)); } @@ -2473,6 +2469,8 @@ void Courtroom::unpack_chatmessage(QStringList p_contents) { for (int n_string = 0; n_string < MS_MAXIMUM; ++n_string) { + m_previous_chatmessage[n_string] = m_chatmessage[n_string]; + // Note that we have added stuff that vanilla clients and servers simply // won't send. So now, we have to check if the thing we want even exists // amongst the packet's content. We also have to check if the server even @@ -2502,7 +2500,7 @@ void Courtroom::unpack_chatmessage(QStringList p_contents) text_state = 0; anim_state = 0; evidence_presented = false; - ui_vp_objection->stop(); + ui_vp_objection->stopPlayback(); chat_tick_timer->stop(); ui_vp_evidence_display->reset(); // This chat msg is not objection so we're not waiting on the objection animation to finish to display the character. @@ -2699,8 +2697,7 @@ bool Courtroom::handle_objection() ui_vp_message->setVisible(chatbox_always_show); ui_vp_chat_arrow->setVisible(chatbox_always_show); ui_vp_showname->setVisible(chatbox_always_show); - ui_vp_objection->set_static_duration(shout_static_time); - ui_vp_objection->set_max_duration(shout_max_time); + ui_vp_objection->setMaximumDurationPerFrame(shout_max_time); QString filename; switch (objection_mod) { @@ -2731,9 +2728,9 @@ bool Courtroom::handle_objection() break; m_chatmessage[EMOTE_MOD] = QChar(PREANIM); } - ui_vp_objection->load_image(filename, m_chatmessage[CHAR_NAME], ao_app->get_chat(m_chatmessage[CHAR_NAME])); + ui_vp_objection->loadAndPlayAnimation(filename, m_chatmessage[CHAR_NAME], ao_app->get_chat(m_chatmessage[CHAR_NAME])); sfx_player->stopAll(); // Objection played! Cut all sfx. - ui_vp_player_char->set_play_once(true); + ui_vp_player_char->setPlayOnce(true); return true; } if (m_chatmessage[EMOTE] != "") @@ -2747,8 +2744,8 @@ void Courtroom::display_character() { // Stop all previously playing animations, effects etc. ui_vp_speedlines->hide(); - ui_vp_player_char->stop(); - ui_vp_effect->stop(); + ui_vp_player_char->stopPlayback(); + ui_vp_effect->stopPlayback(); // Clear all looping sfx to prevent obnoxiousness sfx_player->stopAllLoopingStream(); // Hide the message and chatbox and handle the emotes @@ -2757,12 +2754,12 @@ void Courtroom::display_character() // Show it if chatbox always shows if (Options::getInstance().characterStickerEnabled() && chatbox_always_show) { - ui_vp_sticker->load_image(m_chatmessage[CHAR_NAME]); + ui_vp_sticker->loadAndPlayAnimation(m_chatmessage[CHAR_NAME]); } // Hide the face sticker else { - ui_vp_sticker->stop(); + ui_vp_sticker->stopPlayback(); } // Arrange the netstrings of the frame SFX for the character to know about @@ -2770,15 +2767,15 @@ void Courtroom::display_character() { // ORDER IS IMPORTANT!! QStringList netstrings = {m_chatmessage[FRAME_SCREENSHAKE], m_chatmessage[FRAME_REALIZATION], m_chatmessage[FRAME_SFX]}; - ui_vp_player_char->set_network_string(netstrings); + ui_vp_player_char->setFrameEffects(netstrings); } else { - ui_vp_player_char->set_network_string(QStringList()); + ui_vp_player_char->setFrameEffects(QStringList()); } // Determine if we should flip the character or not - ui_vp_player_char->set_flipped(m_chatmessage[FLIP].toInt() == 1); + ui_vp_player_char->setFlipped(m_chatmessage[FLIP].toInt() == 1); } void Courtroom::display_pair_character(QString other_charid, QString other_offset) @@ -2833,20 +2830,22 @@ void Courtroom::display_pair_character(QString other_charid, QString other_offse break; } } + + // Play the other pair character's idle animation + ui_vp_sideplayer_char->loadCharacterEmote(m_chatmessage[OTHER_NAME], m_chatmessage[OTHER_EMOTE], kal::CharacterAnimationLayer::IdleEmote); + ui_vp_sideplayer_char->setPlayOnce(false); + // Flip the pair character if (ao_app->m_serverdata.get_feature(server::BASE_FEATURE_SET::FLIPPING) && m_chatmessage[OTHER_FLIP].toInt() == 1) { - ui_vp_sideplayer_char->set_flipped(true); + ui_vp_sideplayer_char->setFlipped(true); } else { - ui_vp_sideplayer_char->set_flipped(false); + ui_vp_sideplayer_char->setFlipped(false); } - // Play the other pair character's idle animation - QString filename = "(a)" + m_chatmessage[OTHER_EMOTE]; - ui_vp_sideplayer_char->set_play_once(false); - ui_vp_sideplayer_char->load_image(filename, m_chatmessage[OTHER_NAME], 0, false); + ui_vp_sideplayer_char->startPlayback(); } } } @@ -2973,33 +2972,35 @@ void Courtroom::do_screenshake() screenshake_animation_group->start(); } -void Courtroom::do_transition(QString p_desk_mod, QString old_pos, QString new_pos) +void Courtroom::do_transition(QString p_desk_mod, QString oldPosId, QString newPosId) { if (m_chatmessage[EMOTE] != "") + { display_character(); + } const QStringList legacy_pos = {"def", "wit", "pro"}; - QString t_old_pos = old_pos; - QString t_new_pos = new_pos; + QString t_old_pos = oldPosId; + QString t_new_pos = newPosId; if (file_exists(ao_app->get_image_suffix(ao_app->get_background_path("court")))) { - if (legacy_pos.contains(old_pos)) + if (legacy_pos.contains(oldPosId)) { - t_old_pos = "court:" + old_pos; + t_old_pos = "court:" + oldPosId; } - if (legacy_pos.contains(new_pos)) + if (legacy_pos.contains(newPosId)) { - t_new_pos = "court:" + new_pos; + t_new_pos = "court:" + newPosId; } } - QPair old_pos_pair = ao_app->get_pos_path(t_old_pos); - QPair new_pos_pair = ao_app->get_pos_path(t_new_pos); + QPair old_pos_pair = ao_app->get_pos_path(t_old_pos); + QPair new_pos_pair = ao_app->get_pos_path(t_new_pos); int duration = ao_app->get_pos_transition_duration(t_old_pos, t_new_pos); // conditions to stop slide - if (old_pos == new_pos || old_pos_pair.first != new_pos_pair.first || new_pos_pair.second == -1 || !Options::getInstance().slidesEnabled() || m_chatmessage[SLIDE] != "1" || duration == -1 || m_chatmessage[EMOTE_MOD].toInt() == ZOOM || m_chatmessage[EMOTE_MOD].toInt() == PREANIM_ZOOM) + if (oldPosId == newPosId || old_pos_pair.first != new_pos_pair.first || !new_pos_pair.second.isValid() || !Options::getInstance().slidesEnabled() || m_chatmessage[SLIDE] != "1" || duration == -1 || m_chatmessage[EMOTE_MOD].toInt() == ZOOM || m_chatmessage[EMOTE_MOD].toInt() == PREANIM_ZOOM) { #ifdef DEBUG_TRANSITION qDebug() << "skipping transition - not applicable"; @@ -3016,130 +3017,105 @@ void Courtroom::do_transition(QString p_desk_mod, QString old_pos, QString new_p qDebug() << "STARTING TRANSITION, CURRENT TIME:" << transition_animation_group->currentTime(); #endif - set_scene(p_desk_mod.toInt(), old_pos); - - QList affected_list = {ui_vp_background, ui_vp_desk, ui_vp_player_char}; - - bool paired = false; - if (!ui_vp_sideplayer_char->isHidden()) - { - affected_list.append(ui_vp_sideplayer_char); - paired = true; - } - - // Set up the background, desk, and player objects' animations + set_scene(p_desk_mod.toInt(), oldPosId); - float scaling_factor = ui_vp_background->get_scaling_factor(); - int offset = (float(old_pos_pair.second) * scaling_factor) - (float(new_pos_pair.second) * scaling_factor); + int viewport_width = ui_viewport->width(); + int viewport_height = ui_viewport->height(); + double scale = double(viewport_height) / double(ui_vp_background->frameSize().height()); + QPoint scaled_old_pos = QPoint(old_pos_pair.second.x() * scale, 0); + QPoint scaled_new_pos = QPoint(new_pos_pair.second.x() * scale, 0); - for (AOLayer *ui_element : affected_list) + QList affected_list = {ui_vp_background, ui_vp_desk}; + for (kal::AnimationLayer *ui_element : affected_list) { QPropertyAnimation *transition_animation = new QPropertyAnimation(ui_element, "pos", this); - transition_animation->setStartValue(ui_element->pos()); transition_animation->setDuration(duration); - transition_animation->setEndValue(QPoint(ui_element->pos().x() + offset, ui_element->pos().y())); transition_animation->setEasingCurve(QEasingCurve::InOutCubic); + transition_animation->setStartValue(QPoint(-scaled_old_pos.x(), 0)); + transition_animation->setEndValue(QPoint(-scaled_new_pos.x(), 0)); transition_animation_group->addAnimation(transition_animation); } - // Setting up the dummy characters to work for us as our stand-in for the next characters - // This should be easy. But it isn't - - QString slide_emote; - if (m_chatmessage[OBJECTION_MOD].contains("4") || m_chatmessage[OBJECTION_MOD].toInt() == 2) - { - slide_emote = "(a)" + ao_app->read_char_ini(m_chatmessage[CHAR_NAME], "objection_pose", "Options"); - if (slide_emote == "(a)") + auto calculate_offset_and_setup_layer = [&, this](kal::CharacterAnimationLayer *layer, QPoint newPos, QString rawOffset) { + QPoint offset; + QStringList offset_data = rawOffset.split(","); + offset.setX(viewport_width * offset_data.at(0).toInt() * 0.01); + if (offset_data.size() > 1) { - slide_emote = "(a)" + m_chatmessage[EMOTE]; + offset.setY(viewport_height * offset_data.at(1).toInt() * 0.01); } - } - else - slide_emote = "(a)" + m_chatmessage[EMOTE]; - QString other_slide_emote = "(a)" + m_chatmessage[OTHER_EMOTE]; + layer->setParent(ui_vp_background); + layer->setPlayOnce(false); + layer->pausePlayback(true); + layer->startPlayback(); + layer->move(newPos); + layer->show(); + }; - // Load the image we're going to use to get scaling information, and move it into the final position for animation data - ui_vp_dummy_char->set_flipped(m_chatmessage[FLIP].toInt()); - ui_vp_dummy_char->load_image(slide_emote, m_chatmessage[CHAR_NAME], 0, false); - set_self_offset(m_chatmessage[SELF_OFFSET], ui_vp_dummy_char); + ui_vp_player_char->loadCharacterEmote(m_chatmessage[CHAR_NAME], m_chatmessage[EMOTE], kal::CharacterAnimationLayer::IdleEmote); + ui_vp_player_char->setFlipped(m_chatmessage[FLIP].toInt() == 1); + calculate_offset_and_setup_layer(ui_vp_player_char, scaled_new_pos, m_chatmessage[SELF_OFFSET]); - QPoint starting_position = QPoint(ui_vp_player_char->pos().x() - offset, ui_vp_player_char->pos().y()); - QPropertyAnimation *ui_vp_dummy_animation = new QPropertyAnimation(ui_vp_dummy_char, "pos", this); - - ui_vp_dummy_animation->setDuration(duration); - ui_vp_dummy_animation->setStartValue(starting_position); - ui_vp_dummy_animation->setEndValue(ui_vp_dummy_char->pos()); - ui_vp_dummy_animation->setEasingCurve(QEasingCurve::InOutCubic); - transition_animation_group->addAnimation(ui_vp_dummy_animation); - - ui_vp_dummy_char->move(starting_position.x(), starting_position.y()); - - // If the new message is paired, do it all again for the pair character. Yippee! - if (m_chatmessage[OTHER_CHARID].toInt() != -1 && !m_chatmessage[OTHER_NAME].isEmpty()) + auto is_pairing = [](QString *data) { + return (data[OTHER_CHARID].toInt() != -1 && !data[OTHER_NAME].isEmpty()); + }; + auto is_pair_under = [](QString data) -> bool { + QStringList pair_data = data.split("^"); + return (pair_data.size() > 1) ? (pair_data.at(1).toInt() == 1) : false; + }; + if (is_pairing(m_chatmessage)) { - ui_vp_sidedummy_char->set_flipped(m_chatmessage[OTHER_FLIP].toInt()); - ui_vp_sidedummy_char->load_image(other_slide_emote, m_chatmessage[OTHER_NAME], 0, false); - set_self_offset(m_chatmessage[OTHER_OFFSET], ui_vp_sidedummy_char); - QStringList args = m_chatmessage[OTHER_CHARID].split("^"); - if (args.size() > 1) + ui_vp_sideplayer_char->loadCharacterEmote(m_chatmessage[OTHER_NAME], m_chatmessage[OTHER_EMOTE], kal::CharacterAnimationLayer::IdleEmote); + calculate_offset_and_setup_layer(ui_vp_sideplayer_char, scaled_new_pos, m_chatmessage[OTHER_OFFSET]); + if (is_pair_under(m_chatmessage[OTHER_CHARID])) { - // Change the order of appearance based on the pair order variable - int order = args.at(1).toInt(); - switch (order) - { - case 0: // Our character is in front - ui_vp_sidedummy_char->stackUnder(ui_vp_dummy_char); - break; - case 1: // Our character is behind - ui_vp_dummy_char->stackUnder(ui_vp_sidedummy_char); - break; - default: - break; - } + ui_vp_player_char->stackUnder(ui_vp_sideplayer_char); } + else + { + ui_vp_sideplayer_char->stackUnder(ui_vp_player_char); + } + } - QPoint other_starting_position = QPoint(ui_vp_sideplayer_char->pos().x() - offset, ui_vp_sideplayer_char->pos().y()); - QPropertyAnimation *ui_vp_sidedummy_animation = new QPropertyAnimation(ui_vp_sidedummy_char, "pos", this); - - ui_vp_sidedummy_animation->setDuration(duration); - ui_vp_sidedummy_animation->setStartValue(starting_position); - ui_vp_sidedummy_animation->setEndValue(ui_vp_sidedummy_char->pos()); - ui_vp_sidedummy_animation->setEasingCurve(QEasingCurve::InOutCubic); - transition_animation_group->addAnimation(ui_vp_sidedummy_animation); + ui_vp_dummy_char->loadCharacterEmote(m_previous_chatmessage[CHAR_NAME], m_previous_chatmessage[EMOTE], kal::CharacterAnimationLayer::IdleEmote); + ui_vp_dummy_char->setFlipped(m_previous_chatmessage[FLIP].toInt() == 1); + calculate_offset_and_setup_layer(ui_vp_dummy_char, scaled_old_pos, m_previous_chatmessage[SELF_OFFSET]); - ui_vp_sidedummy_char->move(starting_position.x(), starting_position.y()); - } - else + if (is_pairing(m_previous_chatmessage)) { - ui_vp_sidedummy_char->stop(); + ui_vp_sidedummy_char->loadCharacterEmote(m_previous_chatmessage[OTHER_NAME], m_previous_chatmessage[OTHER_EMOTE], kal::CharacterAnimationLayer::IdleEmote); + ui_vp_sidedummy_char->setFlipped(m_previous_chatmessage[OTHER_FLIP].toInt() == 1); + calculate_offset_and_setup_layer(ui_vp_sidedummy_char, scaled_old_pos, m_previous_chatmessage[OTHER_OFFSET]); + if (is_pair_under(m_previous_chatmessage[OTHER_CHARID])) + { + ui_vp_dummy_char->stackUnder(ui_vp_sidedummy_char); + } + else + { + ui_vp_sidedummy_char->stackUnder(ui_vp_dummy_char); + } } - ui_vp_player_char->freeze(); - ui_vp_player_char->show(); - if (paired) - { - ui_vp_sideplayer_char->freeze(); - ui_vp_sideplayer_char->show(); - } - else - { - ui_vp_sideplayer_char->stop(); - } - ui_vp_dummy_char->freeze(); - ui_vp_sidedummy_char->freeze(); - QTimer::singleShot(TRANSITION_BOOKEND_DELAY, transition_animation_group, SLOT(start())); + transition_animation_group->start(); } -void Courtroom::on_transition_finish() +void Courtroom::post_transition_cleanup() { transition_animation_group->clear(); - transition_animation_group->setCurrentTime(0); - QTimer::singleShot(TRANSITION_BOOKEND_DELAY, this, SLOT(post_transition_cleanup())); -} -void Courtroom::post_transition_cleanup() -{ + for (kal::CharacterAnimationLayer *layer : qAsConst(ui_vp_char_list)) + { + bool is_visible = layer->isVisible(); + layer->stopPlayback(); + layer->pausePlayback(false); + layer->setParent(ui_viewport); + layer->setVisible(is_visible); + } + + ui_vp_dummy_char->hide(); + ui_vp_sidedummy_char->hide(); + set_scene(m_chatmessage[DESK_MOD].toInt(), m_chatmessage[SIDE]); // Move the character on the viewport according to the offsets @@ -3148,10 +3124,6 @@ void Courtroom::post_transition_cleanup() int emote_mod = m_chatmessage[EMOTE_MOD].toInt(); bool immediate = m_chatmessage[IMMEDIATE].toInt() == 1; - // Reset the pair character - ui_vp_sideplayer_char->stop(); - ui_vp_sideplayer_char->move_and_center(0, 0); - // If the emote_mod is not zooming if (emote_mod != ZOOM && emote_rows != PREANIM_ZOOM) { @@ -3159,12 +3131,6 @@ void Courtroom::post_transition_cleanup() display_pair_character(m_chatmessage[OTHER_CHARID], m_chatmessage[OTHER_OFFSET]); } - // Reset tweedle dee and tweedle dummy - ui_vp_dummy_char->stop(); - ui_vp_dummy_char->move_and_center(0, 0); - ui_vp_sidedummy_char->stop(); - ui_vp_sidedummy_char->move_and_center(0, 0); - // Parse the emote_mod part of the chat message handle_emote_mod(emote_mod, immediate); } @@ -3203,11 +3169,11 @@ void Courtroom::do_effect(QString fx_path, QString fx_sound, QString p_char, QSt { return; } - ui_vp_effect->transform_mode = ao_app->get_scaling(ao_app->get_effect_property(fx_path, p_char, p_folder, "scaling")); - ui_vp_effect->stretch = ao_app->get_effect_property(fx_path, p_char, p_folder, "stretch").startsWith("true"); - ui_vp_effect->set_flipped(ao_app->get_effect_property(fx_path, p_char, p_folder, "respect_flip").startsWith("true") && m_chatmessage[FLIP].toInt() == 1); - ui_vp_effect->set_play_once(false); // The effects themselves dictate whether or not they're looping. - // Static effects will linger. + ui_vp_effect->setTransformationMode(ao_app->get_scaling(ao_app->get_effect_property(fx_path, p_char, p_folder, "scaling"))); + ui_vp_effect->setStretchToFit(ao_app->get_effect_property(fx_path, p_char, p_folder, "stretch").startsWith("true")); + ui_vp_effect->setFlipped(ao_app->get_effect_property(fx_path, p_char, p_folder, "respect_flip").startsWith("true") && m_chatmessage[FLIP].toInt() == 1); + ui_vp_effect->setPlayOnce(false); // The effects themselves dictate whether or not they're looping. + // Static effects will linger. bool looping = ao_app->get_effect_property(fx_path, p_char, p_folder, "loop").startsWith("true"); @@ -3269,10 +3235,9 @@ void Courtroom::do_effect(QString fx_path, QString fx_sound, QString p_char, QSt } ui_vp_effect->move(effect_x, effect_y); - ui_vp_effect->set_static_duration(max_duration); - ui_vp_effect->set_max_duration(max_duration); - ui_vp_effect->load_image(effect, looping); - ui_vp_effect->set_cull_image(cull); + ui_vp_effect->setMaximumDurationPerFrame(max_duration); + ui_vp_effect->loadAndPlayAnimation(effect, looping); + ui_vp_effect->setHideWhenStopped(cull); } void Courtroom::play_char_sfx(QString sfx_name) @@ -3384,7 +3349,7 @@ void Courtroom::initialize_chatbox() else { ui_vp_chat_arrow->move(design_ini_result.x + ui_vp_chatbox->x(), design_ini_result.y + ui_vp_chatbox->y()); - ui_vp_chat_arrow->combo_resize(design_ini_result.width, design_ini_result.height); + ui_vp_chat_arrow->resize(design_ini_result.width, design_ini_result.height); } QString font_name; @@ -3464,8 +3429,8 @@ void Courtroom::handle_ic_speaking() // We're zooming, so hide the pair character and ignore pair offsets. This ain't about them. ui_vp_sideplayer_char->hide(); - ui_vp_player_char->move_and_center(0, 0); - ui_vp_speedlines->load_image(filename, m_chatmessage[CHAR_NAME], ao_app->get_chat(m_chatmessage[CHAR_NAME])); + ui_vp_player_char->move(0, 0); + ui_vp_speedlines->loadAndPlayAnimation(filename, m_chatmessage[CHAR_NAME], ao_app->get_chat(m_chatmessage[CHAR_NAME])); } // Check if this is a talking color (white text, etc.) @@ -3474,23 +3439,22 @@ void Courtroom::handle_ic_speaking() // If color is talking, and our state isn't already talking if (color_is_talking && text_state == 1 && anim_state < 2) { - // Stop the previous animation and play the talking animation - ui_vp_player_char->stop(); - ui_vp_player_char->set_play_once(false); - filename = "(b)" + m_chatmessage[EMOTE]; - ui_vp_player_char->load_image(filename, m_chatmessage[CHAR_NAME], 0, false); - // Set the anim state accordingly + // Play the talking animation anim_state = 2; + filename = m_chatmessage[EMOTE]; + ui_vp_player_char->loadCharacterEmote(m_chatmessage[CHAR_NAME], m_chatmessage[EMOTE], kal::CharacterAnimationLayer::TalkEmote); + ui_vp_player_char->setPlayOnce(false); + ui_vp_player_char->startPlayback(); + // Set the anim state accordingly } else if (anim_state < 3 && anim_state != 3) // Set it to idle as we're not on that already { - // Stop the previous animation and play the idle animation - ui_vp_player_char->stop(); - ui_vp_player_char->set_play_once(false); - filename = "(a)" + m_chatmessage[EMOTE]; - ui_vp_player_char->load_image(filename, m_chatmessage[CHAR_NAME], 0, false); - // Set the anim state accordingly + // Play the idle animation anim_state = 3; + filename = m_chatmessage[EMOTE]; + ui_vp_player_char->loadCharacterEmote(m_chatmessage[CHAR_NAME], m_chatmessage[EMOTE], kal::CharacterAnimationLayer::IdleEmote); + ui_vp_player_char->setPlayOnce(false); + ui_vp_player_char->startPlayback(); } // Begin parsing through the chatbox message @@ -4025,15 +3989,16 @@ void Courtroom::play_preanim(bool immediate) qWarning() << "could not find preanim" << f_preanim << "for character" << f_char; return; } - ui_vp_player_char->set_static_duration(preanim_duration); - ui_vp_player_char->set_play_once(true); - ui_vp_player_char->load_image(f_preanim, f_char, preanim_duration, true); + + ui_vp_player_char->loadCharacterEmote(f_char, f_preanim, kal::CharacterAnimationLayer::PreEmote, preanim_duration); + ui_vp_player_char->setPlayOnce(true); + ui_vp_player_char->startPlayback(); switch (m_chatmessage[DESK_MOD].toInt()) { case DESK_EMOTE_ONLY_EX: ui_vp_sideplayer_char->hide(); - ui_vp_player_char->move_and_center(0, 0); + ui_vp_player_char->move(0, 0); [[fallthrough]]; case DESK_EMOTE_ONLY: case DESK_HIDE: @@ -4100,7 +4065,7 @@ void Courtroom::start_chat_ticking() case DESK_PRE_ONLY_EX: ui_vp_sideplayer_char->hide(); - ui_vp_player_char->move_and_center(0, 0); + ui_vp_player_char->move(0, 0); [[fallthrough]]; case DESK_PRE_ONLY: case DESK_HIDE: @@ -4154,12 +4119,12 @@ void Courtroom::start_chat_ticking() // Show it if chatbox always shows if (Options::getInstance().characterStickerEnabled() && chatbox_always_show) { - ui_vp_sticker->load_image(m_chatmessage[CHAR_NAME]); + ui_vp_sticker->loadAndPlayAnimation(m_chatmessage[CHAR_NAME]); } // Hide the face sticker else { - ui_vp_sticker->stop(); + ui_vp_sticker->stopPlayback(); } } // If we're not already waiting on the next message, start the timer. We could be overriden if there's an objection planned. @@ -4176,7 +4141,7 @@ void Courtroom::start_chat_ticking() if (Options::getInstance().characterStickerEnabled()) { - ui_vp_sticker->load_image(m_chatmessage[CHAR_NAME]); + ui_vp_sticker->loadAndPlayAnimation(m_chatmessage[CHAR_NAME]); } if (m_chatmessage[ADDITIVE] != "1") @@ -4226,8 +4191,6 @@ void Courtroom::chat_tick() // Due to our new text speed system, we always need to stop the timer now. chat_tick_timer->stop(); - ui_vp_player_char->set_static_duration(0); - QString filename; if (tick_pos >= f_message.size()) { @@ -4239,20 +4202,21 @@ void Courtroom::chat_tick() { QStringList c_paths = {ao_app->get_image_suffix(ao_app->get_character_path(m_chatmessage[CHAR_NAME], "(c)" + m_chatmessage[EMOTE])), ao_app->get_image_suffix(ao_app->get_character_path(m_chatmessage[CHAR_NAME], "(c)/" + m_chatmessage[EMOTE]))}; // if there is a (c) animation for this emote and we haven't played it already - if (file_exists(ui_vp_player_char->find_image(c_paths)) && (!c_played)) + if (file_exists(ao_app->find_image(c_paths)) && (!c_played)) { anim_state = 5; - ui_vp_player_char->set_play_once(true); - filename = "(c)" + m_chatmessage[EMOTE]; c_played = true; + ui_vp_player_char->loadCharacterEmote(m_chatmessage[CHAR_NAME], m_chatmessage[EMOTE], kal::CharacterAnimationLayer::PostEmote); + ui_vp_player_char->setPlayOnce(true); + ui_vp_player_char->startPlayback(); } else { anim_state = 3; - ui_vp_player_char->set_play_once(false); - filename = "(a)" + m_chatmessage[EMOTE]; + ui_vp_player_char->loadCharacterEmote(m_chatmessage[CHAR_NAME], m_chatmessage[EMOTE], kal::CharacterAnimationLayer::IdleEmote); + ui_vp_player_char->setPlayOnce(false); + ui_vp_player_char->startPlayback(); } - ui_vp_player_char->load_image(filename, m_chatmessage[CHAR_NAME], 0, false); } } else // We're a narrator msg @@ -4266,8 +4230,8 @@ void Courtroom::chat_tick() f_char = m_chatmessage[CHAR_NAME]; f_custom_theme = ao_app->get_chat(f_char); } - ui_vp_chat_arrow->transform_mode = ao_app->get_misc_scaling(f_custom_theme); - ui_vp_chat_arrow->load_image("chat_arrow", f_custom_theme); // Chat stopped being processed, indicate that. + ui_vp_chat_arrow->setTransformationMode(ao_app->get_misc_scaling(f_custom_theme)); + ui_vp_chat_arrow->loadAndPlayAnimation("chat_arrow", f_custom_theme); // Chat stopped being processed, indicate that. QString f_message_filtered = filter_ic_text(f_message, true, -1, m_chatmessage[TEXT_COLOR].toInt()); for (int c = 0; c < max_colors; ++c) { @@ -4503,19 +4467,17 @@ void Courtroom::chat_tick() if (color_is_talking && anim_state != 2 && anim_state < 4) // Set it to talking as we're not on that already (though we have // to avoid interrupting a non-interrupted preanim) { - ui_vp_player_char->stop(); - ui_vp_player_char->set_play_once(false); - filename = "(b)" + m_chatmessage[EMOTE]; - ui_vp_player_char->load_image(filename, m_chatmessage[CHAR_NAME], 0, false); anim_state = 2; + ui_vp_player_char->loadCharacterEmote(m_chatmessage[CHAR_NAME], m_chatmessage[EMOTE], kal::CharacterAnimationLayer::TalkEmote); + ui_vp_player_char->setPlayOnce(false); + ui_vp_player_char->startPlayback(); } else if (!color_is_talking && anim_state < 3 && anim_state != 3) // Set it to idle as we're not on that already { - ui_vp_player_char->stop(); - ui_vp_player_char->set_play_once(false); - filename = "(a)" + m_chatmessage[EMOTE]; - ui_vp_player_char->load_image(filename, m_chatmessage[CHAR_NAME], 0, false); anim_state = 3; + ui_vp_player_char->loadCharacterEmote(m_chatmessage[CHAR_NAME], m_chatmessage[EMOTE], kal::CharacterAnimationLayer::IdleEmote); + ui_vp_player_char->setPlayOnce(false); + ui_vp_player_char->startPlayback(); } } // Continue ticking @@ -4544,12 +4506,21 @@ void Courtroom::play_sfx() void Courtroom::set_scene(bool show_desk, const QString f_side) { - QPair bg_pair = ao_app->get_pos_path(f_side); - QPair desk_pair = ao_app->get_pos_path(f_side, true); - ui_vp_background->load_image(bg_pair.first, bg_pair.second); - ui_vp_desk->load_image(desk_pair.first, desk_pair.second); - last_side = f_side; + QPair bg_pair = ao_app->get_pos_path(f_side); + QPair desk_pair = ao_app->get_pos_path(f_side, true); + + ui_vp_background->loadAndPlayAnimation(bg_pair.first); + ui_vp_desk->loadAndPlayAnimation(desk_pair.first); + double scale = double(ui_viewport->height()) / double(ui_vp_background->frameSize().height()); + QSize scaled_size = ui_vp_background->frameSize() * scale; + QPoint scaled_offset = QPoint(-(bg_pair.second.x() * scale), 0); + ui_vp_background->resize(scaled_size); + ui_vp_background->move(scaled_offset); + ui_vp_desk->resize(scaled_size); + ui_vp_desk->move(scaled_offset); + + last_side = f_side; if (show_desk) { ui_vp_desk->show(); @@ -4560,7 +4531,7 @@ void Courtroom::set_scene(bool show_desk, const QString f_side) } } -void Courtroom::set_self_offset(const QString &p_list, AOLayer *p_layer) +void Courtroom::set_self_offset(const QString &p_list, kal::AnimationLayer *p_layer) { QStringList self_offsets = p_list.split("&"); int self_offset = self_offsets[0].toInt(); @@ -4573,7 +4544,7 @@ void Courtroom::set_self_offset(const QString &p_list, AOLayer *p_layer) { self_offset_v = self_offsets[1].toInt(); } - p_layer->move_and_center(ui_viewport->width() * self_offset / 100, ui_viewport->height() * self_offset_v / 100); + p_layer->move(ui_viewport->width() * self_offset / 100, ui_viewport->height() * self_offset_v / 100); } void Courtroom::set_ip_list(QString p_list) @@ -4729,15 +4700,14 @@ void Courtroom::handle_wtce(QString p_wtce, int variant) QString bg_misc = ao_app->read_design_ini("misc", ao_app->get_background_path("design.ini")); QString sfx_name; QString filename; - ui_vp_wtce->set_static_duration(wtce_static_time); - ui_vp_wtce->set_max_duration(wtce_max_time); + ui_vp_wtce->setMaximumDurationPerFrame(wtce_max_time); // witness testimony if (p_wtce == "testimony1") { // End testimony indicator if (variant == 1) { - ui_vp_testimony->kill(); + ui_vp_testimony->stopPlayback(); return; } sfx_name = ao_app->get_court_sfx("witness_testimony", bg_misc); @@ -4746,7 +4716,7 @@ void Courtroom::handle_wtce(QString p_wtce, int variant) sfx_name = ao_app->get_court_sfx("witnesstestimony", bg_misc); } filename = "witnesstestimony_bubble"; - ui_vp_testimony->load_image("testimony", "", bg_misc); + ui_vp_testimony->loadAndPlayAnimation("testimony", "", bg_misc); } // cross examination else if (p_wtce == "testimony2") @@ -4757,12 +4727,11 @@ void Courtroom::handle_wtce(QString p_wtce, int variant) sfx_name = ao_app->get_court_sfx("crossexamination", bg_misc); } filename = "crossexamination_bubble"; - ui_vp_testimony->kill(); + ui_vp_testimony->stopPlayback(); } else { - ui_vp_wtce->set_static_duration(verdict_static_time); - ui_vp_wtce->set_max_duration(verdict_max_time); + ui_vp_wtce->setMaximumDurationPerFrame(verdict_max_time); // Verdict? if (p_wtce == "judgeruling") { @@ -4774,13 +4743,13 @@ void Courtroom::handle_wtce(QString p_wtce, int variant) sfx_name = ao_app->get_court_sfx("notguilty", bg_misc); } filename = "notguilty_bubble"; - ui_vp_testimony->kill(); + ui_vp_testimony->stopPlayback(); } else if (variant == 1) { sfx_name = ao_app->get_court_sfx("guilty", bg_misc); filename = "guilty_bubble"; - ui_vp_testimony->kill(); + ui_vp_testimony->stopPlayback(); } } // Completely custom WTCE @@ -4791,8 +4760,8 @@ void Courtroom::handle_wtce(QString p_wtce, int variant) } } sfx_player->findAndPlaySfx(sfx_name); - ui_vp_wtce->load_image(filename, "", bg_misc); - ui_vp_wtce->set_play_once(true); + ui_vp_wtce->loadAndPlayAnimation(filename, "", bg_misc); + ui_vp_wtce->setPlayOnce(true); } void Courtroom::set_hp_bar(int p_bar, int p_state) diff --git a/src/courtroom.h b/src/courtroom.h index 0370892b9..a7ca23544 100644 --- a/src/courtroom.h +++ b/src/courtroom.h @@ -1,5 +1,6 @@ #pragma once +#include "animationlayer.h" #include "aoapplication.h" #include "aoblipplayer.h" #include "aobutton.h" @@ -10,7 +11,6 @@ #include "aoevidencebutton.h" #include "aoevidencedisplay.h" #include "aoimage.h" -#include "aolayer.h" #include "aomusicplayer.h" #include "aopacket.h" #include "aosfxplayer.h" @@ -152,7 +152,7 @@ class Courtroom : public QMainWindow // sets p_layer according to SELF_OFFSET, only a function bc it's used with // desk_mod 4 and 5 - void set_self_offset(const QString &p_list, AOLayer *p_layer); + void set_self_offset(const QString &p_list, kal::AnimationLayer *p_layer); // takes in serverD-formatted IP list as prints a converted version to server // OOC admittedly poorly named @@ -217,7 +217,7 @@ class Courtroom : public QMainWindow void handle_ic_message(); // Start the logic for doing a courtroom pan slide - void do_transition(QString desk_mod, QString old_pos, QString new_pos); + void do_transition(QString desk_mod, QString oldPosId, QString new_pos); // Display the character. void display_character(); @@ -444,6 +444,7 @@ class Courtroom : public QMainWindow static const int MS_MINIMUM = 15; static const int MS_MAXIMUM = 32; QString m_chatmessage[MS_MAXIMUM]; + QString m_previous_chatmessage[MS_MAXIMUM]; /** * @brief The amount of time to wait at the start and end of slide @@ -451,7 +452,6 @@ class Courtroom : public QMainWindow */ static const int TRANSITION_BOOKEND_DELAY = 300; - QString previous_ic_message; QString additive_previous; // char id, muted or not @@ -608,22 +608,23 @@ class Courtroom : public QMainWindow AOImage *ui_background; QWidget *ui_viewport; - BackgroundLayer *ui_vp_background; - SplashLayer *ui_vp_speedlines; - CharLayer *ui_vp_player_char; - CharLayer *ui_vp_sideplayer_char; - CharLayer *ui_vp_dummy_char; - CharLayer *ui_vp_sidedummy_char; - BackgroundLayer *ui_vp_desk; + kal::BackgroundAnimationLayer *ui_vp_background; + kal::SplashAnimationLayer *ui_vp_speedlines; + kal::CharacterAnimationLayer *ui_vp_player_char; + kal::CharacterAnimationLayer *ui_vp_sideplayer_char; + kal::CharacterAnimationLayer *ui_vp_dummy_char; + kal::CharacterAnimationLayer *ui_vp_sidedummy_char; + QList ui_vp_char_list; + kal::BackgroundAnimationLayer *ui_vp_desk; AOEvidenceDisplay *ui_vp_evidence_display; AOImage *ui_vp_chatbox; AOChatboxLabel *ui_vp_showname; - InterfaceLayer *ui_vp_chat_arrow; + kal::InterfaceAnimationLayer *ui_vp_chat_arrow; QTextEdit *ui_vp_message; - SplashLayer *ui_vp_testimony; - SplashLayer *ui_vp_wtce; - EffectLayer *ui_vp_effect; - SplashLayer *ui_vp_objection; + kal::SplashAnimationLayer *ui_vp_testimony; + kal::SplashAnimationLayer *ui_vp_wtce; + kal::EffectAnimationLayer *ui_vp_effect; + kal::SplashAnimationLayer *ui_vp_objection; QTextEdit *ui_ic_chatlog; @@ -635,9 +636,9 @@ class Courtroom : public QMainWindow QTreeWidget *ui_music_list; ScrollText *ui_music_name; - InterfaceLayer *ui_music_display; + kal::InterfaceAnimationLayer *ui_music_display; - StickerLayer *ui_vp_sticker; + kal::StickerAnimationLayer *ui_vp_sticker; static const int max_clocks = 5; AOClockLabel *ui_clock[max_clocks]; @@ -805,7 +806,6 @@ public Q_SLOTS: void objection_done(); void preanim_done(); void do_screenshake(); - void on_transition_finish(); void do_flash(); void do_effect(QString fx_path, QString fx_sound, QString p_char, QString p_folder); void play_char_sfx(QString sfx_name); @@ -978,7 +978,7 @@ private Q_SLOTS: // Proceed to parse the oldest chatmessage and remove it from the stack void chatmessage_dequeue(); - void preview_emote(QString emote); + void preview_emote(QString emote, kal::CharacterAnimationLayer::EmoteType emoteType); void update_emote_preview(); // After attempting to play a transition animation, clean up the viewport diff --git a/src/emotes.cpp b/src/emotes.cpp index 4023b5217..c120d596d 100644 --- a/src/emotes.cpp +++ b/src/emotes.cpp @@ -231,17 +231,16 @@ void Courtroom::update_emote_preview() { return; } - QString emote; + QString pre = ao_app->get_pre_emote(current_char, current_emote); if (ui_pre->isChecked() && !pre.isEmpty() && pre != "-") { - emote = pre; + preview_emote(pre, kal::CharacterAnimationLayer::PreEmote); } else { - emote = "(b)" + ao_app->get_emote(current_char, current_emote); + preview_emote(ao_app->get_emote(current_char, current_emote), kal::CharacterAnimationLayer::IdleEmote); } - preview_emote(emote); } void Courtroom::on_emote_clicked(int p_id) @@ -270,30 +269,30 @@ void Courtroom::show_emote_menu(const QPoint &pos) QString f_pre = ao_app->get_pre_emote(current_char, emote_num); if (!f_pre.isEmpty() && f_pre != "-") { - emote_menu->addAction("Preview pre: " + f_pre, this, [this, f_pre] { preview_emote(f_pre); }); + emote_menu->addAction("Preview pre: " + f_pre, this, [this, f_pre] { preview_emote(f_pre, kal::CharacterAnimationLayer::PreEmote); }); } QString f_emote = ao_app->get_emote(current_char, emote_num); if (!f_emote.isEmpty()) { - emote_menu->addAction("Preview idle: " + f_emote, this, [this, f_emote] { preview_emote("(a)" + f_emote); }); - emote_menu->addAction("Preview talk: " + f_emote, this, [this, f_emote] { preview_emote("(b)" + f_emote); }); + emote_menu->addAction("Preview idle: " + f_emote, this, [this, f_emote] { preview_emote(f_emote, kal::CharacterAnimationLayer::IdleEmote); }); + emote_menu->addAction("Preview talk: " + f_emote, this, [this, f_emote] { preview_emote(f_emote, kal::CharacterAnimationLayer::TalkEmote); }); QStringList c_paths = {ao_app->get_image_suffix(ao_app->get_character_path(current_char, "(c)" + f_emote)), ao_app->get_image_suffix(ao_app->get_character_path(current_char, "(c)/" + f_emote))}; // if there is a (c) animation - if (file_exists(ui_vp_player_char->find_image(c_paths))) + if (file_exists(ao_app->find_image(c_paths))) { - emote_menu->addAction("Preview segway: " + f_emote, this, [this, f_emote] { preview_emote("(c)" + f_emote); }); + emote_menu->addAction("Preview segway: " + f_emote, this, [this, f_emote] { preview_emote(f_emote, kal::CharacterAnimationLayer::PostEmote); }); } } emote_menu->popup(button->mapToGlobal(pos)); } -void Courtroom::preview_emote(QString f_emote) +void Courtroom::preview_emote(QString f_emote, kal::CharacterAnimationLayer::EmoteType emoteType) { emote_preview->show(); emote_preview->raise(); emote_preview->updateViewportGeometry(); - emote_preview->display(current_char, f_emote, ui_flip->isChecked(), ui_pair_offset_spinbox->value(), ui_pair_vert_offset_spinbox->value()); + emote_preview->display(current_char, f_emote, emoteType, ui_flip->isChecked(), ui_pair_offset_spinbox->value(), ui_pair_vert_offset_spinbox->value()); } void Courtroom::on_emote_left_clicked() diff --git a/src/path_functions.cpp b/src/path_functions.cpp index 186024539..4ab5c6601 100644 --- a/src/path_functions.cpp +++ b/src/path_functions.cpp @@ -78,7 +78,7 @@ VPath AOApplication::get_default_background_path(QString p_file) return VPath("background/default/" + p_file); } -QPair AOApplication::get_pos_path(const QString &pos, const bool desk) +QPair AOApplication::get_pos_path(const QString &pos, const bool desk) { // witness is default if pos is invalid QString f_pos = pos; @@ -96,14 +96,18 @@ QPair AOApplication::get_pos_path(const QString &pos, const bool d f_pos = "court:wit"; } QStringList f_pos_split = f_pos.split(":"); - int f_center = -1; + + QRect f_rect; if (f_pos_split.size() > 1) { // Subposition, get center info - bool bOk; - int subpos_center = read_design_ini(f_pos + "/pos_center", get_background_path("design.ini")).toInt(&bOk); - if (bOk) + QStringList arglist = read_design_ini(f_pos + "/pos_center", get_background_path("design.ini")).split(","); + if (arglist.size() == 4) { - f_center = subpos_center; + f_rect = QRect(arglist[0].toInt(), arglist[1].toInt(), arglist[2].toInt(), arglist[3].toInt()); + if (!f_rect.isValid()) + { + f_rect = QRect(); + } } } QString f_background; @@ -166,11 +170,12 @@ QPair AOApplication::get_pos_path(const QString &pos, const bool d { f_desk_image = desk_override; } + if (desk) { - return {f_desk_image, f_center}; + return {f_desk_image, f_rect}; } - return {f_background, f_center}; + return {f_background, f_rect}; } VPath AOApplication::get_evidence_path(QString p_file) diff --git a/src/text_file_functions.cpp b/src/text_file_functions.cpp index b123da370..418de47c0 100644 --- a/src/text_file_functions.cpp +++ b/src/text_file_functions.cpp @@ -929,5 +929,7 @@ int AOApplication::get_pos_transition_duration(const QString &old_pos, const QSt return duration; } else + { return -1; // invalid + } } From b04ccee735fe61b3644012ddd144ba0aca7d6849 Mon Sep 17 00:00:00 2001 From: TrickyLeifa Date: Wed, 22 May 2024 22:12:56 +0200 Subject: [PATCH 10/16] Added enclosed DEBUG_MOVIE debug line --- src/animationloader.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/animationloader.cpp b/src/animationloader.cpp index d35744833..5f57cbe5d 100644 --- a/src/animationloader.cpp +++ b/src/animationloader.cpp @@ -66,7 +66,9 @@ AnimationFrame AnimationLoader::frame(int frameNumber) m_task_lock.lock(); while (m_frames.size() < frameNumber + 1) { +#ifdef DEBUG_MOVIE qDebug().noquote() << "Waiting for frame" << frameNumber << QString("(file: %1, frame count: %2)").arg(m_file_name).arg(m_frame_count); +#endif m_task_signal.wait(&m_task_lock); } From e80c7d4ef23d97f1e6701cda6e809ccbcd133313 Mon Sep 17 00:00:00 2001 From: TrickyLeifa Date: Wed, 22 May 2024 22:17:43 +0200 Subject: [PATCH 11/16] Added missing include for Qt5 --- src/animationlayer.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/animationlayer.cpp b/src/animationlayer.cpp index 310f97078..f764f295f 100644 --- a/src/animationlayer.cpp +++ b/src/animationlayer.cpp @@ -4,6 +4,7 @@ #include "options.h" #include +#include static QThreadPool *thread_pool; From 3df5b1cf985d96c263fea3f2a136ca2a9200ce9b Mon Sep 17 00:00:00 2001 From: TrickyLeifa Date: Wed, 22 May 2024 22:22:11 +0200 Subject: [PATCH 12/16] Re-enable some functionality that were disabled during debugging --- src/animationlayer.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/animationlayer.cpp b/src/animationlayer.cpp index f764f295f..2d44a5f79 100644 --- a/src/animationlayer.cpp +++ b/src/animationlayer.cpp @@ -164,20 +164,16 @@ void AnimationLayer::setTransformationMode(Qt::TransformationMode mode) void AnimationLayer::setMinimumDurationPerFrame(int duration) { - return; m_minimum_duration = duration; } void AnimationLayer::setMaximumDurationPerFrame(int duration) { - return; m_maximum_duration = duration; } void AnimationLayer::setMaskingRect(QRect rect) { - return; // TODO re-enable - m_mask_rect_hint = rect; calculateFrameGeometry(); } From 52fc8d359426e068d9057a80fd456eda70c00683 Mon Sep 17 00:00:00 2001 From: TrickyLeifa Date: Wed, 22 May 2024 22:38:26 +0200 Subject: [PATCH 13/16] Slight tweak to further improve performance --- src/animationlayer.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/animationlayer.cpp b/src/animationlayer.cpp index 2d44a5f79..1f1c4f66c 100644 --- a/src/animationlayer.cpp +++ b/src/animationlayer.cpp @@ -17,6 +17,8 @@ AnimationLayer::AnimationLayer(QWidget *parent) m_ticker = new QTimer(this); m_ticker->setSingleShot(true); + m_ticker->setTimerType(Qt::PreciseTimer); + connect(m_ticker, &QTimer::timeout, this, &AnimationLayer::frameTicker); if (!thread_pool) @@ -258,11 +260,8 @@ void AnimationLayer::finishPlayback() void AnimationLayer::prepareNextTick() { - int duration = m_current_frame.duration; - - duration = (m_minimum_duration > 0) ? qMax(m_minimum_duration, duration) : duration; + int duration = qMax(m_minimum_duration, m_current_frame.duration); duration = (m_maximum_duration > 0) ? qMin(m_maximum_duration, duration) : duration; - m_ticker->start(duration); } @@ -348,7 +347,7 @@ void AnimationLayer::frameTicker() if (!m_pause) { - m_ticker->start(m_current_frame.duration); + prepareNextTick(); } } From d135bbc51144667fb3ee323a7635e3dbfc3f41a8 Mon Sep 17 00:00:00 2001 From: TrickyLeifa Date: Wed, 22 May 2024 23:30:31 +0200 Subject: [PATCH 14/16] Fixed emote synchronization, ... * Fixed emote synchronization * Still requires identical frame count. Will still cause freeze as it syncs. * Fixed frame effects not working on idle and talk emotes. * Characters are now repositioned after background sliding is over. --- src/animationlayer.cpp | 39 ++++++++++++++++++++++++++++++++++----- src/animationlayer.h | 3 +++ src/aoapplication.h | 1 + src/courtroom.cpp | 1 + src/path_functions.cpp | 14 +++++++++++--- 5 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/animationlayer.cpp b/src/animationlayer.cpp index 1f1c4f66c..a21e20c43 100644 --- a/src/animationlayer.cpp +++ b/src/animationlayer.cpp @@ -131,12 +131,16 @@ void AnimationLayer::jumpToFrame(int number) return; } + bool is_processing = m_processing; if (m_ticker->isActive()) { m_ticker->stop(); } - m_frame_number = number; - frameTicker(); + m_target_frame_number = number; + if (is_processing) + { + frameTicker(); + } } void AnimationLayer::setPlayOnce(bool enabled) @@ -161,7 +165,7 @@ void AnimationLayer::setFlipped(bool enabled) void AnimationLayer::setTransformationMode(Qt::TransformationMode mode) { - m_transformation_mode = mode; + m_transformation_mode_hint = mode; } void AnimationLayer::setMinimumDurationPerFrame(int duration) @@ -247,6 +251,11 @@ void AnimationLayer::calculateFrameGeometry() // display the frame in its center int x = (m_scaled_frame_size.width() - widget_size.width()) / 2; m_display_rect = QRect(x, 0, widget_size.width(), m_scaled_frame_size.height()); + + if (m_transformation_mode_hint == Qt::FastTransformation) + { + m_transformation_mode = scale < 1.0 ? Qt::SmoothTransformation : Qt::FastTransformation; + } } displayCurrentFrame(); @@ -314,6 +323,8 @@ void AnimationLayer::frameTicker() finishPlayback(); return; } + + return; } if (m_pause && !m_first_frame) @@ -340,6 +351,11 @@ void AnimationLayer::frameTicker() } m_first_frame = false; + if (m_target_frame_number != -1) + { + m_frame_number = m_target_frame_number; + m_target_frame_number = -1; + } m_current_frame = m_loader->frame(m_frame_number); displayCurrentFrame(); Q_EMIT frameNumberChanged(m_frame_number); @@ -380,6 +396,7 @@ void CharacterAnimationLayer::loadCharacterEmote(QString character, QString file m_character = character; m_emote = fileName; + m_resolved_emote = fileName; m_emote_type = emoteType; QStringList prefixes; @@ -410,19 +427,31 @@ void CharacterAnimationLayer::loadCharacterEmote(QString character, QString file } QVector path_list; + QVector prefixed_emote_list; for (const QString &prefix : qAsConst(prefixes)) { path_list << ao_app->get_character_path(character, prefix + m_emote); + prefixed_emote_list << prefix + m_emote; } path_list << ao_app->get_character_path(character, m_emote); + prefixed_emote_list << m_emote; if (placeholder_fallback) { path_list << ao_app->get_character_path(character, QStringLiteral("placeholder")); + prefixed_emote_list << QStringLiteral("placeholder"); path_list << ao_app->get_theme_path("placeholder", ao_app->default_theme); + prefixed_emote_list << QStringLiteral("placeholder"); } - setFileName(ao_app->get_image_path(path_list)); + int index = -1; + QString file_path = ao_app->get_image_path(path_list, index); + if (index != -1) + { + m_resolved_emote = prefixed_emote_list[index]; + } + + setFileName(file_path); setPlayOnce(play_once); setTransformationMode(ao_app->get_scaling(ao_app->get_emote_property(character, fileName, "scaling"))); setStretchToFit(ao_app->get_emote_property(character, fileName, "stretch").startsWith("true")); @@ -519,7 +548,7 @@ void CharacterAnimationLayer::notifyFrameEffect(int frameNumber) { for (const FrameEffect &effect : qAsConst(*it)) { - if (effect.emote_name == m_emote) + if (effect.emote_name == m_resolved_emote) { switch (effect.type) { diff --git a/src/animationlayer.h b/src/animationlayer.h index da64354aa..c67f70268 100644 --- a/src/animationlayer.h +++ b/src/animationlayer.h @@ -90,6 +90,7 @@ public Q_SLOTS: bool m_flipped = false; int m_minimum_duration = 0; int m_maximum_duration = 0; + Qt::TransformationMode m_transformation_mode_hint = Qt::FastTransformation; Qt::TransformationMode m_transformation_mode = Qt::FastTransformation; AnimationLoader *m_loader = nullptr; QSize m_frame_size; @@ -103,6 +104,7 @@ public Q_SLOTS: QTimer *m_ticker = nullptr; bool m_first_frame = false; int m_frame_number = 0; + int m_target_frame_number = -1; int m_frame_count = 0; AnimationFrame m_current_frame; @@ -170,6 +172,7 @@ class CharacterAnimationLayer : public AnimationLayer QString m_character; QString m_emote; + QString m_resolved_emote; EmoteType m_emote_type = NoEmoteType; QTimer *m_duration_timer = nullptr; int m_duration = 0; diff --git a/src/aoapplication.h b/src/aoapplication.h index 6d2b55a3a..1f2b1ed00 100644 --- a/src/aoapplication.h +++ b/src/aoapplication.h @@ -120,6 +120,7 @@ class AOApplication : public QApplication VPath get_evidence_path(QString p_file); QVector get_asset_paths(QString p_element, QString p_theme = QString(), QString p_subtheme = QString(), QString p_default_theme = QString(), QString p_misc = QString(), QString p_character = QString(), QString p_placeholder = QString()); QString get_asset_path(QVector pathlist); + QString get_image_path(QVector pathlist, int &index, bool static_image = false); QString get_image_path(QVector pathlist, bool static_image = false); QString get_sfx_path(QVector pathlist); QString get_config_value(QString p_identifier, QString p_config, QString p_theme = QString(), QString p_subtheme = QString(), QString p_default_theme = QString(), QString p_misc = QString()); diff --git a/src/courtroom.cpp b/src/courtroom.cpp index bf49fdc1c..134679766 100644 --- a/src/courtroom.cpp +++ b/src/courtroom.cpp @@ -3110,6 +3110,7 @@ void Courtroom::post_transition_cleanup() layer->stopPlayback(); layer->pausePlayback(false); layer->setParent(ui_viewport); + layer->stackUnder(ui_vp_desk); layer->setVisible(is_visible); } diff --git a/src/path_functions.cpp b/src/path_functions.cpp index 4ab5c6601..4d22e9b96 100644 --- a/src/path_functions.cpp +++ b/src/path_functions.cpp @@ -239,19 +239,27 @@ QString AOApplication::get_asset_path(QVector pathlist) return QString(); } -QString AOApplication::get_image_path(QVector pathlist, bool static_image) +QString AOApplication::get_image_path(QVector pathlist, int &index, bool static_image) { - for (const VPath &p : pathlist) + for (int i = 0; i < pathlist.size(); i++) { - QString path = get_image_suffix(p, static_image); + QString path = get_image_suffix(pathlist[i], static_image); if (!path.isEmpty()) { + index = i; return path; } } + return QString(); } +QString AOApplication::get_image_path(QVector pathlist, bool static_image) +{ + int dummy; + return get_image_path(pathlist, dummy, static_image); +} + QString AOApplication::get_sfx_path(QVector pathlist) { for (const VPath &p : pathlist) From 29284c0b21408131b280744f58b78b2792d6a826 Mon Sep 17 00:00:00 2001 From: TrickyLeifa Date: Thu, 23 May 2024 00:02:07 +0200 Subject: [PATCH 15/16] Removed theme submodule, ... * Removed theme submodule. * This should be handled by the CI. * Fixed effects not disappearing when they should (cull / repeat) --- .gitmodules | 3 --- bin/base/misc/default/chatbox.png | Bin 505 -> 0 bytes bin/base/misc/default/holdit.opus | Bin 15238 -> 0 bytes bin/base/misc/default/holdit_bubble.gif | Bin 39003 -> 0 bytes bin/base/misc/default/objection.opus | Bin 15238 -> 0 bytes bin/base/misc/default/objection_bubble.gif | Bin 43029 -> 0 bytes bin/base/misc/default/takethat.opus | Bin 15238 -> 0 bytes bin/base/misc/default/takethat_bubble.gif | Bin 40634 -> 0 bytes bin/base/themes | 1 - src/animationlayer.cpp | 21 ++++++++++++++++++--- src/animationlayer.h | 2 ++ src/courtroom.cpp | 4 ++-- 12 files changed, 22 insertions(+), 9 deletions(-) delete mode 100644 .gitmodules delete mode 100644 bin/base/misc/default/chatbox.png delete mode 100644 bin/base/misc/default/holdit.opus delete mode 100644 bin/base/misc/default/holdit_bubble.gif delete mode 100644 bin/base/misc/default/objection.opus delete mode 100644 bin/base/misc/default/objection_bubble.gif delete mode 100644 bin/base/misc/default/takethat.opus delete mode 100644 bin/base/misc/default/takethat_bubble.gif delete mode 160000 bin/base/themes diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index d056f0508..000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "bin/base/themes"] - path = bin/base/themes - url = https://github.com/AttorneyOnline/AO2-Themes.git diff --git a/bin/base/misc/default/chatbox.png b/bin/base/misc/default/chatbox.png deleted file mode 100644 index 0c3bae1e16eba3d6c0d930ef025eda4dd88c2ae4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 505 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5{5aTvXzqQo_#Bsf2;hmB~Pm!Rs4C$ANAJpeYY*nM!ZhADAi2^x#APGmQf` zb;|2F4oJwyI~*2cWNc7iY%my7XoEeY0At3DZ~NFD6z!E)VwiJ()6dl3*$mq_7lvNRj2{{@G6?fzia^>FVdQ&MBb@07ERCb^rhX diff --git a/bin/base/misc/default/holdit.opus b/bin/base/misc/default/holdit.opus deleted file mode 100644 index a454be835277dda71317b5aa594b3bfde00da673..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15238 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 D*f(Qj diff --git a/bin/base/misc/default/holdit_bubble.gif b/bin/base/misc/default/holdit_bubble.gif deleted file mode 100644 index 536f8cd7ed6854fc25379527170e61be3424b5cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39003 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 W0000i000000RRC200II;0RTH4|I7RU diff --git a/bin/base/misc/default/objection.opus b/bin/base/misc/default/objection.opus deleted file mode 100644 index 7e15d05e1edf6f7c73f57522e5da39c8f093f9d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15238 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 Dphse* diff --git a/bin/base/misc/default/objection_bubble.gif b/bin/base/misc/default/objection_bubble.gif deleted file mode 100644 index 22c2169bb88fc02ccbd511acfee1cf93aec501fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43029 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 JN)Irk{|N;Qmqh>o diff --git a/bin/base/misc/default/takethat.opus b/bin/base/misc/default/takethat.opus deleted file mode 100644 index 2f143e12261ab087ba290fe602bf7ab79bb49336..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15238 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 Dji+NH diff --git a/bin/base/misc/default/takethat_bubble.gif b/bin/base/misc/default/takethat_bubble.gif deleted file mode 100644 index 795552a8e1b874f3065bdea4e62a635e418973ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40634 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 diff --git a/bin/base/themes b/bin/base/themes deleted file mode 160000 index 32a130d1a..000000000 --- a/bin/base/themes +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 32a130d1a35220b27deecf22f59c83e92cac1fee diff --git a/src/animationlayer.cpp b/src/animationlayer.cpp index a21e20c43..708aef26c 100644 --- a/src/animationlayer.cpp +++ b/src/animationlayer.cpp @@ -143,6 +143,11 @@ void AnimationLayer::jumpToFrame(int number) } } +bool AnimationLayer::isPlayOnce() +{ + return m_play_once; +} + void AnimationLayer::setPlayOnce(bool enabled) { m_play_once = enabled; @@ -324,6 +329,7 @@ void AnimationLayer::frameTicker() return; } + stopPlayback(); return; } @@ -595,11 +601,20 @@ void BackgroundAnimationLayer::loadAndPlayAnimation(QString fileName) } #endif - setFileName(file_path); + bool is_different_file = file_path != this->fileName(); + if (is_different_file) + { + setFileName(file_path); + } + VPath design_path = ao_app->get_background_path("design.ini"); setTransformationMode(ao_app->get_scaling(ao_app->read_design_ini("scaling", design_path))); setStretchToFit(ao_app->read_design_ini("stretch", design_path).startsWith("true")); - startPlayback(); + + if (is_different_file) + { + startPlayback(); + } } SplashAnimationLayer::SplashAnimationLayer(AOApplication *ao_app, QWidget *parent) @@ -640,7 +655,7 @@ void EffectAnimationLayer::setHideWhenStopped(bool enabled) void EffectAnimationLayer::maybeHide() { - if (m_hide_when_stopped) + if (m_hide_when_stopped && isPlayOnce()) { hide(); } diff --git a/src/animationlayer.h b/src/animationlayer.h index c67f70268..820703d60 100644 --- a/src/animationlayer.h +++ b/src/animationlayer.h @@ -62,6 +62,8 @@ class AnimationLayer : public QLabel int currentFrameNumber(); void jumpToFrame(int number); + bool isPlayOnce(); + void setPlayOnce(bool enabled); void setStretchToFit(bool enabled); void setResetCacheWhenStopped(bool enabled); diff --git a/src/courtroom.cpp b/src/courtroom.cpp index 134679766..4540c270f 100644 --- a/src/courtroom.cpp +++ b/src/courtroom.cpp @@ -3157,6 +3157,8 @@ void Courtroom::do_effect(QString fx_path, QString fx_sound, QString p_char, QSt QString effect = ao_app->get_effect(fx_path, p_char, p_folder); if (effect == "") { + ui_vp_effect->stopPlayback(); + ui_vp_effect->hide(); return; } @@ -3173,8 +3175,6 @@ void Courtroom::do_effect(QString fx_path, QString fx_sound, QString p_char, QSt ui_vp_effect->setTransformationMode(ao_app->get_scaling(ao_app->get_effect_property(fx_path, p_char, p_folder, "scaling"))); ui_vp_effect->setStretchToFit(ao_app->get_effect_property(fx_path, p_char, p_folder, "stretch").startsWith("true")); ui_vp_effect->setFlipped(ao_app->get_effect_property(fx_path, p_char, p_folder, "respect_flip").startsWith("true") && m_chatmessage[FLIP].toInt() == 1); - ui_vp_effect->setPlayOnce(false); // The effects themselves dictate whether or not they're looping. - // Static effects will linger. bool looping = ao_app->get_effect_property(fx_path, p_char, p_folder, "loop").startsWith("true"); From eb024cb93131cddba8ec1a6094abde8bf1f4eaf3 Mon Sep 17 00:00:00 2001 From: TrickyLeifa Date: Thu, 23 May 2024 16:10:45 +0200 Subject: [PATCH 16/16] Renamed pos_center to rect from Background/design.ini --- src/courtroom.cpp | 4 ++-- src/path_functions.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/courtroom.cpp b/src/courtroom.cpp index 4540c270f..7ea7d284a 100644 --- a/src/courtroom.cpp +++ b/src/courtroom.cpp @@ -1413,7 +1413,7 @@ void Courtroom::set_background(QString p_background, bool display) const QStringList overrides = {"def", "wit", "pro"}; for (const QString &override_pos : overrides) { - if (!ao_app->read_design_ini("court:" + override_pos + "/pos_center", ao_app->get_background_path("design.ini")).isEmpty()) + if (!ao_app->read_design_ini("court:" + override_pos + "/rect", ao_app->get_background_path("design.ini")).isEmpty()) { pos_list.append(override_pos); } @@ -3155,7 +3155,7 @@ void Courtroom::do_effect(QString fx_path, QString fx_sound, QString p_char, QSt return; } QString effect = ao_app->get_effect(fx_path, p_char, p_folder); - if (effect == "") + if (effect.isEmpty()) { ui_vp_effect->stopPlayback(); ui_vp_effect->hide(); diff --git a/src/path_functions.cpp b/src/path_functions.cpp index 4d22e9b96..4a23ac726 100644 --- a/src/path_functions.cpp +++ b/src/path_functions.cpp @@ -100,7 +100,7 @@ QPair AOApplication::get_pos_path(const QString &pos, const bool QRect f_rect; if (f_pos_split.size() > 1) { // Subposition, get center info - QStringList arglist = read_design_ini(f_pos + "/pos_center", get_background_path("design.ini")).split(","); + QStringList arglist = read_design_ini(f_pos + "/rect", get_background_path("design.ini")).split(","); if (arglist.size() == 4) { f_rect = QRect(arglist[0].toInt(), arglist[1].toInt(), arglist[2].toInt(), arglist[3].toInt());