atrooney-online-2/src/animationlayer.cpp
TrickyLeifa 29284c0b21 Removed theme submodule, ...
* Removed theme submodule.
  * This should be handled by the CI.
* Fixed effects not disappearing when they should (cull / repeat)
2024-05-23 00:02:07 +02:00

703 lines
16 KiB
C++

#include "animationlayer.h"
#include "aoapplication.h"
#include "options.h"
#include <QRectF>
#include <QThreadPool>
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<VPath> path_list;
QVector<QString> 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<EffectType> 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