Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,24 @@ target_include_directories(ChaosMod PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/src/Effects
${CMAKE_CURRENT_SOURCE_DIR}/src/Unlockers
${CMAKE_CURRENT_SOURCE_DIR}/src/Voting
)

# Set UTF-8 flag.
set_target_properties(ChaosMod PROPERTIES COMPILE_FLAGS "/utf-8")

# Add SDK and DX headers dependencies.
find_package(directx-headers CONFIG REQUIRED)
find_package(ixwebsocket CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)

target_link_libraries(ChaosMod
ZHMModSDK
Microsoft::DirectX-Guids
Microsoft::DirectX-Headers
ixwebsocket::ixwebsocket
nlohmann_json::nlohmann_json
bcrypt
)

install(TARGETS ChaosMod
Expand Down
49 changes: 46 additions & 3 deletions src/ChaosMod.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,17 @@ void ChaosMod::Init()
p_pEffect->OnModInitialized();
}
);

for (auto& s_pVotingIntegation : EffectRegistry::GetInstance().GetVotingIntegrations())
{
if (s_pVotingIntegation)
{
Logger::Debug(TAG "Initializing Voting Integration '{}'", s_pVotingIntegation->GetName());
s_pVotingIntegation->Initialize();
}
}

m_pVotingIntegration = GetDefaultVotingIntegration();
}

void ChaosMod::OnEngineInitialized()
Expand Down Expand Up @@ -145,8 +156,12 @@ void ChaosMod::OnLoadOrClearScene()
UpdateEffectTimerEnabled();
m_EffectTimer.Reset();

m_aCurrentVote.clear();
m_aActiveEffects.clear();

if (auto* s_pVoting = GetCurrentVotingIntegration())
{
s_pVoting->EndVote();
}
}

void ChaosMod::UpdateEffectTimerEnabled()
Expand All @@ -156,7 +171,6 @@ void ChaosMod::UpdateEffectTimerEnabled()
if (s_bEnable)
{
// on enable, prepare first vote
m_aCurrentVote.clear();
OnEffectTimerTrigger();
}
else
Expand All @@ -171,12 +185,41 @@ void ChaosMod::UpdateEffectTimerEnabled()
}

m_aActiveEffects.clear();
m_aCurrentVote.clear();

if (auto* s_pVoting = GetCurrentVotingIntegration())
{
s_pVoting->EndVote();
}
}

m_EffectTimer.m_bEnable = s_bEnable;
}

IVotingIntegration* ChaosMod::GetCurrentVotingIntegration()
{
if (!m_pVotingIntegration)
{
Logger::Warn(TAG "No voting integration selected, falling back to default.");
m_pVotingIntegration = GetDefaultVotingIntegration();
}

return m_pVotingIntegration;
}

IVotingIntegration* ChaosMod::GetDefaultVotingIntegration()
{
for (const auto& s_pIntegration : EffectRegistry::GetInstance().GetVotingIntegrations())
{
if (s_pIntegration->GetName() == "ZOfflineVoting")
{
return s_pIntegration.get();
}
}

std::runtime_error("Failed to find default voting integration!");
return nullptr;
}

