Barotrauma Client Doc
SteamAchievementManager.cs
3 using Barotrauma.Steam;
4 using FarseerPhysics;
5 using Microsoft.Xna.Framework;
6 using System;
7 using System.Collections.Generic;
8 using System.Linq;
9 
10 namespace Barotrauma
11 {
12  static class SteamAchievementManager
13  {
14  private const float UpdateInterval = 1.0f;
15 
16  private static readonly HashSet<Identifier> unlockedAchievements = new HashSet<Identifier>();
17 
18  public static bool CheatsEnabled = false;
19 
20  private static float updateTimer;
21 
25  class RoundData
26  {
27  public readonly List<Reactor> Reactors = new List<Reactor>();
28 
29  public readonly HashSet<Character> EnteredCrushDepth = new HashSet<Character>();
30  public readonly HashSet<Character> ReactorMeltdown = new HashSet<Character>();
31 
32  public bool SubWasDamaged;
33  }
34 
35  private static RoundData roundData;
36 
37  // Used for the Extravehicular Activity ("crewaway") achievement
38  private static PathFinder pathFinder;
39  private static readonly Dictionary<Character, CachedDistance> cachedDistances = new Dictionary<Character, CachedDistance>();
40 
41  public static void OnStartRound()
42  {
43  roundData = new RoundData();
44  foreach (Item item in Item.ItemList)
45  {
46  if (item.Submarine == null || item.Submarine.Info.Type != SubmarineType.Player) { continue; }
47  Reactor reactor = item.GetComponent<Reactor>();
48  if (reactor != null && reactor.Item.Condition > 0.0f) { roundData.Reactors.Add(reactor); }
49  }
50  pathFinder = new PathFinder(WayPoint.WayPointList, false);
51  cachedDistances.Clear();
52  }
53 
54  public static void Update(float deltaTime)
55  {
56  if (GameMain.GameSession == null) { return; }
57 #if CLIENT
58  if (GameMain.Client != null) { return; }
59 #endif
60 
61  updateTimer -= deltaTime;
62  if (updateTimer > 0.0f) { return; }
63  updateTimer = UpdateInterval;
64 
65  if (Level.Loaded != null && roundData != null && Screen.Selected == GameMain.GameScreen)
66  {
67  if (GameMain.GameSession.EventManager.CurrentIntensity > 0.99f)
68  {
69  UnlockAchievement("maxintensity".ToIdentifier(), true, c => c != null && !c.IsDead && !c.IsUnconscious);
70  }
71 
72  foreach (Character c in Character.CharacterList)
73  {
74  if (c.IsDead) { continue; }
75  //achievement for descending below crush depth and coming back
76  if (GameMain.GameSession.RoundDuration > 30.0f)
77  {
78  if (c.Submarine != null && c.Submarine.AtDamageDepth || Level.Loaded.GetRealWorldDepth(c.WorldPosition.Y) > Level.Loaded.RealWorldCrushDepth)
79  {
80  roundData.EnteredCrushDepth.Add(c);
81  }
82  else if (Level.Loaded.GetRealWorldDepth(c.WorldPosition.Y) < Level.Loaded.RealWorldCrushDepth - 500.0f)
83  {
84  //all characters that have entered crush depth and are still alive get an achievement
85  if (roundData.EnteredCrushDepth.Contains(c)) { UnlockAchievement(c, "survivecrushdepth".ToIdentifier()); }
86  }
87  }
88  }
89 
90  foreach (Submarine sub in Submarine.Loaded)
91  {
92  foreach (Reactor reactor in roundData.Reactors)
93  {
94  if (reactor.Item.Condition <= 0.0f && reactor.Item.Submarine == sub)
95  {
96  //characters that were inside the sub during a reactor meltdown
97  //get an achievement if they're still alive at the end of the round
98  foreach (Character c in Character.CharacterList)
99  {
100  if (!c.IsDead && c.Submarine == sub) { roundData.ReactorMeltdown.Add(c); }
101  }
102  }
103  }
104 
105  //convert submarine velocity to km/h
106  Vector2 submarineVel = Physics.DisplayToRealWorldRatio * ConvertUnits.ToDisplayUnits(sub.Velocity) * 3.6f;
107  //achievement for going > 100 km/h
108  if (Math.Abs(submarineVel.X) > 100.0f)
109  {
110  //all conscious characters inside the sub get an achievement
111  UnlockAchievement("subhighvelocity".ToIdentifier(), true, c => c != null && c.Submarine == sub && !c.IsDead && !c.IsUnconscious);
112  }
113 
114  //achievement for descending ridiculously deep
115  float realWorldDepth = sub.RealWorldDepth;
116  if (realWorldDepth > 5000.0f && GameMain.GameSession.RoundDuration > 30.0f)
117  {
118  //all conscious characters inside the sub get an achievement
119  UnlockAchievement("subdeep".ToIdentifier(), true, c => c != null && c.Submarine == sub && !c.IsDead && !c.IsUnconscious);
120  }
121  }
122 
123  if (!roundData.SubWasDamaged)
124  {
125  roundData.SubWasDamaged = SubWallsDamaged(Submarine.MainSub);
126  }
127  }
128 
129  if (GameMain.GameSession != null)
130  {
131 #if CLIENT
132  if (Character.Controlled != null && !(GameMain.GameSession.GameMode is TestGameMode))
133  {
134  CheckMidRoundAchievements(Character.Controlled);
135  }
136 #else
137  foreach (Client client in GameMain.Server.ConnectedClients)
138  {
139  if (client.Character != null)
140  {
141  CheckMidRoundAchievements(client.Character);
142  }
143  }
144 #endif
145  }
146  }
147 
148  private static void CheckMidRoundAchievements(Character c)
149  {
150  if (c == null || c.Removed) { return; }
151 
152  if (c.HasEquippedItem("clownmask".ToIdentifier()) &&
153  c.HasEquippedItem("clowncostume".ToIdentifier()))
154  {
155  UnlockAchievement(c, "clowncostume".ToIdentifier());
156  }
157 
158  if (Submarine.MainSub != null && c.Submarine == null && c.SpeciesName == CharacterPrefab.HumanSpeciesName)
159  {
160  float requiredDist = 500 / Physics.DisplayToRealWorldRatio;
161  float distSquared = Vector2.DistanceSquared(c.WorldPosition, Submarine.MainSub.WorldPosition);
162  if (cachedDistances.TryGetValue(c, out var cachedDistance))
163  {
164  if (cachedDistance.ShouldUpdateDistance(c.WorldPosition, Submarine.MainSub.WorldPosition))
165  {
166  cachedDistances.Remove(c);
167  cachedDistance = CalculateNewCachedDistance(c);
168  if (cachedDistance != null)
169  {
170  cachedDistances.Add(c, cachedDistance);
171  }
172  }
173  }
174  else
175  {
176  cachedDistance = CalculateNewCachedDistance(c);
177  if (cachedDistance != null)
178  {
179  cachedDistances.Add(c, cachedDistance);
180  }
181  }
182  if (cachedDistance != null)
183  {
184  distSquared = Math.Max(distSquared, cachedDistance.Distance * cachedDistance.Distance);
185  }
186  if (distSquared > requiredDist * requiredDist)
187  {
188  UnlockAchievement(c, "crewaway".ToIdentifier());
189  }
190 
191  static CachedDistance CalculateNewCachedDistance(Character c)
192  {
193  pathFinder ??= new PathFinder(WayPoint.WayPointList, false);
194  var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(c.WorldPosition), ConvertUnits.ToSimUnits(Submarine.MainSub.WorldPosition));
195  if (path.Unreachable) { return null; }
196  return new CachedDistance(c.WorldPosition, Submarine.MainSub.WorldPosition, path.TotalLength, Timing.TotalTime + Rand.Range(1.0f, 5.0f));
197  }
198  }
199  }
200 
201  private static bool SubWallsDamaged(Submarine sub)
202  {
203  foreach (Structure structure in Structure.WallList)
204  {
205  if (structure.Submarine != sub || structure.HasBody) { continue; }
206  for (int i = 0; i < structure.SectionCount; i++)
207  {
208  if (structure.SectionIsLeaking(i))
209  {
210  return true;
211  }
212  }
213  }
214  return false;
215  }
216 
217  public static void OnBiomeDiscovered(Biome biome)
218  {
219  UnlockAchievement($"discover{biome.Identifier.Value.Replace(" ", "")}".ToIdentifier());
220  }
221 
222  public static void OnCampaignMetadataSet(Identifier identifier, object value, bool unlockClients = false)
223  {
224  if (identifier.IsEmpty || value is null) { return; }
225  UnlockAchievement($"campaignmetadata_{identifier}_{value}".ToIdentifier(), unlockClients);
226  }
227 
228  public static void OnItemRepaired(Item item, Character fixer)
229  {
230 #if CLIENT
231  if (GameMain.Client != null) { return; }
232 #endif
233  if (fixer == null) { return; }
234 
235  UnlockAchievement(fixer, "repairdevice".ToIdentifier());
236  UnlockAchievement(fixer, $"repair{item.Prefab.Identifier}".ToIdentifier());
237  }
238 
239  public static void OnAfflictionReceived(Affliction affliction, Character character)
240  {
241  if (affliction.Prefab.AchievementOnReceived.IsEmpty) { return; }
242 #if CLIENT
243  if (GameMain.Client != null) { return; }
244 #endif
245  UnlockAchievement(character, affliction.Prefab.AchievementOnReceived);
246  }
247 
248  public static void OnAfflictionRemoved(Affliction affliction, Character character)
249  {
250  if (affliction.Prefab.AchievementOnRemoved.IsEmpty) { return; }
251 
252 #if CLIENT
253  if (GameMain.Client != null) { return; }
254 #endif
255  UnlockAchievement(character, affliction.Prefab.AchievementOnRemoved);
256  }
257 
258  public static void OnCharacterRevived(Character character, Character reviver)
259  {
260 #if CLIENT
261  if (GameMain.Client != null) { return; }
262 #endif
263  if (reviver == null) { return; }
264  UnlockAchievement(reviver, "healcrit".ToIdentifier());
265  }
266 
267  public static void OnCharacterKilled(Character character, CauseOfDeath causeOfDeath)
268  {
269 #if CLIENT
270  if (GameMain.Client != null || GameMain.GameSession == null) { return; }
271 
272  if (character != Character.Controlled &&
273  causeOfDeath.Killer != null &&
274  causeOfDeath.Killer == Character.Controlled)
275  {
276  IncrementStat(causeOfDeath.Killer, (character.IsHuman ? "humanskilled" : "monsterskilled").ToIdentifier(), 1);
277  }
278 #elif SERVER
279  if (character != causeOfDeath.Killer && causeOfDeath.Killer != null)
280  {
281  IncrementStat(causeOfDeath.Killer, (character.IsHuman ? "humanskilled" : "monsterskilled").ToIdentifier(), 1);
282  }
283 #endif
284 
285  UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName}".ToIdentifier());
286  if (character.CurrentHull != null)
287  {
288  UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName}indoors".ToIdentifier());
289  }
290  if (character.SpeciesName.EndsWith("boss"))
291  {
292  UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("boss", "")}".ToIdentifier());
293  if (character.CurrentHull != null)
294  {
295  UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("boss", "")}indoors".ToIdentifier());
296  }
297  }
298  if (character.SpeciesName.EndsWith("_m"))
299  {
300  UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("_m", "")}".ToIdentifier());
301  if (character.CurrentHull != null)
302  {
303  UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("_m", "")}indoors".ToIdentifier());
304  }
305  }
306 
307  if (character.HasEquippedItem("clownmask".ToIdentifier()) &&
308  character.HasEquippedItem("clowncostume".ToIdentifier()) &&
309  causeOfDeath.Killer != character)
310  {
311  UnlockAchievement(causeOfDeath.Killer, "killclown".ToIdentifier());
312  }
313 
314  // TODO: should we change this? Morbusine used to be the strongest poison. Now Cyanide is strongest.
315  if (character.CharacterHealth?.GetAffliction("morbusinepoisoning") != null)
316  {
317  UnlockAchievement(causeOfDeath.Killer, "killpoison".ToIdentifier());
318  }
319 
320  if (causeOfDeath.DamageSource is Item item)
321  {
322  if (item.HasTag(Tags.ToolItem))
323  {
324  UnlockAchievement(causeOfDeath.Killer, "killtool".ToIdentifier());
325  }
326  else
327  {
328  // TODO: should we change this? Morbusine used to be the strongest poison. Now Cyanide is strongest.
329  if (item.Prefab.Identifier == "morbusine")
330  {
331  UnlockAchievement(causeOfDeath.Killer, "killpoison".ToIdentifier());
332  }
333  else if (item.Prefab.Identifier == "nuclearshell" ||
334  item.Prefab.Identifier == "nucleardepthcharge")
335  {
336  UnlockAchievement(causeOfDeath.Killer, "killnuke".ToIdentifier());
337  }
338  }
339  }
340 
341 #if SERVER
342  if (GameMain.Server?.TraitorManager != null)
343  {
344  if (GameMain.Server.TraitorManager.IsTraitor(character))
345  {
346  UnlockAchievement(causeOfDeath.Killer, "killtraitor".ToIdentifier());
347  }
348  }
349 #endif
350  }
351 
352  public static void OnTraitorWin(Character character)
353  {
354 #if CLIENT
355  if (GameMain.Client != null || GameMain.GameSession == null) { return; }
356 #endif
357  UnlockAchievement(character, "traitorwin".ToIdentifier());
358  }
359 
360  public static void OnRoundEnded(GameSession gameSession)
361  {
362  if (CheatsEnabled) { return; }
363 
364  //made it to the destination
365  if (gameSession?.Submarine != null && Level.Loaded != null && gameSession.Submarine.AtEndExit)
366  {
367  float levelLengthMeters = Physics.DisplayToRealWorldRatio * Level.Loaded.Size.X;
368  float levelLengthKilometers = levelLengthMeters / 1000.0f;
369  //in multiplayer the client's/host's character must be inside the sub (or end outpost) and alive
370  if (GameMain.NetworkMember != null)
371  {
372 #if CLIENT
373  Character myCharacter = Character.Controlled;
374  if (myCharacter != null &&
375  !myCharacter.IsDead &&
376  (myCharacter.Submarine == gameSession.Submarine || (Level.Loaded?.EndOutpost != null && myCharacter.Submarine == Level.Loaded.EndOutpost)))
377  {
378  IncrementStat("kmstraveled".ToIdentifier(), levelLengthKilometers);
379  }
380 #endif
381  }
382  else
383  {
384  //in sp making it to the end is enough
385  IncrementStat("kmstraveled".ToIdentifier(), levelLengthKilometers);
386  }
387  }
388 
389  //make sure changed stats (kill count, kms traveled) get stored
390  SteamManager.StoreStats();
391 
392  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
393 
394  foreach (Mission mission in gameSession.Missions)
395  {
396  if (mission is CombatMission combatMission && GameMain.GameSession.WinningTeam.HasValue)
397  {
398  //all characters that are alive and in the winning team get an achievement
399  var achvIdentifier =
400  $"{mission.Prefab.AchievementIdentifier}{(int) GameMain.GameSession.WinningTeam}"
401  .ToIdentifier();
402  UnlockAchievement(achvIdentifier, true,
403  c => c != null && !c.IsDead && !c.IsUnconscious && combatMission.IsInWinningTeam(c));
404  }
405  else if (mission.Completed)
406  {
407  //all characters get an achievement
408  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
409  {
410  UnlockAchievement(mission.Prefab.AchievementIdentifier, true, c => c != null);
411  }
412  else
413  {
414  UnlockAchievement(mission.Prefab.AchievementIdentifier);
415  }
416  }
417  }
418 
419  //made it to the destination
420  if (gameSession.Submarine.AtEndExit)
421  {
422  bool noDamageRun = !roundData.SubWasDamaged && !gameSession.Casualties.Any();
423 
424 #if SERVER
425  if (GameMain.Server != null)
426  {
427  //in MP all characters that were inside the sub during reactor meltdown and still alive at the end of the round get an achievement
428  UnlockAchievement("survivereactormeltdown".ToIdentifier(), true, c => c != null && !c.IsDead && roundData.ReactorMeltdown.Contains(c));
429  if (noDamageRun)
430  {
431  UnlockAchievement("nodamagerun".ToIdentifier(), true, c => c != null && !c.IsDead);
432  }
433  }
434 #endif
435 #if CLIENT
436  if (noDamageRun) { UnlockAchievement("nodamagerun".ToIdentifier()); }
437  if (roundData.ReactorMeltdown.Any()) //in SP getting to the destination after a meltdown is enough
438  {
439  UnlockAchievement("survivereactormeltdown".ToIdentifier());
440  }
441 #endif
442  var charactersInSub = Character.CharacterList.FindAll(c =>
443  !c.IsDead &&
444  c.TeamID != CharacterTeamType.FriendlyNPC &&
445  c.AIController is not EnemyAIController &&
446  (c.Submarine == gameSession.Submarine || gameSession.Submarine.GetConnectedSubs().Contains(c.Submarine) || (Level.Loaded?.EndOutpost != null && c.Submarine == Level.Loaded.EndOutpost)));
447 
448  if (charactersInSub.Count == 1)
449  {
450  //there must be some casualties to get the last mant standing achievement
451  if (gameSession.Casualties.Any())
452  {
453  UnlockAchievement(charactersInSub[0], "lastmanstanding".ToIdentifier());
454  }
455 #if CLIENT
456  else if (GameMain.GameSession.CrewManager.GetCharacters().Count() == 1)
457  {
458  UnlockAchievement(charactersInSub[0], "lonesailor".ToIdentifier());
459  }
460 #else
461  //lone sailor achievement if alone in the sub and there are no other characters with the same team ID
462  else if (!Character.CharacterList.Any(c =>
463  c != charactersInSub[0] &&
464  c.TeamID == charactersInSub[0].TeamID &&
465  !(c.AIController is EnemyAIController)))
466  {
467  UnlockAchievement(charactersInSub[0], "lonesailor".ToIdentifier());
468  }
469 #endif
470 
471  }
472  foreach (Character character in charactersInSub)
473  {
474  if (roundData.EnteredCrushDepth.Contains(character))
475  {
476  UnlockAchievement(character, "survivecrushdepth".ToIdentifier());
477  }
478  if (character.Info.Job == null) { continue; }
479  UnlockAchievement(character, $"{character.Info.Job.Prefab.Identifier}round".ToIdentifier());
480  }
481  }
482 
483  pathFinder = null;
484  roundData = null;
485  }
486 
487  private static void UnlockAchievement(Character recipient, Identifier identifier)
488  {
489  if (CheatsEnabled || recipient == null) { return; }
490 #if CLIENT
491  if (recipient == Character.Controlled)
492  {
493  UnlockAchievement(identifier);
494  }
495 #elif SERVER
496  GameMain.Server?.GiveAchievement(recipient, identifier);
497 #endif
498  }
499 
500  private static void IncrementStat(Character recipient, Identifier identifier, int amount)
501  {
502  if (CheatsEnabled || recipient == null) { return; }
503 #if CLIENT
504  if (recipient == Character.Controlled)
505  {
506  SteamManager.IncrementStat(identifier, amount);
507  }
508 #elif SERVER
509  GameMain.Server?.IncrementStat(recipient, identifier, amount);
510 #endif
511  }
512 
513  public static void IncrementStat(Identifier identifier, int amount)
514  {
515  if (CheatsEnabled) { return; }
516  SteamManager.IncrementStat(identifier, amount);
517  }
518 
519  public static void IncrementStat(Identifier identifier, float amount)
520  {
521  if (CheatsEnabled) { return; }
522  SteamManager.IncrementStat(identifier, amount);
523  }
524 
525  public static void UnlockAchievement(Identifier identifier, bool unlockClients = false, Func<Character, bool> conditions = null)
526  {
527  if (CheatsEnabled) { return; }
528  if (Screen.Selected is { IsEditor: true }) { return; }
529 #if CLIENT
530  if (GameMain.GameSession?.GameMode is TestGameMode) { return; }
531 #endif
532 #if SERVER
533  if (unlockClients && GameMain.Server != null)
534  {
535  foreach (Client c in GameMain.Server.ConnectedClients)
536  {
537  if (conditions != null && !conditions(c.Character)) { continue; }
538  GameMain.Server.GiveAchievement(c, identifier);
539  }
540  }
541 #endif
542  //already unlocked, no need to do anything
543  if (unlockedAchievements.Contains(identifier)) { return; }
544  unlockedAchievements.Add(identifier);
545 
546 #if CLIENT
547  if (conditions != null && !conditions(Character.Controlled)) { return; }
548 #endif
549 
550  SteamManager.UnlockAchievement(identifier);
551  }
552  }
553 }
Submarine Submarine
Definition: Entity.cs:53