Compare commits

..

8 commits

Author SHA1 Message Date
HJfod
792e61e2f5 Merge branch 'main' into settings 2024-08-31 23:07:48 +03:00
HJfod
6b8dd4a1ae add restart and config dir buttons to settings popup 2024-08-31 23:07:29 +03:00
HJfod
8a826065a1 add restart required because of settings to moditem and modpopup 2024-08-31 22:00:01 +03:00
HJfod
6eb079735f setting events & restart stuff 2024-08-31 21:36:53 +03:00
HJfod
659c168a14 add generic function helpers 2024-08-31 21:35:41 +03:00
SMJS
d0eb881ebc
Fixed a bug where only 1 word wrap variant can exist (#1058)
* Fixed a bug where only 1 word wrap variant can exist

* Made the delimiters a string view
2024-08-31 12:27:38 +02:00
Cvolton
444a8c198b
add disable/enabledrawarea to ccdrawnode 2024-08-29 23:04:01 +02:00
dankmeme01
a31c68f178 update tuliphook 2024-08-27 21:21:52 +02:00
22 changed files with 354 additions and 39 deletions

View file

@ -243,7 +243,7 @@ if (DEFINED GEODE_TULIPHOOK_REPO_PATH)
message(STATUS "Using ${GEODE_TULIPHOOK_REPO_PATH} for TulipHook")
add_subdirectory(${GEODE_TULIPHOOK_REPO_PATH} ${GEODE_TULIPHOOK_REPO_PATH}/build)
else()
CPMAddPackage("gh:geode-sdk/TulipHook#fbf79b4")
CPMAddPackage("gh:geode-sdk/TulipHook#c8a445c")
endif()
set(CMAKE_WARN_DEPRECATED ON CACHE BOOL "" FORCE)

View file

@ -99,6 +99,8 @@ public:
void drawPreciseCubicBezier(cocos2d::CCPoint const&, cocos2d::CCPoint const&, cocos2d::CCPoint const&, cocos2d::CCPoint const&, unsigned int, cocos2d::_ccColor4F const&);
bool drawLines(cocos2d::CCPoint*, unsigned int, float, cocos2d::_ccColor4F const&);
bool drawRect(cocos2d::CCPoint const&, cocos2d::CCPoint const&, cocos2d::_ccColor4F const&, float, cocos2d::_ccColor4F const&);
void disableDrawArea();
void enableDrawArea(cocos2d::CCRect& rect);
#else
/** draw a dot at a position, with a given radius and color */
void drawDot(const CCPoint &pos, float radius, const ccColor4F &color);

View file

@ -9,6 +9,10 @@ namespace geode {
class Impl;
std::unique_ptr<Impl> m_impl;
friend class ::geode::SettingV3;
void markRestartRequired();
public:
static ModSettingsManager* from(Mod* mod);
@ -42,5 +46,11 @@ namespace geode {
std::shared_ptr<SettingV3> get(std::string_view key);
std::shared_ptr<SettingValue> getLegacy(std::string_view key);
std::optional<Setting> getLegacyDefinition(std::string_view key);
/**
* Returns true if any setting with the `"restart-required"` attribute
* has been altered
*/
bool restartRequired() const;
};
}

View file

@ -4,18 +4,18 @@
#include "Loader.hpp"
#include "Setting.hpp"
#include "Mod.hpp"
#include "SettingV3.hpp"
#include <optional>
namespace geode {
struct GEODE_DLL SettingChangedEvent : public Event {
struct GEODE_DLL [[deprecated("Use SettingChangedEventV3 from SettingV3.hpp instead")]] SettingChangedEvent : public Event {
Mod* mod;
SettingValue* value;
SettingChangedEvent(Mod* mod, SettingValue* value);
};
class GEODE_DLL SettingChangedFilter : public EventFilter<SettingChangedEvent> {
class GEODE_DLL [[deprecated("Use SettingChangedEventV3 from SettingV3.hpp instead")]] SettingChangedFilter : public EventFilter<SettingChangedEvent> {
protected:
std::string m_modID;
std::optional<std::string> m_targetKey;
@ -40,7 +40,7 @@ namespace geode {
* Listen for built-in setting changes
*/
template<class T>
class GeodeSettingChangedFilter : public SettingChangedFilter {
class [[deprecated("Use SettingChangedEventV3 from SettingV3.hpp instead")]] GeodeSettingChangedFilter : public SettingChangedFilter {
public:
using Callback = void(T);
@ -60,21 +60,4 @@ namespace geode {
) : SettingChangedFilter(modID, settingID) {}
GeodeSettingChangedFilter(GeodeSettingChangedFilter const&) = default;
};
template <class T>
std::monostate listenForSettingChanges(
std::string const& settingKey, void (*callback)(T)
) {
(void)new EventListener(
callback, GeodeSettingChangedFilter<T>(getMod()->getID(), settingKey)
);
return std::monostate();
}
static std::monostate listenForAllSettingChanges(void (*callback)(SettingValue*)) {
(void)new EventListener(
callback, SettingChangedFilter(getMod()->getID(), std::nullopt)
);
return std::monostate();
}
}

View file

@ -8,6 +8,7 @@
#include "../utils/cocos.hpp"
// this unfortunately has to be included because of C++ templates
#include "../utils/JsonValidation.hpp"
#include "../utils/function.hpp"
// todo in v4: this can be removed as well as the friend decl in LegacyCustomSettingV3
class LegacyCustomSettingToV3Node;
@ -16,6 +17,8 @@ class ModSettingsPopup;
namespace geode {
class ModSettingsManager;
class SettingNodeV3;
// todo in v4: remove this
class SettingValue;
class GEODE_DLL SettingV3 : public std::enable_shared_from_this<SettingV3> {
private:
@ -27,6 +30,14 @@ namespace geode {
Result<> parseSharedProperties(std::string const& key, std::string const& modID, matjson::Value const& value, bool onlyNameAndDesc = false);
void parseSharedProperties(std::string const& key, std::string const& modID, JsonExpectedValue& value, bool onlyNameAndDesc = false);
/**
* Mark that the value of this setting has changed. This should be
* ALWAYS called on every setter that can modify the setting's state!
*/
void markChanged();
friend class ::geode::SettingValue;
public:
SettingV3();
virtual ~SettingV3();
@ -120,6 +131,7 @@ namespace geode {
}
void setValue(V value) {
this->getValueMut() = this->isValid(value) ? value : this->getDefaultValue();
this->markChanged();
}
virtual Result<> isValid(V value) const = 0;
@ -452,6 +464,38 @@ namespace geode {
std::shared_ptr<SettingV3> getSetting() const;
};
class GEODE_DLL SettingChangedEventV3 final : public Event {
private:
class Impl;
std::shared_ptr<Impl> m_impl;
public:
SettingChangedEventV3(std::shared_ptr<SettingV3> setting);
std::shared_ptr<SettingV3> getSetting() const;
};
class GEODE_DLL SettingChangedFilterV3 final : public EventFilter<SettingChangedEventV3> {
private:
class Impl;
std::shared_ptr<Impl> m_impl;
public:
using Callback = void(std::shared_ptr<SettingV3>);
ListenerResult handle(utils::MiniFunction<Callback> fn, SettingChangedEventV3* event);
/**
* Listen to changes on a setting, or all settings
* @param modID Mod whose settings to listen to
* @param settingKey Setting to listen to, or all settings if nullopt
*/
SettingChangedFilterV3(
std::string const& modID,
std::optional<std::string> const& settingKey
);
SettingChangedFilterV3(Mod* mod, std::optional<std::string> const& settingKey);
SettingChangedFilterV3(SettingChangedFilterV3 const&);
};
class GEODE_DLL SettingNodeSizeChangeEventV3 : public Event {
private:
class Impl;
@ -512,4 +556,25 @@ namespace geode {
struct SettingTypeForValueType<cocos2d::ccColor4B> {
using SettingType = Color4BSettingV3;
};
template <class T>
EventListener<SettingChangedFilterV3>* listenForSettingChanges(std::string_view settingKey, auto&& callback, Mod* mod = getMod()) {
using Ty = typename SettingTypeForValueType<T>::SettingType;
return new EventListener(
[callback = std::move(callback)](std::shared_ptr<SettingV3> setting) {
if (auto ty = typeinfo_pointer_cast<Ty>(setting)) {
callback(ty->getValue());
}
},
SettingChangedFilterV3(mod, std::string(settingKey))
);
}
EventListener<SettingChangedFilterV3>* listenForSettingChanges(std::string_view settingKey, auto&& callback, Mod* mod = getMod()) {
using T = std::remove_cvref_t<utils::function::Arg<0, decltype(callback)>>;
return listenForSettingChanges<T>(settingKey, std::move(callback), mod);
}
GEODE_DLL EventListener<SettingChangedFilterV3>* listenForAllSettingChanges(
std::function<void(std::shared_ptr<SettingV3>)> const& callback,
Mod* mod = getMod()
);
}

View file

@ -0,0 +1,59 @@
#pragma once
#include <tuple>
namespace geode::utils::function {
namespace detail {
template <class F>
struct ImplExtract;
template <class R, class... A>
struct ImplExtract<R(A...)> {
using Type = R(A...);
using Return = R;
using Args = std::tuple<A...>;
static constexpr std::size_t ARG_COUNT = std::tuple_size_v<Args>;
};
template <class R, class... A>
struct ImplExtract<R(*)(A...)> {
using Type = R(A...);
using Return = R;
using Args = std::tuple<A...>;
static constexpr std::size_t ARG_COUNT = std::tuple_size_v<Args>;
};
template <class R, class C, class... A>
struct ImplExtract<R(C::*)(A...)> {
using Type = R(A...);
using Class = C;
using Return = R;
using Args = std::tuple<A...>;
static constexpr std::size_t ARG_COUNT = std::tuple_size_v<Args>;
};
template <class R, class C, class... A>
struct ImplExtract<R(C::*)(A...) const> {
using Type = R(A...);
using Class = C;
using Return = R;
using Args = std::tuple<A...>;
static constexpr std::size_t ARG_COUNT = std::tuple_size_v<Args>;
};
template <class F>
requires requires { &F::operator(); }
struct ImplExtract<F> : public ImplExtract<decltype(&F::operator())> {};
template <class F>
using Extract = ImplExtract<std::remove_cvref_t<F>>;
}
template <class F>
using FunctionInfo = detail::Extract<F>;
template <class F>
using Return = typename detail::Extract<F>::Return;
template <class F>
using Args = typename detail::Extract<F>::Args;
template <std::size_t Ix, class F>
using Arg = std::tuple_element_t<Ix, typename detail::Extract<F>::Args>;
}

View file

@ -17,13 +17,22 @@
namespace geode {
using ByteVector = std::vector<uint8_t>;
// todo in v4: remove this
template <typename T>
[[deprecated("Use geode::toBytes instead")]]
ByteVector toByteArray(T const& a) {
ByteVector out;
out.resize(sizeof(T));
std::memcpy(out.data(), &a, sizeof(T));
return out;
}
template <typename T>
ByteVector toBytes(T const& a) {
ByteVector out;
out.resize(sizeof(T));
std::memcpy(out.data(), &a, sizeof(T));
return out;
}
namespace utils {
// helper for std::visit

View file

@ -78,6 +78,7 @@ public:
};
std::string modID;
std::unordered_map<std::string, SettingInfo> settings;
bool restartRequired = false;
void createSettings() {
for (auto& [key, setting] : settings) {
@ -103,6 +104,7 @@ public:
};
ModSettingsManager* ModSettingsManager::from(Mod* mod) {
if (!mod) return nullptr;
return ModImpl::getImpl(mod)->m_settings.get();
}
@ -136,6 +138,10 @@ ModSettingsManager::ModSettingsManager(ModMetadata const& metadata)
ModSettingsManager::~ModSettingsManager() {}
ModSettingsManager::ModSettingsManager(ModSettingsManager&&) = default;
void ModSettingsManager::markRestartRequired() {
m_impl->restartRequired = true;
}
Result<> ModSettingsManager::registerCustomSettingType(std::string_view type, SettingGenerator generator) {
GEODE_UNWRAP(SharedSettingTypesPool::get().add(m_impl->modID, type, generator));
m_impl->createSettings();
@ -230,3 +236,7 @@ std::optional<Setting> ModSettingsManager::getLegacyDefinition(std::string_view
}
return std::nullopt;
}
bool ModSettingsManager::restartRequired() const {
return m_impl->restartRequired;
}

View file

@ -280,10 +280,10 @@ std::string SettingValue::getModID() const {
}
void SettingValue::valueChanged() {
// this is actually p neat because now if the mod gets disabled this wont
// post the event so that side-effect is automatically handled :3
if (auto mod = Loader::get()->getLoadedMod(m_modID)) {
SettingChangedEvent(mod, this).post();
if (auto sett = mod->getSettingV3(m_key)) {
sett->markChanged();
}
}
}

View file

@ -125,7 +125,7 @@ void SettingNodeV3::updateState() {
m_impl->errorLabel->setString(desc->c_str());
}
}
if (m_impl->setting->requiresRestart() && (this->hasUncommittedChanges() || m_impl->committed)) {
if (m_impl->setting->requiresRestart() && m_impl->committed) {
m_impl->errorLabel->setVisible(true);
m_impl->errorLabel->setColor("mod-list-restart-required-label"_cc3b);
m_impl->errorLabel->setString("Restart Required");

View file

@ -1,4 +1,6 @@
#include <Geode/loader/SettingV3.hpp>
#include <Geode/loader/SettingEvent.hpp>
#include <Geode/loader/ModSettingsManager.hpp>
#include <Geode/utils/JsonValidation.hpp>
#include <regex>
#include "SettingNodeV3.hpp"
@ -397,6 +399,63 @@ namespace enable_if_parsing {
};
}
class SettingChangedEventV3::Impl final {
public:
std::shared_ptr<SettingV3> setting;
};
SettingChangedEventV3::SettingChangedEventV3(std::shared_ptr<SettingV3> setting)
: m_impl(std::make_shared<Impl>())
{
m_impl->setting = setting;
}
std::shared_ptr<SettingV3> SettingChangedEventV3::getSetting() const {
return m_impl->setting;
}
class SettingChangedFilterV3::Impl final {
public:
std::string modID;
std::optional<std::string> settingKey;
};
ListenerResult SettingChangedFilterV3::handle(utils::MiniFunction<Callback> fn, SettingChangedEventV3* event) {
if (
event->getSetting()->getModID() == m_impl->modID &&
!m_impl->settingKey || event->getSetting()->getKey() == m_impl->settingKey
) {
fn(event->getSetting());
}
return ListenerResult::Propagate;
}
SettingChangedFilterV3::SettingChangedFilterV3(
std::string const& modID,
std::optional<std::string> const& settingKey
) : m_impl(std::make_shared<Impl>())
{
m_impl->modID = modID;
m_impl->settingKey = settingKey;
}
SettingChangedFilterV3::SettingChangedFilterV3(Mod* mod, std::optional<std::string> const& settingKey)
: SettingChangedFilterV3(mod->getID(), settingKey) {}
SettingChangedFilterV3::SettingChangedFilterV3(SettingChangedFilterV3 const&) = default;
EventListener<SettingChangedFilterV3>* geode::listenForAllSettingChanges(
std::function<void(std::shared_ptr<SettingV3>)> const& callback,
Mod* mod
) {
return new EventListener(
[callback](std::shared_ptr<SettingV3> setting) {
callback(setting);
},
SettingChangedFilterV3(mod->getID(), std::nullopt)
);
}
class SettingV3::GeodeImpl {
public:
std::string modID;
@ -485,6 +544,19 @@ Mod* SettingV3::getMod() const {
return Loader::get()->getInstalledMod(m_impl->modID);
}
void SettingV3::markChanged() {
auto manager = ModSettingsManager::from(this->getMod());
if (m_impl->requiresRestart) {
manager->markRestartRequired();
}
SettingChangedEventV3(shared_from_this()).post();
if (manager) {
// Use ModSettingsManager rather than convertToLegacyValue since it
// caches the result and we want to have that for performance
SettingChangedEvent(this->getMod(), manager->getLegacy(this->getKey()).get()).post();
}
}
std::optional<Setting> SettingV3::convertToLegacy() const {
return std::nullopt;
}

View file

@ -90,6 +90,11 @@ bool ModsStatusNode::init() {
m_downloadListener.bind([this](auto) { this->updateState(); });
m_settingNodeListener.bind([this](SettingNodeValueChangeEventV3* ev) {
this->updateState();
return ListenerResult::Propagate;
});
this->updateState();
return true;

View file

@ -12,6 +12,7 @@
#include "sources/ModListSource.hpp"
#include "UpdateModListState.hpp"
#include <server/DownloadManager.hpp>
#include <Geode/loader/SettingV3.hpp>
using namespace geode::prelude;
@ -39,6 +40,7 @@ protected:
EventListener<UpdateModListStateFilter> m_updateStateListener;
EventListener<server::ModDownloadFilter> m_downloadListener;
DownloadState m_lastState = DownloadState::None;
EventListener<EventFilter<SettingNodeValueChangeEventV3>> m_settingNodeListener;
bool init();
void updateState();

View file

@ -283,6 +283,11 @@ bool ModItem::init(ModSource&& source) {
m_downloadListener.bind([this](auto) { this->updateState(); });
m_downloadListener.setFilter(server::ModDownloadFilter(m_source.getID()));
m_settingNodeListener.bind([this](SettingNodeValueChangeEventV3*) {
this->updateState();
return ListenerResult::Propagate;
});
return true;
}

View file

@ -35,6 +35,7 @@ protected:
EventListener<server::ServerRequest<std::optional<server::ServerModUpdate>>> m_checkUpdateListener;
EventListener<server::ModDownloadFilter> m_downloadListener;
std::optional<server::ServerModUpdate> m_availableUpdate;
EventListener<EventFilter<SettingNodeValueChangeEventV3>> m_settingNodeListener;
/**
* @warning Make sure `getMetadata` and `createModLogo` are callable

View file

@ -3,6 +3,7 @@
#include <Geode/ui/MDTextArea.hpp>
#include <Geode/utils/web.hpp>
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/ModSettingsManager.hpp>
#include <Geode/ui/GeodeUI.hpp>
#include <Geode/utils/ColorProvider.hpp>
#include "ConfirmUninstallPopup.hpp"
@ -557,6 +558,13 @@ bool ModPopup::setup(ModSource&& src) {
mainContainer->updateLayout();
m_mainLayer->addChildAtPosition(mainContainer, Anchor::Center);
m_settingsBG = CCScale9Sprite::create("square02b_001.png");
m_settingsBG->setColor({ 0, 0, 0 });
m_settingsBG->setOpacity(75);
m_settingsBG->setScale(.3f);
m_settingsBG->setContentSize(ccp(35, 30) / linksBG->getScale());
m_buttonMenu->addChildAtPosition(m_settingsBG, Anchor::BottomLeft, ccp(28, 25));
auto settingsSpr = createGeodeCircleButton(CCSprite::createWithSpriteFrameName("settings.png"_spr));
settingsSpr->setScale(.6f);
auto settingsBtn = CCMenuItemSpriteExtra::create(
@ -601,15 +609,30 @@ bool ModPopup::setup(ModSource&& src) {
m_downloadListener.bind([this](auto) { this->updateState(); });
m_downloadListener.setFilter(m_source.getID());
m_settingNodeListener.bind([this](SettingNodeValueChangeEventV3*) {
this->updateState();
return ListenerResult::Propagate;
});
return true;
}
void ModPopup::updateState() {
auto asMod = m_source.asMod();
auto wantsRestart = m_source.wantsRestart();
auto wantsRestartBecauseOfSettings = asMod && ModSettingsManager::from(asMod)->restartRequired();
m_installBG->setColor(wantsRestart ? to3B(ColorProvider::get()->color("mod-list-restart-required-label"_spr)) : ccc3(0, 0, 0));
m_installBG->setOpacity(wantsRestart ? 40 : 75);
m_installBG->setColor((wantsRestart && !wantsRestartBecauseOfSettings) ?
to3B(ColorProvider::get()->color("mod-list-restart-required-label"_spr)) :
ccBLACK
);
m_installBG->setOpacity((wantsRestart && !wantsRestartBecauseOfSettings) ? 40 : 75);
m_settingsBG->setColor(wantsRestartBecauseOfSettings ?
to3B(ColorProvider::get()->color("mod-list-restart-required-label"_spr)) :
ccBLACK
);
m_settingsBG->setOpacity(wantsRestartBecauseOfSettings ? 40 : 75);
m_settingsBG->setVisible(wantsRestartBecauseOfSettings);
m_restartRequiredLabel->setVisible(wantsRestart);
if (!wantsRestart && asMod) {

View file

@ -30,6 +30,7 @@ protected:
CCMenuItemSpriteExtra* m_cancelBtn;
CCLabelBMFont* m_installStatusLabel;
CCScale9Sprite* m_installBG;
CCScale9Sprite* m_settingsBG;
CCLabelBMFont* m_enabledStatusLabel;
ButtonSprite* m_restartRequiredLabel;
CCNode* m_rightColumn;
@ -40,6 +41,7 @@ protected:
EventListener<server::ServerRequest<std::optional<server::ServerModUpdate>>> m_checkUpdateListener;
EventListener<UpdateModListStateFilter> m_updateStateListener;
EventListener<server::ModDownloadFilter> m_downloadListener;
EventListener<EventFilter<SettingNodeValueChangeEventV3>> m_settingNodeListener;
bool setup(ModSource&& src) override;
void updateState();

View file

@ -2,6 +2,7 @@
#include <Geode/binding/ButtonSprite.hpp>
#include <Geode/loader/Mod.hpp>
#include <Geode/loader/Setting.hpp>
#include <Geode/loader/ModSettingsManager.hpp>
#include <Geode/ui/ScrollLayer.hpp>
#include <Geode/utils/cocos.hpp>
#include <Geode/ui/General.hpp>
@ -63,13 +64,26 @@ bool ModSettingsPopup::setup(Mod* mod) {
// buttons
m_applyMenu = CCMenu::create();
m_applyMenu->setContentWidth(150);
m_applyMenu->setLayout(RowLayout::create());
m_applyMenu->getLayout()->ignoreInvisibleChildren(true);
auto restartBtnSpr = createGeodeButton("Restart Now", true);
restartBtnSpr->setScale(.6f);
m_restartBtn = CCMenuItemSpriteExtra::create(
restartBtnSpr, this, menu_selector(ModSettingsPopup::onRestart)
);
m_applyMenu->addChildAtPosition(m_restartBtn, Anchor::Bottom, ccp(0, 20));
m_applyBtnSpr = createGeodeButton("Apply", true);
m_applyBtnSpr->setScale(.6f);
m_applyBtn = CCMenuItemSpriteExtra::create(
m_applyBtnSpr, this, menu_selector(ModSettingsPopup::onApply)
);
m_buttonMenu->addChildAtPosition(m_applyBtn, Anchor::Bottom, ccp(0, 20));
m_applyMenu->addChildAtPosition(m_applyBtn, Anchor::Bottom, ccp(0, 20));
m_mainLayer->addChildAtPosition(m_applyMenu, Anchor::Bottom, ccp(0, 20));
auto resetBtnSpr = createGeodeButton("Reset All", true);
resetBtnSpr->setScale(.6f);
@ -79,13 +93,28 @@ bool ModSettingsPopup::setup(Mod* mod) {
);
m_buttonMenu->addChildAtPosition(resetBtn, Anchor::BottomLeft, ccp(45, 20));
auto openDirBtnSpr = createGeodeButton("Open Folder", true);
openDirBtnSpr->setScale(.6f);
auto configFolderSpr = CCSprite::createWithSpriteFrameName("folderIcon_001.png");
m_openConfigDirBtnSpr = createGeodeButton(configFolderSpr, "");
m_openConfigDirBtnSpr->setScale(.6f);
m_openConfigDirBtnSpr->getIcon()->setScale(m_openConfigDirBtnSpr->getIcon()->getScale() * 1.4f);
auto openConfigDirBtn = CCMenuItemSpriteExtra::create(
m_openConfigDirBtnSpr, this, menu_selector(ModSettingsPopup::onOpenConfigDirectory)
);
m_buttonMenu->addChildAtPosition(openConfigDirBtn, Anchor::BottomRight, ccp(-50, 20));
auto settingFolderSpr = CCSprite::createWithSpriteFrameName("folderIcon_001.png");
auto settingFolderSprSub = CCSprite::createWithSpriteFrameName("settings.png"_spr);
settingFolderSprSub->setColor(ccBLACK);
settingFolderSprSub->setOpacity(155);
settingFolderSprSub->setScale(.55f);
settingFolderSpr->addChildAtPosition(settingFolderSprSub, Anchor::Center, ccp(0, -3));
auto openDirBtnSpr = createGeodeButton(settingFolderSpr, "");
openDirBtnSpr->setScale(.6f);
openDirBtnSpr->getIcon()->setScale(openDirBtnSpr->getIcon()->getScale() * 1.4f);
auto openDirBtn = CCMenuItemSpriteExtra::create(
openDirBtnSpr, this, menu_selector(ModSettingsPopup::onOpenSaveDirectory)
);
m_buttonMenu->addChildAtPosition(openDirBtn, Anchor::BottomRight, ccp(-53, 20));
m_buttonMenu->addChildAtPosition(openDirBtn, Anchor::BottomRight, ccp(-20, 20));
m_changeListener.bind([this](auto* ev) {
this->updateState(ev->getNode());
@ -108,7 +137,21 @@ void ModSettingsPopup::onApply(CCObject*) {
FLAlertLayer::create("Info", "No changes have been made.", "OK")->show();
}
}
void ModSettingsPopup::onRestart(CCObject*) {
// Update button state to let user know it's restarting but it might take a bit
m_restartBtn->setEnabled(false);
static_cast<ButtonSprite*>(m_restartBtn->getNormalImage())->setString("Restarting...");
m_restartBtn->updateSprite();
// Actually restart
Loader::get()->queueInMainThread([] {
// Delayed by 2 frames - one is needed to render the "Restarting text"
Loader::get()->queueInMainThread([] {
// the other never finishes rendering because the game actually restarts at this point
game::restart();
});
});
}
void ModSettingsPopup::onResetAll(CCObject*) {
createQuickPopup(
"Reset All",
@ -124,8 +167,24 @@ void ModSettingsPopup::onResetAll(CCObject*) {
}
);
}
void ModSettingsPopup::onOpenSaveDirectory(CCObject*) {
file::openFolder(m_mod->getSaveDir());
}
void ModSettingsPopup::onOpenConfigDirectory(CCObject*) {
file::openFolder(m_mod->getConfigDir());
this->updateState();
}
void ModSettingsPopup::updateState(SettingNodeV3* invoker) {
m_restartBtn->setVisible(ModSettingsManager::from(m_mod)->restartRequired());
m_applyMenu->updateLayout();
auto configDirExists = std::filesystem::exists(m_mod->getConfigDir(false));
m_openConfigDirBtnSpr->setCascadeColorEnabled(true);
m_openConfigDirBtnSpr->setCascadeOpacityEnabled(true);
m_openConfigDirBtnSpr->setColor(configDirExists ? ccWHITE : ccGRAY);
m_openConfigDirBtnSpr->setOpacity(configDirExists ? 255 : 155);
// Update all settings with "enable-if" schemes
for (auto& sett : m_settings) {
// Avoid infinite loops
@ -136,6 +195,7 @@ void ModSettingsPopup::updateState(SettingNodeV3* invoker) {
sett->updateState();
}
}
m_applyBtnSpr->setCascadeColorEnabled(true);
m_applyBtnSpr->setCascadeOpacityEnabled(true);
if (this->hasUncommitted()) {
@ -175,10 +235,6 @@ void ModSettingsPopup::onClose(CCObject* sender) {
Popup<Mod*>::onClose(sender);
}
void ModSettingsPopup::onOpenSaveDirectory(CCObject*) {
file::openFolder(m_mod->getSaveDir());
}
ModSettingsPopup* ModSettingsPopup::create(Mod* mod) {
auto ret = new ModSettingsPopup();
if (ret->init(440, 280, mod)) {

View file

@ -11,8 +11,11 @@ class ModSettingsPopup : public GeodePopup<Mod*> {
protected:
Mod* m_mod;
std::vector<SettingNodeV3*> m_settings;
CCMenu* m_applyMenu;
CCMenuItemSpriteExtra* m_applyBtn;
CCMenuItemSpriteExtra* m_restartBtn;
ButtonSprite* m_applyBtnSpr;
IconButtonSprite* m_openConfigDirBtnSpr;
EventListener<EventFilter<SettingNodeValueChangeEventV3>> m_changeListener;
bool setup(Mod* mod) override;
@ -20,8 +23,10 @@ protected:
bool hasUncommitted() const;
void onClose(CCObject*) override;
void onApply(CCObject*);
void onRestart(CCObject*);
void onResetAll(CCObject*);
void onOpenSaveDirectory(CCObject*);
void onOpenConfigDirectory(CCObject*);
public:
static ModSettingsPopup* create(Mod* mod);

View file

@ -1,5 +1,6 @@
#include "ModListSource.hpp"
#include <server/DownloadManager.hpp>
#include <Geode/loader/ModSettingsManager.hpp>
#define FTS_FUZZY_MATCH_IMPLEMENTATION
#include <Geode/external/fts/fts_fuzzy_match.h>
@ -90,6 +91,9 @@ bool ModListSource::isRestartRequired() {
if (mod->getRequestedAction() != ModRequestedAction::None) {
return true;
}
if (ModSettingsManager::from(mod)->restartRequired()) {
return true;
}
}
if (server::ModDownloadManager::get()->wantsRestart()) {
return true;

View file

@ -1,6 +1,7 @@
#include "ModSource.hpp"
#include <Geode/loader/ModMetadata.hpp>
#include <Geode/loader/ModSettingsManager.hpp>
#include <Geode/ui/GeodeUI.hpp>
#include <server/DownloadManager.hpp>
#include <Geode/binding/GameObject.hpp>
@ -106,7 +107,8 @@ bool ModSource::wantsRestart() const {
}
return std::visit(makeVisitor {
[](Mod* mod) {
return mod->getRequestedAction() != ModRequestedAction::None;
return mod->getRequestedAction() != ModRequestedAction::None ||
ModSettingsManager::from(mod)->restartRequired();
},
[](server::ServerModMetadata const& metdata) {
return false;

View file

@ -209,7 +209,7 @@ void SimpleTextArea::updateLinesNoWrap() {
void SimpleTextArea::updateLinesWordWrap(bool spaceWrap) {
this->charIteration([this, spaceWrap](CCLabelBMFont* line, const char c, const float top) {
static const std::string delimiters(spaceWrap ? " " : " `~!@#$%^&*()-_=+[{}];:'\",<.>/?\\|");
const std::string_view delimiters(spaceWrap ? " " : " `~!@#$%^&*()-_=+[{}];:'\",<.>/?\\|");
if (delimiters.find(c) == std::string_view::npos) {
const std::string& text = line->getString();