Client LuaCsForBarotrauma
AchievementManager.cs
3 using Barotrauma.Steam;
4 using FarseerPhysics;
5 using Microsoft.Xna.Framework;
6 using System;
7 using System.Collections.Generic;
8 using System.Collections.Immutable;
9 using System.Linq;
10 
11 namespace Barotrauma
12 {
14  internal readonly record struct NetIncrementedStat(AchievementStat Stat, float Amount) : INetSerializableStruct;
15 
16  static class AchievementManager
17  {
18  private const float UpdateInterval = 1.0f;
19 
20  private static readonly HashSet<Identifier> unlockedAchievements = new HashSet<Identifier>();
21 
22  public static bool CheatsEnabled = false;
23 
24  private static float updateTimer;
25 
29  private sealed class RoundData
30  {
31  public readonly List<Reactor> Reactors = new List<Reactor>();
32 
33  public readonly HashSet<Character> EnteredCrushDepth = new HashSet<Character>();
34  public readonly HashSet<Character> ReactorMeltdown = new HashSet<Character>();
35 
36  public bool SubWasDamaged;
37  }
38 
39  private static RoundData roundData;
40 
41  // Used for the Extravehicular Activity ("crewaway") achievement
42  private static PathFinder pathFinder;
43  private static readonly Dictionary<Character, CachedDistance> cachedDistances = new Dictionary<Character, CachedDistance>();
44 
45  public static void OnStartRound()
46  {
47  roundData = new RoundData();
48  foreach (Item item in Item.ItemList)
49  {
50  if (item.Submarine == null || item.Submarine.Info.Type != SubmarineType.Player) { continue; }
51  Reactor reactor = item.GetComponent<Reactor>();
52  if (reactor != null && reactor.Item.Condition > 0.0f) { roundData.Reactors.Add(reactor); }
53  }
54  pathFinder = new PathFinder(WayPoint.WayPointList, false);
55  cachedDistances.Clear();
56  }
57 
58  public static void Update(float deltaTime)
59  {
60  if (GameMain.GameSession == null) { return; }
61 #if CLIENT
62  if (GameMain.Client != null) { return; }
63 #endif
64 
65  updateTimer -= deltaTime;
66  if (updateTimer > 0.0f) { return; }
67  updateTimer = UpdateInterval;
68 
69  if (Level.Loaded != null && roundData != null && Screen.Selected == GameMain.GameScreen)
70  {
72  {
73  UnlockAchievement(
74  identifier: "maxintensity".ToIdentifier(),
75  unlockClients: true,
76  conditions: static c => c is { IsDead: false, IsUnconscious: false });
77  }
78 
79  foreach (Character c in Character.CharacterList)
80  {
81  if (c.IsDead) { continue; }
82  //achievement for descending below crush depth and coming back
84  {
86  {
87  roundData.EnteredCrushDepth.Add(c);
88  }
90  {
91  //all characters that have entered crush depth and are still alive get an achievement
92  if (roundData.EnteredCrushDepth.Contains(c)) { UnlockAchievement(c, "survivecrushdepth".ToIdentifier()); }
93  }
94  }
95  }
96 
97  foreach (Submarine sub in Submarine.Loaded)
98  {
99  foreach (Reactor reactor in roundData.Reactors)
100  {
101  if (reactor.Item.Condition <= 0.0f && reactor.Item.Submarine == sub)
102  {
103  //characters that were inside the sub during a reactor meltdown
104  //get an achievement if they're still alive at the end of the round
105  foreach (Character c in Character.CharacterList)
106  {
107  if (!c.IsDead && c.Submarine == sub) { roundData.ReactorMeltdown.Add(c); }
108  }
109  }
110  }
111 
112  //convert submarine velocity to km/h
113  Vector2 submarineVel = Physics.DisplayToRealWorldRatio * ConvertUnits.ToDisplayUnits(sub.Velocity) * 3.6f;
114  //achievement for going > 100 km/h
115  if (Math.Abs(submarineVel.X) > 100.0f)
116  {
117  //all conscious characters inside the sub get an achievement
118  UnlockAchievement("subhighvelocity".ToIdentifier(), true, c => c != null && c.Submarine == sub && !c.IsDead && !c.IsUnconscious);
119  }
120 
121  //achievement for descending ridiculously deep
122  float realWorldDepth = sub.RealWorldDepth;
123  if (realWorldDepth > 5000.0f && GameMain.GameSession.RoundDuration > 30.0f)
124  {
125  //all conscious characters inside the sub get an achievement
126  UnlockAchievement("subdeep".ToIdentifier(), true, c => c != null && c.Submarine == sub && !c.IsDead && !c.IsUnconscious);
127  }
128  }
129 
130  if (!roundData.SubWasDamaged)
131  {
132  roundData.SubWasDamaged = SubWallsDamaged(Submarine.MainSub);
133  }
134  }
135 
136  if (GameMain.GameSession != null)
137  {
138 #if CLIENT
140  {
141  CheckMidRoundAchievements(Character.Controlled);
142  }
143 #else
144  foreach (Client client in GameMain.Server.ConnectedClients)
145  {
146  if (client.Character != null)
147  {
148  CheckMidRoundAchievements(client.Character);
149  }
150  }
151 #endif
152  }
153  }
154 
155  private static void CheckMidRoundAchievements(Character c)
156  {
157  if (c == null || c.Removed) { return; }
158 
159  if (c.HasEquippedItem("clownmask".ToIdentifier()) &&
160  c.HasEquippedItem("clowncostume".ToIdentifier()))
161  {
162  UnlockAchievement(c, "clowncostume".ToIdentifier());
163  }
164 
165  if (Submarine.MainSub != null && c.Submarine == null && c.SpeciesName == CharacterPrefab.HumanSpeciesName)
166  {
167  float requiredDist = 500 / Physics.DisplayToRealWorldRatio;
168  float distSquared = Vector2.DistanceSquared(c.WorldPosition, Submarine.MainSub.WorldPosition);
169  if (cachedDistances.TryGetValue(c, out var cachedDistance))
170  {
171  if (cachedDistance.ShouldUpdateDistance(c.WorldPosition, Submarine.MainSub.WorldPosition))
172  {
173  cachedDistances.Remove(c);
174  cachedDistance = CalculateNewCachedDistance(c);
175  if (cachedDistance != null)
176  {
177  cachedDistances.Add(c, cachedDistance);
178  }
179  }
180  }
181  else
182  {
183  cachedDistance = CalculateNewCachedDistance(c);
184  if (cachedDistance != null)
185  {
186  cachedDistances.Add(c, cachedDistance);
187  }
188  }
189  if (cachedDistance != null)
190  {
191  distSquared = Math.Max(distSquared, cachedDistance.Distance * cachedDistance.Distance);
192  }
193  if (distSquared > requiredDist * requiredDist)
194  {
195  UnlockAchievement(c, "crewaway".ToIdentifier());
196  }
197 
198  static CachedDistance CalculateNewCachedDistance(Character c)
199  {
200  pathFinder ??= new PathFinder(WayPoint.WayPointList, false);
201  var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(c.WorldPosition), ConvertUnits.ToSimUnits(Submarine.MainSub.WorldPosition));
202  if (path.Unreachable) { return null; }
203  return new CachedDistance(c.WorldPosition, Submarine.MainSub.WorldPosition, path.TotalLength, Timing.TotalTime + Rand.Range(1.0f, 5.0f));
204  }
205  }
206  }
207 
208  private static bool SubWallsDamaged(Submarine sub)
209  {
210  foreach (Structure structure in Structure.WallList)
211  {
212  if (structure.Submarine != sub || structure.HasBody) { continue; }
213  for (int i = 0; i < structure.SectionCount; i++)
214  {
215  if (structure.SectionIsLeaking(i))
216  {
217  return true;
218  }
219  }
220  }
221  return false;
222  }
223 
224  public static void OnBiomeDiscovered(Biome biome)
225  {
226  UnlockAchievement($"discover{biome.Identifier.Value.Replace(" ", "")}".ToIdentifier());
227  }
228 
229  public static void OnCampaignMetadataSet(Identifier identifier, object value, bool unlockClients = false)
230  {
231  if (identifier.IsEmpty || value is null) { return; }
232  UnlockAchievement($"campaignmetadata_{identifier}_{value}".ToIdentifier(), unlockClients);
233  }
234 
235  public static void OnItemRepaired(Item item, Character fixer)
236  {
237 #if CLIENT
238  if (GameMain.Client != null) { return; }
239 #endif
240  if (fixer == null) { return; }
241 
242  UnlockAchievement(fixer, "repairdevice".ToIdentifier());
243  UnlockAchievement(fixer, $"repair{item.Prefab.Identifier}".ToIdentifier());
244  }
245 
246  public static void OnAfflictionReceived(Affliction affliction, Character character)
247  {
248  if (affliction.Prefab.AchievementOnReceived.IsEmpty) { return; }
249 #if CLIENT
250  if (GameMain.Client != null) { return; }
251 #endif
252  UnlockAchievement(character, affliction.Prefab.AchievementOnReceived);
253  }
254 
255  public static void OnAfflictionRemoved(Affliction affliction, Character character)
256  {
257  if (affliction.Prefab.AchievementOnRemoved.IsEmpty) { return; }
258 
259 #if CLIENT
260  if (GameMain.Client != null) { return; }
261 #endif
262  UnlockAchievement(character, affliction.Prefab.AchievementOnRemoved);
263  }
264 
265  public static void OnCharacterRevived(Character character, Character reviver)
266  {
267 #if CLIENT
268  if (GameMain.Client != null) { return; }
269 #endif
270  if (reviver == null) { return; }
271  UnlockAchievement(reviver, "healcrit".ToIdentifier());
272  }
273 
274  public static void OnCharacterKilled(Character character, CauseOfDeath causeOfDeath)
275  {
276 #if CLIENT
277  if (GameMain.Client != null || GameMain.GameSession == null) { return; }
278 
279  if (character != Character.Controlled &&
280  causeOfDeath.Killer != null &&
281  causeOfDeath.Killer == Character.Controlled)
282  {
283  IncrementStat(causeOfDeath.Killer, character.IsHuman ? AchievementStat.HumansKilled : AchievementStat.MonstersKilled , 1);
284  }
285 #elif SERVER
286  if (character != causeOfDeath.Killer && causeOfDeath.Killer != null)
287  {
288  IncrementStat(causeOfDeath.Killer, character.IsHuman ? AchievementStat.HumansKilled : AchievementStat.MonstersKilled , 1);
289  }
290 #endif
291 
292  UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName}".ToIdentifier());
293  if (character.CurrentHull != null)
294  {
295  UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName}indoors".ToIdentifier());
296  }
297  if (character.SpeciesName.EndsWith("boss"))
298  {
299  UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("boss", "")}".ToIdentifier());
300  if (character.CurrentHull != null)
301  {
302  UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("boss", "")}indoors".ToIdentifier());
303  }
304  }
305  if (character.SpeciesName.EndsWith("_m"))
306  {
307  UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("_m", "")}".ToIdentifier());
308  if (character.CurrentHull != null)
309  {
310  UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("_m", "")}indoors".ToIdentifier());
311  }
312  }
313 
314  if (character.HasEquippedItem("clownmask".ToIdentifier()) &&
315  character.HasEquippedItem("clowncostume".ToIdentifier()) &&
316  causeOfDeath.Killer != character)
317  {
318  UnlockAchievement(causeOfDeath.Killer, "killclown".ToIdentifier());
319  }
320 
321  // TODO: should we change this? Morbusine used to be the strongest poison. Now Cyanide is strongest.
322  if (character.CharacterHealth?.GetAffliction("morbusinepoisoning") != null)
323  {
324  UnlockAchievement(causeOfDeath.Killer, "killpoison".ToIdentifier());
325  }
326 
327  if (causeOfDeath.DamageSource is Item item)
328  {
329  if (item.HasTag(Tags.ToolItem))
330  {
331  UnlockAchievement(causeOfDeath.Killer, "killtool".ToIdentifier());
332  }
333  else
334  {
335  // TODO: should we change this? Morbusine used to be the strongest poison. Now Cyanide is strongest.
336  if (item.Prefab.Identifier == "morbusine")
337  {
338  UnlockAchievement(causeOfDeath.Killer, "killpoison".ToIdentifier());
339  }
340  else if (item.Prefab.Identifier == "nuclearshell" ||
341  item.Prefab.Identifier == "nucleardepthcharge")
342  {
343  UnlockAchievement(causeOfDeath.Killer, "killnuke".ToIdentifier());
344  }
345  }
346  }
347 
348 #if SERVER
349  if (GameMain.Server?.TraitorManager != null)
350  {
351  if (GameMain.Server.TraitorManager.IsTraitor(character))
352  {
353  UnlockAchievement(causeOfDeath.Killer, "killtraitor".ToIdentifier());
354  }
355  }
356 #endif
357  }
358 
359  public static void OnTraitorWin(Character character)
360  {
361 #if CLIENT
362  if (GameMain.Client != null || GameMain.GameSession == null) { return; }
363 #endif
364  UnlockAchievement(character, "traitorwin".ToIdentifier());
365  }
366 
367  public static void OnRoundEnded(GameSession gameSession)
368  {
369  if (CheatsEnabled) { return; }
370 
371  //made it to the destination
372  if (gameSession?.Submarine != null && Level.Loaded != null && gameSession.Submarine.AtEndExit)
373  {
374  float levelLengthMeters = Physics.DisplayToRealWorldRatio * Level.Loaded.Size.X;
375  float levelLengthKilometers = levelLengthMeters / 1000.0f;
376  //in multiplayer the client's/host's character must be inside the sub (or end outpost) and alive
377  if (GameMain.NetworkMember != null)
378  {
379 #if CLIENT
380  Character myCharacter = Character.Controlled;
381  if (myCharacter != null &&
382  !myCharacter.IsDead &&
383  (myCharacter.Submarine == gameSession.Submarine || (Level.Loaded?.EndOutpost != null && myCharacter.Submarine == Level.Loaded.EndOutpost)))
384  {
385  IncrementStat(AchievementStat.KMsTraveled, levelLengthKilometers);
386  }
387 #endif
388  }
389  else
390  {
391  //in sp making it to the end is enough
392  IncrementStat(AchievementStat.KMsTraveled, levelLengthKilometers);
393  }
394  }
395 
396  //make sure changed stats (kill count, kms traveled) get stored
397  SteamManager.StoreStats();
398 
399  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
400 
401  foreach (Mission mission in gameSession.Missions)
402  {
403  if (mission is CombatMission combatMission && GameMain.GameSession.WinningTeam.HasValue)
404  {
405  //all characters that are alive and in the winning team get an achievement
406  var achvIdentifier =
407  $"{mission.Prefab.AchievementIdentifier}{(int) GameMain.GameSession.WinningTeam}"
408  .ToIdentifier();
409  UnlockAchievement(achvIdentifier, true,
410  c => c != null && !c.IsDead && !c.IsUnconscious && combatMission.IsInWinningTeam(c));
411  }
412  else if (mission.Completed)
413  {
414  //all characters get an achievement
415  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
416  {
417  UnlockAchievement(mission.Prefab.AchievementIdentifier, true, c => c != null);
418  }
419  else
420  {
421  UnlockAchievement(mission.Prefab.AchievementIdentifier);
422  }
423  }
424  }
425 
426  //made it to the destination
427  if (gameSession.Submarine.AtEndExit)
428  {
429  bool noDamageRun = !roundData.SubWasDamaged && !gameSession.Casualties.Any();
430 
431 #if SERVER
432  if (GameMain.Server != null)
433  {
434  //in MP all characters that were inside the sub during reactor meltdown and still alive at the end of the round get an achievement
435  UnlockAchievement("survivereactormeltdown".ToIdentifier(), true, c => c != null && !c.IsDead && roundData.ReactorMeltdown.Contains(c));
436  if (noDamageRun)
437  {
438  UnlockAchievement("nodamagerun".ToIdentifier(), true, c => c != null && !c.IsDead);
439  }
440  }
441 #endif
442 #if CLIENT
443  if (noDamageRun) { UnlockAchievement("nodamagerun".ToIdentifier()); }
444  if (roundData.ReactorMeltdown.Any()) //in SP getting to the destination after a meltdown is enough
445  {
446  UnlockAchievement("survivereactormeltdown".ToIdentifier());
447  }
448 #endif
449  var charactersInSub = Character.CharacterList.FindAll(c =>
450  !c.IsDead &&
451  c.TeamID != CharacterTeamType.FriendlyNPC &&
452  c.AIController is not EnemyAIController &&
453  (c.Submarine == gameSession.Submarine || gameSession.Submarine.GetConnectedSubs().Contains(c.Submarine) || (Level.Loaded?.EndOutpost != null && c.Submarine == Level.Loaded.EndOutpost)));
454 
455  if (charactersInSub.Count == 1)
456  {
457  //there must be some casualties to get the last mant standing achievement
458  if (gameSession.Casualties.Any())
459  {
460  UnlockAchievement(charactersInSub[0], "lastmanstanding".ToIdentifier());
461  }
462 #if CLIENT
463  else if (GameMain.GameSession.CrewManager.GetCharacters().Count() == 1)
464  {
465  UnlockAchievement(charactersInSub[0], "lonesailor".ToIdentifier());
466  }
467 #else
468  //lone sailor achievement if alone in the sub and there are no other characters with the same team ID
469  else if (!Character.CharacterList.Any(c =>
470  c != charactersInSub[0] &&
471  c.TeamID == charactersInSub[0].TeamID &&
473  {
474  UnlockAchievement(charactersInSub[0], "lonesailor".ToIdentifier());
475  }
476 #endif
477 
478  }
479  foreach (Character character in charactersInSub)
480  {
481  if (roundData.EnteredCrushDepth.Contains(character))
482  {
483  UnlockAchievement(character, "survivecrushdepth".ToIdentifier());
484  }
485  if (character.Info.Job == null) { continue; }
486  UnlockAchievement(character, $"{character.Info.Job.Prefab.Identifier}round".ToIdentifier());
487  }
488  }
489 
490  pathFinder = null;
491  roundData = null;
492  }
493 
494  private static void UnlockAchievement(Character recipient, Identifier identifier)
495  {
496  if (CheatsEnabled || recipient == null) { return; }
497 #if CLIENT
498  if (recipient == Character.Controlled)
499  {
500  UnlockAchievement(identifier);
501  }
502 #elif SERVER
503  GameMain.Server?.GiveAchievement(recipient, identifier);
504 #endif
505  }
506 
507  private static void IncrementStat(Character recipient, AchievementStat stat, int amount)
508  {
509  if (CheatsEnabled || recipient == null) { return; }
510 #if CLIENT
511  if (recipient == Character.Controlled)
512  {
513  IncrementStat(stat, amount);
514  }
515 #elif SERVER
516  GameMain.Server?.IncrementStat(recipient, stat, amount);
517 #endif
518  }
519 
520  public static void UnlockAchievement(Identifier identifier, bool unlockClients = false, Func<Character, bool> conditions = null)
521  {
522  if (CheatsEnabled) { return; }
523  if (Screen.Selected is { IsEditor: true }) { return; }
524 #if CLIENT
525  if (GameMain.GameSession?.GameMode is TestGameMode) { return; }
526 #endif
527 #if SERVER
528  if (unlockClients && GameMain.Server != null)
529  {
530  foreach (Client c in GameMain.Server.ConnectedClients)
531  {
532  if (conditions != null && !conditions(c.Character)) { continue; }
533  GameMain.Server.GiveAchievement(c, identifier);
534  }
535  }
536 #endif
537 
538 #if CLIENT
539  if (conditions != null && !conditions(Character.Controlled)) { return; }
540 #endif
541 
542  UnlockAchievementsOnPlatforms(identifier);
543  }
544 
545  private static void UnlockAchievementsOnPlatforms(Identifier identifier)
546  {
547  if (unlockedAchievements.Contains(identifier)) { return; }
548 
549  if (SteamManager.IsInitialized)
550  {
551  if (SteamManager.UnlockAchievement(identifier))
552  {
553  unlockedAchievements.Add(identifier);
554  }
555  }
556 
557  if (EosInterface.Core.IsInitialized)
558  {
559  TaskPool.Add("Eos.UnlockAchievementsOnPlatforms", EosInterface.Achievements.UnlockAchievements(identifier), t =>
560  {
561  if (!t.TryGetResult(out Result<uint, EosInterface.AchievementUnlockError> result)) { return; }
562  if (result.IsSuccess) { unlockedAchievements.Add(identifier); }
563  });
564  }
565  }
566 
567  public static void IncrementStat(AchievementStat stat, float amount)
568  {
569  if (CheatsEnabled) { return; }
570 
571  IncrementStatOnPlatforms(stat, amount);
572  }
573 
574  private static void IncrementStatOnPlatforms(AchievementStat stat, float amount)
575  {
576  if (SteamManager.IsInitialized)
577  {
578  SteamManager.IncrementStats(stat.ToSteam(amount));
579  }
580 
581  if (EosInterface.Core.IsInitialized)
582  {
583  TaskPool.Add("Eos.IncrementStat", EosInterface.Achievements.IngestStats(stat.ToEos(amount)), TaskPool.IgnoredCallback);
584  }
585  }
586 
587  public static void SyncBetweenPlatforms()
588  {
589  if (!SteamManager.IsInitialized || !EosInterface.Core.IsInitialized) { return; }
590 
591  var steamStats = SteamManager.GetAllStats();
592 
593  TaskPool.AddWithResult("Eos.SyncBetweenPlatforms.QueryStats", EosInterface.Achievements.QueryStats(AchievementStatExtension.EosStats), result =>
594  {
595  result.Match(
596  success: stats => SyncStats(stats, steamStats),
597  failure: static error => DebugConsole.ThrowError($"Failed to query stats from EOS: {error}"));
598  });
599 
600  static void SyncStats(ImmutableDictionary<AchievementStat, int> eosStats,
601  ImmutableDictionary<AchievementStat, float> steamStats)
602  {
603  var steamStatsConverted = steamStats.Select(static s => s.Key.ToEos(s.Value)).ToImmutableDictionary(static s => s.Stat, static s => s.Value);
604  var eosStatsConverted = eosStats.Select(static s => s.Key.ToEos(s.Value)).ToImmutableDictionary(static s => s.Stat, static s => s.Value);
605 
606  static int GetStatValue(AchievementStat stat, ImmutableDictionary<AchievementStat, int> stats) => stats.TryGetValue(stat, out int value) ? value : 0;
607 
608  var highestStats = AchievementStatExtension.EosStats.ToDictionary(
609  static key => key,
610  value =>
611  Math.Max(
612  GetStatValue(value, steamStatsConverted),
613  GetStatValue(value, eosStatsConverted)));
614 
615  List<(AchievementStat Stat, int Value)> eosStatsToIngest = new(),
616  steamStatsToIncrement = new();
617 
618  foreach (var (stat, value) in highestStats)
619  {
620  int steamDiff = value - GetStatValue(stat, steamStatsConverted),
621  eosDiff = value - GetStatValue(stat, eosStatsConverted);
622 
623  if (steamDiff > 0) { steamStatsToIncrement.Add((stat, steamDiff)); }
624  if (eosDiff > 0) { eosStatsToIngest.Add((stat, eosDiff)); }
625  }
626 
627  if (steamStatsToIncrement.Any())
628  {
629  SteamManager.IncrementStats(steamStatsToIncrement.Select(static s => s.Stat.ToSteam(s.Value)).ToArray());
630  SteamManager.StoreStats();
631  }
632 
633  if (eosStatsToIngest.Any())
634  {
635  TaskPool.Add("Eos.SyncBetweenPlatforms.IngestStats", EosInterface.Achievements.IngestStats(eosStatsToIngest.ToArray()), TaskPool.IgnoredCallback);
636  }
637  }
638 
639  if (!SteamManager.TryGetUnlockedAchievements(out List<Steamworks.Data.Achievement> steamUnlockedAchievements))
640  {
641  DebugConsole.ThrowError("Failed to query unlocked achievements from Steam");
642  return;
643  }
644 
645  TaskPool.AddWithResult("Eos.SyncBetweenPlatforms.QueryPlayerAchievements", EosInterface.Achievements.QueryPlayerAchievements(), t =>
646  {
647  t.Match(
648  success: eosAchievements => SyncAchievements(eosAchievements, steamUnlockedAchievements),
649  failure: static error => DebugConsole.ThrowError($"Failed to query achievements from EOS: {error}"));
650  });
651 
652  static void SyncAchievements(
653  ImmutableDictionary<Identifier, double> eosAchievements,
654  List<Steamworks.Data.Achievement> steamUnlockedAchievements)
655  {
656  foreach (var (identifier, progress) in eosAchievements)
657  {
658  if (!IsUnlocked(progress)) { continue; }
659 
660  if (steamUnlockedAchievements.Any(a => a.Identifier.ToIdentifier() == identifier)) { continue; }
661 
662  SteamManager.UnlockAchievement(identifier);
663  }
664 
665  List<Identifier> eosAchievementsToUnlock = new();
666  foreach (var achievement in steamUnlockedAchievements)
667  {
668  Identifier identifier = achievement.Identifier.ToIdentifier();
669  if (eosAchievements.TryGetValue(identifier, out double progress) && IsUnlocked(progress)) { continue; }
670 
671  eosAchievementsToUnlock.Add(achievement.Identifier.ToIdentifier());
672  }
673 
674  if (eosAchievementsToUnlock.Any())
675  {
676  TaskPool.Add("Eos.SyncBetweenPlatforms.UnlockAchievements", EosInterface.Achievements.UnlockAchievements(eosAchievementsToUnlock.ToArray()), TaskPool.IgnoredCallback);
677  }
678 
679  static bool IsUnlocked(double progress) => progress >= 100.0d;
680  }
681  }
682  }
683 }
readonly AfflictionPrefab Prefab
Definition: Affliction.cs:12
readonly Identifier AchievementOnReceived
readonly Identifier AchievementOnRemoved
Steam achievement given when the affliction is removed from the controlled character.
readonly Character Killer
Definition: CauseOfDeath.cs:14
readonly Entity DamageSource
Definition: CauseOfDeath.cs:15
Affliction GetAffliction(string identifier, bool allowLimbAfflictions=true)
bool HasEquippedItem(Item item, InvSlotType? slotType=null, Func< InvSlotType, bool > predicate=null)
static readonly Identifier HumanSpeciesName
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
static GameSession?? GameSession
Definition: GameMain.cs:88
static GameScreen GameScreen
Definition: GameMain.cs:52
static NetworkMember NetworkMember
Definition: GameMain.cs:190
static GameClient Client
Definition: GameMain.cs:188
static readonly List< Item > ItemList
float GetRealWorldDepth(float worldPositionY)
Calculate the "real" depth in meters from the surface of Europa (the value you see on the nav termina...
float RealWorldCrushDepth
The crush depth of a non-upgraded submarine in "real world units" (meters from the surface of Europa)...
Marks fields and properties as to be serialized and deserialized by INetSerializableStruct....
SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub=null, string errorMsgStr=null, float minGapSize=0, Func< PathNode, bool > startNodeFilter=null, Func< PathNode, bool > endNodeFilter=null, Func< PathNode, bool > nodeFilter=null, bool checkVisibility=true)
Definition: PathFinder.cs:173
bool SectionIsLeaking(int sectionIndex)
Sections that are leaking have a gap placed on them
IEnumerable< Submarine > GetConnectedSubs()
Returns a list of all submarines that are connected to this one via docking ports,...
float? RealWorldDepth
How deep down the sub is from the surface of Europa in meters (affected by level type,...