파피루스 말고 C++ F4SE로 띄우는법
CommonLibF4 기반
어도비 애니메이트 필요함 난 2022 불법 씀..
관심 없는 사람은 뒤로 가기 누르기
F4SE C++ 모딩 할줄 알거라는 전제로 적은 글임
나도 가이드 쓸 정도로 잘하는건 아님
클릭 이벤트도 제대로 연결 못시켜서 그냥 좌표로 떼움
그래도 기초같은거라도 적어봄
모드 두개 소스
https://mega.nz/file/a0twxLwD#sHhXrnNGeYQL4Qh6oYrtlo2j22A7m-4dqKkeEkKtZOI
링크 파일안에 모드 두개 소스 들어있늗네
하나는 MessageBox Extended고 다른건 Custom Survival HUD임
MessageBox Extended는 Next-gen 기반이라 용어가 좀 다른게 있는데 그나마 코드가 깔끔해서 넣었고
Custom Survival HUD는 하여간 코드가 길어서 넣어봤음
c++ 안에 Class customSurvivalHUD_Setting 이건 개야매니까 보지말고..
입력 이벤트쪽은 MessageBox Extended가 그나마 잘되어있음
swf를 게임화면에 띄우려면 일단 swf를 만들어서 interface 폴더에 넣어야하는데
https://arca.live/b/nakonakoni/128315965
여기에 대충 애니메이트로 파일 만드는법 참고하고
지금 설명할 방법은 어차피 F4SE로 다해서 딱히 콜백은 필요없으니까
package {
import Shared.F4SE.ICodeObject;
import flash.display.MovieClip;
이런 부분은 필요없음 그냥 파일만들고 문서클래스 만들고 심볼 만들고 이런거만 보셈
그럼 이제 시작한다
먼저 f4se에서 swf을 등록하려면 C++에서 클래스를 만들어야함
만일 계속 떠있는 HUD를 만들겠다면 보통 기본은 이런식이 됨
const static std::string HUDName{ "customSurvivalHUD" };
class CustomSurvivalHUD : public IMenu
{
public:
static CustomSurvivalHUD* instance;
CustomSurvivalHUD() :
IMenu()
{
if (instance) {
delete (instance);
}
instance = this;
instance->menuFlags = (UI_MENU_FLAGS)0;
instance->UpdateFlag(UI_MENU_FLAGS::kAllowSaving, true);
instance->UpdateFlag(UI_MENU_FLAGS::kDontHideCursorWhenTopmost, true);
instance->UpdateFlag(UI_MENU_FLAGS::kAlwaysOpen, true);
instance->depthPriority = UI_DEPTH_PRIORITY::kHUD;
instance->inputEventHandlingEnabled = false;
BSScaleformManager* sfm = BSScaleformManager::GetSingleton();
bool succ = sfm->LoadMovieEx(*instance, "Interface/customSurvivalHUD.swf", "root", BSScaleformManager::ScaleModeType::kShowAll, 0.0f);
if (succ) {
instance->menuObj.SetMember("menuFlags", Scaleform::GFx::Value(instance->menuFlags.underlying()));
instance->menuObj.SetMember("movieFlags", Scaleform::GFx::Value(3));
instance->menuObj.SetMember("extendedFlags", Scaleform::GFx::Value(3));
}
}
static CustomSurvivalHUD* GetSingleton()
{
return instance;
}
};
CustomSurvivalHUD* CustomSurvivalHUD::instance = nullptr;
그리고 이 클래스를 이용해서 메뉴를 등록해야하는데
가능하면 메인화면에서 1회 등록하는게 좋음
void RegisterMenu()
{
ui = UI::GetSingleton();
msgQ = UIMessageQueue::GetSingleton();
if (ui) {
ui->RegisterMenu(HUDName.c_str(), [](const UIMessage&) -> IMenu* {
CustomSurvivalHUD* bpHUD = CustomSurvivalHUD::GetSingleton();
if (!bpHUD) {
bpHUD = new CustomSurvivalHUD();
}
return bpHUD;
});
msgQ->AddMessage(HUDName, RE::UI_MESSAGE_TYPE::kShow);
}
}
void OnF4SEMessage(F4SE::MessagingInterface::Message* msg)
{
switch (msg->type) {
case F4SE::MessagingInterface::kGameDataReady:
{
RegisterMenu();
break;
}
msgQ->AddMessage(HUDName, RE::UI_MESSAGE_TYPE::kShow);
이 부분의 kShow 상태가 아니면 swf 내부의 함수나 변수에 접근이 안됨
아니 확실한건 아니고 안되는거 같음...
문제는 일부 메뉴모드로 들어가면 등록한 메뉴가 자동으로 kHide 상태로 들어간다는거임
kHIde가 되면 보이지도 않고 함수호출도 안되니 다시 kShow로 해야함
근데 또 핍보이나 거래메뉴에서 HUD가 뜨는걸 원하지도 않을거임
그래서 아래처럼 함
std::vector<std::string> hideMenuList = { "BarterMenu", "ContainerMenu", "CookingMenu", "CreditsMenu", "DialogueMenu", "ExamineMenu", "LevelUpMenu",
"LockpickingMenu", "LooksMenu", "MessageBoxMenu", "PauseMenu", "PipboyMenu", "BookMenu", "LoadingMenu",
"SPECIALMenu", "TerminalHolotapeMenu", "TerminalMenu", "VATSMenu", "WorkshopMenu", "SitWaitMenu", "SleepWaitMenu", "F4QMWMenu",
"Console", "VignetteMenu" };
class MenuWatcher : public BSTEventSink<MenuOpenCloseEvent>
{
virtual BSEventNotifyControl ProcessEvent(const MenuOpenCloseEvent& evn, BSTEventSource<MenuOpenCloseEvent>* src) override
{
bool bHideHUDMenu = false;
if (evn.menuName == HUDName) {
return BSEventNotifyControl::kContinue;
}
bool isHiding = false;
for (std::string menuName : hideMenuList) {
if (ui->GetMenuOpen(menuName)) {
isHiding = true;
break;
}
}
if (msgQ && isHiding) {
msgQ->AddMessage(HUDName, RE::UI_MESSAGE_TYPE::kHide);
} else {
msgQ->AddMessage(HUDName, RE::UI_MESSAGE_TYPE::kShow);
}
return BSEventNotifyControl::kContinue;
}
};
// 이부분은 마찬가지로 kGameDataReady 시점에 1회 등록
MenuWatcher* mw = new MenuWatcher();
UI::GetSingleton()->GetEventSource<MenuOpenCloseEvent>()->RegisterSink(mw);
이러면 HUD를 감추고 싶은 메뉴가 메뉴 떠있는 리스트에 있으면 kHide를 호출하고 리스트에 없으면 kShow를 호출하는데
이 이벤트는 메뉴의 변경이 있을때마다 수신하니까
결국 뭔가 다른 메뉴가 떠서 의도하지 않고 kHide 상태로 들어가더라도
감추고 싶은 메뉴가 안떠있을땐 곧바로 kShow 상태가 되서 항상 HUD가 표시되게 됨
HUD를 숨기기는 해야겠는데 또 제어는 해야겠으면 kHide가 아니라 swf의 as3 함수내에서 visible 속성으로 감추면 됨
kShow, kHide로 보이고 안보이고를 제어하는건 그다지 좋은 방식이 아닌거 같음..
여긴 어차피 kHide가 되니까 kShow 호출할겸 하는거고
여기 외에는 아래처럼 as3이랑 연계해서 visible 속성으로하는게 좋음
//as3에서의 함수
public function setHUDVisible(isVisible: Boolean): void {
GrandParent.visible = isVisible;
}
// ... C++ class CustomSurvivalHUD 의 멤버 함수
void setHUDVIsible(bool bDisplayed)
{
if (uiMovie && uiMovie->asMovieRoot) {
Scaleform::GFx::Value args;
args[0] = bDisplayed;
uiMovie->asMovieRoot->Invoke("root.setHUDVisible", nullptr, &args, 1);
}
}
////////
// ... 숨기려는 포인트에서 호출
CustomSurvivalHUD::GetSingleton()->setHUDVIsible(false)
as3 부분의 GrandParent은 내가 이 as3 코드에서 HUD 무비클립들을 죄다 여기 자식으로 집어넣어둔 최상위 무비클립임
그래서 얘만 visible = false로 하면 hud가 사라짐
부모의 자식의 자식의 자식의... 이런식으로 되니까 몇단계로 나눠서 각각 제어하거나 할수도 있음
함수 앞에 public 안붙이면 외부에서 호출안되니 꼭 확인하고 솔직히 개인단위면 그냥 전부 public해도 되지 않나 싶음...
Invoke가 swf의 함수를 호출하는 함수임
이건 소스에 인수 1개 보내서 받는법, 배열로 보내서 각각 받는법
메세지소스쪽에는 배열로 보내서 ..arg 배열로 받아내는법도 있으니까 확인하고
이유는 모르겠는데 인수가 필요없다고 인수를 안넣으니까 함수 호출이 안되는거 같음
더미 인수라도 한개 넣어서 함수 쓰셈
GetVariable이나 SetVariable로 public 아닌 변수값 직접 받아오거나 조작할수도 있는거 같은데
자작 swf면 조금 돌아가도 Invoke로 다 해먹을수 있으니까 일단 얘만 되면 어떻게든 됨
그리고 HUD는 아무래도 모든 해상도에서 같은 위치에 있어야하는데
나같은 경우는 아래 코드를 항상 넣어서 kGameDataReady 때 변수값을 넣고
swf에 값을 넣어둠
// swf의 0, 0이 게임화면 내의 가장 좌측 위가 되게 하는 보정값
float FirstPosX = 0;
float FirstPosY = 0;
// 백분율 기준
float movePerX;
float movePerY;
// 게임화면 비율과 swf 화면 비율 보정
float scalePerX;
float scalePerY;
void setHUDPositionRatio()
{
INIPrefSettingCollection* iniPrefSetting = INIPrefSettingCollection::GetSingleton();
Setting* gameHeight01 = iniPrefSetting->GetSetting("iSize W:Display"sv);
Setting* gameHeight02 = iniPrefSetting->GetSetting("iSize H:Display"sv);
uint32_t iResolutionH;
uint32_t iResolutionW;
if (gameHeight01 && gameHeight02) {
iResolutionW = gameHeight01->GetInt();
iResolutionH = gameHeight02->GetInt();
} else {
iResolutionW = 1920;
iResolutionH = 1080;
}
// 이부분은 실제로 만든 swf의 스테이지 해상도를 적어야함
float swf_width = 1280.0f;
float swf_height = 720.0f;
// 타겟 화면 비율 계산
float target_aspect = (float)iResolutionW / (float)iResolutionH;
// SWF 가로 세로 비율 계산
float swf_aspect = swf_width / swf_height;
// 화면에 SWF 맞추기
float target_width, target_height;
if (target_aspect > swf_aspect) {
// 화면이 더 넓을 때
target_width = swf_height * target_aspect;
target_height = swf_height;
} else {
// 화면이 더 좁을 때
target_width = swf_width;
target_height = swf_width / target_aspect;
}
// X, Y 오프셋 계산
FirstPosX = (swf_width - target_width) / 2.0f;
FirstPosY = (swf_height - target_height) / 2.0f;
// 백분율 기반 이동 거리 (UI 배치나 퍼센트 기반 애니메이션용)
movePerX = target_width / 100.0f;
movePerY = target_height / 100.0f;
// 게임 해상도 기준 → SWF 이동 거리 보정용 (커서 위치 등)
scalePerX = target_width / (float)iResolutionW;
scalePerY = target_height / (float)iResolutionH;
}
FirstPosX, Y는 게임 화면 가장 왼쪽위일때 swf상의 좌표가 얼마일까를 구해두는거임
보통 비율이라면 y는 0이고 x는 -200쯤 됨
scalePerX, Y는 게임 화면의 x y에 이 값을 곱하면 swf에서의 x y가 됨
movePerX, Y는 1% 마다의 움직임 거리임 0이면 HUD는 가장 왼쪽위 100이면 가장 오른쪽 아래에 있게됨
HUD 모드라면 scalePer는 그다지 필요없고 movePer를 쓰게됨
실제 HUD를 표시할때는 as3 코드에서 이런식이 됨
movieclip.x = FirstPosX + movePerX * //원하는 화면 % 위치//
movieclip.y = FirstPosY + movePerY * //원하는 화면 % 위치//
이 방식으로 16:9든 4:3이든 1:10이든 32:9든 무조건 대응할수 있음
입력은 HUD에서는 필요없는데 일단 게임을 멈춰야함
왜냐면 instance->UpdateFlag(RE::UI_MENU_FLAGS::kUsesCursor, true); 을 넣어서
화면에 커서가 뜨면 캐릭터의 시점이 돌아가지 않기 때문
근데 메뉴 플래그에 instance->UpdateFlag(RE::UI_MENU_FLAGS::kPausesGame, true);
를 넣으면 kShow 상태에서는 당연히 visible로 가리든 말든 조작이 불가능해짐
그래서 입력을 받는 메뉴는 평소에는 kHide 상태로 뒀다가
필요할때만 kShow로 호출하고 필요한 데이터를 넣는게 좋음
내 메세지 모드에서는 플래그 둘다 없어서 참고가 안되는데
왜냐면 바닐라 메세지 뜬 상태에서 visible false로 가려두기만 해서
이미 바닐라 메세지 박스 떠있는 상태라그럼..
입력 받는 메뉴면 메뉴 초기화 함수에서 플래그 두개를 추가해주셈
instance->UpdateFlag(RE::UI_MENU_FLAGS::kUsesCursor, true);
instance->UpdateFlag(RE::UI_MENU_FLAGS::kPausesGame, true);
instance->inputEventHandlingEnabled = false; // < 이건 true 안해도 된다 내 방식에서는 안씀
그리고 아래 방식으로 구현함
// as3에서 입력 처리함수
// 이 함수는 배열에 넣어둔 버튼들의 좌표와 클릭 순간의 좌표를 비교하는 함수임
public function checkHitButton(cursorX: Number, cursorY: Number): int {
for (var i: int = 0; i < buttons.length; i++) {
var btn: msgButton = buttons[i];
// 버튼이 보이는 상태일 때만 검사
if (!btn.visible) {
continue;
}
// 버튼 영역 좌표 계산
var globalPos: Point = btn.localToGlobal(new Point(0, 0));
var left: Number = globalPos.x;
var top: Number = globalPos.y;
var right: Number = left + btn.width;
var bottom: Number = top + btn.height - 12; // 숫자는 커서 크기때문에 오프셋값
var adjustedCursorX: Number = cursorX + originPosX; // c++에서 구해둔 FirstX 값을 더함
var adjustedCursorY: Number = cursorY + originPosY;
// 커서가 버튼 영역 안에 있으면 인덱스 반환
if (adjustedCursorX >= left && adjustedCursorX <= right &&
adjustedCursorY >= top && adjustedCursorY <= bottom) {
return i;
}
}
// 어떤 버튼도 히트되지 않음
return -1;
}
public function checkClickMouse(cursorX: Number, cursorY: Number): int {
// 버튼 영역 좌표 계산, 클릭 순간의 마우스 좌표를 전달받음
var hitIndex: int = checkHitButton(cursorX, cursorY);
return hitIndex;
}
//... 입력을 받을 C++ 메뉴 클래스의 멤버함수
void checkClickMouse()
{
std::array<Scaleform::GFx::Value, 2> args;
args[0] = currentPosX * scalePerX;
args[1] = currentPosY * scalePerX;
Scaleform::GFx::Value result = -1; // Invoke 함수로 as3의 return 값을 받아올수 있음
uiMovie->asMovieRoot->Invoke("root.checkClickMouse", &result, args.data(), args.size());
int selectButtonIndex = result.GetInt();
if (selectButtonIndex >= 0) {
// 하고 싶은 처리하기
}
}
static constexpr int32_t mouse_left = 0;
// 커스텀 메세지 박스 열릴때 입력 이벤트 후킹해서 커서이동, 선택 처리
class InputEventReceiverOverride : public BSInputEventReceiver
public:
typedef void (InputEventReceiverOverride::*FnPerformInputProcessing)(const InputEvent* a_queueHead);
void ProcessButtonEvent(const InputEvent* evn)
{
auto btn = evn->As<ButtonEvent>();
// 좌클릭을 뗀 순간에 멤버함수 호출
if (btn) {
if (btn->device == INPUT_DEVICE::kMouse) {
if (btn->QReleased() && btn->idCode == mouse_left) {
messageboxExtendedHUD::GetSingleton()->checkClickMouse();
}
}
}
auto cursor = evn->As<CursorMoveEvent>();
// 커서의 위치를 움직일때마다 변수에 넣어둠
if (cursor) {
currentPosX = cursor->cursorPosX;
currentPosY = cursor->cursorPosY;
}
// 이거 안하면 다음 입력을 기다리는 쪽에 전달이 안되므로 그냥 리턴하지 말고 반드시 실행
if (evn->next) {
ProcessButtonEvent(evn->next);
}
}
void HookedPerformInputProcessing(const InputEvent* a_queueHead)
{
if (a_queueHead) {
const InputEvent* ev = a_queueHead;
ProcessButtonEvent(ev);
}
FnPerformInputProcessing fn = fnHash.at(*(uint64_t*)this);
if (fn) {
(this->*fn)(a_queueHead); // 원본 함수 호출
}
}
void HookSink()
{
uint64_t vtable = *(uint64_t*)this;
auto it = fnHash.find(vtable);
if (it == fnHash.end()) {
FnPerformInputProcessing fn = SafeWrite64Function(vtable, &InputEventReceiverOverride::HookedPerformInputProcessing);
fnHash.insert(std::pair<uint64_t, FnPerformInputProcessing>(vtable, fn));
}
}
void UnHookSink()
{
uint64_t vtable = *(uint64_t*)this;
auto it = fnHash.find(vtable);
if (it == fnHash.end())
return;
SafeWrite64Function(vtable, it->second);
fnHash.erase(it);
}
protected:
static std::unordered_map<uint64_t, FnPerformInputProcessing> fnHash;
};
std::unordered_map<uint64_t, InputEventReceiverOverride::FnPerformInputProcessing> InputEventReceiverOverride::fnHash;
// 메세지박스에서 커서이동, 클릭 이벤트 등록
void registerMessageBox()
{
((InputEventReceiverOverride*)((uint64_t)ui))->HookSink();
}
void unregisterMessageBox()
{
msgQ->AddMessage(HUDName.c_str(), RE::UI_MESSAGE_TYPE::kHide);
shouldChangeMessageLayout = false;
F4SE::GetTaskInterface()->AddTask([]() {
((InputEventReceiverOverride*)((uint64_t)ui))->UnHookSink();
});
}
///.... 입력 메뉴를 kShow 할때 registerMessageBox()로 입력 후킹
///.... kHide할때 unregisterMessageBox()로 입력 언훅
메뉴 함수를 kShow할때 입력을 후킹하고
마우스 커서를 움직일때마다 좌표를 전역변수에 넣어두고
클릭할때 그 젼역변수를 as3에 전달하고
as3에서는 받은 전역변수에 받아서 넣어둔 FirstX Y 값을 오프셋으로 더하고
그 위치가 버튼이 있는 곳이 맞나 좌표로 확인하는거임
솔직히 이 입력방식은 딴방법 몰라서 어거지로 하는거고 좀 쉽게 편한 방법 있으면 나도 좀 알려다오
근데 next-gen에서는 클래스 속성에 input = true하니까 게임에 커서 올라가자마자 크래시 나서 뭐 어케 할지 모르겠음...
F4SE로 HUD UI 띄우기 정보글이전
