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:
+10
-1
@@ -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,8 +6,9 @@ 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
|
||||
|
||||
@@ -6199,3 +6199,50 @@ 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.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user