diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index d056f05..0000000 --- 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/CMakeLists.txt b/CMakeLists.txt index cbd32fa..b1f2529 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/bin/base/misc/default/chatbox.png b/bin/base/misc/default/chatbox.png deleted file mode 100644 index 0c3bae1..0000000 Binary files a/bin/base/misc/default/chatbox.png and /dev/null differ diff --git a/bin/base/misc/default/holdit.opus b/bin/base/misc/default/holdit.opus deleted file mode 100644 index a454be8..0000000 Binary files a/bin/base/misc/default/holdit.opus and /dev/null differ diff --git a/bin/base/misc/default/holdit_bubble.gif b/bin/base/misc/default/holdit_bubble.gif deleted file mode 100644 index 536f8cd..0000000 Binary files a/bin/base/misc/default/holdit_bubble.gif and /dev/null differ diff --git a/bin/base/misc/default/objection.opus b/bin/base/misc/default/objection.opus deleted file mode 100644 index 7e15d05..0000000 Binary files a/bin/base/misc/default/objection.opus and /dev/null differ diff --git a/bin/base/misc/default/objection_bubble.gif b/bin/base/misc/default/objection_bubble.gif deleted file mode 100644 index 22c2169..0000000 Binary files a/bin/base/misc/default/objection_bubble.gif and /dev/null differ diff --git a/bin/base/misc/default/takethat.opus b/bin/base/misc/default/takethat.opus deleted file mode 100644 index 2f143e1..0000000 Binary files a/bin/base/misc/default/takethat.opus and /dev/null differ diff --git a/bin/base/misc/default/takethat_bubble.gif b/bin/base/misc/default/takethat_bubble.gif deleted file mode 100644 index 795552a..0000000 Binary files a/bin/base/misc/default/takethat_bubble.gif and /dev/null differ diff --git a/bin/base/themes b/bin/base/themes deleted file mode 160000 index 32a130d..0000000 --- a/bin/base/themes +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 32a130d1a35220b27deecf22f59c83e92cac1fee diff --git a/data/ui/options_dialog.ui b/data/ui/options_dialog.ui index db6db72..19100cd 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: + + + + + + + + + + diff --git a/src/animationlayer.cpp b/src/animationlayer.cpp new file mode 100644 index 0000000..708aef2 --- /dev/null +++ b/src/animationlayer.cpp @@ -0,0 +1,702 @@ +#include "animationlayer.h" + +#include "aoapplication.h" +#include "options.h" + +#include +#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); + m_ticker->setTimerType(Qt::PreciseTimer); + + 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; + } + + bool is_processing = m_processing; + if (m_ticker->isActive()) + { + m_ticker->stop(); + } + m_target_frame_number = number; + if (is_processing) + { + frameTicker(); + } +} + +bool AnimationLayer::isPlayOnce() +{ + return m_play_once; +} + +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_hint = mode; +} + +void AnimationLayer::setMinimumDurationPerFrame(int duration) +{ + m_minimum_duration = duration; +} + +void AnimationLayer::setMaximumDurationPerFrame(int duration) +{ + m_maximum_duration = duration; +} + +void AnimationLayer::setMaskingRect(QRect rect) +{ + 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()); + + if (m_transformation_mode_hint == Qt::FastTransformation) + { + m_transformation_mode = scale < 1.0 ? Qt::SmoothTransformation : Qt::FastTransformation; + } + } + + displayCurrentFrame(); +} + +void AnimationLayer::finishPlayback() +{ + stopPlayback(); + Q_EMIT finishedPlayback(); +} + +void AnimationLayer::prepareNextTick() +{ + 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); +} + +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; + } + + stopPlayback(); + 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; + 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); + ++m_frame_number; + + if (!m_pause) + { + prepareNextTick(); + } +} + +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_resolved_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; + 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"); + } + + 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")); + 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_resolved_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 + + 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")); + + if (is_different_file) + { + 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 && isPlayOnce()) + { + 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 0000000..820703d --- /dev/null +++ b/src/animationlayer.h @@ -0,0 +1,266 @@ +#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); + + bool isPlayOnce(); + + 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_hint = Qt::FastTransformation; + 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_target_frame_number = -1; + 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; + QString m_resolved_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 0000000..5f57cbe --- /dev/null +++ b/src/animationloader.cpp @@ -0,0 +1,105 @@ +#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) + { +#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); + } + + 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 0000000..a3a1036 --- /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 a3ed6b9..f3260ed 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 c2a0191..1f2b1ed 100644 --- a/src/aoapplication.h +++ b/src/aoapplication.h @@ -120,16 +120,19 @@ public: 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()); 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 = {""}); + QString find_image(QStringList p_list); + ////// Functions for reading and writing files ////// // Implementations file_functions.cpp @@ -250,6 +253,12 @@ public: // 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/aoemotepreview.cpp b/src/aoemotepreview.cpp index e427cb8..a5a6a27 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 0cf88c9..a83a0d9 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 @@ private: 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 489699e..27afdb6 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 b23bc0f..a259073 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 @@ private: 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 8284230..0000000 --- a/src/aolayer.cpp +++ /dev/null @@ -1,716 +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()) - { - 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) -{ - 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 (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 - } -} - -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 - } -} - -void BackgroundLayer::load_image(QString p_filename) -{ - 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) - { - // 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)) || 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; - 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 0a44d8f..0000000 --- a/src/aolayer.h +++ /dev/null @@ -1,267 +0,0 @@ -#pragma once - -#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); - - // 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); - - // 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); - -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; - - int frame = 0; - int max_frames = 0; - int last_max_frames = 0; - - int speed = 100; - - bool m_flipped = false; - - 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); - // Center the QLabel in the viewport based on the dimensions of f_pixmap - void center_pixmap(QPixmap f_pixmap); - -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); -}; - -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/aosfxplayer.cpp b/src/aosfxplayer.cpp index 0775eb9..ac3b851 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 89c5c75..7ea7d28 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,22 +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_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_desk = new BackgroundLayer(ao_app, ui_viewport); + 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 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_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_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"); @@ -70,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); @@ -91,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"); @@ -151,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"); @@ -315,6 +316,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(); @@ -406,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); @@ -487,10 +493,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 +514,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::post_transition_cleanup); + set_widgets(); set_char_select(); @@ -747,24 +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(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()); @@ -773,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(); @@ -867,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++) { @@ -881,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"); @@ -1077,6 +1091,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 +1165,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"); @@ -1365,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 @@ -1390,9 +1408,21 @@ 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 + "/rect", 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(",")) { - if (file_exists(ao_app->get_image_suffix(ao_app->get_background_path(pos)))) + QString real_pos = pos.split(":")[0]; + if ((file_exists(ao_app->get_image_suffix(ao_app->get_background_path(real_pos))))) { pos_list.append(pos); } @@ -1403,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(); @@ -1428,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; @@ -1489,7 +1518,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 +1666,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(); } @@ -1693,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()); } @@ -1857,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")); @@ -2272,7 +2304,12 @@ 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)); } @@ -2310,6 +2347,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) @@ -2429,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 @@ -2458,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. @@ -2655,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) { @@ -2687,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] != "") @@ -2703,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 @@ -2713,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 @@ -2726,17 +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); - // Move the character on the viewport according to the offsets - set_self_offset(m_chatmessage[SELF_OFFSET]); + ui_vp_player_char->setFlipped(m_chatmessage[FLIP].toInt() == 1); } void Courtroom::display_pair_character(QString other_charid, QString other_offset) @@ -2791,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(); } } } @@ -2870,32 +2911,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 +2972,170 @@ void Courtroom::do_screenshake() screenshake_animation_group->start(); } +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 = oldPosId; + QString t_new_pos = newPosId; + if (file_exists(ao_app->get_image_suffix(ao_app->get_background_path("court")))) + { + if (legacy_pos.contains(oldPosId)) + { + t_old_pos = "court:" + oldPosId; + } + if (legacy_pos.contains(newPosId)) + { + 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); + + int duration = ao_app->get_pos_transition_duration(t_old_pos, t_new_pos); + + // conditions to stop slide + 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"; +#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(), oldPosId); + + 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); + + 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->setDuration(duration); + 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); + } + + 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) + { + offset.setY(viewport_height * offset_data.at(1).toInt() * 0.01); + } + + layer->setParent(ui_vp_background); + layer->setPlayOnce(false); + layer->pausePlayback(true); + layer->startPlayback(); + layer->move(newPos); + layer->show(); + }; + + 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]); + + 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_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])) + { + ui_vp_player_char->stackUnder(ui_vp_sideplayer_char); + } + else + { + ui_vp_sideplayer_char->stackUnder(ui_vp_player_char); + } + } + + 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]); + + if (is_pairing(m_previous_chatmessage)) + { + 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); + } + } + + transition_animation_group->start(); +} + +void Courtroom::post_transition_cleanup() +{ + transition_animation_group->clear(); + + for (kal::CharacterAnimationLayer *layer : qAsConst(ui_vp_char_list)) + { + bool is_visible = layer->isVisible(); + layer->stopPlayback(); + layer->pausePlayback(false); + layer->setParent(ui_viewport); + layer->stackUnder(ui_vp_desk); + 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 + 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; + + // 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]); + } + + // Parse the emote_mod part of the chat message + handle_emote_mod(emote_mod, immediate); +} + void Courtroom::do_flash() { if (!Options::getInstance().effectsEnabled()) @@ -2975,8 +3155,10 @@ 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(); return; } @@ -2990,11 +3172,9 @@ 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); bool looping = ao_app->get_effect_property(fx_path, p_char, p_folder, "loop").startsWith("true"); @@ -3056,10 +3236,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) @@ -3171,7 +3350,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; @@ -3222,7 +3401,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()); } } @@ -3240,7 +3419,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"; } @@ -3251,8 +3430,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.) @@ -3261,23 +3440,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 @@ -3812,15 +3990,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: @@ -3878,7 +4057,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: @@ -3887,7 +4066,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: @@ -3941,12 +4120,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. @@ -3963,7 +4142,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") @@ -4013,8 +4192,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()) { @@ -4026,20 +4203,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 @@ -4053,8 +4231,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) { @@ -4290,19 +4468,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 @@ -4331,9 +4507,21 @@ 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->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(); @@ -4344,7 +4532,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, kal::AnimationLayer *p_layer) { QStringList self_offsets = p_list.split("&"); int self_offset = self_offsets[0].toInt(); @@ -4357,7 +4545,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(ui_viewport->width() * self_offset / 100, ui_viewport->height() * self_offset_v / 100); } void Courtroom::set_ip_list(QString p_list) @@ -4381,7 +4569,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()); @@ -4513,15 +4701,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); @@ -4530,7 +4717,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") @@ -4541,12 +4728,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") { @@ -4558,13 +4744,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 @@ -4575,8 +4761,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) @@ -5005,7 +5191,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 +5238,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 +5376,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 +5531,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 +5887,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 +5907,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 +5927,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 +5947,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 +6000,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 +6016,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 +6219,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 +6237,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 +6270,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 +6282,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 +6294,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 +6306,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 +6393,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,16 +6401,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() { if (ui_additive->isChecked()) @@ -6234,10 +6410,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 +6421,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 0f49839..a7ca235 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" @@ -150,8 +150,9 @@ public: // 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, kal::AnimationLayer *p_layer); // takes in serverD-formatted IP list as prints a converted version to server // OOC admittedly poorly named @@ -215,6 +216,9 @@ public: // 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 oldPosId, QString new_pos); + // Display the character. void display_character(); @@ -308,6 +312,8 @@ private: 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,10 +442,16 @@ private: // 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]; + QString m_previous_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; // char id, muted or not @@ -570,6 +582,11 @@ private: 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; @@ -591,20 +608,23 @@ private: AOImage *ui_background; QWidget *ui_viewport; - BackgroundLayer *ui_vp_background; - SplashLayer *ui_vp_speedlines; - CharLayer *ui_vp_player_char; - CharLayer *ui_vp_sideplayer_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; @@ -616,9 +636,9 @@ private: 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]; @@ -693,6 +713,8 @@ private: QCheckBox *ui_immediate; QCheckBox *ui_showname_enable; + QCheckBox *ui_slide_enable; + AOButton *ui_custom_objection; QMenu *custom_obj_menu; AOButton *ui_realization; @@ -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(); @@ -958,6 +978,10 @@ 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 + // 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 9463a47..ac9c646 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/emotes.cpp b/src/emotes.cpp index 4023b52..c120d59 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/networkmanager.cpp b/src/networkmanager.cpp index 69ff839..b574b63 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); } diff --git a/src/options.cpp b/src/options.cpp index 8006076..d06c5e4 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 088e34b..1ea7621 100644 --- a/src/options.h +++ b/src/options.h @@ -95,6 +95,11 @@ public: 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 d68535c..4a23ac7 100644 --- a/src/path_functions.cpp +++ b/src/path_functions.cpp @@ -78,9 +78,38 @@ 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(":"); + + QRect f_rect; + if (f_pos_split.size() > 1) + { // Subposition, get center info + 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()); + if (!f_rect.isValid()) + { + f_rect = QRect(); + } + } + } QString f_background; QString f_desk_image; if (file_exists(get_image_suffix(get_background_path("witnessempty")))) @@ -130,10 +159,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")); @@ -141,11 +170,12 @@ QString AOApplication::get_pos_path(const QString &pos, const bool desk) { f_desk_image = desk_override; } + if (desk) { - return f_desk_image; + return {f_desk_image, f_rect}; } - return f_background; + return {f_background, f_rect}; } VPath AOApplication::get_evidence_path(QString p_file) @@ -209,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) diff --git a/src/text_file_functions.cpp b/src/text_file_functions.cpp index e7c4511..418de47 100644 --- a/src/text_file_functions.cpp +++ b/src/text_file_functions.cpp @@ -912,3 +912,24 @@ 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 beb7e55..a0b7d7c 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 bccec58..b16be33 100644 --- a/src/widgets/aooptionsdialog.h +++ b/src/widgets/aooptionsdialog.h @@ -47,6 +47,7 @@ private: 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;