Start first-time join in COVault109 and trigger quest

Change first-time session launch to use the COVault109 start cell and invoke a start quest so the quest can place the player at its preset spawn. Replaces coc SanctuaryExt with coc COVault109, changes the spawn-detection to match the COVault109 editor ID, adds a g_vault109StartQuestIssued flag and IssueVault109StartQuest() to run `startquest CO_Vault109StartQuest` once, and updates logs and reset logic. Also updates docs (changelog.md, docs/dev-log.md) and includes the updated CommonwealthOnline_Vault109.esp binary.
This commit is contained in:
2026-06-26 17:59:57 +12:00
parent 8772e024f8
commit 904394999a
4 changed files with 66 additions and 24 deletions
+2
View File
@@ -40,6 +40,8 @@ For testing notes, milestone summaries, known issues, and next steps, see [`docs
- World time no longer re-applies on every `worldState` heartbeat; weather heartbeats continue at ~1 Hz while time stays in sync during normal gameplay.
- Updated Weather/Time UI and protocol wording to use world-state host terminology instead of hardcoded player-1 wording.
- 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.
## [0.0.1] - 2026-06-23
Binary file not shown.
+33
View File
@@ -9,6 +9,39 @@ failed experiments, successful tests, and next steps.
---
## 2026-06-26 - First-Time Join Starts in `COVault109`
### Summary
Updated first-time session launch flow to start in the custom `COVault109` cell from `CommonwealthOnline_Vault109.esp` instead of `SanctuaryExt`.
### Files Changed
- `plugin/src/F4TSessionLaunch.cpp`
- `changelog.md`
- `docs/dev-log.md`
### Details
- Replaced first-time launch command from `coc SanctuaryExt` to `coc COVault109`.
- Updated first-time spawn readiness checks to validate the loaded parent cell by editor ID `COVault109`.
- Added first-time-flow quest trigger to run `startquest CO_Vault109StartQuest` once after entering `COVault109` so the quest script can move the player to its preset location.
- Updated first-time flow logging and timeout messages to reference the new start cell name.
### Testing
- Not run in-game in this session.
- Recommended manual checks:
- Start the server and connect one Fallout 4 instance from main menu for first-time join; confirm launch uses `COVault109`.
- Complete Looks/SPECIAL flow and confirm profile save binding still completes.
- Reconnect to the same server and confirm associated save load behavior remains unchanged.
- Connect two Fallout 4 instances and verify transform packets still send/relay, remote state parsing still works, and proxy actor movement remains visible in `F4TTestCell01`.
- Check `CommonwealthOnline.log` for `coc COVault109` and first-time flow progress logs.
### Known Issues
- If `CommonwealthOnline_Vault109.esp` is missing or the `COVault109` editor ID changes, first-time launch will timeout waiting for the start cell.
### Next Steps
- If needed, add a fallback path or explicit user-facing error when `COVault109` cannot be loaded.
---
## 2026-06-26 - Deleted Save Recreates First-Time Flow
### Summary
+31 -24
View File
@@ -21,8 +21,7 @@
namespace
{
constexpr std::uint32_t kCommonwealthWorldspaceFormId = 0x0000003C;
constexpr std::uint32_t kSanctuaryExtCellFormId = 0x0000DD60;
constexpr std::string_view kFirstJoinStartCellEditorId = "COVault109";
constexpr int kSpawnPollIntervalFrames = 15;
constexpr int kMaxSpawnWaitAttempts = 600;
constexpr int kWorldReadyDelayPolls = 10;
@@ -43,6 +42,7 @@ namespace
CharGenPhase g_charGenPhase = CharGenPhase::None;
bool g_pendingProfileBind = false;
bool g_cocIssued = false;
bool g_vault109StartQuestIssued = false;
bool g_sawLooksMenuOpen = false;
bool g_sawSpecialMenuOpen = false;
int g_spawnPollFrameCounter = 0;
@@ -117,7 +117,7 @@ namespace
return a_saveName.size() >= 8 && _strnicmp(a_saveName.data(), "Exitsave", 8) == 0;
}
bool IsPlayerInSanctuaryExt(const RE::PlayerCharacter& a_player)
bool IsPlayerInFirstJoinStartCell(const RE::PlayerCharacter& a_player)
{
if (IsPlaceholderSpawnPosition(a_player)) {
return false;
@@ -128,16 +128,8 @@ namespace
return false;
}
if (parentCell->GetFormID() == kSanctuaryExtCellFormId) {
return true;
}
if (!parentCell->IsExterior()) {
return false;
}
const auto* worldspace = parentCell->worldSpace ? parentCell->worldSpace : a_player.cachedWorldspace;
return worldspace && worldspace->GetFormID() == kCommonwealthWorldspaceFormId;
const auto* editorId = parentCell->GetFormEditorID();
return editorId && _stricmp(editorId, kFirstJoinStartCellEditorId.data()) == 0;
}
void LogPlayerLocation(std::string_view a_label, const RE::PlayerCharacter& a_player)
@@ -258,6 +250,7 @@ namespace
g_pendingProfileBind = false;
g_pendingProfileBindServerKey.clear();
g_cocIssued = false;
g_vault109StartQuestIssued = false;
g_sawLooksMenuOpen = false;
g_sawSpecialMenuOpen = false;
g_spawnPollFrameCounter = 0;
@@ -271,6 +264,7 @@ namespace
g_pendingProfileBind = true;
g_pendingProfileBindServerKey = a_serverKey;
g_cocIssued = false;
g_vault109StartQuestIssued = false;
g_sawLooksMenuOpen = false;
g_sawSpecialMenuOpen = false;
g_spawnPollFrameCounter = 0;
@@ -279,15 +273,26 @@ namespace
LogInfo(std::format("Queued first-time coc join for server {}.", a_serverKey));
}
void IssueCocSanctuaryExt()
void IssueCocFirstJoinStartCell()
{
if (g_cocIssued) {
return;
}
g_cocIssued = true;
RE::Console::ExecuteCommand("coc SanctuaryExt");
LogInfo("Issued coc SanctuaryExt from main menu.");
RE::Console::ExecuteCommand("coc COVault109");
LogInfo("Issued coc COVault109 from main menu.");
}
void IssueVault109StartQuest()
{
if (g_vault109StartQuestIssued) {
return;
}
g_vault109StartQuestIssued = true;
RE::Console::ExecuteCommand("startquest CO_Vault109StartQuest");
LogInfo("Issued startquest CO_Vault109StartQuest for first-time COVault109 flow.");
}
void OpenLooksMenu()
@@ -406,13 +411,13 @@ namespace
if (g_charGenPhase == CharGenPhase::CocLoading) {
if (!g_cocIssued) {
IssueCocSanctuaryExt();
IssueCocFirstJoinStartCell();
}
const auto* player = RE::PlayerCharacter::GetSingleton();
if (!player || !IsPlayerInSanctuaryExt(*player)) {
if (!player || !IsPlayerInFirstJoinStartCell(*player)) {
if (++g_spawnWaitAttempts >= kMaxSpawnWaitAttempts) {
LogWarning("Timed out waiting for coc SanctuaryExt to finish loading.");
LogWarning("Timed out waiting for coc COVault109 to finish loading.");
ResetFirstJoinState();
}
return;
@@ -421,16 +426,18 @@ namespace
g_charGenPhase = CharGenPhase::WaitingForWorldReady;
g_spawnWaitAttempts = 0;
g_worldReadyPolls = 0;
LogPlayerLocation("SanctuaryExt loaded, waiting for loading screen to finish", *player);
LogPlayerLocation("COVault109 loaded, waiting for loading screen to finish", *player);
return;
}
if (g_charGenPhase == CharGenPhase::WaitingForWorldReady) {
const auto* player = RE::PlayerCharacter::GetSingleton();
if (!player || !IsPlayerInSanctuaryExt(*player)) {
if (!player || !IsPlayerInFirstJoinStartCell(*player)) {
return;
}
IssueVault109StartQuest();
if (!IsWorldReadyForCharGen(*player)) {
return;
}
@@ -534,7 +541,7 @@ namespace F4T::SessionLaunch
LogWarning(std::format("Server profile '{}' has no bound save file; running first-time flow.", profileKey));
QueueFirstTimeCocJoin(profileKey);
IssueCocSanctuaryExt();
IssueCocFirstJoinStartCell();
return true;
}
@@ -578,7 +585,7 @@ namespace F4T::SessionLaunch
*saveFile,
profileKey));
QueueFirstTimeCocJoin(profileKey);
IssueCocSanctuaryExt();
IssueCocFirstJoinStartCell();
return true;
}
} else {
@@ -607,7 +614,7 @@ namespace F4T::SessionLaunch
}
QueueFirstTimeCocJoin(serverKey);
IssueCocSanctuaryExt();
IssueCocFirstJoinStartCell();
return true;
}