DEFINE_PLUGIN_DETOUR(ChaosMod, void, OnLoadScene, ZEntitySceneContext* th, SSceneInitParameters&)
{
OnLoadOrClearScene();
Expand Down
7 changes: 6 additions & 1 deletion src/ChaosMod.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@

#include "Helpers/ZTimer.h"
#include "IChaosEffect.h"
#include "IVotingIntegration.h"

#include <vector>
#include <queue>
#include <functional>
#include <memory>

class ChaosMod : public IPluginInterface
{
Expand Down Expand Up @@ -72,9 +74,12 @@ class ChaosMod : public IPluginInterface
float32 m_fFullEffectDuration;
int m_nVoteOptions;

std::vector <IChaosEffect*> m_aCurrentVote;
IVotingIntegration* m_pVotingIntegration = nullptr;
std::vector<SActiveEffect> m_aActiveEffects;

IVotingIntegration* GetCurrentVotingIntegration();
IVotingIntegration* GetDefaultVotingIntegration();

void UpdateEffectTimerEnabled();
void OnEffectTimerTrigger();
void ActivateEffect(IChaosEffect* p_pEffect);
Expand Down
21 changes: 12 additions & 9 deletions src/ChaosModSelection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,18 @@

void ChaosMod::OnEffectTimerTrigger()
{
// select random from current vote
// if no vote, only prepare the next one
if (!m_aCurrentVote.empty())
auto* s_pVotingIntegration = GetCurrentVotingIntegration();
if (!s_pVotingIntegration)
{
// TODO: if social voting was to be implemented, votes processing would go here
auto s_pSelectedEffect = Math::SelectRandomElement(m_aCurrentVote);
// something went very wrong...
Logger::Error(TAG "ChaosMod::OnEffectTimerTrigger called but no voting integration is set!");
return;
}

// select random from current vote
// if no vote, skip and prepare next vote
if (auto s_pSelectedEffect = s_pVotingIntegration->EndVote())
{
// fallback to randomly selected effect if selected is unavailable
// no compatibility check here, as there should only be compatible effects in the vote
// this should be fairly rare
Expand All @@ -33,12 +38,10 @@ void ChaosMod::OnEffectTimerTrigger()
}

ActivateEffect(s_pSelectedEffect);

m_aCurrentVote.clear();
}

// prepare next vote
m_aCurrentVote = GetRandomEffectSelection(m_nVoteOptions);
// start next vote
s_pVotingIntegration->StartVote(GetRandomEffectSelection(m_nVoteOptions));
}

void ChaosMod::ActivateEffect(IChaosEffect* p_pEffect)
Expand Down
53 changes: 47 additions & 6 deletions src/ChaosModUI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ void ChaosMod::InitAuthorNames()

// core authors
s_AuthorNames.insert("shadow578");
s_AuthorNames.insert("OrfeasZ");

// gather effect authors
for (const auto& s_Effect : EffectRegistry::GetInstance().GetEffects())
Expand Down Expand Up @@ -95,7 +96,7 @@ void ChaosMod::DrawMainUI(const bool p_bHasFocus)
}

// start at a sensible size
ImGui::SetNextWindowSize({ 400.0f, 350.0f }, ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize({ 450.0f, 500.0f }, ImGuiCond_FirstUseEver);

ImGui::PushFont(SDK()->GetImGuiBlackFont());
const auto s_ConfigShowing = ImGui::Begin(ICON_MD_QUESTION_MARK "CHAOS MOD CONFIGURATION", &m_bMenuActive);
Expand Down Expand Up @@ -145,6 +146,43 @@ void ChaosMod::DrawConfigurationContents()
5.0,
120.0
);

ImGui::SeparatorText("Voting");

auto* s_pVoting = GetCurrentVotingIntegration();

ImGui::TextUnformatted("Voting Mode");
ImGui::SameLine();
if (ImGui::BeginCombo("##Voting Mode", s_pVoting ? s_pVoting->GetDisplayName().c_str() : ""))
{
for (auto& s_pOption : EffectRegistry::GetInstance().GetVotingIntegrations())
{
if (ImGui::Selectable(
s_pOption->GetDisplayName().c_str(),
s_pVoting == s_pOption.get()
))
{
if (s_pVoting)
{
s_pVoting->Deactivate();
}

s_pVoting = s_pOption.get();

m_pVotingIntegration = s_pVoting;
m_pVotingIntegration->Activate();

Logger::Debug(TAG "Selected voting option {}", s_pVoting->GetName());
}
}

ImGui::EndCombo();
}

if (s_pVoting)
{
s_pVoting->DrawConfigUI();
}
}

void ChaosMod::DrawUnlockersContents()
Expand Down Expand Up @@ -226,12 +264,9 @@ void ChaosMod::DrawOverlayContents()
s_fRemainingToNext / m_EffectTimer.m_fIntervalSeconds,
fmt::format("Next Effect in {:.0f} Seconds", s_fRemainingToNext).c_str()
);

ImGui::SeparatorText("Current Vote");
for (const auto& s_Effect : m_aCurrentVote)
{
ImGui::BulletText(s_Effect->GetDisplayName(true).c_str());
}
GetCurrentVotingIntegration()->DrawOverlayUI();

ImGui::SeparatorText("Active Effects");
for (const auto& s_ActiveEffect : m_aActiveEffects)
Expand Down Expand Up @@ -293,6 +328,12 @@ void ChaosMod::DrawDebugUI(const bool p_bHasFocus)
m_EffectTimer.m_bEnable ? "True" : "False"
).c_str());

auto* s_pVoting = GetCurrentVotingIntegration();
ImGui::TextUnformatted(fmt::format(
"Using Voting Integration: {}",
s_pVoting ? s_pVoting->GetName() : "<null>"
).c_str());

ImGui::Checkbox("Menu Always Visible", &m_bDebugMenuAlwaysVisible);

ImGui::Separator();
Expand Down
42 changes: 41 additions & 1 deletion src/EffectRegistry.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

#include "IChaosEffect.h"
#include "IUnlocker.h"
#include "Logging.h"
#include "IVotingIntegration.h"

#include <Logging.h>


