5 using Microsoft.Xna.Framework;
7 using System.Collections.Generic;
12 static class SteamAchievementManager
14 private const float UpdateInterval = 1.0f;
16 private static readonly HashSet<Identifier> unlockedAchievements =
new HashSet<Identifier>();
18 public static bool CheatsEnabled =
false;
20 private static float updateTimer;
27 public readonly List<Reactor> Reactors =
new List<Reactor>();
29 public readonly HashSet<Character> EnteredCrushDepth =
new HashSet<Character>();
30 public readonly HashSet<Character> ReactorMeltdown =
new HashSet<Character>();
32 public bool SubWasDamaged;
35 private static RoundData roundData;
38 private static PathFinder pathFinder;
39 private static readonly Dictionary<Character, CachedDistance> cachedDistances =
new Dictionary<Character, CachedDistance>();
41 public static void OnStartRound()
43 roundData =
new RoundData();
44 foreach (Item item
in Item.ItemList)
46 if (item.Submarine ==
null || item.Submarine.Info.Type !=
SubmarineType.Player) {
continue; }
48 if (reactor !=
null && reactor.
Item.
Condition > 0.0f) { roundData.Reactors.Add(reactor); }
50 pathFinder =
new PathFinder(WayPoint.WayPointList,
false);
51 cachedDistances.Clear();
54 public static void Update(
float deltaTime)
56 if (GameMain.GameSession ==
null) {
return; }
58 if (GameMain.Client !=
null) {
return; }
61 updateTimer -= deltaTime;
62 if (updateTimer > 0.0f) {
return; }
63 updateTimer = UpdateInterval;
65 if (Level.Loaded !=
null && roundData !=
null && Screen.Selected == GameMain.GameScreen)
67 if (GameMain.GameSession.EventManager.CurrentIntensity > 0.99f)
69 UnlockAchievement(
"maxintensity".ToIdentifier(),
true, c => c !=
null && !c.IsDead && !c.IsUnconscious);
72 foreach (Character c
in Character.CharacterList)
74 if (c.IsDead) {
continue; }
76 if (GameMain.GameSession.RoundDuration > 30.0f)
78 if (c.Submarine !=
null && c.Submarine.AtDamageDepth || Level.Loaded.GetRealWorldDepth(c.WorldPosition.Y) > Level.Loaded.RealWorldCrushDepth)
80 roundData.EnteredCrushDepth.Add(c);
82 else if (Level.Loaded.GetRealWorldDepth(c.WorldPosition.Y) < Level.Loaded.RealWorldCrushDepth - 500.0f)
85 if (roundData.EnteredCrushDepth.Contains(c)) { UnlockAchievement(c,
"survivecrushdepth".ToIdentifier()); }
90 foreach (Submarine sub
in Submarine.Loaded)
92 foreach (
Reactor reactor
in roundData.Reactors)
98 foreach (Character c
in Character.CharacterList)
100 if (!c.IsDead && c.Submarine == sub) { roundData.ReactorMeltdown.Add(c); }
106 Vector2 submarineVel = Physics.DisplayToRealWorldRatio * ConvertUnits.ToDisplayUnits(sub.Velocity) * 3.6f;
108 if (Math.Abs(submarineVel.X) > 100.0f)
111 UnlockAchievement(
"subhighvelocity".ToIdentifier(),
true, c => c !=
null && c.Submarine == sub && !c.IsDead && !c.IsUnconscious);
115 float realWorldDepth = sub.RealWorldDepth;
116 if (realWorldDepth > 5000.0f && GameMain.GameSession.RoundDuration > 30.0f)
119 UnlockAchievement(
"subdeep".ToIdentifier(),
true, c => c !=
null && c.Submarine == sub && !c.IsDead && !c.IsUnconscious);
123 if (!roundData.SubWasDamaged)
125 roundData.SubWasDamaged = SubWallsDamaged(
Submarine.MainSub);
129 if (GameMain.GameSession !=
null)
132 if (
Character.Controlled !=
null && !(GameMain.GameSession.GameMode is TestGameMode))
134 CheckMidRoundAchievements(
Character.Controlled);
137 foreach (
Client client
in GameMain.Server.ConnectedClients)
141 CheckMidRoundAchievements(client.
Character);
148 private static void CheckMidRoundAchievements(Character c)
150 if (c ==
null || c.Removed) {
return; }
152 if (c.HasEquippedItem(
"clownmask".ToIdentifier()) &&
153 c.HasEquippedItem(
"clowncostume".ToIdentifier()))
155 UnlockAchievement(c,
"clowncostume".ToIdentifier());
158 if (
Submarine.MainSub !=
null && c.Submarine ==
null && c.SpeciesName == CharacterPrefab.HumanSpeciesName)
160 float requiredDist = 500 / Physics.DisplayToRealWorldRatio;
161 float distSquared = Vector2.DistanceSquared(c.WorldPosition,
Submarine.MainSub.WorldPosition);
162 if (cachedDistances.TryGetValue(c, out var cachedDistance))
164 if (cachedDistance.ShouldUpdateDistance(c.WorldPosition,
Submarine.MainSub.WorldPosition))
166 cachedDistances.Remove(c);
167 cachedDistance = CalculateNewCachedDistance(c);
168 if (cachedDistance !=
null)
170 cachedDistances.Add(c, cachedDistance);
176 cachedDistance = CalculateNewCachedDistance(c);
177 if (cachedDistance !=
null)
179 cachedDistances.Add(c, cachedDistance);
182 if (cachedDistance !=
null)
184 distSquared = Math.Max(distSquared, cachedDistance.Distance * cachedDistance.Distance);
186 if (distSquared > requiredDist * requiredDist)
188 UnlockAchievement(c,
"crewaway".ToIdentifier());
191 static CachedDistance CalculateNewCachedDistance(Character c)
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));
201 private static bool SubWallsDamaged(Submarine sub)
203 foreach (Structure structure
in Structure.WallList)
205 if (structure.Submarine != sub || structure.HasBody) {
continue; }
206 for (
int i = 0; i < structure.SectionCount; i++)
208 if (structure.SectionIsLeaking(i))
217 public static void OnBiomeDiscovered(Biome biome)
219 UnlockAchievement($
"discover{biome.Identifier.Value.Replace(" ", "")}".ToIdentifier());
222 public static void OnCampaignMetadataSet(Identifier identifier,
object value,
bool unlockClients =
false)
224 if (identifier.IsEmpty || value is
null) {
return; }
225 UnlockAchievement($
"campaignmetadata_{identifier}_{value}".ToIdentifier(), unlockClients);
228 public static void OnItemRepaired(Item item, Character fixer)
231 if (GameMain.Client !=
null) {
return; }
233 if (fixer ==
null) {
return; }
235 UnlockAchievement(fixer,
"repairdevice".ToIdentifier());
236 UnlockAchievement(fixer, $
"repair{item.Prefab.Identifier}".ToIdentifier());
239 public static void OnAfflictionReceived(Affliction affliction, Character character)
241 if (affliction.Prefab.AchievementOnReceived.IsEmpty) {
return; }
243 if (GameMain.Client !=
null) {
return; }
245 UnlockAchievement(character, affliction.Prefab.AchievementOnReceived);
248 public static void OnAfflictionRemoved(Affliction affliction, Character character)
250 if (affliction.Prefab.AchievementOnRemoved.IsEmpty) {
return; }
253 if (GameMain.Client !=
null) {
return; }
255 UnlockAchievement(character, affliction.Prefab.AchievementOnRemoved);
258 public static void OnCharacterRevived(Character character, Character reviver)
261 if (GameMain.Client !=
null) {
return; }
263 if (reviver ==
null) {
return; }
264 UnlockAchievement(reviver,
"healcrit".ToIdentifier());
267 public static void OnCharacterKilled(Character character, CauseOfDeath causeOfDeath)
270 if (GameMain.Client !=
null || GameMain.GameSession ==
null) {
return; }
273 causeOfDeath.Killer !=
null &&
274 causeOfDeath.Killer ==
Character.Controlled)
276 IncrementStat(causeOfDeath.Killer, (character.IsHuman ?
"humanskilled" :
"monsterskilled").ToIdentifier(), 1);
279 if (character != causeOfDeath.Killer && causeOfDeath.Killer !=
null)
281 IncrementStat(causeOfDeath.Killer, (character.IsHuman ?
"humanskilled" :
"monsterskilled").ToIdentifier(), 1);
285 UnlockAchievement(causeOfDeath.Killer, $
"kill{character.SpeciesName}".ToIdentifier());
286 if (character.CurrentHull !=
null)
288 UnlockAchievement(causeOfDeath.Killer, $
"kill{character.SpeciesName}indoors".ToIdentifier());
290 if (character.SpeciesName.EndsWith(
"boss"))
292 UnlockAchievement(causeOfDeath.Killer, $
"kill{character.SpeciesName.Replace("boss
", "")}".ToIdentifier());
293 if (character.CurrentHull !=
null)
295 UnlockAchievement(causeOfDeath.Killer, $
"kill{character.SpeciesName.Replace("boss
", "")}indoors".ToIdentifier());
298 if (character.SpeciesName.EndsWith(
"_m"))
300 UnlockAchievement(causeOfDeath.Killer, $
"kill{character.SpeciesName.Replace("_m
", "")}".ToIdentifier());
301 if (character.CurrentHull !=
null)
303 UnlockAchievement(causeOfDeath.Killer, $
"kill{character.SpeciesName.Replace("_m
", "")}indoors".ToIdentifier());
307 if (character.HasEquippedItem(
"clownmask".ToIdentifier()) &&
308 character.HasEquippedItem(
"clowncostume".ToIdentifier()) &&
309 causeOfDeath.Killer != character)
311 UnlockAchievement(causeOfDeath.Killer,
"killclown".ToIdentifier());
315 if (character.CharacterHealth?.GetAffliction(
"morbusinepoisoning") !=
null)
317 UnlockAchievement(causeOfDeath.Killer,
"killpoison".ToIdentifier());
320 if (causeOfDeath.DamageSource is Item item)
322 if (item.HasTag(Tags.ToolItem))
324 UnlockAchievement(causeOfDeath.Killer,
"killtool".ToIdentifier());
329 if (item.Prefab.Identifier ==
"morbusine")
331 UnlockAchievement(causeOfDeath.Killer,
"killpoison".ToIdentifier());
333 else if (item.Prefab.Identifier ==
"nuclearshell" ||
334 item.Prefab.Identifier ==
"nucleardepthcharge")
336 UnlockAchievement(causeOfDeath.Killer,
"killnuke".ToIdentifier());
342 if (GameMain.Server?.TraitorManager !=
null)
344 if (GameMain.Server.TraitorManager.IsTraitor(character))
346 UnlockAchievement(causeOfDeath.Killer,
"killtraitor".ToIdentifier());
352 public static void OnTraitorWin(Character character)
355 if (GameMain.Client !=
null || GameMain.GameSession ==
null) {
return; }
357 UnlockAchievement(character,
"traitorwin".ToIdentifier());
360 public static void OnRoundEnded(GameSession gameSession)
362 if (CheatsEnabled) {
return; }
365 if (gameSession?.Submarine !=
null && Level.Loaded !=
null && gameSession.Submarine.AtEndExit)
367 float levelLengthMeters = Physics.DisplayToRealWorldRatio * Level.Loaded.Size.X;
368 float levelLengthKilometers = levelLengthMeters / 1000.0f;
370 if (GameMain.NetworkMember !=
null)
374 if (myCharacter !=
null &&
375 !myCharacter.IsDead &&
376 (myCharacter.Submarine == gameSession.Submarine || (Level.Loaded?.EndOutpost !=
null && myCharacter.Submarine == Level.Loaded.EndOutpost)))
378 IncrementStat(
"kmstraveled".ToIdentifier(), levelLengthKilometers);
385 IncrementStat(
"kmstraveled".ToIdentifier(), levelLengthKilometers);
390 SteamManager.StoreStats();
392 if (GameMain.NetworkMember !=
null && GameMain.NetworkMember.IsClient) {
return; }
394 foreach (Mission mission
in gameSession.Missions)
396 if (mission is CombatMission combatMission && GameMain.GameSession.WinningTeam.HasValue)
400 $
"{mission.Prefab.AchievementIdentifier}{(int) GameMain.GameSession.WinningTeam}"
402 UnlockAchievement(achvIdentifier,
true,
403 c => c !=
null && !c.IsDead && !c.IsUnconscious && combatMission.IsInWinningTeam(c));
405 else if (mission.Completed)
408 if (GameMain.NetworkMember !=
null && GameMain.NetworkMember.IsServer)
410 UnlockAchievement(mission.Prefab.AchievementIdentifier,
true, c => c !=
null);
414 UnlockAchievement(mission.Prefab.AchievementIdentifier);
420 if (gameSession.Submarine.AtEndExit)
422 bool noDamageRun = !roundData.SubWasDamaged && !gameSession.Casualties.Any();
425 if (GameMain.Server !=
null)
428 UnlockAchievement(
"survivereactormeltdown".ToIdentifier(),
true, c => c !=
null && !c.IsDead && roundData.ReactorMeltdown.Contains(c));
431 UnlockAchievement(
"nodamagerun".ToIdentifier(),
true, c => c !=
null && !c.IsDead);
436 if (noDamageRun) { UnlockAchievement(
"nodamagerun".ToIdentifier()); }
437 if (roundData.ReactorMeltdown.Any())
439 UnlockAchievement(
"survivereactormeltdown".ToIdentifier());
442 var charactersInSub =
Character.CharacterList.FindAll(c =>
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)));
448 if (charactersInSub.Count == 1)
451 if (gameSession.Casualties.Any())
453 UnlockAchievement(charactersInSub[0],
"lastmanstanding".ToIdentifier());
456 else if (GameMain.GameSession.CrewManager.GetCharacters().Count() == 1)
458 UnlockAchievement(charactersInSub[0],
"lonesailor".ToIdentifier());
462 else if (!
Character.CharacterList.Any(c =>
463 c != charactersInSub[0] &&
464 c.TeamID == charactersInSub[0].TeamID &&
465 !(c.AIController is EnemyAIController)))
467 UnlockAchievement(charactersInSub[0],
"lonesailor".ToIdentifier());
472 foreach (Character character
in charactersInSub)
474 if (roundData.EnteredCrushDepth.Contains(character))
476 UnlockAchievement(character,
"survivecrushdepth".ToIdentifier());
478 if (character.Info.Job ==
null) {
continue; }
479 UnlockAchievement(character, $
"{character.Info.Job.Prefab.Identifier}round".ToIdentifier());
487 private static void UnlockAchievement(Character recipient, Identifier identifier)
489 if (CheatsEnabled || recipient ==
null) {
return; }
493 UnlockAchievement(identifier);
496 GameMain.Server?.GiveAchievement(recipient, identifier);
500 private static void IncrementStat(Character recipient, Identifier identifier,
int amount)
502 if (CheatsEnabled || recipient ==
null) {
return; }
506 SteamManager.IncrementStat(identifier, amount);
509 GameMain.Server?.IncrementStat(recipient, identifier, amount);
513 public static void IncrementStat(Identifier identifier,
int amount)
515 if (CheatsEnabled) {
return; }
516 SteamManager.IncrementStat(identifier, amount);
519 public static void IncrementStat(Identifier identifier,
float amount)
521 if (CheatsEnabled) {
return; }
522 SteamManager.IncrementStat(identifier, amount);
525 public static void UnlockAchievement(Identifier identifier,
bool unlockClients =
false, Func<Character, bool> conditions =
null)
527 if (CheatsEnabled) {
return; }
528 if (Screen.Selected is { IsEditor: true }) {
return; }
530 if (GameMain.GameSession?.GameMode is TestGameMode) {
return; }
533 if (unlockClients && GameMain.Server !=
null)
535 foreach (
Client c
in GameMain.Server.ConnectedClients)
537 if (conditions !=
null && !conditions(c.
Character)) {
continue; }
538 GameMain.Server.GiveAchievement(c, identifier);
543 if (unlockedAchievements.Contains(identifier)) {
return; }
544 unlockedAchievements.Add(identifier);
547 if (conditions !=
null && !conditions(
Character.Controlled)) {
return; }
550 SteamManager.UnlockAchievement(identifier);