Improve Vault109 chargen flow and LooksMenu handling

Move first-time character menu orchestration out of Papyrus and into the C++ session-launch flow. CO_Vault109StartQuestScript.psc now only teleports the player and sets stage 10. F4TSessionLaunch.cpp adds camera preparation/restoration, direct PlayerCamera/ThirdPersonState face-camera tuning, primary console ShowLooksMenu 14 path, Actor.ShowLooksMenu fallback, retry/timeouts and guarded state resets. Added logging and counters for Looks/SPECIAL menu polling. Updated changelog.md and docs/dev-log.md to describe the new flow and testing notes.
This commit is contained in:
2026-06-28 17:39:47 +12:00
parent b02c183ea9
commit 22394a77ff
4 changed files with 203 additions and 9 deletions
+142 -5
View File
@@ -10,7 +10,9 @@
#include "RE/G/GameScript.h"
#include "RE/L/LoadingMenu.h"
#include "RE/M/MainMenu.h"
#include "RE/P/PlayerCamera.h"
#include "RE/P/PlayerCharacter.h"
#include "RE/T/ThirdPersonState.h"
#include "RE/T/TESWorldSpace.h"
#include "RE/U/UI.h"
@@ -25,6 +27,11 @@ namespace
constexpr int kSpawnPollIntervalFrames = 15;
constexpr int kMaxSpawnWaitAttempts = 600;
constexpr int kWorldReadyDelayPolls = 10;
constexpr int kLooksMenuFallbackDelayPolls = 40;
constexpr int kLooksMenuOpenMaxPolls = 300;
constexpr int kLooksMenuCameraNudgeMaxPolls = 20;
constexpr float kPi = 3.14159265358979323846F;
constexpr float kLooksMenuFaceCameraZoomOffset = -90.0F;
enum class CharGenPhase
{
@@ -45,9 +52,13 @@ namespace
bool g_vault109StartQuestIssued = false;
bool g_sawLooksMenuOpen = false;
bool g_sawSpecialMenuOpen = false;
bool g_looksMenuFallbackIssued = false;
bool g_looksMenuCameraContextApplied = false;
int g_spawnPollFrameCounter = 0;
int g_spawnWaitAttempts = 0;
int g_worldReadyPolls = 0;
int g_looksMenuOpenPolls = 0;
int g_looksMenuCameraNudgePolls = 0;
std::string GetLogPrefix()
{
@@ -244,8 +255,92 @@ namespace
return true;
}
float NormalizeRadians(float a_angle)
{
while (a_angle > kPi) {
a_angle -= 2.0F * kPi;
}
while (a_angle < -kPi) {
a_angle += 2.0F * kPi;
}
return a_angle;
}
bool ForcePlayerThirdPersonCamera()
{
auto* camera = RE::PlayerCamera::GetSingleton();
if (!camera) {
LogWarning("PlayerCamera unavailable while forcing third-person camera.");
return false;
}
auto thirdPersonState = camera->GetState(RE::CameraState::k3rdPerson);
if (!thirdPersonState) {
LogWarning("Third-person camera state unavailable while forcing Looks menu camera.");
return false;
}
if (auto* state = static_cast<RE::ThirdPersonState*>(thirdPersonState.get())) {
const auto* player = RE::PlayerCharacter::GetSingleton();
const float faceCameraYaw = NormalizeRadians((player ? player->GetHeading() : camera->heading) + kPi);
state->SetFreeRotationMode(true, true);
state->freeRotation.x = faceCameraYaw;
state->freeRotation.y = 0.0F;
state->targetYaw = faceCameraYaw;
state->currentYaw = faceCameraYaw;
state->targetShoulderOffset = RE::NiPoint3{ 0.0F, 0.0F, 0.0F };
state->currentShoulderOffset = RE::NiPoint3{ 0.0F, 0.0F, 0.0F };
state->sideOffsetCollisionBlend = RE::NiPoint3{ 0.0F, 0.0F, 0.0F };
state->targetZoomOffset = kLooksMenuFaceCameraZoomOffset;
state->currentZoomOffset = kLooksMenuFaceCameraZoomOffset;
state->cameraHeightAdjust = 0.0F;
state->show3rdPersonModel = true;
state->zoomingInto1st = false;
state->preserveRotation = true;
state->applyOffsets = true;
}
camera->SetState(thirdPersonState.get());
return camera->QCameraEquals(RE::CameraState::k3rdPerson);
}
void PrepareCharGenCameraContext()
{
ForcePlayerThirdPersonCamera();
RE::Console::ExecuteCommand("ForceThirdPerson");
LogInfo("Prepared camera context before Looks menu.");
}
void ApplyLooksMenuCameraContext()
{
if (g_looksMenuCameraContextApplied) {
return;
}
RE::Console::ExecuteCommand("SetInChargen 1");
ForcePlayerThirdPersonCamera();
RE::Console::ExecuteCommand("ForceThirdPerson");
g_looksMenuCameraContextApplied = true;
g_looksMenuCameraNudgePolls = 0;
LogInfo("Applied chargen camera context after Looks menu open.");
}
void RestoreCharGenCameraContext()
{
if (!g_looksMenuCameraContextApplied) {
return;
}
RE::Console::ExecuteCommand("SetInChargen 0");
g_looksMenuCameraContextApplied = false;
g_looksMenuCameraNudgePolls = 0;
LogInfo("Restored chargen camera context after Looks menu.");
}
void ResetFirstJoinState()
{
RestoreCharGenCameraContext();
g_charGenPhase = CharGenPhase::None;
g_pendingProfileBind = false;
g_pendingProfileBindServerKey.clear();
@@ -253,13 +348,18 @@ namespace
g_vault109StartQuestIssued = false;
g_sawLooksMenuOpen = false;
g_sawSpecialMenuOpen = false;
g_looksMenuFallbackIssued = false;
g_looksMenuCameraContextApplied = false;
g_spawnPollFrameCounter = 0;
g_spawnWaitAttempts = 0;
g_worldReadyPolls = 0;
g_looksMenuOpenPolls = 0;
g_looksMenuCameraNudgePolls = 0;
}
void QueueFirstTimeCocJoin(const std::string& a_serverKey)
{
RestoreCharGenCameraContext();
g_charGenPhase = CharGenPhase::CocLoading;
g_pendingProfileBind = true;
g_pendingProfileBindServerKey = a_serverKey;
@@ -267,9 +367,13 @@ namespace
g_vault109StartQuestIssued = false;
g_sawLooksMenuOpen = false;
g_sawSpecialMenuOpen = false;
g_looksMenuFallbackIssued = false;
g_looksMenuCameraContextApplied = false;
g_spawnPollFrameCounter = 0;
g_spawnWaitAttempts = 0;
g_worldReadyPolls = 0;
g_looksMenuOpenPolls = 0;
g_looksMenuCameraNudgePolls = 0;
LogInfo(std::format("Queued first-time coc join for server {}.", a_serverKey));
}
@@ -295,7 +399,14 @@ namespace
LogInfo("Issued startquest CO_Vault109StartQuest for first-time COVault109 flow.");
}
void OpenLooksMenu()
void OpenLooksMenuPrimary()
{
PrepareCharGenCameraContext();
RE::Console::ExecuteCommand("ShowLooksMenu 14");
LogInfo("Issued 'ShowLooksMenu 14' for first-time character creation.");
}
void OpenLooksMenuFallback()
{
auto* gameVM = RE::GameVM::GetSingleton();
auto* player = RE::PlayerCharacter::GetSingleton();
@@ -307,13 +418,13 @@ namespace
"Actor",
"ShowLooksMenu",
nullptr);
LogInfo("Dispatched Actor.ShowLooksMenu for first-time character creation.");
LogInfo("Fallback: dispatched Actor.ShowLooksMenu for first-time character creation.");
return;
}
}
RE::Console::ExecuteCommand("CallFunction Actor ShowLooksMenu player");
LogInfo("Fell back to CallFunction Actor ShowLooksMenu player.");
LogInfo("Fallback: used CallFunction Actor ShowLooksMenu player.");
}
void OpenSpecialMenu()
@@ -397,6 +508,7 @@ namespace
void CompleteFirstTimeJoin(RE::PlayerCharacter& a_player)
{
RestoreCharGenCameraContext();
LogPlayerLocation("First-time join complete after character creation", a_player);
g_charGenPhase = CharGenPhase::Complete;
g_spawnWaitAttempts = 0;
@@ -452,21 +564,46 @@ namespace
}
if (g_charGenPhase == CharGenPhase::OpeningLooksMenu) {
OpenLooksMenu();
g_sawLooksMenuOpen = false;
g_looksMenuFallbackIssued = false;
g_looksMenuOpenPolls = 0;
g_looksMenuCameraNudgePolls = 0;
OpenLooksMenuPrimary();
g_charGenPhase = CharGenPhase::WaitingForLooksMenuClose;
return;
}
if (g_charGenPhase == CharGenPhase::WaitingForLooksMenuClose) {
if (IsMenuOpen("LooksMenu")) {
g_sawLooksMenuOpen = true;
if (!g_sawLooksMenuOpen) {
g_sawLooksMenuOpen = true;
ApplyLooksMenuCameraContext();
}
if (g_looksMenuCameraNudgePolls < kLooksMenuCameraNudgeMaxPolls) {
++g_looksMenuCameraNudgePolls;
ForcePlayerThirdPersonCamera();
RE::Console::ExecuteCommand("ForceThirdPerson");
}
return;
}
if (!g_sawLooksMenuOpen) {
++g_looksMenuOpenPolls;
if (!g_looksMenuFallbackIssued && g_looksMenuOpenPolls >= kLooksMenuFallbackDelayPolls) {
g_looksMenuFallbackIssued = true;
LogWarning("Looks menu did not open after primary ShowLooksMenu command; issuing Actor.ShowLooksMenu fallback.");
OpenLooksMenuFallback();
}
if (g_looksMenuOpenPolls >= kLooksMenuOpenMaxPolls) {
LogWarning("Timed out waiting for Looks menu to open.");
ResetFirstJoinState();
}
return;
}
RestoreCharGenCameraContext();
g_charGenPhase = CharGenPhase::OpeningSpecialMenu;
return;
}