|
|
|
@@ -0,0 +1,782 @@
|
|
|
|
|
#include "pch.h"
|
|
|
|
|
|
|
|
|
|
#include "F4TMainMenuInject.h"
|
|
|
|
|
#include "F4TNetworking.h"
|
|
|
|
|
#include "F4TPrismaUI.h"
|
|
|
|
|
|
|
|
|
|
#include "Scaleform/G/GFx_FunctionHandler.h"
|
|
|
|
|
#include "Scaleform/G/GFx_Movie.h"
|
|
|
|
|
#include "Scaleform/G/GFx_Value.h"
|
|
|
|
|
|
|
|
|
|
#include <Windows.h>
|
|
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <cstring>
|
|
|
|
|
#include <string>
|
|
|
|
|
#include <string_view>
|
|
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
|
|
namespace F4T::MainMenuInject
|
|
|
|
|
{
|
|
|
|
|
namespace
|
|
|
|
|
{
|
|
|
|
|
constexpr auto kMultiplayerIndex = 15;
|
|
|
|
|
constexpr auto kLoadIndex = 4;
|
|
|
|
|
constexpr auto kResyncThrottleFrames = 30;
|
|
|
|
|
constexpr auto kItemPressListenerPriority = 100;
|
|
|
|
|
constexpr auto kOnButtonEventVtableIndex = 0x08;
|
|
|
|
|
constexpr auto kEntryListPath = "MainPanel_mc.List_mc.entryList";
|
|
|
|
|
constexpr auto kListWidgetPath = "MainPanel_mc.List_mc";
|
|
|
|
|
constexpr std::array<std::string_view, 3> kItemPressEventCandidates{
|
|
|
|
|
"itemPress",
|
|
|
|
|
"listItemPress",
|
|
|
|
|
"listPress"
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
bool g_hooksInstalled = false;
|
|
|
|
|
bool g_resyncTaskStarted = false;
|
|
|
|
|
bool g_itemPressListenerRegistered = false;
|
|
|
|
|
bool g_buttonEventHookInstalled = false;
|
|
|
|
|
int g_resyncFrameCounter = 0;
|
|
|
|
|
int g_injectFailLogCooldown = 0;
|
|
|
|
|
|
|
|
|
|
std::string GetLogPrefix()
|
|
|
|
|
{
|
|
|
|
|
return F4T::Networking::GetLocalPlayerLogPrefix() + " [MainMenuInject]";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void LogInfo(std::string_view a_message)
|
|
|
|
|
{
|
|
|
|
|
REX::INFO("{} {}", GetLogPrefix(), a_message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void LogWarning(std::string_view a_message)
|
|
|
|
|
{
|
|
|
|
|
REX::WARN("{} {}", GetLogPrefix(), a_message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void LogInjectFailure(std::string_view a_reason)
|
|
|
|
|
{
|
|
|
|
|
if (g_injectFailLogCooldown > 0) {
|
|
|
|
|
--g_injectFailLogCooldown;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
g_injectFailLogCooldown = 120;
|
|
|
|
|
LogWarning(a_reason);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::optional<std::int32_t> GetEntryLogicalIndex(const Scaleform::GFx::Value& a_entry)
|
|
|
|
|
{
|
|
|
|
|
Scaleform::GFx::Value indexValue;
|
|
|
|
|
if (!a_entry.GetMember("index", &indexValue)) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (indexValue.IsInt()) {
|
|
|
|
|
return indexValue.GetInt();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (indexValue.IsUInt()) {
|
|
|
|
|
return static_cast<std::int32_t>(indexValue.GetUInt());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (indexValue.IsNumber()) {
|
|
|
|
|
return static_cast<std::int32_t>(indexValue.GetNumber());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool IsPauseMode(Scaleform::GFx::Movie* a_movie)
|
|
|
|
|
{
|
|
|
|
|
if (!a_movie) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Scaleform::GFx::Value pauseMode;
|
|
|
|
|
if (!a_movie->GetVariable(&pauseMode, "PauseMode") || !pauseMode.IsBoolean()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return pauseMode.GetBoolean();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool IsMainStateForMenu(RE::MainMenu* a_mainMenu)
|
|
|
|
|
{
|
|
|
|
|
if (!a_mainMenu || !a_mainMenu->uiMovie) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Scaleform::GFx::Value state;
|
|
|
|
|
if (a_mainMenu->menuObj.IsObject() && a_mainMenu->menuObj.GetMember("currentState", &state)) {
|
|
|
|
|
if (state.IsString()) {
|
|
|
|
|
const char* currentState = state.GetString();
|
|
|
|
|
return currentState == nullptr || std::strcmp(currentState, "MAIN_STATE") == 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (a_mainMenu->uiMovie->GetVariable(&state, "currentState") && state.IsString()) {
|
|
|
|
|
const char* currentState = state.GetString();
|
|
|
|
|
return currentState == nullptr || std::strcmp(currentState, "MAIN_STATE") == 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool IsTitleScreenMainMenu(RE::StartMenuBase* a_menu)
|
|
|
|
|
{
|
|
|
|
|
if (!a_menu || !a_menu->uiMovie) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto* ui = RE::UI::GetSingleton();
|
|
|
|
|
if (!ui) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto mainMenu = ui->GetMenu<RE::MainMenu>();
|
|
|
|
|
if (!mainMenu || mainMenu.get() != a_menu) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return !IsPauseMode(a_menu->uiMovie.get());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool EntryHasIndex(const Scaleform::GFx::Value& a_entry, std::int32_t a_index)
|
|
|
|
|
{
|
|
|
|
|
const auto logicalIndex = GetEntryLogicalIndex(a_entry);
|
|
|
|
|
return logicalIndex.has_value() && *logicalIndex == a_index;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool EntryListHasIndex(const Scaleform::GFx::Value& a_entryList, std::int32_t a_index)
|
|
|
|
|
{
|
|
|
|
|
if (!a_entryList.IsArray()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto size = a_entryList.GetArraySize();
|
|
|
|
|
for (std::uint32_t i = 0; i < size; ++i) {
|
|
|
|
|
Scaleform::GFx::Value entry;
|
|
|
|
|
if (a_entryList.GetElement(i, &entry) && EntryHasIndex(entry, a_index)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::optional<std::uint32_t> FindEntryArrayIndex(const Scaleform::GFx::Value& a_entryList, std::int32_t a_index)
|
|
|
|
|
{
|
|
|
|
|
if (!a_entryList.IsArray()) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto size = a_entryList.GetArraySize();
|
|
|
|
|
for (std::uint32_t i = 0; i < size; ++i) {
|
|
|
|
|
Scaleform::GFx::Value entry;
|
|
|
|
|
if (a_entryList.GetElement(i, &entry) && EntryHasIndex(entry, a_index)) {
|
|
|
|
|
return i;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ResolveMainList(
|
|
|
|
|
RE::MainMenu* a_mainMenu,
|
|
|
|
|
Scaleform::GFx::Value& a_outListWidget,
|
|
|
|
|
Scaleform::GFx::Value& a_outEntryList)
|
|
|
|
|
{
|
|
|
|
|
if (!a_mainMenu || !a_mainMenu->uiMovie) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (a_mainMenu->menuObj.IsObject()) {
|
|
|
|
|
Scaleform::GFx::Value mainPanel;
|
|
|
|
|
if (a_mainMenu->menuObj.GetMember("MainPanel_mc", &mainPanel) && mainPanel.IsObject()) {
|
|
|
|
|
Scaleform::GFx::Value listWidget;
|
|
|
|
|
if (mainPanel.GetMember("List_mc", &listWidget) && listWidget.IsObject()) {
|
|
|
|
|
Scaleform::GFx::Value entryList;
|
|
|
|
|
if (listWidget.GetMember("entryList", &entryList) &&
|
|
|
|
|
(entryList.IsArray() || entryList.IsObject())) {
|
|
|
|
|
a_outListWidget = listWidget;
|
|
|
|
|
a_outEntryList = entryList;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!a_mainMenu->uiMovie->GetVariable(&a_outListWidget, kListWidgetPath) || !a_outListWidget.IsObject()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!a_outListWidget.GetMember("entryList", &a_outEntryList) &&
|
|
|
|
|
!a_mainMenu->uiMovie->GetVariable(&a_outEntryList, kEntryListPath)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return a_outEntryList.IsArray() || a_outEntryList.IsObject();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool InsertEntryAfterIndex(
|
|
|
|
|
Scaleform::GFx::Movie* a_movie,
|
|
|
|
|
Scaleform::GFx::Value& a_entryList,
|
|
|
|
|
std::int32_t a_afterLogicalIndex,
|
|
|
|
|
const Scaleform::GFx::Value& a_newEntry)
|
|
|
|
|
{
|
|
|
|
|
const auto insertAt = FindEntryArrayIndex(a_entryList, a_afterLogicalIndex);
|
|
|
|
|
if (!insertAt.has_value()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto targetIndex = *insertAt + 1;
|
|
|
|
|
const auto oldSize = a_entryList.GetArraySize();
|
|
|
|
|
|
|
|
|
|
for (std::uint32_t i = oldSize; i > targetIndex; --i) {
|
|
|
|
|
Scaleform::GFx::Value entry;
|
|
|
|
|
if (!a_entryList.GetElement(i - 1, &entry)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!a_entryList.SetElement(i, entry)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
(void)a_movie;
|
|
|
|
|
return a_entryList.SetElement(targetIndex, a_newEntry);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool InjectMultiplayerRow(RE::MainMenu* a_mainMenu)
|
|
|
|
|
{
|
|
|
|
|
if (!a_mainMenu || !a_mainMenu->uiMovie || IsPauseMode(a_mainMenu->uiMovie.get())) {
|
|
|
|
|
LogInjectFailure("Injection skipped: MainMenu movie unavailable or pause mode active.");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Scaleform::GFx::Value listWidget;
|
|
|
|
|
Scaleform::GFx::Value entryList;
|
|
|
|
|
if (!ResolveMainList(a_mainMenu, listWidget, entryList)) {
|
|
|
|
|
LogInjectFailure("Injection skipped: could not resolve MainPanel_mc.List_mc.entryList.");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!entryList.IsArray()) {
|
|
|
|
|
LogInjectFailure("Injection skipped: entryList is not a GFx array.");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (EntryListHasIndex(entryList, kMultiplayerIndex)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Scaleform::GFx::Value newEntry;
|
|
|
|
|
a_mainMenu->uiMovie->CreateObject(&newEntry);
|
|
|
|
|
if (!newEntry.IsObject()) {
|
|
|
|
|
LogInjectFailure("Injection skipped: failed to create GFx entry object.");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Scaleform::GFx::Value textValue{ "MULTIPLAYER" };
|
|
|
|
|
Scaleform::GFx::Value indexValue{ kMultiplayerIndex };
|
|
|
|
|
if (!newEntry.SetMember("text", textValue) || !newEntry.SetMember("index", indexValue)) {
|
|
|
|
|
LogInjectFailure("Injection skipped: failed to populate Multiplayer entry members.");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!InsertEntryAfterIndex(a_mainMenu->uiMovie.get(), entryList, kLoadIndex, newEntry)) {
|
|
|
|
|
LogInjectFailure(std::format(
|
|
|
|
|
"Injection skipped: could not find Load row (logical index {}).",
|
|
|
|
|
kLoadIndex));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
listWidget.Invoke("InvalidateData");
|
|
|
|
|
LogInfo("Injected Multiplayer row after Load.");
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TryInjectForStartMenu(RE::StartMenuBase* a_menu)
|
|
|
|
|
{
|
|
|
|
|
if (!IsTitleScreenMainMenu(a_menu)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
InjectMultiplayerRow(static_cast<RE::MainMenu*>(a_menu));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RegisterItemPressListener(RE::MainMenu* a_mainMenu);
|
|
|
|
|
|
|
|
|
|
void TryInjectOpenMainMenu()
|
|
|
|
|
{
|
|
|
|
|
auto* ui = RE::UI::GetSingleton();
|
|
|
|
|
if (!ui) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto mainMenu = ui->GetMenu<RE::MainMenu>();
|
|
|
|
|
if (!mainMenu || !mainMenu->uiMovie) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!ui->GetMenuOpen<RE::MainMenu>() && !mainMenu->menuCanBeVisible) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LogInfo("Running catch-up injection for already-open MainMenu.");
|
|
|
|
|
RegisterItemPressListener(mainMenu.get());
|
|
|
|
|
InjectMultiplayerRow(mainMenu.get());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
using InitMainListFn = void (*)(RE::StartMenuBase*);
|
|
|
|
|
InitMainListFn g_originalInitMainList = nullptr;
|
|
|
|
|
|
|
|
|
|
void InitMainListHook(RE::StartMenuBase* a_this)
|
|
|
|
|
{
|
|
|
|
|
if (g_originalInitMainList) {
|
|
|
|
|
g_originalInitMainList(a_this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!a_this) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!a_this->GetIsMenuReady()) {
|
|
|
|
|
const auto* taskInterface = F4SE::GetTaskInterface();
|
|
|
|
|
if (taskInterface) {
|
|
|
|
|
taskInterface->AddTask([a_this]() {
|
|
|
|
|
TryInjectForStartMenu(a_this);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TryInjectForStartMenu(a_this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void OpenMultiplayerMenu(RE::MainMenu* a_mainMenu)
|
|
|
|
|
{
|
|
|
|
|
if (a_mainMenu && a_mainMenu->uiMovie) {
|
|
|
|
|
Scaleform::GFx::Value bgsCode;
|
|
|
|
|
if (a_mainMenu->menuObj.IsObject() &&
|
|
|
|
|
a_mainMenu->menuObj.GetMember("BGSCodeObj", &bgsCode) &&
|
|
|
|
|
bgsCode.IsObject()) {
|
|
|
|
|
bgsCode.Invoke("PlayOKSound");
|
|
|
|
|
} else if (a_mainMenu->uiMovie->GetVariable(&bgsCode, "BGSCodeObj") && bgsCode.IsObject()) {
|
|
|
|
|
bgsCode.Invoke("PlayOKSound");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto* taskInterface = F4SE::GetTaskInterface();
|
|
|
|
|
if (taskInterface) {
|
|
|
|
|
taskInterface->AddTask([]() {
|
|
|
|
|
F4T::PrismaUI::ShowServerBrowser();
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
F4T::PrismaUI::ShowServerBrowser();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool IsMultiplayerRowSelected(RE::MainMenu* a_mainMenu)
|
|
|
|
|
{
|
|
|
|
|
if (!a_mainMenu || !a_mainMenu->uiMovie || IsPauseMode(a_mainMenu->uiMovie.get())) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!IsMainStateForMenu(a_mainMenu)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Scaleform::GFx::Value listWidget;
|
|
|
|
|
Scaleform::GFx::Value entryList;
|
|
|
|
|
if (!ResolveMainList(a_mainMenu, listWidget, entryList)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Scaleform::GFx::Value selectedEntry;
|
|
|
|
|
if (!listWidget.GetMember("selectedEntry", &selectedEntry) || !selectedEntry.IsObject()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto selectedIndex = GetEntryLogicalIndex(selectedEntry);
|
|
|
|
|
return selectedIndex.has_value() && *selectedIndex == kMultiplayerIndex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool TryHandleMultiplayerSelection(RE::MainMenu* a_mainMenu)
|
|
|
|
|
{
|
|
|
|
|
if (!IsMultiplayerRowSelected(a_mainMenu)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
OpenMultiplayerMenu(a_mainMenu);
|
|
|
|
|
LogInfo("Opened multiplayer menu from MainMenu selection.");
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::optional<std::string> TryReadClassItemPressEvent(
|
|
|
|
|
Scaleform::GFx::Movie* a_movie,
|
|
|
|
|
const char* a_className)
|
|
|
|
|
{
|
|
|
|
|
if (!a_movie || a_className == nullptr) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const std::array<Scaleform::GFx::Value, 1> classNameArg{ Scaleform::GFx::Value{ a_className } };
|
|
|
|
|
Scaleform::GFx::Value classDef;
|
|
|
|
|
if (!a_movie->Invoke(
|
|
|
|
|
"flash.utils.getDefinitionByName",
|
|
|
|
|
&classDef,
|
|
|
|
|
classNameArg.data(),
|
|
|
|
|
static_cast<std::uint32_t>(classNameArg.size())) ||
|
|
|
|
|
!classDef.IsObject()) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Scaleform::GFx::Value itemPressEvent;
|
|
|
|
|
if (!classDef.GetMember("ITEM_PRESS", &itemPressEvent) || !itemPressEvent.IsString()) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const char* eventName = itemPressEvent.GetString();
|
|
|
|
|
if (eventName == nullptr || *eventName == '\0') {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return std::string{ eventName };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<std::string> ResolveItemPressEventNames(Scaleform::GFx::Movie* a_movie)
|
|
|
|
|
{
|
|
|
|
|
std::vector<std::string> eventNames;
|
|
|
|
|
eventNames.reserve(kItemPressEventCandidates.size() + 1);
|
|
|
|
|
|
|
|
|
|
const std::array<const char*, 2> kClassNames{
|
|
|
|
|
"Shared.AS3.BSScrollingList",
|
|
|
|
|
"BSScrollingList"
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (const auto* className : kClassNames) {
|
|
|
|
|
if (const auto resolved = TryReadClassItemPressEvent(a_movie, className)) {
|
|
|
|
|
if (std::find(eventNames.begin(), eventNames.end(), *resolved) == eventNames.end()) {
|
|
|
|
|
eventNames.push_back(*resolved);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Scaleform::GFx::Value itemPressEvent;
|
|
|
|
|
if (a_movie &&
|
|
|
|
|
a_movie->GetVariable(&itemPressEvent, "Shared.AS3.BSScrollingList.ITEM_PRESS") &&
|
|
|
|
|
itemPressEvent.IsString() &&
|
|
|
|
|
itemPressEvent.GetString() != nullptr) {
|
|
|
|
|
const auto variableName = std::string{ itemPressEvent.GetString() };
|
|
|
|
|
if (std::find(eventNames.begin(), eventNames.end(), variableName) == eventNames.end()) {
|
|
|
|
|
eventNames.push_back(variableName);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const auto candidate : kItemPressEventCandidates) {
|
|
|
|
|
const auto candidateName = std::string{ candidate };
|
|
|
|
|
if (std::find(eventNames.begin(), eventNames.end(), candidateName) == eventNames.end()) {
|
|
|
|
|
eventNames.push_back(candidateName);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return eventNames;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class ItemPressHandler :
|
|
|
|
|
public Scaleform::GFx::FunctionHandler
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
void Call(const Params& a_params) override
|
|
|
|
|
{
|
|
|
|
|
if (a_params.argCount < 1) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto* mainMenu = static_cast<RE::MainMenu*>(a_params.userData);
|
|
|
|
|
if (!mainMenu || !mainMenu->uiMovie) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Scaleform::GFx::Value target;
|
|
|
|
|
if (!a_params.args[0].GetMember("target", &target) || !target.IsObject()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Scaleform::GFx::Value selectedEntry;
|
|
|
|
|
if (!target.GetMember("selectedEntry", &selectedEntry) || !selectedEntry.IsObject()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto selectedIndex = GetEntryLogicalIndex(selectedEntry);
|
|
|
|
|
if (!selectedIndex.has_value() || *selectedIndex != kMultiplayerIndex) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
OpenMultiplayerMenu(mainMenu);
|
|
|
|
|
LogInfo("Opened multiplayer menu from ITEM_PRESS handler.");
|
|
|
|
|
a_params.args[0].Invoke("stopImmediatePropagation");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ItemPressHandler g_itemPressHandler;
|
|
|
|
|
|
|
|
|
|
void RegisterItemPressListener(RE::MainMenu* a_mainMenu)
|
|
|
|
|
{
|
|
|
|
|
if (!a_mainMenu || !a_mainMenu->uiMovie || g_itemPressListenerRegistered) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!a_mainMenu->menuObj.IsObject()) {
|
|
|
|
|
const auto* taskInterface = F4SE::GetTaskInterface();
|
|
|
|
|
if (taskInterface) {
|
|
|
|
|
taskInterface->AddTask([a_mainMenu]() {
|
|
|
|
|
RegisterItemPressListener(a_mainMenu);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Scaleform::GFx::Value listWidget;
|
|
|
|
|
Scaleform::GFx::Value entryList;
|
|
|
|
|
if (!ResolveMainList(a_mainMenu, listWidget, entryList)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto eventNames = ResolveItemPressEventNames(a_mainMenu->uiMovie.get());
|
|
|
|
|
|
|
|
|
|
Scaleform::GFx::Value handler;
|
|
|
|
|
a_mainMenu->uiMovie->CreateFunction(&handler, &g_itemPressHandler, a_mainMenu);
|
|
|
|
|
|
|
|
|
|
std::size_t registeredListeners = 0;
|
|
|
|
|
for (const auto& eventName : eventNames) {
|
|
|
|
|
const std::array<Scaleform::GFx::Value, 4> args{
|
|
|
|
|
Scaleform::GFx::Value{ eventName.c_str() },
|
|
|
|
|
handler,
|
|
|
|
|
Scaleform::GFx::Value{ true },
|
|
|
|
|
Scaleform::GFx::Value{ kItemPressListenerPriority }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (a_mainMenu->menuObj.Invoke("addEventListener", nullptr, args.data(), args.size())) {
|
|
|
|
|
++registeredListeners;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (listWidget.Invoke("addEventListener", nullptr, args.data(), args.size())) {
|
|
|
|
|
++registeredListeners;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (registeredListeners == 0) {
|
|
|
|
|
LogWarning("Failed to register ITEM_PRESS listeners on MainMenu.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
g_itemPressListenerRegistered = true;
|
|
|
|
|
LogInfo(std::format(
|
|
|
|
|
"Registered {} ITEM_PRESS listener(s); candidates: {}",
|
|
|
|
|
registeredListeners,
|
|
|
|
|
eventNames.size()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
using OnButtonEventFn = void (*)(RE::IMenu*, const RE::ButtonEvent*);
|
|
|
|
|
OnButtonEventFn g_originalOnButtonEvent = nullptr;
|
|
|
|
|
|
|
|
|
|
void OnButtonEventHook(RE::IMenu* a_this, const RE::ButtonEvent* a_event)
|
|
|
|
|
{
|
|
|
|
|
if (a_this && a_event && a_event->QJustPressed() && !a_event->QDisabled()) {
|
|
|
|
|
const auto& userEvent = a_event->QUserEvent();
|
|
|
|
|
if (userEvent == "Accept"sv) {
|
|
|
|
|
const auto* ui = RE::UI::GetSingleton();
|
|
|
|
|
const auto mainMenu = ui ? ui->GetMenu<RE::MainMenu>() : nullptr;
|
|
|
|
|
if (mainMenu && mainMenu.get() == a_this && TryHandleMultiplayerSelection(mainMenu.get())) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (g_originalOnButtonEvent) {
|
|
|
|
|
g_originalOnButtonEvent(a_this, a_event);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void PollMultiplayerActivation()
|
|
|
|
|
{
|
|
|
|
|
const auto* ui = RE::UI::GetSingleton();
|
|
|
|
|
if (!ui || !ui->GetMenuOpen<RE::MainMenu>() || !IsMainMenuOnMainPanel(ui)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto mainMenu = ui->GetMenu<RE::MainMenu>();
|
|
|
|
|
if (!mainMenu || !IsMultiplayerRowSelected(mainMenu.get())) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool lastLeftButtonDown = false;
|
|
|
|
|
static bool lastAcceptDown = false;
|
|
|
|
|
static auto lastActivationTime = std::chrono::steady_clock::time_point{};
|
|
|
|
|
|
|
|
|
|
const auto leftButtonDown = (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0;
|
|
|
|
|
const auto acceptDown = (GetAsyncKeyState(VK_RETURN) & 0x8000) != 0;
|
|
|
|
|
const auto leftButtonPressed = leftButtonDown && !lastLeftButtonDown;
|
|
|
|
|
const auto acceptPressed = acceptDown && !lastAcceptDown;
|
|
|
|
|
lastLeftButtonDown = leftButtonDown;
|
|
|
|
|
lastAcceptDown = acceptDown;
|
|
|
|
|
|
|
|
|
|
if (!leftButtonPressed && !acceptPressed) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto now = std::chrono::steady_clock::now();
|
|
|
|
|
if (lastActivationTime.time_since_epoch().count() != 0 && now - lastActivationTime < 300ms) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lastActivationTime = now;
|
|
|
|
|
OpenMultiplayerMenu(mainMenu.get());
|
|
|
|
|
LogInfo("Opened multiplayer menu from activation poll.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ResyncMultiplayerRow()
|
|
|
|
|
{
|
|
|
|
|
if (++g_resyncFrameCounter < kResyncThrottleFrames) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
g_resyncFrameCounter = 0;
|
|
|
|
|
|
|
|
|
|
const auto* ui = RE::UI::GetSingleton();
|
|
|
|
|
if (!ui || !ui->GetMenuOpen<RE::MainMenu>() || !IsMainMenuOnMainPanel(ui)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto mainMenu = ui->GetMenu<RE::MainMenu>();
|
|
|
|
|
if (!mainMenu || !mainMenu->uiMovie || IsPauseMode(mainMenu->uiMovie.get())) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Scaleform::GFx::Value listWidget;
|
|
|
|
|
Scaleform::GFx::Value entryList;
|
|
|
|
|
if (!ResolveMainList(mainMenu.get(), listWidget, entryList) || !entryList.IsArray()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!EntryListHasIndex(entryList, kMultiplayerIndex)) {
|
|
|
|
|
InjectMultiplayerRow(mainMenu.get());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!g_itemPressListenerRegistered) {
|
|
|
|
|
RegisterItemPressListener(mainMenu.get());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class MainMenuOpenCloseSink :
|
|
|
|
|
public RE::BSTEventSink<RE::MenuOpenCloseEvent>
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
RE::BSEventNotifyControl ProcessEvent(
|
|
|
|
|
const RE::MenuOpenCloseEvent& a_event,
|
|
|
|
|
RE::BSTEventSource<RE::MenuOpenCloseEvent>* a_source) override
|
|
|
|
|
{
|
|
|
|
|
(void)a_source;
|
|
|
|
|
|
|
|
|
|
if (a_event.menuName != RE::MainMenu::MENU_NAME) {
|
|
|
|
|
return RE::BSEventNotifyControl::kContinue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (a_event.opening) {
|
|
|
|
|
const auto* ui = RE::UI::GetSingleton();
|
|
|
|
|
if (!ui) {
|
|
|
|
|
return RE::BSEventNotifyControl::kContinue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto mainMenu = ui->GetMenu<RE::MainMenu>();
|
|
|
|
|
if (!mainMenu) {
|
|
|
|
|
return RE::BSEventNotifyControl::kContinue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
g_itemPressListenerRegistered = false;
|
|
|
|
|
|
|
|
|
|
const auto* taskInterface = F4SE::GetTaskInterface();
|
|
|
|
|
if (taskInterface) {
|
|
|
|
|
taskInterface->AddTask([mainMenu = mainMenu.get()]() {
|
|
|
|
|
RegisterItemPressListener(mainMenu);
|
|
|
|
|
TryInjectForStartMenu(mainMenu);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
g_itemPressListenerRegistered = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return RE::BSEventNotifyControl::kContinue;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
MainMenuOpenCloseSink g_mainMenuOpenCloseSink;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool IsMainMenuOnMainPanel(const RE::UI* a_ui)
|
|
|
|
|
{
|
|
|
|
|
if (!a_ui) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto mainMenu = a_ui->GetMenu<RE::MainMenu>();
|
|
|
|
|
if (!mainMenu || !mainMenu->uiMovie) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return IsMainStateForMenu(mainMenu.get());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Install()
|
|
|
|
|
{
|
|
|
|
|
if (g_hooksInstalled) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
REL::Relocation<std::uintptr_t> startMenuVtable{ RE::StartMenuBase::VTABLE[0] };
|
|
|
|
|
g_originalInitMainList = reinterpret_cast<InitMainListFn>(
|
|
|
|
|
startMenuVtable.write_vfunc(0x15, &InitMainListHook));
|
|
|
|
|
|
|
|
|
|
if (!g_originalInitMainList) {
|
|
|
|
|
LogWarning("InitMainList vtable hook failed to install.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
g_originalOnButtonEvent = reinterpret_cast<OnButtonEventFn>(
|
|
|
|
|
startMenuVtable.write_vfunc(kOnButtonEventVtableIndex, &OnButtonEventHook));
|
|
|
|
|
g_buttonEventHookInstalled = g_originalOnButtonEvent != nullptr;
|
|
|
|
|
if (!g_buttonEventHookInstalled) {
|
|
|
|
|
LogWarning("StartMenuBase::OnButtonEvent hook failed to install; Accept on MULTIPLAYER may not open the browser.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto* ui = RE::UI::GetSingleton();
|
|
|
|
|
if (ui) {
|
|
|
|
|
ui->RegisterSink(std::addressof(g_mainMenuOpenCloseSink));
|
|
|
|
|
} else {
|
|
|
|
|
LogWarning("UI singleton unavailable; MainMenu open/close listener not registered.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto* taskInterface = F4SE::GetTaskInterface();
|
|
|
|
|
if (taskInterface) {
|
|
|
|
|
if (!g_resyncTaskStarted) {
|
|
|
|
|
taskInterface->AddTaskPermanent([]() {
|
|
|
|
|
PollMultiplayerActivation();
|
|
|
|
|
ResyncMultiplayerRow();
|
|
|
|
|
});
|
|
|
|
|
g_resyncTaskStarted = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
taskInterface->AddTask([]() {
|
|
|
|
|
TryInjectOpenMainMenu();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
g_hooksInstalled = true;
|
|
|
|
|
LogInfo("InitMainList hook and MainMenu injection tasks installed.");
|
|
|
|
|
}
|
|
|
|
|
}
|