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(Biome biome = null)
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 #if CLIENT
58  // If this is a multiplayer game, the client should let the server handle achievements
59  if (GameMain.Client != null) { return; }
60 #endif
61 
62  if (biome != null && GameMain.GameSession?.GameMode is CampaignMode)
63  {
64  string shortBiomeIdentifier = biome.Identifier.Value.Replace(" ", "");
65  UnlockAchievement($"discover{shortBiomeIdentifier}".ToIdentifier(), unlockClients: true);
66 
67  // Just got out of Cold Caverns
68  if (shortBiomeIdentifier == "europanridge".ToIdentifier() &&
69  GameMain.NetworkMember?.ServerSettings?.RespawnMode == RespawnMode.Permadeath)
70  {
71  UnlockAchievement("getoutalive".ToIdentifier(), unlockClients: true,
72  clientConditions: static client => GameMain.GameSession.PermadeathCountForAccount(client.AccountId) <= 0);
73  }
74  }
75  }
76 
77  public static void Update(float deltaTime)
78  {
79  if (GameMain.GameSession == null) { return; }
80 #if CLIENT
81  // If this is a multiplayer game, the client should let the server handle achievements
82  if (GameMain.Client != null) { return; }
83 #endif
84 
85  updateTimer -= deltaTime;
86  if (updateTimer > 0.0f) { return; }
87  updateTimer = UpdateInterval;
88 
89  if (Level.Loaded != null && roundData != null && Screen.Selected == GameMain.GameScreen)
90  {
92  {
93  UnlockAchievement(
94  identifier: "maxintensity".ToIdentifier(),
95  unlockClients: true,
96  characterConditions: static c => c is { IsDead: false, IsUnconscious: false });
97  }
98 
99  foreach (Character c in Character.CharacterList)
100  {
101  if (c.IsDead) { continue; }
102  //achievement for descending below crush depth and coming back
103  if (GameMain.GameSession.RoundDuration > 30.0f)
104  {
106  {
107  roundData.EnteredCrushDepth.Add(c);
108  }
110  {
111  //all characters that have entered crush depth and are still alive get an achievement
112  if (roundData.EnteredCrushDepth.Contains(c)) { UnlockAchievement(c, "survivecrushdepth".ToIdentifier()); }
113  }
114  }
115  }
116 
117  foreach (Submarine sub in Submarine.Loaded)
118  {
119  foreach (Reactor reactor in roundData.Reactors)
120  {
121  if (reactor.Item.Condition <= 0.0f && reactor.Item.Submarine == sub)
122  {
123  //characters that were inside the sub during a reactor meltdown
124  //get an achievement if they're still alive at the end of the round
125  foreach (Character c in Character.CharacterList)
126  {
127  if (!c.IsDead && c.Submarine == sub) { roundData.ReactorMeltdown.Add(c); }
128  }
129  }
130  }
131 
132  //convert submarine velocity to km/h
133  Vector2 submarineVel = Physics.DisplayToRealWorldRatio * ConvertUnits.ToDisplayUnits(sub.Velocity) * 3.6f;
134  //achievement for going > 100 km/h
135  if (Math.Abs(submarineVel.X) > 100.0f)
136  {
137  //all conscious characters inside the sub get an achievement
138  UnlockAchievement("subhighvelocity".ToIdentifier(), true, c => c != null && c.Submarine == sub && !c.IsDead && !c.IsUnconscious);
139  }
140 
141  //achievement for descending ridiculously deep
142  float realWorldDepth = sub.RealWorldDepth;
143  if (realWorldDepth > 5000.0f && GameMain.GameSession.RoundDuration > 30.0f)
144  {
145  //all conscious characters inside the sub get an achievement
146  UnlockAchievement("subdeep".ToIdentifier(), true, c => c != null && c.Submarine == sub && !c.IsDead && !c.IsUnconscious);
147  }
148  }
149 
150  if (!roundData.SubWasDamaged)
151  {
152  roundData.SubWasDamaged = SubWallsDamaged(Submarine.MainSub);
153  }
154  }
155 
156  if (GameMain.GameSession != null)
157  {
158 #if CLIENT
160  {
161  CheckMidRoundAchievements(Character.Controlled);
162  }
163 #else
164  foreach (Client client in GameMain.Server.ConnectedClients)
165  {
166  if (client.Character != null)
167  {
168  CheckMidRoundAchievements(client.Character);
169  }
170  }
171 #endif
172  }
173  }
174 
175  private static void CheckMidRoundAchievements(Character c)
176  {
177  if (c == null || c.Removed) { return; }
178 
179  if (c.HasEquippedItem("clownmask".ToIdentifier()) &&
180  c.HasEquippedItem("clowncostume".ToIdentifier()))
181  {
182  UnlockAchievement(c, "clowncostume".ToIdentifier());
183  }
184 
185  if (Submarine.MainSub != null && c.Submarine == null && c.SpeciesName == CharacterPrefab.HumanSpeciesName)
186  {
187  float requiredDist = 500 / Physics.DisplayToRealWorldRatio;
188  float distSquared = Vector2.DistanceSquared(c.WorldPosition, Submarine.MainSub.WorldPosition);
189  if (cachedDistances.TryGetValue(c, out var cachedDistance))
190  {
191  if (cachedDistance.ShouldUpdateDistance(c.WorldPosition, Submarine.MainSub.WorldPosition))
192  {
193  cachedDistances.Remove(c);
194  cachedDistance = CalculateNewCachedDistance(c);
195  if (cachedDistance != null)
196  {
197  cachedDistances.Add(c, cachedDistance);
198  }
199  }
200  }
201  else
202  {
203  cachedDistance = CalculateNewCachedDistance(c);
204  if (cachedDistance != null)
205  {
206  cachedDistances.Add(c, cachedDistance);
207  }
208  }
209  if (cachedDistance != null)
210  {
211  distSquared = Math.Max(distSquared, cachedDistance.Distance * cachedDistance.Distance);
212  }
213  if (distSquared > requiredDist * requiredDist)
214  {
215  UnlockAchievement(c, "crewaway".ToIdentifier());
216  }
217 
218  static CachedDistance CalculateNewCachedDistance(Character c)
219  {
220  pathFinder ??= new PathFinder(WayPoint.WayPointList, false);
221  var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(c.WorldPosition), ConvertUnits.ToSimUnits(Submarine.MainSub.WorldPosition));
222  if (path.Unreachable) { return null; }
223  return new CachedDistance(c.WorldPosition, Submarine.MainSub.WorldPosition, path.TotalLength, Timing.TotalTime + Rand.Range(1.0f, 5.0f));
224  }
225  }
226  }
227 
228  private static bool SubWallsDamaged(Submarine sub)
229  {
230  foreach (Structure structure in Structure.WallList)
231  {
232  if (structure.Submarine != sub || structure.HasBody) { continue; }
233  for (int i = 0; i < structure.SectionCount; i++)
234  {
235  if (structure.SectionIsLeaking(i))
236  {
237  return true;
238  }
239  }
240  }
241  return false;
242  }
243 
244  public static void OnCampaignMetadataSet(Identifier identifier, object value, bool unlockClients = false)
245  {
246  if (identifier.IsEmpty || value is null) { return; }
247  UnlockAchievement($"campaignmetadata_{identifier}_{value}".ToIdentifier(), unlockClients);
248  }
249 
250  public static void OnItemRepaired(Item item, Character fixer)
251  {
252 #if CLIENT
253  // If this is a multiplayer game, the client should let the server handle achievements
254  if (GameMain.Client != null) { return; }
255 #endif
256  if (fixer == null) { return; }
257 
258  UnlockAchievement(fixer, "repairdevice".ToIdentifier());
259  UnlockAchievement(fixer, $"repair{item.Prefab.Identifier}".ToIdentifier());
260  }
261 
262  public static void OnButtonTerminalSignal(Item item, Character user)
263  {
264  if (item == null || user == null) { return; }
265 
266 #if CLIENT
267  // If this is a multiplayer game, the client should let the server handle achievements
268  if (GameMain.Client != null) { return; }
269 #endif
270  if ((item.Prefab.Identifier == "alienterminal" || item.Prefab.Identifier == "alienterminal_new") &&
271  item.Condition <= 0)
272  {
273  UnlockAchievement(user, "ancientnovelty".ToIdentifier());
274  }
275  }
276 
277  public static void OnAfflictionReceived(Affliction affliction, Character character)
278  {
279  if (affliction.Prefab.AchievementOnReceived.IsEmpty) { return; }
280 #if CLIENT
281  // If this is a multiplayer game, the client should let the server handle achievements
282  if (GameMain.Client != null) { return; }
283 #endif
284  UnlockAchievement(character, affliction.Prefab.AchievementOnReceived);
285  }
286 
287  public static void OnAfflictionRemoved(Affliction affliction, Character character)
288  {
289  if (affliction.Prefab.AchievementOnRemoved.IsEmpty) { return; }
290 
291 #if CLIENT
292  // If this is a multiplayer game, the client should let the server handle achievements
293  if (GameMain.Client != null) { return; }
294 #endif
295  UnlockAchievement(character, affliction.Prefab.AchievementOnRemoved);
296  }
297 
298  public static void OnCharacterRevived(Character character, Character reviver)
299  {
300 #if CLIENT
301  // If this is a multiplayer game, the client should let the server handle achievements
302  if (GameMain.Client != null) { return; }
303 #endif
304  if (reviver == null) { return; }
305  UnlockAchievement(reviver, "healcrit".ToIdentifier());
306  }
307 
308  public static void OnCharacterKilled(Character character, CauseOfDeath causeOfDeath)
309  {
310 #if CLIENT
311  // If this is a multiplayer game, the client should let the server handle achievements
312  if (GameMain.Client != null || GameMain.GameSession == null) { return; }
313 
314  if (character != Character.Controlled &&
315  causeOfDeath.Killer != null &&
316  causeOfDeath.Killer == Character.Controlled)
317  {
318  IncrementStat(causeOfDeath.Killer, character.IsHuman ? AchievementStat.HumansKilled : AchievementStat.MonstersKilled , 1);
319  }
320 #elif SERVER
321  if (character != causeOfDeath.Killer && causeOfDeath.Killer != null)
322  {
323  IncrementStat(causeOfDeath.Killer, character.IsHuman ? AchievementStat.HumansKilled : AchievementStat.MonstersKilled , 1);
324  }
325 #endif
326 
327  UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName}".ToIdentifier());
328  if (character.CurrentHull != null)
329  {
330  UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName}indoors".ToIdentifier());
331  }
332  if (character.SpeciesName.EndsWith("boss"))
333  {
334  UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("boss", "")}".ToIdentifier());
335  if (character.CurrentHull != null)
336  {
337  UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("boss", "")}indoors".ToIdentifier());
338  }
339  }
340  if (character.SpeciesName.EndsWith("_m"))
341  {
342  UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("_m", "")}".ToIdentifier());
343  if (character.CurrentHull != null)
344  {
345  UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("_m", "")}indoors".ToIdentifier());
346  }
347  }
348 #if SERVER
349  if (character.SpeciesName == "Jove" &&
351  GameMain.Server?.ServerSettings is { IronmanModeActive: true })
352  {
353  UnlockAchievement(
354  identifier: "europasfinest".ToIdentifier(),
355  unlockClients: true,
356  characterConditions: static c => c is { IsDead: false });
357  }
358 #endif
359 
360  if (character.HasEquippedItem("clownmask".ToIdentifier()) &&
361  character.HasEquippedItem("clowncostume".ToIdentifier()) &&
362  causeOfDeath.Killer != character)
363  {
364  UnlockAchievement(causeOfDeath.Killer, "killclown".ToIdentifier());
365  }
366 
367  if (character.CharacterHealth?.GetAffliction("psychoclown") != null &&
368  character.CurrentHull?.Submarine.Info is { Type: SubmarineType.BeaconStation })
369  {
370  UnlockAchievement(causeOfDeath.Killer, "whatsmirksbelow".ToIdentifier());
371  }
372 
373  // TODO: should we change this? Morbusine used to be the strongest poison. Now Cyanide is strongest.
374  if (character.CharacterHealth?.GetAffliction("morbusinepoisoning") != null)
375  {
376  UnlockAchievement(causeOfDeath.Killer, "killpoison".ToIdentifier());
377  }
378 
379  if (causeOfDeath.DamageSource is Item item)
380  {
381  if (item.HasTag(Tags.ToolItem))
382  {
383  UnlockAchievement(causeOfDeath.Killer, "killtool".ToIdentifier());
384  }
385  else
386  {
387  // TODO: should we change this? Morbusine used to be the strongest poison. Now Cyanide is strongest.
388  if (item.Prefab.Identifier == "morbusine")
389  {
390  UnlockAchievement(causeOfDeath.Killer, "killpoison".ToIdentifier());
391  }
392  else if (item.Prefab.Identifier == "nuclearshell" ||
393  item.Prefab.Identifier == "nucleardepthcharge")
394  {
395  UnlockAchievement(causeOfDeath.Killer, "killnuke".ToIdentifier());
396  }
397  }
398  }
399 
400 #if SERVER
401  if (GameMain.Server?.ServerSettings?.RespawnMode == RespawnMode.Permadeath)
402  {
403  UnlockAchievement(character, "abyssbeckons".ToIdentifier());
404  }
405 
406  if (GameMain.Server?.TraitorManager != null)
407  {
408  if (GameMain.Server.TraitorManager.IsTraitor(character))
409  {
410  UnlockAchievement(causeOfDeath.Killer, "killtraitor".ToIdentifier());
411  }
412  }
413 #endif
414  }
415 
416  public static void OnTraitorWin(Character character)
417  {
418 #if CLIENT
419  // If this is a multiplayer game, the client should let the server handle achievements
420  if (GameMain.Client != null || GameMain.GameSession == null) { return; }
421 #endif
422  UnlockAchievement(character, "traitorwin".ToIdentifier());
423  }
424 
425  public static void OnRoundEnded(GameSession gameSession)
426  {
427  if (CheatsEnabled) { return; }
428 
429  //made it to the destination
430  if (gameSession?.Submarine != null && Level.Loaded != null && gameSession.Submarine.AtEndExit)
431  {
432  float levelLengthMeters = Physics.DisplayToRealWorldRatio * Level.Loaded.Size.X;
433  float levelLengthKilometers = levelLengthMeters / 1000.0f;
434  //in multiplayer the client's/host's character must be inside the sub (or end outpost) and alive
435  if (GameMain.NetworkMember != null)
436  {
437 #if CLIENT
438  Character myCharacter = Character.Controlled;
439  if (myCharacter != null &&
440  !myCharacter.IsDead &&
441  (myCharacter.Submarine == gameSession.Submarine || (Level.Loaded?.EndOutpost != null && myCharacter.Submarine == Level.Loaded.EndOutpost)))
442  {
443  IncrementStat(AchievementStat.KMsTraveled, levelLengthKilometers);
444  }
445 #endif
446  }
447  else
448  {
449  //in sp making it to the end is enough
450  IncrementStat(AchievementStat.KMsTraveled, levelLengthKilometers);
451  }
452  }
453 
454  //make sure changed stats (kill count, kms traveled) get stored
455  SteamManager.StoreStats();
456 
457  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
458 
459  foreach (Mission mission in gameSession.Missions)
460  {
461  // For PvP missions, all characters on the winning team that are still alive get achievements (if available)
462  if (mission is CombatMission && GameMain.GameSession.WinningTeam.HasValue)
463  {
464  // Attempt unlocking team-specific achievement (if one has been set in the achievement backend)
465  var achvIdentifier =
466  $"{mission.Prefab.AchievementIdentifier}{(int) GameMain.GameSession.WinningTeam}"
467  .ToIdentifier();
468  UnlockAchievement(achvIdentifier, true,
469  c => c != null && !c.IsDead && !c.IsUnconscious && CombatMission.IsInWinningTeam(c));
470 
471  // Attempt unlocking mission-specific achievement (if one has been set in the achievement backend)
472  UnlockAchievement(mission.Prefab.AchievementIdentifier, true,
473  c => c != null && !c.IsDead && !c.IsUnconscious && CombatMission.IsInWinningTeam(c));
474  }
475  else if (mission is not CombatMission && mission.Completed)
476  {
477  //all characters get an achievement
478  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
479  {
480  UnlockAchievement(mission.Prefab.AchievementIdentifier, true, c => c != null);
481  }
482  else
483  {
484  UnlockAchievement(mission.Prefab.AchievementIdentifier);
485  }
486  }
487  }
488 
489  //made it to the destination
490  if (gameSession.Submarine != null && gameSession.Submarine.AtEndExit)
491  {
492  bool noDamageRun = !roundData.SubWasDamaged && !gameSession.Casualties.Any();
493 
494 #if SERVER
495  if (GameMain.Server != null)
496  {
497  //in MP all characters that were inside the sub during reactor meltdown and still alive at the end of the round get an achievement
498  UnlockAchievement("survivereactormeltdown".ToIdentifier(), true, c => c != null && !c.IsDead && roundData.ReactorMeltdown.Contains(c));
499  if (noDamageRun)
500  {
501  UnlockAchievement("nodamagerun".ToIdentifier(), true, c => c != null && !c.IsDead);
502  }
503  }
504 #endif
505 #if CLIENT
506  if (noDamageRun) { UnlockAchievement("nodamagerun".ToIdentifier()); }
507  if (roundData.ReactorMeltdown.Any()) //in SP getting to the destination after a meltdown is enough
508  {
509  UnlockAchievement("survivereactormeltdown".ToIdentifier());
510  }
511 #endif
512  var charactersInSub = Character.CharacterList.FindAll(c =>
513  !c.IsDead &&
514  c.TeamID != CharacterTeamType.FriendlyNPC &&
515  c.AIController is not EnemyAIController &&
516  (c.Submarine == gameSession.Submarine || gameSession.Submarine.GetConnectedSubs().Contains(c.Submarine) || (Level.Loaded?.EndOutpost != null && c.Submarine == Level.Loaded.EndOutpost)));
517 
518  if (charactersInSub.Count == 1)
519  {
520  //there must be some casualties to get the last man standing achievement
521  if (gameSession.Casualties.Any())
522  {
523  UnlockAchievement(charactersInSub[0], "lastmanstanding".ToIdentifier());
524  }
525 #if CLIENT
526  else if (GameMain.GameSession.CrewManager.GetCharacters().Count() == 1)
527  {
528  UnlockAchievement(charactersInSub[0], "lonesailor".ToIdentifier());
529  }
530 #else
531  //lone sailor achievement if alone in the sub and there are no other characters with the same team ID
532  else if (!Character.CharacterList.Any(c =>
533  c != charactersInSub[0] &&
534  c.TeamID == charactersInSub[0].TeamID &&
536  {
537  UnlockAchievement(charactersInSub[0], "lonesailor".ToIdentifier());
538  }
539 #endif
540 
541  }
542  foreach (Character character in charactersInSub)
543  {
544  if (roundData.EnteredCrushDepth.Contains(character))
545  {
546  UnlockAchievement(character, "survivecrushdepth".ToIdentifier());
547  }
548  if (character.Info.Job == null) { continue; }
549  UnlockAchievement(character, $"{character.Info.Job.Prefab.Identifier}round".ToIdentifier());
550  }
551  }
552 
553  pathFinder = null;
554  roundData = null;
555  }
556 
557  private static void UnlockAchievement(Character recipient, Identifier identifier)
558  {
559  if (CheatsEnabled || recipient == null) { return; }
560 #if CLIENT
561  if (recipient == Character.Controlled)
562  {
563  UnlockAchievement(identifier);
564  }
565 #elif SERVER
566  GameMain.Server?.GiveAchievement(recipient, identifier);
567 #endif
568  }
569 
570  private static void IncrementStat(Character recipient, AchievementStat stat, int amount)
571  {
572  if (CheatsEnabled || recipient == null) { return; }
573 #if CLIENT
574  if (recipient == Character.Controlled)
575  {
576  IncrementStat(stat, amount);
577  }
578 #elif SERVER
579  GameMain.Server?.IncrementStat(recipient, stat, amount);
580 #endif
581  }
582 
583  public static void UnlockAchievement(Identifier identifier, bool unlockClients = false, Func<Character, bool> characterConditions = null, Func<Client, bool> clientConditions = null)
584  {
585  if (CheatsEnabled) { return; }
586  if (Screen.Selected is { IsEditor: true }) { return; }
587 #if CLIENT
588  if (GameMain.GameSession?.GameMode is TestGameMode) { return; }
589 #endif
590 #if SERVER
591  if (unlockClients && GameMain.Server != null)
592  {
593  foreach (Client client in GameMain.Server.ConnectedClients)
594  {
595  if (clientConditions != null && !clientConditions(client)) { continue; }
596  if (characterConditions != null && !characterConditions(client.Character)) { continue; }
597  GameMain.Server.GiveAchievement(client, identifier);
598  }
599  }
600 #endif
601 
602 #if CLIENT
603  if (characterConditions != null && !characterConditions(Character.Controlled)) { return; }
604 #endif
605 
606  UnlockAchievementsOnPlatforms(identifier);
607  }
608 
609  private static void UnlockAchievementsOnPlatforms(Identifier identifier)
610  {
611  if (unlockedAchievements.Contains(identifier)) { return; }
612 
613  if (SteamManager.IsInitialized)
614  {
615  if (SteamManager.UnlockAchievement(identifier))
616  {
617  unlockedAchievements.Add(identifier);
618  }
619  }
620 
621  if (EosInterface.Core.IsInitialized)
622  {
623  TaskPool.Add("Eos.UnlockAchievementsOnPlatforms", EosInterface.Achievements.UnlockAchievements(identifier), t =>
624  {
625  if (!t.TryGetResult(out Result<uint, EosInterface.AchievementUnlockError> result)) { return; }
626  if (result.IsSuccess) { unlockedAchievements.Add(identifier); }
627  });
628  }
629  }
630 
631  public static void IncrementStat(AchievementStat stat, float amount)
632  {
633  if (CheatsEnabled) { return; }
634 
635  IncrementStatOnPlatforms(stat, amount);
636  }
637 
638  private static void IncrementStatOnPlatforms(AchievementStat stat, float amount)
639  {
640  if (SteamManager.IsInitialized)
641  {
642  SteamManager.IncrementStats(stat.ToSteam(amount));
643  }
644 
645  if (EosInterface.Core.IsInitialized)
646  {
647  TaskPool.Add("Eos.IncrementStat", EosInterface.Achievements.IngestStats(stat.ToEos(amount)), TaskPool.IgnoredCallback);
648  }
649  }
650 
651  public static void SyncBetweenPlatforms()
652  {
653  if (!SteamManager.IsInitialized || !EosInterface.Core.IsInitialized) { return; }
654 
655  var steamStats = SteamManager.GetAllStats();
656 
657  TaskPool.AddWithResult("Eos.SyncBetweenPlatforms.QueryStats", EosInterface.Achievements.QueryStats(AchievementStatExtension.EosStats), result =>
658  {
659  result.Match(
660  success: stats => SyncStats(stats, steamStats),
661  failure: static error => DebugConsole.ThrowError($"Failed to query stats from EOS: {error}"));
662  });
663 
664  static void SyncStats(ImmutableDictionary<AchievementStat, int> eosStats,
665  ImmutableDictionary<AchievementStat, float> steamStats)
666  {
667  var steamStatsConverted = steamStats.Select(static s => s.Key.ToEos(s.Value)).ToImmutableDictionary(static s => s.Stat, static s => s.Value);
668  var eosStatsConverted = eosStats.Select(static s => s.Key.ToEos(s.Value)).ToImmutableDictionary(static s => s.Stat, static s => s.Value);
669 
670  static int GetStatValue(AchievementStat stat, ImmutableDictionary<AchievementStat, int> stats) => stats.TryGetValue(stat, out int value) ? value : 0;
671 
672  var highestStats = AchievementStatExtension.EosStats.ToDictionary(
673  static key => key,
674  value =>
675  Math.Max(
676  GetStatValue(value, steamStatsConverted),
677  GetStatValue(value, eosStatsConverted)));
678 
679  List<(AchievementStat Stat, int Value)> eosStatsToIngest = new(),
680  steamStatsToIncrement = new();
681 
682  foreach (var (stat, value) in highestStats)
683  {
684  int steamDiff = value - GetStatValue(stat, steamStatsConverted),
685  eosDiff = value - GetStatValue(stat, eosStatsConverted);
686 
687  if (steamDiff > 0) { steamStatsToIncrement.Add((stat, steamDiff)); }
688  if (eosDiff > 0) { eosStatsToIngest.Add((stat, eosDiff)); }
689  }
690 
691  if (steamStatsToIncrement.Any())
692  {
693  SteamManager.IncrementStats(steamStatsToIncrement.Select(static s => s.Stat.ToSteam(s.Value)).ToArray());
694  SteamManager.StoreStats();
695  }
696 
697  if (eosStatsToIngest.Any())
698  {
699  TaskPool.Add("Eos.SyncBetweenPlatforms.IngestStats", EosInterface.Achievements.IngestStats(eosStatsToIngest.ToArray()), TaskPool.IgnoredCallback);
700  }
701  }
702 
703  if (!SteamManager.TryGetUnlockedAchievements(out List<Steamworks.Data.Achievement> steamUnlockedAchievements))
704  {
705  DebugConsole.ThrowError("Failed to query unlocked achievements from Steam");
706  return;
707  }
708 
709  TaskPool.AddWithResult("Eos.SyncBetweenPlatforms.QueryPlayerAchievements", EosInterface.Achievements.QueryPlayerAchievements(), t =>
710  {
711  t.Match(
712  success: eosAchievements => SyncAchievements(eosAchievements, steamUnlockedAchievements),
713  failure: static error => DebugConsole.ThrowError($"Failed to query achievements from EOS: {error}"));
714  });
715 
716  static void SyncAchievements(
717  ImmutableDictionary<Identifier, double> eosAchievements,
718  List<Steamworks.Data.Achievement> steamUnlockedAchievements)
719  {
720  foreach (var (identifier, progress) in eosAchievements)
721  {
722  if (!IsUnlocked(progress)) { continue; }
723 
724  if (steamUnlockedAchievements.Any(a => a.Identifier.ToIdentifier() == identifier)) { continue; }
725 
726  SteamManager.UnlockAchievement(identifier);
727  }
728 
729  List<Identifier> eosAchievementsToUnlock = new();
730  foreach (var achievement in steamUnlockedAchievements)
731  {
732  Identifier identifier = achievement.Identifier.ToIdentifier();
733  if (eosAchievements.TryGetValue(identifier, out double progress) && IsUnlocked(progress)) { continue; }
734 
735  eosAchievementsToUnlock.Add(achievement.Identifier.ToIdentifier());
736  }
737 
738  if (eosAchievementsToUnlock.Any())
739  {
740  TaskPool.Add("Eos.SyncBetweenPlatforms.UnlockAchievements", EosInterface.Achievements.UnlockAchievements(eosAchievementsToUnlock.ToArray()), TaskPool.IgnoredCallback);
741  }
742 
743  static bool IsUnlocked(double progress) => progress >= 100.0d;
744  }
745  }
746  }
747 }
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
int PermadeathCountForAccount(Option< AccountId > accountId)
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
readonly Identifier Identifier
Definition: Prefab.cs:34
bool SectionIsLeaking(int sectionIndex)
Sections that are leaking have a gap placed on them
static Submarine MainSub
Note that this can be null in some situations, e.g. editors and missions that don't load a submarine.
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,...