class EffectRegistry
Expand All @@ -14,6 +16,7 @@ class EffectRegistry
EffectRegistry() = default;
std::vector<std::unique_ptr<IChaosEffect>> m_aEffects;
std::vector<std::unique_ptr<IUnlocker>> m_aUnlockers;
std::vector<std::unique_ptr<IVotingIntegration>> m_aVotingIntegrations;

public:
static EffectRegistry& GetInstance()
Expand All @@ -35,6 +38,12 @@ class EffectRegistry
m_aUnlockers.push_back(std::move(p_Unlocker));
}

void RegisterVotingIntegration(std::unique_ptr<IVotingIntegration> p_Integration)
{
Logger::Debug("[EffectRegistry] Registered voting integration '{}'", p_Integration->GetName());
m_aVotingIntegrations.push_back(std::move(p_Integration));
}

const std::vector<std::unique_ptr<IChaosEffect>>& GetEffects() const
{
return m_aEffects;
Expand All @@ -45,6 +54,11 @@ class EffectRegistry
return m_aUnlockers;
}

const std::vector<std::unique_ptr<IVotingIntegration>>& GetVotingIntegrations() const
{
return m_aVotingIntegrations;
}

void Sort()
{
std::sort(
Expand All @@ -64,6 +78,15 @@ class EffectRegistry
return a->GetName() < b->GetName();
}
);

std::sort(
m_aVotingIntegrations.begin(),
m_aVotingIntegrations.end(),
[](const std::unique_ptr<IVotingIntegration>& a, const std::unique_ptr<IVotingIntegration>& b)
{
return a->GetName() < b->GetName();
}
);
}
};

Expand All @@ -83,6 +106,14 @@ struct UnlockerRegistrar
}
};

struct VotingIntegrationRegistrar
{
explicit VotingIntegrationRegistrar(std::unique_ptr<IVotingIntegration> p_Integration)
{
EffectRegistry::GetInstance().RegisterVotingIntegration(std::move(p_Integration));
}
};

/**
* Register an effect class with the EffectRegistry.
* Effecs should be registered in the global scope of the Effect's .cpp file.
Expand All @@ -108,3 +139,12 @@ struct UnlockerRegistrar
static UnlockerRegistrar g_UnlockerRegistrar_##UNLOCKER_CLASS( \
std::make_unique<UNLOCKER_CLASS>() \
);

/**
* Register a voting integration class with the Registry.
* Voting integrations are registered similarly to effects, ... you get the idea.
*/
#define REGISTER_VOTING_INTEGRATION(INTEGRATION_CLASS) \
static VotingIntegrationRegistrar g_VotingIntegrationRegistrar_##INTEGRATION_CLASS( \
std::make_unique<INTEGRATION_CLASS>() \
);
19 changes: 15 additions & 4 deletions src/Helpers/ImGuiExtras.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ namespace ImGuiEx

/**
* Like ImGui::ProgressBar, but automatically resizes so text fits fully.
* Text is always left-aligned.
* @param p_fFraction Progress fraction. 0.0 - 1.0.
* @param p_sOverlayText Text to overlay in the progress bar.
* @param p_fTextPadding Horizontal padding of the overlay text.
Expand All @@ -37,13 +38,23 @@ namespace ImGuiEx
const char* p_sOverlayText,
const float p_fTextPadding = 10.0f)
{
const auto s_vTextSize = ImGui::CalcTextSize(p_sOverlayText);
const auto s_vTextSize = ImGui::CalcTextSize(p_sOverlayText);
const auto s_fTextWidth = s_vTextSize.x + (p_fTextPadding * 2.0f);
const auto s_fWindowWidth = ImGui::GetContentRegionAvail().x;
const auto s_fBarWidth = max(s_fWindowWidth, s_fTextWidth);

ImGui::ProgressBar(
p_fFraction,
ImVec2(max(s_fWindowWidth, s_fTextWidth), 0.0f),
// Draw progress bar without overlay text
ImGui::ProgressBar(p_fFraction, ImVec2(s_fBarWidth, 0.0f), "");

// Draw left-aligned text on top of the progress bar
const auto s_vBarMin = ImGui::GetItemRectMin();
const auto s_vBarMax = ImGui::GetItemRectMax();
const auto s_fTextX = s_vBarMin.x + p_fTextPadding;
const auto s_fTextY = s_vBarMin.y + (s_vBarMax.y - s_vBarMin.y - s_vTextSize.y) * 0.5f;

ImGui::GetWindowDrawList()->AddText(
ImVec2(s_fTextX, s_fTextY),
ImGui::GetColorU32(ImGuiCol_Text),
p_sOverlayText
);
}
Expand Down
Loading