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
+10 -1
View File
@@ -33,6 +33,15 @@ For testing notes, milestone summaries, known issues, and next steps, see [`docs
- Replaced invalid default GUI weather IDs with valid Fallout 4 weather form IDs so `fw <id>` no longer fails with "Invalid weather ...".
- Corrected `Clear` preset ID typo from `0002852a` to valid `0002b52a` (`CommonwealthClear`).
- Time apply now converts `timeHHmm` to Fallout game-hour float before executing `set gamehour`, fixing 2200-style values applying incorrectly.
- Corrected `CO_Vault109StartQuestScript.psc` to compile against Fallout 4 Papyrus by replacing unsupported `Actor.ShowLooksMenu`/`UI.IsMenuOpen` calls with `Game.ShowRaceMenu` and `Utility.IsInMenuMode`.
- Race customization now forces third-person camera and a short pre-menu settle delay before `ShowRaceMenu`, fixing cases where the menu opens but the player model is not visible.
- `CO_Vault109StartQuestScript` now uses configurable `RaceMenuMode` (default `0`) plus `RaceMenuFallbackMode` (default `1`) for `ShowRaceMenu`.
- Race menu open flow now retries with fallback mode if the primary mode does not enter menu mode.
- Moved first-time character customization menu control out of `CO_Vault109StartQuestScript.psc`; the quest now only handles spawn/stage while plugin C++ session-launch flow owns Looks/SPECIAL sequencing.
- First-time C++ character creation now prepares camera context with `ForceThirdPerson`, opens Looks via `ShowLooksMenu 14`, retries with `Actor.ShowLooksMenu` fallback, and times out/reset safely if Looks menu never opens.
- When `LooksMenu` is confirmed open, C++ now applies chargen camera context (`SetInChargen 1` + `ForceThirdPerson`) and restores it on close, fixing first-person camera persistence during customization.
- Looks menu camera correction now directly sets `PlayerCamera` to third-person state for several polls while `LooksMenu` is open, avoiding reliance on console-only camera commands.
- Looks menu camera correction now configures the `ThirdPersonState` for face editing by zeroing shoulder offsets, flipping camera yaw toward the player, enabling free rotation, and applying a closer zoom offset.
### Changed
- Weather presets store the exact `fw` console argument (8-digit hex); edit `server/world_state_presets.py` only if a preset ID fails in your console.
@@ -42,7 +51,7 @@ For testing notes, milestone summaries, known issues, and next steps, see [`docs
- Reformatted `changelog.md` to follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- First-time character creation launch now uses `coc COVault109` and waits for the `COVault109` start cell instead of `SanctuaryExt`.
- First-time `COVault109` launch now starts `CO_Vault109StartQuest` once per first-time join flow so the quest can move the player to its preset spawn marker.
- `CO_Vault109StartQuest` now opens `ShowRaceMenu(player, 1)` after stage-10 spawn completion, waits for menu mode to close, then opens `ShowSPECIALMenu` for name/S.P.E.C.I.A.L. setup.
## [0.0.1] - 2026-06-23
@@ -6,9 +6,10 @@ Event OnQuestInit()
If IsStageDone(10) == False
Utility.Wait(1.0)
Actor playerRef = Game.GetPlayer()
Debug.Notification("Vault 109 spawn quest ran")
Game.GetPlayer().MoveTo(Vault109SpawnMarker)
playerRef.MoveTo(Vault109SpawnMarker)
SetStage(10)
EndIf
EndEvent
EndEvent
+48 -1
View File
@@ -6198,4 +6198,51 @@ internet via port forwarding) can connect using the host PC's IP and port.
### Next Steps
- Test two PCs on the same LAN using the host machine's IP in Direct Connect.
- Add optional config file loading for `host` and `port` overrides.
- Add optional config file loading for `host` and `port` overrides.
---
## 2026-06-28 - Vault 109 post-spawn character setup flow
### Summary
Updated the Vault 109 start quest script so first-time spawn now chains directly into
character setup menus using compile-safe Fallout 4 Papyrus calls, then the SPECIAL/name setup menu.
### Files Changed
- `creation-kit/scripts/source/CO_Vault109StartQuestScript.psc`
- `plugin/src/F4TSessionLaunch.cpp`
- `changelog.md`
- `docs/dev-log.md`
### Details
- Kept the existing spawn behavior (move player to `Vault109SpawnMarker`) and stage completion gate (`SetStage(10)`).
- Reverted quest-side menu orchestration: `CO_Vault109StartQuestScript.psc` now only teleports to `Vault109SpawnMarker` and sets stage 10.
- First-time character setup UI is now fully owned by plugin C++ session-launch flow (`Actor.ShowLooksMenu` then `ShowSPECIALMenu`) instead of Papyrus `ShowRaceMenu`.
- Added C++ camera prep before Looks (`ForceThirdPerson`) without forcing global chargen state.
- Changed primary Looks open path to C++ console `ShowLooksMenu 14` for compatibility in this runtime.
- Added guarded fallback to existing `Actor.ShowLooksMenu` call if Looks does not open quickly, plus a max-open timeout that resets first-time state instead of hanging indefinitely.
- Added post-open camera correction: when `LooksMenu` is detected open, C++ applies `SetInChargen 1` + `ForceThirdPerson`, then restores with `SetInChargen 0` after Looks closes.
- Added direct `PlayerCamera` third-person state correction and repeated short camera nudges while `LooksMenu` remains open, so the menu cannot remain stuck in first-person due to ignored console camera commands.
- Added face-edit camera tuning on `ThirdPersonState`: zero shoulder offsets, front-facing yaw based on player heading, free rotation, and closer zoom.
### Testing
- Not run in this session (Creation Kit/Papyrus runtime not available in this environment).
- Expected flow:
- Start first-time join flow that triggers `CO_Vault109StartQuest`.
- Player is moved to `Vault109SpawnMarker`.
- C++ first attempts `ShowLooksMenu 14` with camera prep; if it fails to open, fallback `Actor.ShowLooksMenu` is issued.
- Looks menu opens from C++ first-time flow after Vault 109 spawn quest completes.
- On first LooksMenu-open detection, camera context is re-applied so customization does not remain in first-person.
- Direct `PlayerCamera`/`ThirdPersonState` face-camera correction runs for the first few LooksMenu polls.
- On exiting Looks menu, SPECIAL menu opens from C++.
- Stage 10 remains completed and quest does not re-run on subsequent init.
- Verify in logs:
- `CommonwealthOnline.log` for quest trigger ordering around first-time launch.
- Papyrus logs for timeout traces only if menu open/close fails.
### Known Issues
- Not yet runtime-validated in-game; menu timing can vary with load state and other UI transitions.
### Next Steps
- Run in-game first-time flow and verify C++ LooksMenu then SPECIAL menu sequence.
- If SPECIAL does not reliably include name entry in your setup, evaluate explicit `ShowNameMenu` before or after SPECIAL based on observed FO4 behavior.
+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;
}