mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-14 07:47:13 +00:00
Merge exp/seamless-transfer-2: seamless instance moves keep the player in place
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
@@ -499,8 +499,9 @@ void EntityManager::SerializeEntity(const Entity& entity) {
|
||||
}
|
||||
}
|
||||
|
||||
void EntityManager::DestructAllEntities(const SystemAddress& sysAddr) {
|
||||
void EntityManager::DestructAllEntities(const SystemAddress& sysAddr, const LWOOBJID except) {
|
||||
for (auto* entity : m_Entities | std::views::values) {
|
||||
if (entity && except != LWOOBJID_EMPTY && entity->GetObjectID() == except) continue;
|
||||
DestructEntity(entity, sysAddr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,8 @@ public:
|
||||
void SerializeEntity(const Entity& entity);
|
||||
|
||||
void ConstructAllEntities(const SystemAddress& sysAddr);
|
||||
void DestructAllEntities(const SystemAddress& sysAddr);
|
||||
// Takes every object away from one client, except `except` when given
|
||||
void DestructAllEntities(const SystemAddress& sysAddr, LWOOBJID except = LWOOBJID_EMPTY);
|
||||
|
||||
void SetGhostDistanceMax(float value);
|
||||
void SetGhostDistanceMin(float value);
|
||||
|
||||
@@ -227,12 +227,20 @@ void CharacterComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInit
|
||||
outBitStream.Write(m_RacesFinished);
|
||||
outBitStream.Write(m_FirstPlaceRaceFinishes);
|
||||
|
||||
outBitStream.Write0();
|
||||
outBitStream.Write(m_IsLanding);
|
||||
if (m_IsLanding) {
|
||||
outBitStream.Write<uint16_t>(m_LastRocketConfig.size());
|
||||
for (uint16_t character : m_LastRocketConfig) {
|
||||
outBitStream.Write(character);
|
||||
// The world transition state, two bits: 0 none, 1 arriving by rocket, 2 leaving. The client's
|
||||
// LWOCharacterComponent::PostCharacterLoaded (0x00c71660) fades the screen in from white and pushes its
|
||||
// gameplay state again when it reads 0. A client that kept its scene gets 3, which nothing acts on.
|
||||
if (m_KeepClientScene) {
|
||||
outBitStream.Write1();
|
||||
outBitStream.Write1();
|
||||
} else {
|
||||
outBitStream.Write0();
|
||||
outBitStream.Write(m_IsLanding);
|
||||
if (m_IsLanding) {
|
||||
outBitStream.Write<uint16_t>(m_LastRocketConfig.size());
|
||||
for (uint16_t character : m_LastRocketConfig) {
|
||||
outBitStream.Write(character);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,13 @@ public:
|
||||
void SetLastCustomBuild(const std::u16string& tokenizedLOTList) { m_LastRocketConfig = tokenizedLOTList; }
|
||||
bool GetIsLanding() const { return m_IsLanding; }
|
||||
|
||||
/**
|
||||
* Constructions written while this is set tell the client there is no world transition to show (state 3 instead
|
||||
* of 0 or the rocket landing). For the one construction a client gets of its own player after a seamless instance
|
||||
* move (docs/SeamlessTransfer.md).
|
||||
*/
|
||||
void SetKeepClientScene(const bool value) { m_KeepClientScene = value; }
|
||||
|
||||
/**
|
||||
* Find a player's rocket
|
||||
* @param player the entity that triggered the event
|
||||
@@ -435,6 +442,11 @@ private:
|
||||
*/
|
||||
bool m_IsLanding;
|
||||
|
||||
/**
|
||||
* Whether constructions say there is no world transition to show (SetKeepClientScene)
|
||||
*/
|
||||
bool m_KeepClientScene = false;
|
||||
|
||||
/**
|
||||
* The configuration of the last used rocket, essentially a string of LOTs separated by commas
|
||||
*/
|
||||
|
||||
@@ -143,8 +143,11 @@ void ControllablePhysicsComponent::Serialize(RakNet::BitStream& outBitStream, bo
|
||||
if (!bIsInitialUpdate) m_DirtyBubble = false;
|
||||
}
|
||||
|
||||
outBitStream.Write(m_DirtyPosition || bIsInitialUpdate);
|
||||
if (m_DirtyPosition || bIsInitialUpdate) {
|
||||
// A client that kept its player across a seamless instance move knows where it stands better than the save does:
|
||||
// its construction leaves the frame out, and LWOBasePhysComponent::Deserialize (0x00bf5a10) then sets nothing
|
||||
const bool writePosition = bIsInitialUpdate ? !m_KeepClientPosition : m_DirtyPosition;
|
||||
outBitStream.Write(writePosition);
|
||||
if (writePosition) {
|
||||
outBitStream.Write(m_Position.x);
|
||||
outBitStream.Write(m_Position.y);
|
||||
outBitStream.Write(m_Position.z);
|
||||
|
||||
@@ -216,6 +216,13 @@ public:
|
||||
*/
|
||||
bool GetIsTeleporting() const { return m_IsTeleporting; }
|
||||
|
||||
/**
|
||||
* Constructions written while this is set carry no position, rotation or velocity. For the one construction a
|
||||
* client gets of its own player after a seamless instance move (docs/SeamlessTransfer.md): the client applies a
|
||||
* construction's frame to its player at once, which would pull it back to where it was saved.
|
||||
*/
|
||||
void SetKeepClientPosition(const bool value) { m_KeepClientPosition = value; }
|
||||
|
||||
/**
|
||||
* Returns the Physics entity for the component
|
||||
* @return Physics entity for the component
|
||||
@@ -400,6 +407,11 @@ private:
|
||||
*/
|
||||
void UpdateSpeedBoost();
|
||||
|
||||
/**
|
||||
* Whether constructions leave the position out (SetKeepClientPosition)
|
||||
*/
|
||||
bool m_KeepClientPosition = false;
|
||||
|
||||
/**
|
||||
* The list of speed boosts for this entity
|
||||
*/
|
||||
|
||||
@@ -70,7 +70,11 @@ namespace GameMessages {
|
||||
return;
|
||||
}
|
||||
const auto* const zoneControlEntity = Game::entityManager->GetZoneControlEntity();
|
||||
SendPlayerReady(entity->GetObjectID(), zoneControlEntity ? zoneControlEntity->GetObjectID() : LWOOBJID_EMPTY, sysAddr);
|
||||
// A client that kept its player across a seamless instance move is ready already; PlayerReady would only put
|
||||
// its camera's pitch back to the default (LWOCharacterComponent::iSendMessage, 0x00d34330)
|
||||
if (!WorldMigration::ArrivesSeamlessly(entity->GetObjectID())) {
|
||||
SendPlayerReady(entity->GetObjectID(), zoneControlEntity ? zoneControlEntity->GetObjectID() : LWOOBJID_EMPTY, sysAddr);
|
||||
}
|
||||
entity->SetPlayerReadyForUpdates();
|
||||
|
||||
// Venture vision from equipped items was sent before the client's UI existed on a fresh login
|
||||
|
||||
@@ -40,6 +40,10 @@ namespace {
|
||||
// Give up on a migration after this long moving; whoever is left stays (or is saved when the world shuts down)
|
||||
constexpr float MAX_MOVE_SECONDS = 150.0f;
|
||||
constexpr float STATUS_INTERVAL = 1.0f;
|
||||
// How long after it was sent a seamless arrival is expected (the reconnect takes a second or two)
|
||||
constexpr float SEAMLESS_ARRIVAL_SECONDS = 30.0f;
|
||||
// A client that arrived without reloading and hasn't sent PlayerLoaded after this gets it run for them
|
||||
constexpr float SEAMLESS_LOADED_FALLBACK_SECONDS = 5.0f;
|
||||
// Carried state for a player who never arrives
|
||||
constexpr float CARRIED_EXPIRY_SECONDS = 300.0f;
|
||||
|
||||
@@ -185,10 +189,13 @@ namespace {
|
||||
MasterPackets::SendToMaster(carried);
|
||||
}
|
||||
|
||||
// Seamless: take every object we sent away first. The client deletes them but keeps its own player object when
|
||||
// that ghost goes (LWOGhostComponent::OnGhostReceiveDestruction), and the target's construction of the
|
||||
// player is adopted by it. Same ordered channel as the transfer, so these arrive before it.
|
||||
if (order.seamless) Game::entityManager->DestructAllEntities(sysAddr);
|
||||
// Seamless: take every object we sent away first, except their own player. The client keeps its player object
|
||||
// either way (LWOGhostComponent::OnGhostReceiveDestruction, 0x00c99410), but losing its last ghost puts it out
|
||||
// of scope: it fades out, and fades back in with the login effect when the target constructs it
|
||||
// (LWOBaseRenderComponent::OnEnterScope, 0x00be2150). With its old ghost left alone, the target's
|
||||
// construction just takes over (LwoClientGhostManager::OnReceiveConstruction, 0x00b31c30). Same ordered
|
||||
// channel as the transfer, so these arrive before it.
|
||||
if (order.seamless) Game::entityManager->DestructAllEntities(sysAddr, playerId);
|
||||
|
||||
ClientPackets::TransferToWorld transfer;
|
||||
transfer.serverIP = LUString(order.targetIp);
|
||||
@@ -391,7 +398,14 @@ void WorldMigration::OnDisconnected(const SystemAddress& sysAddr) {
|
||||
|
||||
bool WorldMigration::ArrivesSeamlessly(LWOOBJID characterId) {
|
||||
const auto it = g_Carried.find(characterId);
|
||||
return it != g_Carried.end() && it->second.first.seamless;
|
||||
// Only for a while: a client that logs in again later has no scene and needs LOAD_STATIC_ZONE
|
||||
return it != g_Carried.end() && it->second.first.seamless && it->second.second <= SEAMLESS_ARRIVAL_SECONDS;
|
||||
}
|
||||
|
||||
void WorldMigration::KeepClientPlayer(Entity* player, const bool keep) {
|
||||
if (!player || (keep && !ArrivesSeamlessly(player->GetObjectID()))) return;
|
||||
if (auto* physics = player->GetComponent<ControllablePhysicsComponent>()) physics->SetKeepClientPosition(keep);
|
||||
if (auto* characterComponent = player->GetComponent<CharacterComponent>()) characterComponent->SetKeepClientScene(keep);
|
||||
}
|
||||
|
||||
void WorldMigration::OnSeamlessArrival(Entity* player) {
|
||||
@@ -399,10 +413,11 @@ void WorldMigration::OnSeamlessArrival(Entity* player) {
|
||||
const auto playerId = player->GetObjectID();
|
||||
const auto sysAddr = player->GetSystemAddress();
|
||||
LOG("Player %llu arrived without reloading the zone", playerId);
|
||||
// The client already finished loading on the old instance and won't send PlayerLoaded again: do what it would
|
||||
// trigger, once the constructions have had a moment to arrive
|
||||
player->AddCallbackTimer(1.0f, [playerId, sysAddr]() {
|
||||
if (!Game::entityManager->GetEntity(playerId)) return;
|
||||
// The client sends PlayerLoaded itself once its character got the construction
|
||||
// (LWOCharacterComponent::PostCharacterLoaded, 0x00c71660). Should that not come, do what it would trigger.
|
||||
player->AddCallbackTimer(SEAMLESS_LOADED_FALLBACK_SECONDS, [playerId, sysAddr]() {
|
||||
if (!g_Carried.contains(playerId) || !Game::entityManager->GetEntity(playerId)) return;
|
||||
LOG("Player %llu sent no PlayerLoaded after arriving without a reload, running it for them", playerId);
|
||||
RakNet::BitStream empty;
|
||||
GameMessageHandler::HandleMessage(empty, sysAddr, playerId, MessageType::Game::PLAYER_LOADED);
|
||||
});
|
||||
|
||||
@@ -51,7 +51,12 @@ namespace WorldMigration {
|
||||
// not be sent LOAD_STATIC_ZONE (that tears the scene down); load it at once instead
|
||||
bool ArrivesSeamlessly(LWOOBJID characterId);
|
||||
|
||||
// Its player entity was created and constructed: run what the client's PlayerLoaded would have
|
||||
// While `keep` is set, constructions of a seamlessly arriving player leave out what would reset its client's own
|
||||
// player: the position (the client stands where it is) and the world transition (no fade from white). Set only
|
||||
// around the construction that goes to the player's own client; does nothing for other players.
|
||||
void KeepClientPlayer(Entity* player, bool keep);
|
||||
|
||||
// Its player entity was created and constructed: the client sends PlayerLoaded itself; a fallback runs it if not
|
||||
void OnSeamlessArrival(Entity* player);
|
||||
|
||||
// MIGRATE_STATUS from master: tell the GM who asked, if they are here
|
||||
|
||||
@@ -1227,7 +1227,11 @@ void LoadPlayer(const SystemAddress& sysAddr) {
|
||||
}
|
||||
}
|
||||
|
||||
// Their own client's construction of the player: one that kept its player (seamless instance move) is not
|
||||
// to be put back where it was saved or shown a world transition
|
||||
WorldMigration::KeepClientPlayer(player, true);
|
||||
Game::entityManager->ConstructAllEntities(sysAddr);
|
||||
WorldMigration::KeepClientPlayer(player, false);
|
||||
|
||||
characterComponent->RocketUnEquip(player);
|
||||
|
||||
|
||||
@@ -18,10 +18,13 @@ lu_packets.
|
||||
server as a localized announcement. Live used this to evacuate instances.
|
||||
* Moving within the same zone keeps the player where they stood. Merging instances therefore works, but the player sees
|
||||
the warning, a short loading screen, and the "shift succeeded" notice.
|
||||
* **A transfer without a loading screen looks possible.** The client's replica code can **adopt** an object it already
|
||||
has for a new server's construction, and it keeps its own player object when the old server takes its ghost away. So
|
||||
a new server can take over if it skips `LOAD_STATIC_ZONE`. This is built as an opt-in experiment (see below) and has
|
||||
**not been tried with the real client yet**.
|
||||
* **A transfer within the same zone works without a loading screen.** The client's replica code **adopts** an object
|
||||
it already has for a new server's construction, and it never deletes its own player. So a new server can take over
|
||||
if it skips `LOAD_STATIC_ZONE`. This is the opt-in seamless mode (see below). The player keeps its object, position
|
||||
and camera; the other objects still fade out and come back.
|
||||
* **A transfer to another zone cannot skip the loading screen.** The client loads a zone only on `LOAD_STATIC_ZONE`
|
||||
(or its own script command), and that handler always shows the loading screen and deletes every object first. No
|
||||
server message loads or unloads single scenes.
|
||||
|
||||
## What the client does (1.10.64)
|
||||
|
||||
@@ -72,6 +75,28 @@ network state message.
|
||||
* When the world connection closes, nothing is deleted. `ReplicaManager::RemoveParticipant` @ `00a478d0` leaves the
|
||||
objects alone.
|
||||
|
||||
### A second construction of the local player
|
||||
|
||||
What the client does when a construction arrives for the player object it already has (the seamless mode's case):
|
||||
|
||||
* `LWOGameObject::Unserialize` @ `00f99690` runs every component's construction read on the live object.
|
||||
* `LWOBasePhysComponent::Deserialize` @ `00bf5a10`: when the construction has a frame, it is applied at once
|
||||
(position, rotation, both velocities), local player or not. Without the frame bit nothing is set.
|
||||
* `LWOInventoryComponent_Client::Deserialize` @ `00d52860` compares the equipped items with the ones it has and only
|
||||
equips or unequips the difference.
|
||||
* `LWOCharacterComponent::Unserialize` @ `00ca3a40` reads the 2-bit world transition state (0 none, 1 arriving by
|
||||
rocket, 2 leaving) and ends in `LWOCharacterComponent::PostCharacterLoaded` @ `00c71660`. With state 0 that pushes
|
||||
the gameplay state again and posts a **full screen fade from white**. In every case it sends `PlayerLoaded` to the
|
||||
server. State 3 is read but nothing acts on it.
|
||||
* `LWOGhostComponent::OnGhostReceiveDestruction` @ `00c99410`: when the local player's last ghost is destroyed the
|
||||
object stays, but it leaves scope (`LWOBaseRenderComponent::OnLeaveScope` @ `00c49140` hides it). The next
|
||||
construction brings it back through `LWOBaseRenderComponent::OnEnterScope` @ `00be2150`, which fades it in with the
|
||||
`login` effect. A destruction for a ghost that a newer construction already replaced does nothing.
|
||||
* `PlayerReady` (`LWOCharacterComponent::iSendMessage` @ `00d34330`) puts the gameplay camera's pitch back to the
|
||||
default; the rest of it refreshes UI.
|
||||
* `ConnectToWorldServer` posts `UILoginFeedback` state 4, which only calls `_root.setLoginFeedback` on the current
|
||||
Flash scene (`NDUIScaleformScene::SendMessage` @ `00bc2d10`). Nothing shows in game.
|
||||
|
||||
### Connection loss vs. a transfer
|
||||
|
||||
`LwoNetClient::OnConnectionDropped_World` @ `00b22fb0` shows `NET_CONNDROP_WORLD_LOST`, `..._TIMEOUT` or
|
||||
@@ -169,21 +194,29 @@ the rest of the 33 bytes uninitialised, port 2005, flag 0). lcdr-utils' `found.z
|
||||
|
||||
Built from the replica behaviour above:
|
||||
|
||||
* **Source**: after saving and locking the player, it sends a destruction for every object it sent them
|
||||
(`DestructAllEntities`) and then `TRANSFER_TO_WORLD` without `mythranShift`. Both go out on the same ordered channel,
|
||||
so the destructions arrive first. The client deletes those objects but keeps its player, terrain and UI.
|
||||
* **Target**: the carried state is marked seamless. When that character's session is validated, the target skips
|
||||
`LOAD_STATIC_ZONE` and runs the level-load step at once (`LoadPlayer`): it creates the player entity, sends
|
||||
* **Source**: after saving and locking the player, it sends a destruction for every object it sent them **except their
|
||||
own player**, and then `TRANSFER_TO_WORLD` without `mythranShift`. Both go out on the same ordered channel, so the
|
||||
destructions arrive first. The client deletes those objects and keeps its player (in scope, with its old ghost),
|
||||
terrain and UI.
|
||||
* **Target**: the carried state is marked seamless. When that character's session is validated within 30 s, the target
|
||||
skips `LOAD_STATIC_ZONE` and runs the level-load step at once (`LoadPlayer`): it creates the player entity, sends
|
||||
`CREATE_CHARACTER` (which the client ignores because the object exists), and constructs the player and everything
|
||||
else. The client adopts its existing player object for the new ghost. One second later the server does what the
|
||||
client's `PlayerLoaded` would have triggered, because the client never sends it again.
|
||||
* **Untested**:
|
||||
* how the client reacts to `ServerDoneLoadingAllObjects` / `PlayerReady` without a load in progress
|
||||
* the UI state `ConnectToWorldServer` posts
|
||||
* the camera reset in `PlayerReady`
|
||||
* whether objects visibly pop out and back in
|
||||
|
||||
If it goes wrong the worst case should be a stuck client that has to log in again. The character is already saved.
|
||||
else. The client adopts its existing player object for the new ghost.
|
||||
* The construction of the player that goes to **its own client** differs from the one everyone else gets
|
||||
(`WorldMigration::KeepClientPlayer`):
|
||||
* no position frame, so the client stays where it stands instead of snapping back to where it was saved;
|
||||
* world transition state 3, so there is no fade from white.
|
||||
* The client sends `PlayerLoaded` by itself after that construction. If it has not after 5 s the server runs it.
|
||||
`PlayerReady` is not sent (the client is ready, and it would reset the camera pitch).
|
||||
* **First test (2026-09-30), before these changes**: no loading screen, but a white flash and the character came back
|
||||
with the login effect. Cause: the source destroyed the player's own ghost too, and the construction said transition
|
||||
state 0.
|
||||
* **Still visible**: the other objects fade out when the source takes them away and come back when the target
|
||||
constructs them. Objects placed in the level have the same object ID on both instances and could be handed over
|
||||
without that (construct on the target first, destroy the source's ghosts after); that needs network ID ranges that
|
||||
don't overlap between instances and is not built.
|
||||
* **Untested with these changes**: everything. If it goes wrong the worst case should be a stuck client that has to
|
||||
log in again. The character is already saved.
|
||||
|
||||
### What carries over
|
||||
|
||||
@@ -231,6 +264,8 @@ Both need DEVELOPER, act on the instance the GM is in, and report progress to th
|
||||
### Limits
|
||||
|
||||
* Normal mode always shows a loading screen, because the client has no way around it once `LOAD_STATIC_ZONE` is sent.
|
||||
* The seamless mode only works within one zone: another zone's terrain and scenes only load through
|
||||
`LOAD_STATIC_ZONE`.
|
||||
Staying in the same zone keeps it short: the zone files are warm in the OS cache and the target is already running.
|
||||
* Only public, non-clone, non-activity instances can be moved.
|
||||
* The target world starts its own simulation. Enemies, smashables, quickbuilds and dropped loot are the target's, not
|
||||
|
||||
@@ -544,6 +544,87 @@ TEST_F(ReplicaConstructionTest, ControllablePhysicsConstructionWritesCheatsOnlyW
|
||||
EXPECT_FLOAT_EQ(cheats.second, 1.05f);
|
||||
}
|
||||
|
||||
// A client that kept its player across a seamless instance move (docs/SeamlessTransfer.md): its own player's
|
||||
// construction carries no frame, because LWOBasePhysComponent::Deserialize (0x00bf5a10) applies a construction's
|
||||
// position, rotation and velocities to the object at once. Everyone else's construction still has it.
|
||||
TEST_F(ReplicaConstructionTest, ControllablePhysicsConstructionCanLeaveThePositionOut) {
|
||||
Entity entity(15, info);
|
||||
auto* const physics = entity.AddComponent<ControllablePhysicsComponent>(-1);
|
||||
physics->SetPosition(NiPoint3(1.0f, 2.0f, 3.0f));
|
||||
|
||||
// no jetpack, stun immunities, no cheats, equipped item info (flag, pickup radius, in jetpack), bubble (flag, off)
|
||||
constexpr uint32_t beforePosition = 1 + 1 + 7 * 32 + 1 + (1 + 32 + 1) + (1 + 1);
|
||||
const auto positionBit = [physics, beforePosition](uint32_t& bitsAfter) {
|
||||
RakNet::BitStream stream;
|
||||
physics->Serialize(stream, true);
|
||||
stream.IgnoreBits(beforePosition);
|
||||
bool hasPosition{};
|
||||
EXPECT_TRUE(stream.Read(hasPosition));
|
||||
bitsAfter = stream.GetNumberOfUnreadBits();
|
||||
return hasPosition;
|
||||
};
|
||||
|
||||
uint32_t bitsAfter{};
|
||||
EXPECT_TRUE(positionBit(bitsAfter));
|
||||
EXPECT_GT(bitsAfter, 7u * 32u);
|
||||
|
||||
physics->SetKeepClientPosition(true);
|
||||
EXPECT_FALSE(positionBit(bitsAfter));
|
||||
EXPECT_EQ(bitsAfter, 0u);
|
||||
|
||||
// Serializations are untouched: a moved player is still sent
|
||||
RakNet::BitStream flushed;
|
||||
physics->Serialize(flushed, false);
|
||||
RakNet::BitStream serialization;
|
||||
physics->SetPosition(NiPoint3(4.0f, 5.0f, 6.0f));
|
||||
physics->Serialize(serialization, false);
|
||||
serialization.IgnoreBits(3); // no cheats, equipped item info or bubble change
|
||||
bool hasPosition{};
|
||||
EXPECT_TRUE(serialization.Read(hasPosition));
|
||||
EXPECT_TRUE(hasPosition);
|
||||
|
||||
physics->SetKeepClientPosition(false);
|
||||
EXPECT_TRUE(positionBit(bitsAfter));
|
||||
}
|
||||
|
||||
// The same client's character construction: world transition state 3. With 0 the client's
|
||||
// LWOCharacterComponent::PostCharacterLoaded (0x00c71660) fades the whole screen in from white and pushes its gameplay
|
||||
// state a second time; 1 is the rocket landing and 2 hides the character. The blocks after it stay where they were.
|
||||
TEST_F(ReplicaConstructionTest, CharacterConstructionCanSkipTheWorldTransition) {
|
||||
User user(UNASSIGNED_SYSTEM_ADDRESS, "tester", "key");
|
||||
Character character(1, &user);
|
||||
info.lot = 1;
|
||||
Entity player(1152921506064087003, info);
|
||||
player.SetCharacter(&character);
|
||||
character.SetEntity(&player);
|
||||
auto* const component = player.AddComponent<CharacterComponent>(-1, &character, UNASSIGNED_SYSTEM_ADDRESS);
|
||||
|
||||
// 4 absent claim codes, 10 u32 appearance fields, 4 u64s, the free-trial bit and 27 u64 statistics
|
||||
constexpr uint32_t beforeTransition = 4 + 10 * 32 + 4 * 64 + 1 + 27 * 64;
|
||||
const auto transition = [component, beforeTransition](uint32_t& bitsAfter) {
|
||||
RakNet::BitStream construction;
|
||||
component->Serialize(construction, true);
|
||||
construction.IgnoreBits(beforeTransition);
|
||||
uint8_t state{};
|
||||
EXPECT_TRUE(construction.ReadBits(&state, 2));
|
||||
bitsAfter = construction.GetNumberOfUnreadBits();
|
||||
return state;
|
||||
};
|
||||
|
||||
uint32_t normalBits{};
|
||||
EXPECT_EQ(transition(normalBits), 0);
|
||||
|
||||
component->SetKeepClientScene(true);
|
||||
uint32_t keptBits{};
|
||||
EXPECT_EQ(transition(keptBits), 3);
|
||||
EXPECT_EQ(keptBits, normalBits);
|
||||
|
||||
component->SetKeepClientScene(false);
|
||||
EXPECT_EQ(transition(keptBits), 0);
|
||||
|
||||
player.SetCharacter(nullptr);
|
||||
}
|
||||
|
||||
// Simple physics: live sent motion type Fixed for objects without a motionType (never the invalid 0 DLU sent when the
|
||||
// level gave none) and no velocity for fixed objects (38,970 Fixed constructions, all without velocity); moving
|
||||
// platforms were Keyframed with a velocity (1,059 of 1,059).
|
||||
|
||||
Reference in New Issue
Block a user