@@ -34,7 +34,13 @@ private FieldValidator Create(int maxEmoteIdLength = 64, int maxRealmLength = 12
3434
3535 private static PeerState NewState ( ) => new ( PeerConnectionState . AUTHENTICATED ) ;
3636
37- private static PlayerState ValidPlayerState ( ) => new ( ) { ParcelIndex = 100 } ;
37+ private static PlayerState ValidPlayerState ( ) =>
38+ new ( )
39+ {
40+ ParcelIndex = 100 ,
41+ Position = new Vector3 ( ) ,
42+ Velocity = new Vector3 ( ) ,
43+ } ;
3844
3945 private static EmoteStart ValidEmoteStart ( string emoteId = "wave" , uint ? durationMs = 3000 ) =>
4046 new ( )
@@ -183,6 +189,107 @@ public void OversizedRealm_Rejects()
183189 transport . Received ( 1 ) . Disconnect ( PEER , DisconnectReason . INVALID_TELEPORT_FIELD ) ;
184190 }
185191
192+ // ── Numeric finiteness (NaN/Inf) ─────────────────────────────────
193+
194+ private static PlayerStateInput InputWith ( Action < PlayerState > mutate )
195+ {
196+ PlayerState s = ValidPlayerState ( ) ;
197+ mutate ( s ) ;
198+ return new PlayerStateInput { State = s } ;
199+ }
200+
201+ [ Test ]
202+ public void NaNPosition_InInput_Rejects ( )
203+ {
204+ FieldValidator v = Create ( ) ;
205+ PlayerStateInput msg = InputWith ( s => s . Position = new Vector3 { X = float . NaN , Y = 0 , Z = 0 } ) ;
206+
207+ Assert . That ( v . ValidatePlayerStateInput ( PEER , NewState ( ) , msg ) , Is . False ) ;
208+ transport . Received ( 1 ) . Disconnect ( PEER , DisconnectReason . INVALID_INPUT_FIELD ) ;
209+ }
210+
211+ [ Test ]
212+ public void InfinityVelocity_InInput_Rejects ( )
213+ {
214+ FieldValidator v = Create ( ) ;
215+ PlayerStateInput msg = InputWith ( s => s . Velocity = new Vector3 { X = 0 , Y = float . PositiveInfinity , Z = 0 } ) ;
216+
217+ Assert . That ( v . ValidatePlayerStateInput ( PEER , NewState ( ) , msg ) , Is . False ) ;
218+ transport . Received ( 1 ) . Disconnect ( PEER , DisconnectReason . INVALID_INPUT_FIELD ) ;
219+ }
220+
221+ [ Test ]
222+ public void NaNRotationY_InInput_Rejects ( )
223+ {
224+ FieldValidator v = Create ( ) ;
225+ PlayerStateInput msg = InputWith ( s => s . RotationY = float . NaN ) ;
226+
227+ Assert . That ( v . ValidatePlayerStateInput ( PEER , NewState ( ) , msg ) , Is . False ) ;
228+ }
229+
230+ [ Test ]
231+ public void NaNMovementBlend_InInput_Rejects ( )
232+ {
233+ FieldValidator v = Create ( ) ;
234+ PlayerStateInput msg = InputWith ( s => s . MovementBlend = float . NaN ) ;
235+
236+ Assert . That ( v . ValidatePlayerStateInput ( PEER , NewState ( ) , msg ) , Is . False ) ;
237+ }
238+
239+ [ Test ]
240+ public void NaNHeadYaw_InInput_Rejects ( )
241+ {
242+ FieldValidator v = Create ( ) ;
243+ PlayerStateInput msg = InputWith ( s => s . HeadYaw = float . NaN ) ;
244+
245+ Assert . That ( v . ValidatePlayerStateInput ( PEER , NewState ( ) , msg ) , Is . False ) ;
246+ }
247+
248+ [ Test ]
249+ public void UnsetHeadYaw_IsIgnored ( )
250+ {
251+ // HasHeadYaw is false by default; finiteness check should skip it regardless of value.
252+ FieldValidator v = Create ( ) ;
253+ var msg = new PlayerStateInput { State = ValidPlayerState ( ) } ;
254+
255+ Assert . That ( v . ValidatePlayerStateInput ( PEER , NewState ( ) , msg ) , Is . True ) ;
256+ }
257+
258+ [ Test ]
259+ public void NullPosition_InInput_Rejects ( )
260+ {
261+ // Malformed proto with unset Position would NRE in the handler; validator must reject.
262+ FieldValidator v = Create ( ) ;
263+ var msg = new PlayerStateInput
264+ {
265+ State = new PlayerState { ParcelIndex = 100 , Velocity = new Vector3 ( ) } ,
266+ } ;
267+
268+ Assert . That ( v . ValidatePlayerStateInput ( PEER , NewState ( ) , msg ) , Is . False ) ;
269+ }
270+
271+ [ Test ]
272+ public void NaNPosition_InEmote_RejectsWithEmoteField ( )
273+ {
274+ FieldValidator v = Create ( ) ;
275+ EmoteStart msg = ValidEmoteStart ( ) ;
276+ msg . PlayerState . Position = new Vector3 { X = float . NaN , Y = 0 , Z = 0 } ;
277+
278+ Assert . That ( v . ValidateEmoteStart ( PEER , NewState ( ) , msg ) , Is . False ) ;
279+ transport . Received ( 1 ) . Disconnect ( PEER , DisconnectReason . INVALID_EMOTE_FIELD ) ;
280+ }
281+
282+ [ Test ]
283+ public void NaNPosition_InTeleport_RejectsWithTeleportField ( )
284+ {
285+ FieldValidator v = Create ( ) ;
286+ TeleportRequest msg = ValidTeleport ( ) ;
287+ msg . Position = new Vector3 { X = float . NaN , Y = 0 , Z = 0 } ;
288+
289+ Assert . That ( v . ValidateTeleport ( PEER , NewState ( ) , msg ) , Is . False ) ;
290+ transport . Received ( 1 ) . Disconnect ( PEER , DisconnectReason . INVALID_TELEPORT_FIELD ) ;
291+ }
292+
186293 [ Test ]
187294 public void OutOfRangeParcel_InTeleport_Rejects ( )
188295 {
0 commit comments