Restore LooksMenu camera state

Capture the pre-LooksMenu third-person camera settings and restore them when character customization closes, so first-time customization returns to the normal over-the-shoulder view and movement controls. Also clear the saved snapshot during reset paths so cleanup still runs if the menu flow exits early.
This commit is contained in:
2026-06-28 18:59:59 +12:00
parent 22394a77ff
commit 2f37feb5b7
3 changed files with 133 additions and 3 deletions
+1
View File
@@ -42,6 +42,7 @@ For testing notes, milestone summaries, known issues, and next steps, see [`docs
- 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.
- Looks menu camera cleanup now restores the pre-customization third-person offsets, yaw, zoom, and free-rotation flags so movement controls return to normal after first-time customization closes.
### 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.
+28
View File
@@ -6246,3 +6246,31 @@ character setup menus using compile-safe Fallout 4 Papyrus calls, then the SPECI
### 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.
---
## 2026-06-28 - Looks menu camera restoration
### Summary
Fixed the first-time character customization close path so the third-person camera returns to its pre-LooksMenu state instead of staying in the front-facing face-edit view.
### Files Changed
- `plugin/src/F4TSessionLaunch.cpp`
- `changelog.md`
- `docs/dev-log.md`
### Details
- Added a pre-LooksMenu third-person camera snapshot before the C++ flow forces the face-edit camera.
- Restored shoulder offsets, zoom offsets, yaw, free-rotation state, model visibility, and offset application flags when LooksMenu closes.
- Made reset/retry cleanup restore the snapshot even if the menu flow exits before `SetInChargen 1` was applied.
### Testing
- Not run in this session (Fallout 4/F4SE runtime not available in this environment).
- Expected manual check: after closing LooksMenu during first-time server join, third-person camera returns behind the player and `W`/`S`/strafe inputs move normally.
- Verify in `CommonwealthOnline.log` that the flow logs `Restored third-person camera state after Looks menu.` before opening SPECIAL.
### Known Issues
- Not yet runtime-validated in-game.
### Next Steps
- Run first-time join through Vault 109, close LooksMenu, then confirm camera offset and movement controls before and after SPECIAL closes.
+102 -1
View File
@@ -33,6 +33,24 @@ namespace
constexpr float kPi = 3.14159265358979323846F;
constexpr float kLooksMenuFaceCameraZoomOffset = -90.0F;
struct ThirdPersonCameraSnapshot
{
RE::NiPoint3 targetShoulderOffset;
RE::NiPoint3 currentShoulderOffset;
RE::NiPoint3 sideOffsetCollisionBlend;
RE::NiPoint2 freeRotation;
float targetZoomOffset;
float currentZoomOffset;
float targetYaw;
float currentYaw;
float cameraHeightAdjust;
bool freeRotationEnabled;
bool zoomingInto1st;
bool show3rdPersonModel;
bool preserveRotation;
bool applyOffsets;
};
enum class CharGenPhase
{
None,
@@ -54,6 +72,7 @@ namespace
bool g_sawSpecialMenuOpen = false;
bool g_looksMenuFallbackIssued = false;
bool g_looksMenuCameraContextApplied = false;
std::optional<ThirdPersonCameraSnapshot> g_preLooksCameraSnapshot;
int g_spawnPollFrameCounter = 0;
int g_spawnWaitAttempts = 0;
int g_worldReadyPolls = 0;
@@ -266,6 +285,81 @@ namespace
return a_angle;
}
RE::ThirdPersonState* GetThirdPersonCameraState()
{
auto* camera = RE::PlayerCamera::GetSingleton();
if (!camera) {
return nullptr;
}
auto thirdPersonState = camera->GetState(RE::CameraState::k3rdPerson);
return thirdPersonState ? static_cast<RE::ThirdPersonState*>(thirdPersonState.get()) : nullptr;
}
void CapturePreLooksCameraState()
{
if (g_preLooksCameraSnapshot) {
return;
}
auto* state = GetThirdPersonCameraState();
if (!state) {
LogWarning("Third-person camera state unavailable while capturing pre-Looks camera state.");
return;
}
g_preLooksCameraSnapshot = ThirdPersonCameraSnapshot{
state->targetShoulderOffset,
state->currentShoulderOffset,
state->sideOffsetCollisionBlend,
state->freeRotation,
state->targetZoomOffset,
state->currentZoomOffset,
state->targetYaw,
state->currentYaw,
state->cameraHeightAdjust,
state->freeRotationEnabled,
state->zoomingInto1st,
state->show3rdPersonModel,
state->preserveRotation,
state->applyOffsets
};
}
void RestorePreLooksCameraState()
{
if (!g_preLooksCameraSnapshot) {
return;
}
auto* state = GetThirdPersonCameraState();
if (!state) {
LogWarning("Third-person camera state unavailable while restoring pre-Looks camera state.");
g_preLooksCameraSnapshot.reset();
return;
}
const auto snapshot = *g_preLooksCameraSnapshot;
state->SetFreeRotationMode(snapshot.freeRotationEnabled, false);
state->targetShoulderOffset = snapshot.targetShoulderOffset;
state->currentShoulderOffset = snapshot.currentShoulderOffset;
state->sideOffsetCollisionBlend = snapshot.sideOffsetCollisionBlend;
state->freeRotation = snapshot.freeRotation;
state->targetZoomOffset = snapshot.targetZoomOffset;
state->currentZoomOffset = snapshot.currentZoomOffset;
state->targetYaw = snapshot.targetYaw;
state->currentYaw = snapshot.currentYaw;
state->cameraHeightAdjust = snapshot.cameraHeightAdjust;
state->freeRotationEnabled = snapshot.freeRotationEnabled;
state->zoomingInto1st = snapshot.zoomingInto1st;
state->show3rdPersonModel = snapshot.show3rdPersonModel;
state->preserveRotation = snapshot.preserveRotation;
state->applyOffsets = snapshot.applyOffsets;
g_preLooksCameraSnapshot.reset();
LogInfo("Restored third-person camera state after Looks menu.");
}
bool ForcePlayerThirdPersonCamera()
{
auto* camera = RE::PlayerCamera::GetSingleton();
@@ -307,6 +401,7 @@ namespace
void PrepareCharGenCameraContext()
{
CapturePreLooksCameraState();
ForcePlayerThirdPersonCamera();
RE::Console::ExecuteCommand("ForceThirdPerson");
LogInfo("Prepared camera context before Looks menu.");
@@ -319,6 +414,7 @@ namespace
}
RE::Console::ExecuteCommand("SetInChargen 1");
CapturePreLooksCameraState();
ForcePlayerThirdPersonCamera();
RE::Console::ExecuteCommand("ForceThirdPerson");
g_looksMenuCameraContextApplied = true;
@@ -328,11 +424,14 @@ namespace
void RestoreCharGenCameraContext()
{
if (!g_looksMenuCameraContextApplied) {
if (!g_looksMenuCameraContextApplied && !g_preLooksCameraSnapshot) {
return;
}
if (g_looksMenuCameraContextApplied) {
RE::Console::ExecuteCommand("SetInChargen 0");
}
RestorePreLooksCameraState();
g_looksMenuCameraContextApplied = false;
g_looksMenuCameraNudgePolls = 0;
LogInfo("Restored chargen camera context after Looks menu.");
@@ -350,6 +449,7 @@ namespace
g_sawSpecialMenuOpen = false;
g_looksMenuFallbackIssued = false;
g_looksMenuCameraContextApplied = false;
g_preLooksCameraSnapshot.reset();
g_spawnPollFrameCounter = 0;
g_spawnWaitAttempts = 0;
g_worldReadyPolls = 0;
@@ -369,6 +469,7 @@ namespace
g_sawSpecialMenuOpen = false;
g_looksMenuFallbackIssued = false;
g_looksMenuCameraContextApplied = false;
g_preLooksCameraSnapshot.reset();
g_spawnPollFrameCounter = 0;
g_spawnWaitAttempts = 0;
g_worldReadyPolls = 0;