2 Cafu Engine, http://www.cafu.de/
3 Copyright (c) Carsten Fuchs and other contributors.
4 This project is licensed under the terms of the MIT license.
7 #include "EngineEntity.hpp"
8 #include "NetConst.hpp"
9 #include "ConsoleCommands/ConVar.hpp"
10 #include "MaterialSystem/Renderer.hpp"
11 #include "Math3D/Matrix.hpp"
12 #include "Network/Network.hpp"
13 #include "Win32/Win32PrintHelp.hpp"
14 #include "Ca3DEWorld.hpp"
15 #include "GameSys/CompHumanPlayer.hpp"
16 #include "GameSys/Entity.hpp"
17 #include "GameSys/Interpolator.hpp"
22 ConVarT
UsePrediction("usePrediction", true, ConVarT::FLAG_MAIN_EXE
,
23 "Toggles whether client prediction is used (recommended!).");
25 ConVarT
clientApproxNPCs("clientApproxNPCs", true, ConVarT::FLAG_MAIN_EXE
,
26 "Toggles whether origins and other values are interpolated over client "
27 "frames in order to bridge the larger intervals between server frames.");
36 cf::Network::StateT
EngineEntityT::GetState() const
38 cf::Network::StateT State
;
39 cf::Network::OutStreamT
Stream(State
);
41 m_Entity
->Serialize(Stream
);
47 void EngineEntityT::SetState(const cf::Network::StateT
& State
, bool IsIniting
) const
49 cf::Network::InStreamT
Stream(State
);
51 m_Entity
->Deserialize(Stream
, IsIniting
);
53 // Deserialization has brought new reference values for interpolated values.
54 for (unsigned int CompNr
= 0; true; CompNr
++)
56 IntrusivePtrT
<cf::GameSys::ComponentBaseT
> Comp
= m_Entity
->GetComponent(CompNr
);
58 if (Comp
== NULL
) break;
60 for (unsigned int i
= 0; i
< Comp
->GetInterpolators().Size(); i
++)
62 if (IsIniting
|| !clientApproxNPCs
.GetValueBool())
64 Comp
->GetInterpolators()[i
]->ReInit();
68 Comp
->GetInterpolators()[i
]->UpdateTargetValue();
80 EngineEntityT::EngineEntityT(IntrusivePtrT
<cf::GameSys::EntityT
> Ent
, unsigned long CreationFrameNr
)
82 EntityStateFrameNr(CreationFrameNr
),
84 m_BaseLineFrameNr(CreationFrameNr
),
87 m_BaseLine
=GetState();
89 for (unsigned long OldStateNr
=0; OldStateNr
<16 /*MUST be a power of 2*/; OldStateNr
++)
90 m_OldStates
.PushBack(m_BaseLine
);
94 void EngineEntityT::PreThink(unsigned long ServerFrameNr
)
96 // The goal of the Think() methods is to compute the state for frame ServerFrameNr.
97 // Thus, here, before the thinking starts, record the state of the previous frame.
99 // Note that if this entity has only been created in ServerFrameNr, there is no previous state to record.
100 // As our earliest state is `m_BaseLineFrameNr`, we must have `m_BaseLineFrameNr <= ServerFrameNr - 1`,
101 // thus `m_BaseLineFrameNr < ServerFrameNr`. (If `m_BaseLineFrameNr == ServerFrameNr` was allowed, this
102 // would imply that we had an old state at `m_OldStates[m_BaseLineFrameNr - 1]` and result in
103 // `m_BaseLine != m_OldStates[m_BaseLineFrameNr]`.)
104 if (m_BaseLineFrameNr
>= ServerFrameNr
) return;
106 m_OldStates
[(ServerFrameNr
-1) & (m_OldStates
.Size()-1)] = GetState();
110 void EngineEntityT::Think(float FrameTime
, unsigned long ServerFrameNr
)
112 // As detailed in PreThink().
113 if (m_BaseLineFrameNr
>= ServerFrameNr
) return;
115 // This computes the state of the entity for frame ServerFrameNr.
116 m_Entity
->OnServerFrame(FrameTime
);
118 EntityStateFrameNr
=ServerFrameNr
;
122 void EngineEntityT::WriteNewBaseLine(unsigned long SentClientBaseLineFrameNr
, ArrayT
< ArrayT
<char> >& OutDatas
) const
124 // Nur dann etwas tun, wenn unsere 'BaseLineFrameNr' größer (d.h. jünger) als 'SentClientBaseLineFrameNr' ist,
125 // d.h. unsere 'BaseLineFrameNr' noch nie / noch nicht an den Client gesendet wurde.
126 if (SentClientBaseLineFrameNr
>= m_BaseLineFrameNr
) return;
128 NetDataT NewBaseLineMsg
;
130 NewBaseLineMsg
.WriteByte(SC1_EntityBaseLine
);
131 NewBaseLineMsg
.WriteLong(m_Entity
->GetID());
132 NewBaseLineMsg
.WriteLong(m_Entity
->GetParent().IsNull() ? UINT_MAX
: m_Entity
->GetParent()->GetID());
133 NewBaseLineMsg
.WriteDMsg(m_BaseLine
.GetDeltaMessage(cf::Network::StateT() /*::ALL_ZEROS*/));
135 OutDatas
.PushBack(NewBaseLineMsg
.Data
);
139 bool EngineEntityT::WriteDeltaEntity(bool SendFromBaseLine
, unsigned long ClientFrameNr
, NetDataT
& OutData
, bool ForceInfo
) const
141 // Prüfe, ob die Voraussetzungen für die Parameter (insb. 'ClientFrameNr') eingehalten werden.
142 if (!SendFromBaseLine
)
144 // EntityStateFrameNr wird in Think() gesetzt und ist gleich der ServerFrameNr!
145 // Beachte: OldStates speichert die alten Zustände von ServerFrameNr-1 bis ServerFrameNr-16.
146 const unsigned long FrameDiff
= EntityStateFrameNr
- ClientFrameNr
;
148 if (FrameDiff
< 1 || FrameDiff
> m_OldStates
.Size()) return false;
152 const cf::Network::StateT CurrentState
= GetState();
153 const ArrayT
<uint8_t> DeltaMsg
= CurrentState
.GetDeltaMessage(SendFromBaseLine
? m_BaseLine
: m_OldStates
[ClientFrameNr
& (m_OldStates
.Size()-1)]);
155 if (cf::Network::StateT::IsDeltaMessageEmpty(DeltaMsg
) && !ForceInfo
) return true;
157 // Write the SC1_EntityUpdate message
158 OutData
.WriteByte(SC1_EntityUpdate
);
159 OutData
.WriteLong(m_Entity
->GetID());
160 OutData
.WriteDMsg(DeltaMsg
);
168 /*******************/
169 /*** Client Side ***/
170 /*******************/
173 EngineEntityT::EngineEntityT(IntrusivePtrT
<cf::GameSys::EntityT
> Ent
, NetDataT
& InData
)
175 EntityStateFrameNr(0),
177 m_BaseLineFrameNr(1234), // The m_BaseLineFrameNr is unused on the client.
180 const cf::Network::StateT
CurrentState(cf::Network::StateT() /*::ALL_ZEROS*/, InData
.ReadDMsg());
182 // Pass true for the IsInited parameter in order to indicate that we're constructing the entity.
183 // This is done in order to have it not wrongly process the event counters.
184 SetState(CurrentState
, true);
186 for (unsigned long OldStateNr
=0; OldStateNr
<32 /*MUST be a power of 2*/; OldStateNr
++)
187 m_OldStates
.PushBack(CurrentState
);
189 m_BaseLine
=CurrentState
;
193 bool EngineEntityT::ParseServerDeltaUpdateMessage(unsigned long DeltaFrameNr
, unsigned long ServerFrameNr
, const ArrayT
<uint8_t>* DeltaMessage
)
195 // Sanity-Check: Wir wollen, daß 'DeltaFrameNr<=EntityStateFrameNr<ServerFrameNr' gilt.
196 // Wäre 'DeltaFrameNr>EntityStateFrameNr', so sollten wir gegen einen State dekomprimieren, der in der Zukunft liegt.
197 // Wäre 'EntityStateFrameNr>=ServerFrameNr', so sollten wir uns in einen State begeben, der schon Vergangenheit ist.
198 // Dies hält auch für den Spezialfall 'DeltaFrameNr==0' (Delta-Dekompression gegen die BaseLine).
199 // Im Normalfall 'DeltaFrameNr>0' müssen wir unten außerdem noch sicherstellen, daß der DeltaState nicht zu weit in der Vergangenheit liegt.
201 // ONE possible reason for DeltaFrameNr>EntityStateFrameNr is related to the way how baselines are sent,
202 // see EntityManager.cpp, EntityManagerT::ParseServerDeltaUpdateMessage() for a description, which is essentially repeated here:
203 // When a client joins a level, there can be a LOT of entities. Usually, not all baselines of all entities fit into a single
204 // realiable message at once, and thus the server sends them in batches, contained in subsequent realiable messages.
205 // Between realiable messages however, the server sends also SC1_EntityUpdate messages.
206 // These messages can already refer to entities that the client knows nothing about, because it has not yet seen the (reliable)
207 // introductory baseline message.
208 // Then, the entities that the client already knows about normally receive and process delta updates here in this function,
209 // the others don't (because their non-presence is already detected in EntityManagerT::ParseServerDeltaUpdateMessage()).
210 // However, the frame counters increase normally, as if all entities were present. When finally the remaining entities
211 // arrive (because their baseline got finally through), these entities are likely to have DeltaFrameNr>EntityStateFrameNr.
212 // I turn the "WARNING" into an "INFO", so that ordinary users get a better impression. ;)
213 if (DeltaFrameNr
>EntityStateFrameNr
) { EnqueueString("CLIENT INFO: %s, L %u: DeltaFrameNr>EntityStateFrameNr (%lu>%lu)\n" , __FILE__
, __LINE__
, DeltaFrameNr
, EntityStateFrameNr
); return false; }
214 if (EntityStateFrameNr
>=ServerFrameNr
) { EnqueueString("CLIENT WARNING: %s, L %u: EntityStateFrameNr>=ServerFrameNr (%lu>%lu)\n", __FILE__
, __LINE__
, EntityStateFrameNr
, ServerFrameNr
); return false; }
217 // Determine the source state to delta-decompress against (an old state or the baseline).
218 const cf::Network::StateT
* DeltaState
=NULL
;
222 // Der oben angekündigte Test, ob der DeltaState nicht schon zu weit in der Vergangenheit liegt.
223 // Einen gültigen State können wir dann nicht mehr produzieren, und dem Calling-Code muß klar sein oder klar werden,
224 // daß er gegen die BaseLines komprimierte Messages anfordern muß.
225 if (EntityStateFrameNr
-DeltaFrameNr
>= m_OldStates
.Size())
227 EnqueueString("CLIENT WARNING: %s, L %u: Delta state too old!\n", __FILE__
, __LINE__
);
231 DeltaState
= &m_OldStates
[DeltaFrameNr
& (m_OldStates
.Size()-1)];
235 DeltaState
= &m_BaseLine
;
238 // Set the result as the new entity state, and record it in the m_OldStates for future reference.
239 EntityStateFrameNr
= ServerFrameNr
;
241 const cf::Network::StateT NewState
= DeltaMessage
? cf::Network::StateT(*DeltaState
, *DeltaMessage
) : *DeltaState
;
243 m_OldStates
[EntityStateFrameNr
& (m_OldStates
.Size()-1)] = NewState
;
244 SetState(NewState
, DeltaFrameNr
== 0); // Don't process events and don't interpolate if we're delta'ing from the baseline (e.g. when re-entering the PVS).
249 void EngineEntityT::Predict(const PlayerCommandT
& PrevPlayerCommand
, const PlayerCommandT
& PlayerCommand
)
251 if (!UsePrediction
.GetValueBool())
254 IntrusivePtrT
<cf::GameSys::ComponentHumanPlayerT
> CompHP
=
255 dynamic_pointer_cast
<cf::GameSys::ComponentHumanPlayerT
>(m_Entity
->GetComponent("HumanPlayer"));
259 EnqueueString("WARNING - Prediction impossible: HumanPlayer component not found in human player entity!\n");
263 // Note that components other than CompHP should *not* Think/Repredict,
264 // e.g. the player's CollisionModel component must not cause OnTrigger() callbacks!
265 CompHP
->Think(PrevPlayerCommand
, PlayerCommand
, false /*ThinkingOnServerSide*/);
268 #endif /* !DEDICATED */