Add COVault109 solo-cell handling
Implement solo-cell logic to suppress transform packet sends and remote proxy representation while the local player is in COVault109, allowing first-time character creation to remain visually single-player. One entry location update is sent so other clients can hold stale proxies before suppression begins. Added F4TSoloCell.h helper for load-order-independent solo cell recognition by editor ID and cached FormID.
This commit is contained in:
@@ -10,6 +10,7 @@ For testing notes, milestone summaries, known issues, and next steps, see [`docs
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- `COVault109` solo-cell handling in the plugin so first-time character-creation space suppresses outgoing transform packets and remote proxy representation.
|
||||
- `compile-papyrus.bat` to compile active Papyrus sources from `creation-kit\scripts\source` into `creation-kit\scripts\compiled` using the Fallout 4 Creation Kit Papyrus compiler, resolving the Fallout 4 install with the same path workflow as `deploy-all.bat`, using saved `.fallout4-path` values without re-prompting during builds, and skipping reference-only `CoSync*.psc` files.
|
||||
- `build-all.bat` now compiles Papyrus scripts before staging the mod package, and still stages the repo build folder for inspection when Papyrus compilation fails.
|
||||
- `deploy-all.bat` now resolves the Fallout 4 path, compiles Papyrus scripts, stages the repo build folder, and then deploys from `build\Fallout 4\Data` into the Fallout 4 `Data` folder.
|
||||
|
||||
Binary file not shown.
+23
-15
@@ -37,23 +37,29 @@ both send and receive paths:
|
||||
`serverWorldState` to all clients for admin weather (`fw`) and time
|
||||
(`set gamehour to HHmm`) commands. Promoted hosts continue with their
|
||||
already-synced local time and weather so remaining clients should not see a jump.
|
||||
12. On the game-thread update path, the proxy controller reads a copied
|
||||
12. The plugin treats `COVault109` as a solo cell for first-time character
|
||||
creation. On entry it can send one location update so other clients hold any
|
||||
stale proxy, then outgoing transform packets are suppressed and remote proxy
|
||||
representation is skipped while the local player remains inside. Looks and
|
||||
SPECIAL setup therefore stay visually single-player. The relay protocol is
|
||||
unchanged; the rule is enforced client-side by the plugin.
|
||||
13. On the game-thread update path, the proxy controller reads a copied
|
||||
remote-player snapshot before attempting any actor work. The snapshot is
|
||||
sorted by `playerId`, and only remote players with valid transform data in
|
||||
the same cell are eligible for runtime proxy representation.
|
||||
13. For each eligible remote `playerId`, the controller maintains a
|
||||
14. For each eligible remote `playerId`, the controller maintains a
|
||||
`remotePlayerId -> runtime proxy slot` mapping. Each slot stores its
|
||||
`ObjectRefHandle`, lifecycle state, PlaceAtMe-backed spawn/candidate state,
|
||||
movement smoothing state, and data-only animation debug state. Stage 4 caps runtime
|
||||
proxy slots at four actors for testing.
|
||||
14. Runtime proxies are demand-driven. A slot executes the Stage 3.7
|
||||
15. Runtime proxies are demand-driven. A slot executes the Stage 3.7
|
||||
PlaceAtMe-backed spawn flow only when that remote player needs a same-cell
|
||||
representation, and only one PlaceAtMe spawn attempt may run at a time so
|
||||
candidate isolation remains unambiguous. Stage 4 captures the remote
|
||||
player's current valid transform before spawning and moves the isolated
|
||||
runtime actor to that transform immediately, so normal remote representation
|
||||
does not visually start at the local player offset.
|
||||
15. Runtime proxies receive a small game-thread neutralization pass when a
|
||||
16. Runtime proxies receive a small game-thread neutralization pass when a
|
||||
PlaceAtMe-backed actor is promoted, when a reusable slot is reassigned, while
|
||||
active on a throttle, and when moved or kept in holding. The pass only uses
|
||||
confirmed CommonLibF4 actor APIs/fields to stop combat, clear combat target
|
||||
@@ -63,60 +69,60 @@ both send and receive paths:
|
||||
alarm, bump, or package movement intent is observed. It does not disable,
|
||||
hide, delete, despawn, alpha-fade, or force animation graph events on the
|
||||
actor.
|
||||
16. Proxy visual updates are independent per slot. Normal movement is smoothed
|
||||
17. Proxy visual updates are independent per slot. Normal movement is smoothed
|
||||
toward that player’s latest target, while `cell_change`, `worldspace_change`,
|
||||
and `teleport` snap directly. Movement and animation-related remote state is
|
||||
still observed and logged only; no visual crouch/sneak, jump, weapon drawn,
|
||||
locomotion, animation graph, or actor-state application is attempted.
|
||||
17. When a remote player leaves the cell or disconnects, only that player’s
|
||||
18. When a remote player leaves the cell or disconnects, only that player’s
|
||||
runtime proxy is moved to the hidden holding position inside
|
||||
`F4TTestCell01`. Runtime actors are not disabled or deleted. Connected
|
||||
players who leave the cell keep their slot reserved, while disconnected
|
||||
held slots become reusable only after the configured grace period. Runtime
|
||||
slots use per-slot holding positions with spacing and a game-thread
|
||||
correction check so held actors do not stack or drift back into view.
|
||||
18. The placed fallback reference `F4TProxyRemotePlayer01REF` remains a single
|
||||
19. The placed fallback reference `F4TProxyRemotePlayer01REF` remains a single
|
||||
backup representation. If runtime spawning fails for one deterministic
|
||||
selected remote player, the fallback can represent that one player only; it
|
||||
is not used for multiple remote players.
|
||||
19. As part of the staged runtime proxy actor manager work, the same game-thread
|
||||
20. As part of the staged runtime proxy actor manager work, the same game-thread
|
||||
proxy controller also logs whether `Fallout4Together_Test.esp` is loaded and
|
||||
whether the actor base `F4T_RemotePlayerProxy` resolves by editor ID.
|
||||
20. Stage 3.1 adds a temporary diagnostic layer around the single runtime proxy:
|
||||
21. Stage 3.1 adds a temporary diagnostic layer around the single runtime proxy:
|
||||
post-spawn visibility data, loaded-3D pointer checks where safe, console
|
||||
helper logs, active source logs, and throttled post-movement diagnostics.
|
||||
The diagnostic mode can force the runtime proxy near the local player to
|
||||
prove whether the created actor is visibly renderable.
|
||||
21. Stage 3.2 adds a temporary near-player visibility hold for that same single
|
||||
22. Stage 3.2 adds a temporary near-player visibility hold for that same single
|
||||
runtime proxy. During the hold, remote state is still selected and validated,
|
||||
but runtime proxy restore and remote movement are deferred long enough to
|
||||
test whether a runtime-created actor ref can load visible 3D while kept near
|
||||
the local player.
|
||||
22. Stage 3.3 adds a temporary vanilla actor-base diagnostic layer. It attempts
|
||||
23. Stage 3.3 adds a temporary vanilla actor-base diagnostic layer. It attempts
|
||||
to resolve Codsworth as a visible vanilla `TESNPC` candidate, then passes
|
||||
that actor base into the same runtime spawn, handle validation, near-player
|
||||
hold, and visibility diagnostic path. This isolates custom actor-base issues
|
||||
from runtime-spawn/init issues without changing the final architecture.
|
||||
23. Stage 3.4 adds a temporary absolute FormID and existing-reference diagnostic
|
||||
24. Stage 3.4 adds a temporary absolute FormID and existing-reference diagnostic
|
||||
layer. It compares Codsworth base-form runtime spawning against moving the
|
||||
existing placed Codsworth reference near the local player as a visibility
|
||||
control. This separates runtime actor creation/init problems from vanilla
|
||||
actor-base or placed-reference movement problems and does not change the
|
||||
final architecture.
|
||||
24. Stage 3.5 adds a temporary PlaceAtMe-equivalent diagnostic after manual
|
||||
25. Stage 3.5 adds a temporary PlaceAtMe-equivalent diagnostic after manual
|
||||
console validation proved `player.placeatme 000179FF 1` can spawn visible
|
||||
Codsworth in `F4TTestCell01`. The plugin executes that console placement path
|
||||
from the local player, resolves the newly created Codsworth actor near the
|
||||
player, and holds/logs it separately from the `CreateReferenceAtLocation`
|
||||
runtime proxy path.
|
||||
25. Stage 3.6 refines that temporary PlaceAtMe diagnostic layer. It snapshots
|
||||
26. Stage 3.6 refines that temporary PlaceAtMe diagnostic layer. It snapshots
|
||||
known Codsworth refs before executing `player.placeatme 000179FF 1`, accepts
|
||||
only a newly isolated Codsworth ref for the primary diagnostic, and preserves
|
||||
that actor at the original console-spawned location during a short settle
|
||||
window. This tests whether the actor becomes visible naturally or requires a
|
||||
render/process refresh such as the manually observed pause-menu or alt-tab
|
||||
behavior. Stage 3.6 does not change the final architecture.
|
||||
26. Stage 3.7 promotes the working PlaceAtMe-style spawn pattern into the
|
||||
27. Stage 3.7 promotes the working PlaceAtMe-style spawn pattern into the
|
||||
preferred single runtime proxy spawn path. The proxy controller uses the
|
||||
resolved runtime FormID for `F4T_RemotePlayerProxy` when available, falls
|
||||
back to Codsworth only for diagnostics, defers active proxy selection during
|
||||
@@ -155,6 +161,8 @@ The native plugin is responsible for:
|
||||
- Loading into Fallout 4
|
||||
- Reading local player state
|
||||
- Sending local player data to the server
|
||||
- Suppressing local transform sends and remote proxy representation while the
|
||||
local player is in solo cells such as `COVault109`
|
||||
- Adding basic data-only movement state to transform packets
|
||||
- Throttling normal transform sends separately from readable local movement logs
|
||||
- Receiving server packets on a background thread
|
||||
|
||||
@@ -9,6 +9,41 @@ failed experiments, successful tests, and next steps.
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-29 - `COVault109` Solo Cell
|
||||
|
||||
### Summary
|
||||
Made the first-time `COVault109` starting cell single-player from the multiplayer sync perspective so character customization and SPECIAL setup are not disturbed by remote player proxies.
|
||||
|
||||
### Files Changed
|
||||
- `plugin/include/F4TSoloCell.h`
|
||||
- `plugin/src/main.cpp`
|
||||
- `plugin/src/F4TProxyActorController.cpp`
|
||||
- `changelog.md`
|
||||
- `docs/dev-log.md`
|
||||
- `docs/architecture.md`
|
||||
- `protocol/player-sync.md`
|
||||
|
||||
### Details
|
||||
- Added a shared solo-cell helper that recognizes `COVault109` by editor ID and cached runtime FormID, avoiding load-order-dependent hardcoded IDs.
|
||||
- Sends one entry location update for `COVault109`, then suppresses outgoing local transform packets while the local player remains inside so other clients can hold stale proxies and stop receiving fresh position updates for a player in character creation.
|
||||
- Suppressed remote proxy representation while the local player is inside `COVault109`; existing proxy slots are held instead of being moved or restored in the starting cell.
|
||||
- Kept the Python relay and transform packet structure unchanged.
|
||||
|
||||
### Testing
|
||||
- Not run in-game in this session.
|
||||
- Expected result: two connected clients can both be in `COVault109` without seeing runtime proxies in that cell.
|
||||
- Expected result: leaving `COVault109` resumes transform sending and normal same-cell proxy behavior outside the solo cell.
|
||||
|
||||
### Known Issues
|
||||
- The solo-cell list is currently code-defined and contains only `COVault109`.
|
||||
|
||||
### Next Steps
|
||||
- Run two Fallout 4 instances through first-time character creation and confirm no proxy actors appear in `COVault109`.
|
||||
- Check `CommonwealthOnline.log` for solo-cell suppression/resume messages.
|
||||
- Verify normal transform and proxy sync still works after leaving `COVault109`.
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-28 - Papyrus Build Integration
|
||||
|
||||
### Summary
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
namespace F4T::SoloCell
|
||||
{
|
||||
inline constexpr auto kVault109EditorId = "COVault109";
|
||||
|
||||
inline std::optional<std::uint32_t>& GetVault109CellIdCache()
|
||||
{
|
||||
static std::optional<std::uint32_t> s_vault109CellId;
|
||||
return s_vault109CellId;
|
||||
}
|
||||
|
||||
inline bool HasEditorId(const RE::TESObjectCELL& a_cell, std::string_view a_editorId)
|
||||
{
|
||||
const auto* cellEditorId = a_cell.GetFormEditorID();
|
||||
return cellEditorId && a_editorId == cellEditorId;
|
||||
}
|
||||
|
||||
inline bool IsSoloCell(const RE::TESObjectCELL& a_cell)
|
||||
{
|
||||
if (HasEditorId(a_cell, kVault109EditorId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto& vault109CellId = GetVault109CellIdCache();
|
||||
if (!vault109CellId) {
|
||||
const auto* vault109Cell = RE::TESForm::GetFormByEditorID<RE::TESObjectCELL>(
|
||||
RE::BSFixedString{ kVault109EditorId });
|
||||
if (vault109Cell) {
|
||||
vault109CellId = vault109Cell->GetFormID();
|
||||
}
|
||||
}
|
||||
|
||||
return vault109CellId && a_cell.GetFormID() == *vault109CellId;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "F4RemoteActionComponent.h"
|
||||
#include "F4TInterpolationSystem.h"
|
||||
#include "F4TProxyGameAPI.h"
|
||||
#include "F4TSoloCell.h"
|
||||
|
||||
#include "RE/A/ACTOR_STANCE.h"
|
||||
#include "RE/A/ActorValue.h"
|
||||
@@ -1700,6 +1701,13 @@ namespace
|
||||
const RE::PlayerCharacter& a_player,
|
||||
const RE::TESObjectCELL& a_parentCell)
|
||||
{
|
||||
if (F4T::SoloCell::IsSoloCell(a_parentCell)) {
|
||||
LogThrottledInfo(
|
||||
"solo_cell_proxy_suppressed",
|
||||
"COVault109 is a solo cell; remote proxy representation is suppressed while local player is inside.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto remoteCellId = ParseHexFormId(a_remotePlayer.cellId);
|
||||
if (!remoteCellId) {
|
||||
LogThrottledWarning(
|
||||
|
||||
+37
-1
@@ -6,6 +6,7 @@
|
||||
#include "F4TProxyActorController.h"
|
||||
#include "F4TProxyAnimationSync.h"
|
||||
#include "F4TProxyPuppet.h"
|
||||
#include "F4TSoloCell.h"
|
||||
#include "F4TWorldStateSync.h"
|
||||
|
||||
#include "RE/A/ACTOR_STANCE.h"
|
||||
@@ -34,6 +35,7 @@ namespace
|
||||
std::uint32_t cellId;
|
||||
bool hasWorldspaceId;
|
||||
std::uint32_t worldspaceId;
|
||||
bool isSoloCell;
|
||||
};
|
||||
|
||||
struct PlayerMovementState
|
||||
@@ -133,13 +135,15 @@ namespace
|
||||
false,
|
||||
0,
|
||||
false,
|
||||
0
|
||||
0,
|
||||
false
|
||||
};
|
||||
|
||||
const auto* parentCell = a_player.GetParentCell();
|
||||
if (parentCell) {
|
||||
location.hasCellId = true;
|
||||
location.cellId = parentCell->GetFormID();
|
||||
location.isSoloCell = F4T::SoloCell::IsSoloCell(*parentCell);
|
||||
}
|
||||
|
||||
if (a_player.cachedWorldspace) {
|
||||
@@ -480,6 +484,7 @@ namespace
|
||||
static auto lastDebugSampleTime = std::chrono::steady_clock::time_point{};
|
||||
static auto debugMovingHoldUntil = std::chrono::steady_clock::time_point{};
|
||||
static auto networkMovingHoldUntil = std::chrono::steady_clock::time_point{};
|
||||
static bool soloCellTransformSuppressed = false;
|
||||
|
||||
const auto currentTransform = GetPlayerTransform(*player);
|
||||
const auto currentLocation = GetPlayerLocation(*player);
|
||||
@@ -501,6 +506,29 @@ namespace
|
||||
debugMovementState.weaponDrawn,
|
||||
debugMovementState.movementSpeed,
|
||||
currentTime);
|
||||
|
||||
if (currentLocation.isSoloCell && soloCellTransformSuppressed) {
|
||||
lastSentTransform = currentTransform;
|
||||
lastDebugSampleTransform = currentTransform;
|
||||
lastKnownLocation = currentLocation;
|
||||
lastSentMovementState = debugMovementState;
|
||||
lastSendTime = currentTime;
|
||||
lastLogTime = currentTime;
|
||||
lastDebugSampleTime = currentTime;
|
||||
hasLastTransform = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentLocation.isSoloCell) {
|
||||
LogInfoWithLocalPlayerPrefix("COVault109 is a solo cell; sending entry location update before suppressing further transforms.");
|
||||
}
|
||||
|
||||
if (soloCellTransformSuppressed) {
|
||||
LogInfoWithLocalPlayerPrefix("Local player left solo cell; transform packet sending resumed.");
|
||||
soloCellTransformSuppressed = false;
|
||||
}
|
||||
|
||||
if (!hasLastTransform) {
|
||||
lastSentTransform = currentTransform;
|
||||
lastDebugSampleTransform = currentTransform;
|
||||
@@ -538,6 +566,9 @@ namespace
|
||||
std::format(", worldspaceId={:08X}", currentLocation.worldspaceId) :
|
||||
std::string{}));
|
||||
}
|
||||
if (currentLocation.isSoloCell) {
|
||||
soloCellTransformSuppressed = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -627,6 +658,11 @@ namespace
|
||||
LogPlayerTransform(currentTransform);
|
||||
lastLogTime = currentTime;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (currentLocation.isSoloCell) {
|
||||
soloCellTransformSuppressed = true;
|
||||
}
|
||||
|
||||
lastDebugSampleTransform = currentTransform;
|
||||
|
||||
@@ -31,6 +31,13 @@ Fallout 4 actors or references. If a remote player is not in the same
|
||||
cell/worldspace, the controller holds that player's proxy at a hidden in-cell
|
||||
position instead of leaving it at the last visible location.
|
||||
|
||||
`COVault109` is treated as a solo cell for the first-time character-creation
|
||||
flow. On entry the plugin can send one location update so other clients hold any
|
||||
stale proxy, then it suppresses outgoing transform packets while the local
|
||||
player remains inside and the proxy controller skips remote representation. This
|
||||
is a client-side visibility/isolation rule and does not change the transform
|
||||
packet schema.
|
||||
|
||||
## Current Remote Player State Model
|
||||
|
||||
The plugin and fake client store remote players by `playerId`.
|
||||
@@ -97,6 +104,8 @@ player exists.
|
||||
resuming smoothing.
|
||||
- Transform packets include movement state data (`isMoving`, `movementSpeed`,
|
||||
`isSprinting`, `isSneaking`, `isJumping`, `weaponDrawn`).
|
||||
- The plugin suppresses transform sends and proxy representation in the
|
||||
`COVault109` solo cell.
|
||||
- The game-thread proxy controller applies confirmed Havok animation graph
|
||||
variables to runtime proxy actors from that remote state (locomotion, sneak,
|
||||
jump, weapon drawn). Transform position/heading sync is unchanged.
|
||||
|
||||
Reference in New Issue
Block a user