Client LuaCsForBarotrauma
BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs
3 using FarseerPhysics;
4 using Microsoft.Xna.Framework;
5 using System;
6 using System.Collections.Immutable;
7 using System.Linq;
8 
9 namespace Barotrauma
10 {
11  partial class Character
12  {
13  partial void UpdateNetInput()
14  {
15  if (GameMain.Client != null)
16  {
17  if (this != Controlled)
18  {
19  if (GameMain.Client.EndCinematic != null &&
20  GameMain.Client.EndCinematic.Running) // Freezes the characters during the ending cinematic
21  {
22  AnimController.Frozen = true;
23  memState.Clear();
24  return;
25  }
26 
27  //freeze AI characters if more than x seconds have passed since last update from the server
28  if (lastRecvPositionUpdateTime < Lidgren.Network.NetTime.Now - NetConfig.FreezeCharacterIfPositionDataMissingDelay)
29  {
30  AnimController.Frozen = true;
31  memState.Clear();
32  //hide after y seconds
33  if (lastRecvPositionUpdateTime < Lidgren.Network.NetTime.Now - NetConfig.DisableCharacterIfPositionDataMissingDelay)
34  {
35  Enabled = false;
36  return;
37  }
38  }
39  }
40  else
41  {
42  var posInfo = new CharacterStateInfo(
51 
52  memLocalState.Add(posInfo);
53 
54  InputNetFlags newInput = InputNetFlags.None;
55  if (IsKeyDown(InputType.Left)) newInput |= InputNetFlags.Left;
56  if (IsKeyDown(InputType.Right)) newInput |= InputNetFlags.Right;
57  if (IsKeyDown(InputType.Up)) newInput |= InputNetFlags.Up;
58  if (IsKeyDown(InputType.Down)) newInput |= InputNetFlags.Down;
59  if (IsKeyDown(InputType.Run)) newInput |= InputNetFlags.Run;
60  if (IsKeyDown(InputType.Crouch)) newInput |= InputNetFlags.Crouch;
61  if (IsKeyHit(InputType.Select)) newInput |= InputNetFlags.Select; //TODO: clean up the way this input is registered
62  if (IsKeyHit(InputType.Deselect)) newInput |= InputNetFlags.Deselect;
63  if (IsKeyHit(InputType.Health)) newInput |= InputNetFlags.Health;
64  if (IsKeyHit(InputType.Grab)) newInput |= InputNetFlags.Grab;
65  if (IsKeyDown(InputType.Use)) newInput |= InputNetFlags.Use;
66  if (IsKeyDown(InputType.Aim)) newInput |= InputNetFlags.Aim;
67  if (IsKeyDown(InputType.Shoot)) newInput |= InputNetFlags.Shoot;
68  if (IsKeyDown(InputType.Attack)) newInput |= InputNetFlags.Attack;
69  if (IsKeyDown(InputType.Ragdoll)) newInput |= InputNetFlags.Ragdoll;
70 
71  if (AnimController.TargetDir == Direction.Left) newInput |= InputNetFlags.FacingLeft;
72 
73  Vector2 relativeCursorPos = cursorPosition - AimRefPosition;
74  relativeCursorPos.Normalize();
75  UInt16 intAngle = (UInt16)(65535.0 * Math.Atan2(relativeCursorPos.Y, relativeCursorPos.X) / (2.0 * Math.PI));
76 
77  NetInputMem newMem = new NetInputMem
78  {
79  states = newInput,
80  intAim = intAngle
81  };
82 
83  if (FocusedCharacter != null &&
85  newMem.states.HasFlag(InputNetFlags.Use))
86  {
87  newMem.interact = FocusedCharacter.ID;
88  }
89  else if (newMem.states.HasFlag(InputNetFlags.Use) && (FocusedCharacter?.IsPet ?? false))
90  {
91  newMem.interact = FocusedCharacter.ID;
92  }
93  else if (focusedItem != null && !CharacterInventory.DraggingItemToWorld &&
94  !newMem.states.HasFlag(InputNetFlags.Grab) && !newMem.states.HasFlag(InputNetFlags.Health))
95  {
96  newMem.interact = focusedItem.ID;
97  }
98  else if (FocusedCharacter != null)
99  {
100  newMem.interact = FocusedCharacter.ID;
101  }
102 
103  memInput.Insert(0, newMem);
105  if (memInput.Count > 60)
106  {
107  memInput.RemoveRange(60, memInput.Count - 60);
108  }
109  }
110  }
111  else //this == Character.Controlled && GameMain.Client == null
112  {
113  AnimController.Frozen = false;
114  }
115  }
116 
118  {
119  segmentTableWriter.StartNewSegment(ClientNetSegment.CharacterInput);
120 
121  if (memInput.Count > 60)
122  {
123  memInput.RemoveRange(60, memInput.Count - 60);
124  }
125 
127  byte inputCount = Math.Min((byte)memInput.Count, (byte)60);
128  msg.WriteByte(inputCount);
129  for (int i = 0; i < inputCount; i++)
130  {
131  msg.WriteRangedInteger((int)memInput[i].states, 0, (int)InputNetFlags.MaxVal);
132  msg.WriteUInt16(memInput[i].intAim);
133  if (memInput[i].states.HasFlag(InputNetFlags.Select) ||
134  memInput[i].states.HasFlag(InputNetFlags.Deselect) ||
135  memInput[i].states.HasFlag(InputNetFlags.Use) ||
136  memInput[i].states.HasFlag(InputNetFlags.Health) ||
137  memInput[i].states.HasFlag(InputNetFlags.Grab))
138  {
139  msg.WriteUInt16(memInput[i].interact);
140  }
141  }
142  }
143 
144  public virtual void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null)
145  {
146  if (extraData is not IEventData eventData) { throw new Exception($"Malformed character event: expected {nameof(Character)}.{nameof(IEventData)}"); }
147 
148  msg.WriteRangedInteger((int)eventData.EventType, (int)EventType.MinValue, (int)EventType.MaxValue);
149  switch (eventData)
150  {
151  case InventoryStateEventData inventoryStateEventData:
152  Inventory.ClientEventWrite(msg, inventoryStateEventData);
153  break;
154  case TreatmentEventData _:
156  break;
158  //do nothing
159  break;
160  case UpdateTalentsEventData _:
161  msg.WriteUInt16((ushort)characterTalents.Count);
162  foreach (var unlockedTalent in characterTalents)
163  {
164  msg.WriteUInt32(unlockedTalent.Prefab.UintIdentifier);
165  }
166  break;
167  default:
168  throw new Exception($"Malformed character event: did not expect {eventData.GetType().Name}");
169  }
170  }
171 
172  public void ClientReadPosition(IReadMessage msg, float sendingTime)
173  {
174  bool facingRight = AnimController.Dir > 0.0f;
175 
176  lastRecvPositionUpdateTime = (float)Lidgren.Network.NetTime.Now;
177 
178  AnimController.Frozen = false;
179  Enabled = true;
180  //if we start receiving position updates, it means the character's no longer disabled
181  if (DisabledByEvent && !Removed)
182  {
183  DisabledByEvent = false;
184  }
185 
186  UInt16 networkUpdateID = 0;
187  if (msg.ReadBoolean())
188  {
189  networkUpdateID = msg.ReadUInt16();
190  }
191  else
192  {
193  bool aimInput = msg.ReadBoolean();
194  keys[(int)InputType.Aim].Held = aimInput;
195  keys[(int)InputType.Aim].SetState(false, aimInput);
196 
197  bool shootInput = msg.ReadBoolean();
198  keys[(int)InputType.Shoot].Held = shootInput;
199  keys[(int)InputType.Shoot].SetState(false, shootInput);
200 
201  bool useInput = msg.ReadBoolean();
202  keys[(int)InputType.Use].Held = useInput;
203  keys[(int)InputType.Use].SetState(false, useInput);
204 
205  bool crouching = msg.ReadBoolean();
207  {
208  keys[(int)InputType.Crouch].Held = crouching;
209  keys[(int)InputType.Crouch].SetState(false, crouching);
210  }
211 
212  bool attackInput = msg.ReadBoolean();
213  keys[(int)InputType.Attack].Held = attackInput;
214  keys[(int)InputType.Attack].SetState(false, attackInput);
215 
216  double aimAngle = msg.ReadUInt16() / 65535.0 * 2.0 * Math.PI;
217  cursorPosition = AimRefPosition + new Vector2((float)Math.Cos(aimAngle), (float)Math.Sin(aimAngle)) * 500.0f;
218 
219  bool ragdollInput = msg.ReadBoolean();
220  keys[(int)InputType.Ragdoll].Held = ragdollInput;
221  keys[(int)InputType.Ragdoll].SetState(false, ragdollInput);
222 
223  facingRight = msg.ReadBoolean();
224  }
225 
226  bool entitySelected = msg.ReadBoolean();
227  Character selectedCharacter = null;
228  Item selectedItem = null, selectedSecondaryItem = null;
229 
231  if (entitySelected)
232  {
233  ushort characterID = msg.ReadUInt16();
234  ushort itemID = msg.ReadUInt16();
235  ushort secondaryItemID = msg.ReadUInt16();
236  selectedCharacter = FindEntityByID(characterID) as Character;
237  selectedItem = FindEntityByID(itemID) as Item;
238  selectedSecondaryItem = FindEntityByID(secondaryItemID) as Item;
239  if (characterID != NullEntityID)
240  {
241  bool doingCpr = msg.ReadBoolean();
242  if (doingCpr && SelectedCharacter != null)
243  {
244  animation = AnimController.Animation.CPR;
245  }
246  }
247  }
248 
249  Vector2 pos = new Vector2(
250  msg.ReadSingle(),
251  msg.ReadSingle());
252  float MaxVel = NetConfig.MaxPhysicsBodyVelocity;
253  Vector2 linearVelocity = new Vector2(
254  msg.ReadRangedSingle(-MaxVel, MaxVel, 12),
255  msg.ReadRangedSingle(-MaxVel, MaxVel, 12));
256  linearVelocity = NetConfig.Quantize(linearVelocity, -MaxVel, MaxVel, 12);
257 
258  bool fixedRotation = msg.ReadBoolean();
259  float? rotation = null;
260  float? angularVelocity = null;
261  if (!fixedRotation)
262  {
263  rotation = msg.ReadSingle();
264  float MaxAngularVel = NetConfig.MaxPhysicsBodyAngularVelocity;
265  angularVelocity = msg.ReadRangedSingle(-MaxAngularVel, MaxAngularVel, 8);
266  angularVelocity = NetConfig.Quantize(angularVelocity.Value, -MaxAngularVel, MaxAngularVel, 8);
267  }
268 
269  bool readStatus = msg.ReadBoolean();
270  if (readStatus)
271  {
272  ReadStatus(msg);
273  bool isEnemyAi = msg.ReadBoolean();
274  if (isEnemyAi)
275  {
276  byte aiState = msg.ReadByte();
277  if (AIController is EnemyAIController enemyAi)
278  {
279  enemyAi.State = (AIState)aiState;
280  }
281  else
282  {
283  DebugConsole.AddWarning($"Received enemy AI data for a character with no {nameof(EnemyAIController)}. Ignoring...");
284  }
285  bool isPet = msg.ReadBoolean();
286  if (isPet)
287  {
288  byte happiness = msg.ReadByte();
289  byte hunger = msg.ReadByte();
290  if ((AIController as EnemyAIController)?.PetBehavior is PetBehavior petBehavior)
291  {
292  petBehavior.Happiness = (float)happiness / byte.MaxValue * petBehavior.MaxHappiness;
293  petBehavior.Hunger = (float)hunger / byte.MaxValue * petBehavior.MaxHunger;
294  }
295  else
296  {
297  DebugConsole.AddWarning($"Received pet AI data for a character with no {nameof(PetBehavior)}. Ignoring...");
298  }
299  }
300  }
301  }
302 
303  msg.ReadPadBits();
304 
305  int index = 0;
306  if (GameMain.Client.Character == this && CanMove)
307  {
308  var posInfo = new CharacterStateInfo(
309  pos, rotation,
310  networkUpdateID,
311  facingRight ? Direction.Right : Direction.Left,
312  selectedCharacter, selectedItem, selectedSecondaryItem, animation);
313 
314  while (index < memState.Count && NetIdUtils.IdMoreRecent(posInfo.ID, memState[index].ID))
315  index++;
316  memState.Insert(index, posInfo);
317  }
318  else
319  {
320  var posInfo = new CharacterStateInfo(
321  pos, rotation,
322  linearVelocity, angularVelocity,
323  sendingTime, facingRight ? Direction.Right : Direction.Left,
324  selectedCharacter, selectedItem, selectedSecondaryItem, animation);
325 
326  while (index < memState.Count && posInfo.Timestamp > memState[index].Timestamp)
327  index++;
328  memState.Insert(index, posInfo);
329  }
330  }
331 
332  public virtual void ClientEventRead(IReadMessage msg, float sendingTime)
333  {
334  EventType eventType = (EventType)msg.ReadRangedInteger((int)EventType.MinValue, (int)EventType.MaxValue);
335  switch (eventType)
336  {
337  case EventType.InventoryState:
338  if (Inventory == null)
339  {
340  string errorMsg = "Received an inventory update message for an entity with no inventory ([name], removed: " + Removed + ")";
341  DebugConsole.ThrowError(errorMsg.Replace("[name]", Name));
342  GameAnalyticsManager.AddErrorEventOnce("CharacterNetworking.ClientRead:NoInventory" + ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", SpeciesName.Value));
343 
344  //read anyway to prevent messing up reading the rest of the message
345  _ = msg.ReadUInt16();
346  byte inventoryItemCount = msg.ReadByte();
347  for (int i = 0; i < inventoryItemCount; i++)
348  {
349  msg.ReadUInt16();
350  }
351  }
352  else
353  {
355  }
356  break;
357  case EventType.Control:
358  bool myCharacter = msg.ReadBoolean();
359  byte ownerID = msg.ReadByte();
360  bool renamingEnabled = msg.ReadBoolean();
361  ResetNetState();
362  if (myCharacter)
363  {
364  if (controlled != null)
365  {
367  }
368 
369  if (!IsDead) { Controlled = this; }
370  IsRemotePlayer = false;
371  GameMain.Client.HasSpawned = true;
372  GameMain.Client.Character = this;
373  GameMain.LightManager.LosEnabled = true;
374  GameMain.LightManager.LosAlpha = 1f;
376  }
377  else
378  {
379  if (controlled == this)
380  {
381  Controlled = null;
382  }
383  if (GameMain.Client?.Character == this)
384  {
385  GameMain.Client.Character = null;
386  }
387  IsRemotePlayer = ownerID > 0;
388  }
389  if (info != null)
390  {
391  info.RenamingEnabled = renamingEnabled;
392  }
393  break;
394  case EventType.Status:
395  ReadStatus(msg);
396  break;
397  case EventType.UpdateSkills:
398  int skillCount = msg.ReadByte();
399  for (int i = 0; i < skillCount; i++)
400  {
401  Identifier skillIdentifier = msg.ReadIdentifier();
402  float skillLevel = msg.ReadSingle();
403  info?.SetSkillLevel(skillIdentifier, skillLevel);
404  }
405  break;
406  case EventType.SetAttackTarget:
407  case EventType.ExecuteAttack:
408  int attackLimbIndex = msg.ReadByte();
409  UInt16 targetEntityID = msg.ReadUInt16();
410  int targetLimbIndex = msg.ReadByte();
411  float targetX = msg.ReadSingle();
412  float targetY = msg.ReadSingle();
413  Vector2 targetSimPos = new Vector2(targetX, targetY);
414  //255 = entity already removed, no need to do anything
415  if (attackLimbIndex == 255 || targetEntityID == NullEntityID || Removed) { break; }
416  if (attackLimbIndex >= AnimController.Limbs.Length)
417  {
418  //it's possible to get these errors when mid-round syncing, as the client may not
419  //yet know about afflictions that have given the character extra limbs (e.g. spineling genes)
420  //ignoring the error should be safe though, not executing the attack should not cause any further issues
422  {
423  string errorMsg = $"Received invalid {(eventType == EventType.SetAttackTarget ? "SetAttackTarget" : "ExecuteAttack")} message. Limb index out of bounds (character: {Name}, limb index: {attackLimbIndex}, limb count: {AnimController.Limbs.Length})";
424  DebugConsole.ThrowError(errorMsg);
425  GameAnalyticsManager.AddErrorEventOnce("Character.ClientEventRead:AttackLimbOutOfBounds", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
426  }
427  break;
428  }
429  Limb attackLimb = AnimController.Limbs[attackLimbIndex];
430  Limb targetLimb = null;
431  IDamageable targetEntity = FindEntityByID(targetEntityID) as IDamageable;
432  if (targetEntity == null && eventType == EventType.SetAttackTarget)
433  {
434  DebugConsole.ThrowError($"Received invalid SetAttackTarget message. Target entity not found (ID {targetEntityID})");
435  GameAnalyticsManager.AddErrorEventOnce("Character.ClientEventRead:TargetNotFound", GameAnalyticsManager.ErrorSeverity.Error, "Received invalid SetAttackTarget message. Target entity not found.");
436  break;
437  }
438  if (targetEntity is Character targetCharacter && targetLimbIndex != 255)
439  {
440  if (targetLimbIndex >= targetCharacter.AnimController.Limbs.Length)
441  {
442  DebugConsole.ThrowError($"Received invalid {(eventType == EventType.SetAttackTarget ? "SetAttackTarget" : "ExecuteAttack")} message. Target limb index out of bounds (target character: {targetCharacter.Name}, limb index: {targetLimbIndex}, limb count: {targetCharacter.AnimController.Limbs.Length})");
443  string errorMsgWithoutName = $"Received invalid {(eventType == EventType.SetAttackTarget ? "SetAttackTarget" : "ExecuteAttack")} message. Target limb index out of bounds (target character: {targetCharacter.SpeciesName}, limb index: {targetLimbIndex}, limb count: {targetCharacter.AnimController.Limbs.Length})";
444  GameAnalyticsManager.AddErrorEventOnce("Character.ClientEventRead:TargetLimbOutOfBounds", GameAnalyticsManager.ErrorSeverity.Error, errorMsgWithoutName);
445  break;
446  }
447  targetLimb = targetCharacter.AnimController.Limbs[targetLimbIndex];
448  }
449  if (attackLimb?.attack != null && Controlled != this)
450  {
451  if (eventType == EventType.SetAttackTarget)
452  {
453  SetAttackTarget(attackLimb, targetEntity, targetSimPos);
454  PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3);
455  }
456  else
457  {
458  attackLimb.ExecuteAttack(targetEntity, targetLimb, out _);
459  }
460  }
461  break;
462  case EventType.AssignCampaignInteraction:
463  byte campaignInteractionType = msg.ReadByte();
464  bool requireConsciousness = msg.ReadBoolean();
465  (GameMain.GameSession?.GameMode as CampaignMode)?.AssignNPCMenuInteraction(this, (CampaignMode.InteractionType)campaignInteractionType);
466  RequireConsciousnessForCustomInteract = requireConsciousness;
467  break;
468  case EventType.ObjectiveManagerState:
469  // 1 = order, 2 = objective
472  (int)AIObjectiveManager.ObjectiveType.MinValue,
473  (int)AIObjectiveManager.ObjectiveType.MaxValue);
474  if (msgType == 0) { break; }
475  bool validData = msg.ReadBoolean();
476  if (!validData) { break; }
477  if (msgType == AIObjectiveManager.ObjectiveType.Order)
478  {
479  UInt32 orderPrefabUintIdentifier = msg.ReadUInt32();
480  var orderPrefab = OrderPrefab.Prefabs.Find(p => p.UintIdentifier == orderPrefabUintIdentifier);
481  Identifier option = Identifier.Empty;
482  if (orderPrefab.HasOptions)
483  {
484  int optionIndex = msg.ReadRangedInteger(-1, orderPrefab.AllOptions.Length);
485  if (optionIndex > -1)
486  {
487  option = orderPrefab.AllOptions[optionIndex];
488  }
489  }
490  GameMain.GameSession?.CrewManager?.SetOrderHighlight(this, orderPrefab.Identifier, option);
491  }
492  else if (msgType == AIObjectiveManager.ObjectiveType.Objective)
493  {
494  Identifier identifier = msg.ReadIdentifier();
495  Identifier option = msg.ReadIdentifier();
496  ushort objectiveTargetEntityId = msg.ReadUInt16();
497  var objectiveTargetEntity = FindEntityByID(objectiveTargetEntityId);
498  GameMain.GameSession?.CrewManager?.CreateObjectiveIcon(this, identifier, option, objectiveTargetEntity);
499  }
500  break;
501  case EventType.TeamChange:
502  byte newTeamId = msg.ReadByte();
503  ChangeTeam((CharacterTeamType)newTeamId);
504  break;
505  case EventType.AddToCrew:
507  ReadItemTeamChange(msg, true);
508  break;
509  case EventType.RemoveFromCrew:
510  GameMain.GameSession.CrewManager.RemoveCharacter(this, removeInfo: true);
511  ReadItemTeamChange(msg, false);
512  break;
513  case EventType.UpdateExperience:
514  int experienceAmount = msg.ReadInt32();
515  info?.SetExperience(experienceAmount);
516  break;
517  case EventType.UpdateTalents:
518  ushort talentCount = msg.ReadUInt16();
519  for (int i = 0; i < talentCount; i++)
520  {
521  bool addedThisRound = msg.ReadBoolean();
522  UInt32 talentIdentifier = msg.ReadUInt32();
523  GiveTalent(talentIdentifier, addedThisRound);
524  }
525  break;
526  case EventType.UpdateMoney:
527  int moneyAmount = msg.ReadInt32();
528  SetMoney(moneyAmount);
529  break;
530  case EventType.UpdatePermanentStats:
531  byte savedStatValueCount = msg.ReadByte();
532  StatTypes statType = (StatTypes)msg.ReadByte();
533  info?.ClearSavedStatValues(statType);
534  for (int i = 0; i < savedStatValueCount; i++)
535  {
536  Identifier statIdentifier = msg.ReadIdentifier();
537  float statValue = msg.ReadSingle();
538  bool removeOnDeath = msg.ReadBoolean();
539  info?.ChangeSavedStatValue(statType, statValue, statIdentifier, removeOnDeath, setValue: true);
540  }
541  break;
542  case EventType.LatchOntoTarget:
543  bool attached = msg.ReadBoolean();
544  if (attached)
545  {
546  Vector2 characterSimPos = new Vector2(msg.ReadSingle(), msg.ReadSingle());
547  Vector2 attachSurfaceNormal = new Vector2(msg.ReadSingle(), msg.ReadSingle());
548  Vector2 attachPos = new Vector2(msg.ReadSingle(), msg.ReadSingle());
549  int attachWallIndex = msg.ReadInt32();
550  UInt16 attachTargetId = msg.ReadUInt16();
551 
552  if (AIController is EnemyAIController { LatchOntoAI: { } latchOntoAi })
553  {
554  var attachTargetEntity = FindEntityByID(attachTargetId);
555  switch (attachTargetEntity)
556  {
557  case Character attachTargetCharacter:
558  latchOntoAi.SetAttachTarget(attachTargetCharacter);
559  break;
560  case Structure attachTargetStructure:
561  latchOntoAi.SetAttachTarget(attachTargetStructure, attachPos, attachSurfaceNormal);
562  break;
563  default:
564  var allLevelWalls = Level.Loaded.GetAllCells();
565  if (attachWallIndex >= 0 && attachWallIndex <= allLevelWalls.Count)
566  {
567  latchOntoAi.SetAttachTarget(allLevelWalls[attachWallIndex]);
568  }
569  break;
570  }
571  latchOntoAi.AttachToBody(attachPos, attachSurfaceNormal, characterSimPos);
572  }
573  }
574  else
575  {
576  if (AIController is EnemyAIController { LatchOntoAI: { } latchOntoAi })
577  {
578  latchOntoAi.DeattachFromBody(reset: false);
579  }
580  }
581 
582  break;
583  }
584  msg.ReadPadBits();
585 
586  static void ReadItemTeamChange(IReadMessage msg, bool allowStealing)
587  {
588  var itemTeamChange = INetSerializableStruct.Read<ItemTeamChange>(msg);
589  foreach (var itemID in itemTeamChange.ItemIds)
590  {
591  if (FindEntityByID(itemID) is not Item item) { continue; }
592  item.AllowStealing = allowStealing;
593  if (item.GetComponent<WifiComponent>() is { } wifiComponent)
594  {
595  wifiComponent.TeamID = itemTeamChange.TeamId;
596  }
597  if (item.GetComponent<IdCard>() is { } idCard)
598  {
599  idCard.TeamID = itemTeamChange.TeamId;
600  idCard.SubmarineSpecificID = 0;
601  }
602  }
603  }
604  }
605 
607  {
608  DebugConsole.Log("Reading character spawn data");
609 
610  if (GameMain.Client == null) { return null; }
611 
612  bool noInfo = inc.ReadBoolean();
613  ushort id = inc.ReadUInt16();
614  string speciesName = inc.ReadString();
615  string seed = inc.ReadString();
616 
617  Vector2 position = new Vector2(inc.ReadSingle(), inc.ReadSingle());
618 
619  bool enabled = inc.ReadBoolean();
620  bool disabledByEvent = inc.ReadBoolean();
621 
622  DebugConsole.Log("Received spawn data for " + speciesName);
623 
624  Character character = null;
625  if (noInfo)
626  {
627  try
628  {
629  character = Create(speciesName, position, seed, characterInfo: null, id: id, isRemotePlayer: false);
630  }
631  catch (Exception e)
632  {
633  DebugConsole.ThrowError($"Failed to spawn character {speciesName}", e);
634  throw;
635  }
636  bool containsStatusData = inc.ReadBoolean();
637  if (containsStatusData)
638  {
639  character.ReadStatus(inc);
640  }
641  }
642  else
643  {
644  bool hasOwner = inc.ReadBoolean();
645  int ownerId = hasOwner ? inc.ReadByte() : -1;
646  float humanPrefabHealthMultiplier = inc.ReadSingle();
647  int balance = inc.ReadInt32();
648  int rewardDistribution = inc.ReadRangedInteger(0, 100);
649  byte teamID = inc.ReadByte();
650  bool hasAi = inc.ReadBoolean();
651  Identifier infoSpeciesName = inc.ReadIdentifier();
652 
653  CharacterInfo info = CharacterInfo.ClientRead(infoSpeciesName, inc);
654  try
655  {
656  character = Create(speciesName, position, seed, characterInfo: info, id: id, isRemotePlayer: ownerId > 0 && GameMain.Client.SessionId != ownerId, hasAi: hasAi);
657  }
658  catch (Exception e)
659  {
660  DebugConsole.ThrowError($"Failed to spawn character {speciesName}", e);
661  throw;
662  }
663  character.TeamID = (CharacterTeamType)teamID;
664  character.CampaignInteractionType = (CampaignMode.InteractionType)inc.ReadByte();
665  if (character.CampaignInteractionType == CampaignMode.InteractionType.Store)
666  {
667  character.MerchantIdentifier = inc.ReadIdentifier();
668  }
669  character.Faction = inc.ReadIdentifier();
670  character.HumanPrefabHealthMultiplier = humanPrefabHealthMultiplier;
671  character.Wallet.Balance = balance;
672  character.Wallet.RewardDistribution = rewardDistribution;
673  if (character.CampaignInteractionType != CampaignMode.InteractionType.None)
674  {
675  (GameMain.GameSession.GameMode as CampaignMode)?.AssignNPCMenuInteraction(character, character.CampaignInteractionType);
676  }
677 
678  // Check if the character has current orders
679  int orderCount = inc.ReadByte();
680  for (int i = 0; i < orderCount; i++)
681  {
682  UInt32 orderPrefabUintIdentifier = inc.ReadUInt32();
683  Entity targetEntity = FindEntityByID(inc.ReadUInt16());
684  Character orderGiver = inc.ReadBoolean() ? FindEntityByID(inc.ReadUInt16()) as Character : null;
685  int orderOptionIndex = inc.ReadByte();
686  int orderPriority = inc.ReadByte();
687  OrderTarget targetPosition = null;
688  if (inc.ReadBoolean())
689  {
690  var x = inc.ReadSingle();
691  var y = inc.ReadSingle();
692  var hull = FindEntityByID(inc.ReadUInt16()) as Hull;
693  targetPosition = new OrderTarget(new Vector2(x, y), hull, creatingFromExistingData: true);
694  }
695 
696  OrderPrefab orderPrefab =
697  OrderPrefab.Prefabs.Find(p => p.UintIdentifier == orderPrefabUintIdentifier);
698  if (orderPrefab != null)
699  {
700  var component = orderPrefab.GetTargetItemComponent(targetEntity as Item);
701  if (!orderPrefab.MustSetTarget || (targetEntity != null && component != null) || targetPosition != null)
702  {
703  var order = targetPosition == null ?
704  new Order(orderPrefab, targetEntity, component, orderGiver: orderGiver) :
705  new Order(orderPrefab, targetPosition, orderGiver: orderGiver);
706  order = order.WithOption(
707  orderOptionIndex >= 0 && orderOptionIndex < orderPrefab.Options.Length
708  ? orderPrefab.Options[orderOptionIndex]
709  : Identifier.Empty)
710  .WithManualPriority(orderPriority)
711  .WithOrderGiver(orderGiver);
712  character.SetOrder(order, isNewOrder: true, speak: false, force: true);
713  }
714  else
715  {
716  DebugConsole.AddSafeError("Could not set order \"" + orderPrefab.Identifier + "\" for character \"" + character.Name + "\" because required target entity was not found.");
717  }
718  }
719  else
720  {
721  DebugConsole.ThrowError("Invalid order prefab index - index (" + orderPrefabUintIdentifier + ") out of bounds.");
722  }
723  }
724 
725  bool containsStatusData = inc.ReadBoolean();
726  if (containsStatusData)
727  {
728  character.ReadStatus(inc);
729  }
730 
731  if (character.IsHuman && character.TeamID != CharacterTeamType.FriendlyNPC && character.TeamID != CharacterTeamType.None)
732  {
733  CharacterInfo duplicateCharacterInfo = GameMain.GameSession.CrewManager.GetCharacterInfos().FirstOrDefault(c => c.ID == info.ID);
734  GameMain.GameSession.CrewManager.RemoveCharacterInfo(duplicateCharacterInfo);
735  if (character.isDead)
736  {
737  //just add the info if dead (displayed in the round summary, and crew list if the character is revived)
739  }
740  else
741  {
743  }
744  }
745 
746  if (GameMain.Client.SessionId == ownerId)
747  {
748  GameMain.Client.HasSpawned = true;
749  GameMain.Client.Character = character;
750  if (!character.IsDead) { Controlled = character; }
751 
752  GameMain.LightManager.LosEnabled = true;
753  GameMain.LightManager.LosAlpha = 1f;
754 
756 
757  character.memInput.Clear();
758  character.memState.Clear();
759  character.memLocalState.Clear();
760  }
761  }
762 
763  if (disabledByEvent)
764  {
765  character.DisabledByEvent = true;
766  }
767  else
768  {
769  character.Enabled = Controlled == character || enabled;
770  }
771 
772  return character;
773  }
774 
775  private void ReadStatus(IReadMessage msg)
776  {
777  bool isDead = msg.ReadBoolean();
778  if (isDead)
779  {
780  CauseOfDeathType causeOfDeathType = (CauseOfDeathType)msg.ReadRangedInteger(0, Enum.GetValues(typeof(CauseOfDeathType)).Length - 1);
781  AfflictionPrefab causeOfDeathAffliction = null;
782  if (causeOfDeathType == CauseOfDeathType.Affliction)
783  {
784  uint afflictionId = msg.ReadUInt32();
785  AfflictionPrefab afflictionPrefab = AfflictionPrefab.Prefabs.Find(p => p.UintIdentifier == afflictionId);
786  if (afflictionPrefab == null)
787  {
788  string errorMsg = $"Error in CharacterNetworking.ReadStatus: affliction not found (id {afflictionId})";
789  causeOfDeathType = CauseOfDeathType.Unknown;
790  GameAnalyticsManager.AddErrorEventOnce("CharacterNetworking.ReadStatus:AfflictionNotFound", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
791  }
792  else
793  {
794  causeOfDeathAffliction = afflictionPrefab;
795  }
796  }
797  bool containsAfflictionData = msg.ReadBoolean();
798  if (!IsDead)
799  {
800  if (causeOfDeathType == CauseOfDeathType.Pressure || causeOfDeathAffliction == AfflictionPrefab.Pressure)
801  {
802  Implode(true);
803  }
804  else
805  {
806  Kill(causeOfDeathType, causeOfDeathAffliction?.Instantiate(1.0f), true);
807  }
808  }
809  if (containsAfflictionData)
810  {
813  }
814  }
815  else
816  {
817  if (IsDead) { Revive(); }
819  }
820  byte severedLimbCount = msg.ReadByte();
821  for (int i = 0; i < severedLimbCount; i++)
822  {
823  int severedJointIndex = msg.ReadByte();
824  if (severedJointIndex < 0 || severedJointIndex >= AnimController.LimbJoints.Length)
825  {
826  string errorMsg = $"Error in CharacterNetworking.ReadStatus: severed joint index out of bounds (index: {severedJointIndex}, joint count: {AnimController.LimbJoints.Length})";
827  GameAnalyticsManager.AddErrorEventOnce("CharacterNetworking.ReadStatus:JointIndexOutOfBounts", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
828  }
829  else
830  {
832  }
833  }
834  }
835  }
836 }
AfflictionPrefab is a prefab that defines a type of affliction that can be applied to a character....
static readonly PrefabCollection< AfflictionPrefab > Prefabs
virtual void ClientEventRead(IReadMessage msg, float sendingTime)
Character(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo=null, ushort id=Entity.NullEntityID, bool isRemotePlayer=false, RagdollParams ragdollParams=null, bool spawnInitialItems=true)
void ClientWriteInput(in SegmentTableWriter< ClientNetSegment > segmentTableWriter, IWriteMessage msg)
static Character Create(CharacterInfo characterInfo, Vector2 position, string seed, ushort id=Entity.NullEntityID, bool isRemotePlayer=false, bool hasAi=true, RagdollParams ragdoll=null, bool spawnInitialItems=true)
Create a new character
void Revive(bool removeAfflictions=true, bool createNetworkEvent=false)
void ClientReadPosition(IReadMessage msg, float sendingTime)
Item????????? SelectedItem
The primary selected item. It can be any device that character interacts with. This excludes items li...
void SetAttackTarget(Limb attackLimb, IDamageable damageTarget, Vector2 attackPos)
virtual void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData=null)
void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage=false, bool log=true)
void PlaySound(CharacterSound.SoundType soundType, float soundIntervalFactor=1.0f, float maxInterval=0)
bool DisabledByEvent
MonsterEvents disable monsters (which includes removing them from the character list,...
Item SelectedSecondaryItem
The secondary selected item. It's an item other than a device (see SelectedItem), e....
bool GiveTalent(Identifier talentIdentifier, bool addingFirstTime=true)
bool IsRemotePlayer
Is the character controlled by another human player (should always be false in single player)
Stores information about the Character that is needed between rounds in the menu etc....
static CharacterInfo ClientRead(Identifier speciesName, IReadMessage inc, bool requireJobPrefabFound=true)
void ChangeSavedStatValue(StatTypes statType, float value, Identifier statIdentifier, bool removeOnDeath, float maxValue=float.MaxValue, bool setValue=false)
void SetSkillLevel(Identifier skillIdentifier, float level)
ushort ID
Unique ID given to character infos in MP. Non-persistent. Used by clients to identify which infos are...
void RemoveCharacter(Character character, bool removeInfo=false, bool resetCrewListIndex=true)
Remove the character from the crew (and crew menus).
void RemoveCharacterInfo(CharacterInfo characterInfo)
Remove info of a selected character. The character will not be visible in any menus or the round summ...
IEnumerable< CharacterInfo > GetCharacterInfos()
Note: this only returns AI characters' infos in multiplayer. The infos are used to manage hiring/firi...
void AddCharacter(Character character, bool sortCrewList=true)
const ushort NullEntityID
Definition: Entity.cs:14
readonly ushort ID
Unique, but non-persistent identifier. Stays the same if the entities are created in the exactly same...
Definition: Entity.cs:43
static Entity FindEntityByID(ushort ID)
Find an entity based on the ID
Definition: Entity.cs:204
static GameSession?? GameSession
Definition: GameMain.cs:88
static NetLobbyScreen NetLobbyScreen
Definition: GameMain.cs:55
static Lights.LightManager LightManager
Definition: GameMain.cs:78
static GameClient Client
Definition: GameMain.cs:188
void ExecuteAttack(IDamageable damageTarget, Limb targetLimb, out AttackResult attackResult)
readonly bool MustSetTarget
Definition: Order.cs:113
static readonly PrefabCollection< OrderPrefab > Prefabs
Definition: Order.cs:41
ItemComponent GetTargetItemComponent(Item item)
Get the target item component based on the target item type
Definition: Order.cs:341
readonly ImmutableArray< Identifier > Options
Definition: Order.cs:106
readonly Identifier Identifier
Definition: Prefab.cs:34
int ReadRangedInteger(int min, int max)
Single ReadRangedSingle(Single min, Single max, int bitCount)
void WriteRangedInteger(int val, int min, int max)
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
Definition: Enums.cs:180