5 using Microsoft.Xna.Framework;
7 using System.Collections.Generic;
8 using System.Collections.Immutable;
14 internal readonly record
struct NetIncrementedStat(AchievementStat Stat, float Amount) : INetSerializableStruct;
16 static class AchievementManager
18 private const float UpdateInterval = 1.0f;
20 private static readonly HashSet<Identifier> unlockedAchievements =
new HashSet<Identifier>();
22 public static bool CheatsEnabled =
false;
24 private static float updateTimer;
29 private sealed
class RoundData
31 public readonly List<Reactor> Reactors =
new List<Reactor>();
33 public readonly HashSet<Character> EnteredCrushDepth =
new HashSet<Character>();
34 public readonly HashSet<Character> ReactorMeltdown =
new HashSet<Character>();
36 public bool SubWasDamaged;
39 private static RoundData roundData;
43 private static readonly Dictionary<Character, CachedDistance> cachedDistances =
new Dictionary<Character, CachedDistance>();
45 public static void OnStartRound(
Biome biome =
null)
47 roundData =
new RoundData();
52 if (reactor !=
null && reactor.
Item.
Condition > 0.0f) { roundData.Reactors.Add(reactor); }
55 cachedDistances.Clear();
64 string shortBiomeIdentifier = biome.Identifier.Value.Replace(
" ",
"");
65 UnlockAchievement($
"discover{shortBiomeIdentifier}".ToIdentifier(), unlockClients:
true);
68 if (shortBiomeIdentifier ==
"europanridge".ToIdentifier() &&
71 UnlockAchievement(
"getoutalive".ToIdentifier(), unlockClients:
true,
77 public static void Update(
float deltaTime)
85 updateTimer -= deltaTime;
86 if (updateTimer > 0.0f) {
return; }
87 updateTimer = UpdateInterval;
94 identifier:
"maxintensity".ToIdentifier(),
96 characterConditions:
static c => c is { IsDead:
false, IsUnconscious:
false });
101 if (c.
IsDead) {
continue; }
107 roundData.EnteredCrushDepth.Add(c);
112 if (roundData.EnteredCrushDepth.Contains(c)) { UnlockAchievement(c,
"survivecrushdepth".ToIdentifier()); }
119 foreach (
Reactor reactor
in roundData.Reactors)
127 if (!c.
IsDead && c.
Submarine == sub) { roundData.ReactorMeltdown.Add(c); }
133 Vector2 submarineVel = Physics.DisplayToRealWorldRatio * ConvertUnits.ToDisplayUnits(sub.
Velocity) * 3.6f;
135 if (Math.Abs(submarineVel.X) > 100.0f)
150 if (!roundData.SubWasDamaged)
168 CheckMidRoundAchievements(client.
Character);
175 private static void CheckMidRoundAchievements(
Character c)
177 if (c ==
null || c.
Removed) {
return; }
182 UnlockAchievement(c,
"clowncostume".ToIdentifier());
187 float requiredDist = 500 / Physics.DisplayToRealWorldRatio;
189 if (cachedDistances.TryGetValue(c, out var cachedDistance))
193 cachedDistances.Remove(c);
194 cachedDistance = CalculateNewCachedDistance(c);
195 if (cachedDistance !=
null)
197 cachedDistances.Add(c, cachedDistance);
203 cachedDistance = CalculateNewCachedDistance(c);
204 if (cachedDistance !=
null)
206 cachedDistances.Add(c, cachedDistance);
209 if (cachedDistance !=
null)
211 distSquared = Math.Max(distSquared, cachedDistance.Distance * cachedDistance.Distance);
213 if (distSquared > requiredDist * requiredDist)
215 UnlockAchievement(c,
"crewaway".ToIdentifier());
222 if (path.Unreachable) {
return null; }
228 private static bool SubWallsDamaged(
Submarine sub)
244 public static void OnCampaignMetadataSet(Identifier identifier,
object value,
bool unlockClients =
false)
246 if (identifier.IsEmpty || value is
null) {
return; }
247 UnlockAchievement($
"campaignmetadata_{identifier}_{value}".ToIdentifier(), unlockClients);
250 public static void OnItemRepaired(
Item item,
Character fixer)
256 if (fixer ==
null) {
return; }
258 UnlockAchievement(fixer,
"repairdevice".ToIdentifier());
259 UnlockAchievement(fixer, $
"repair{item.Prefab.Identifier}".ToIdentifier());
262 public static void OnButtonTerminalSignal(
Item item,
Character user)
264 if (item ==
null || user ==
null) {
return; }
273 UnlockAchievement(user,
"ancientnovelty".ToIdentifier());
304 if (reviver ==
null) {
return; }
305 UnlockAchievement(reviver,
"healcrit".ToIdentifier());
315 causeOfDeath.
Killer !=
null &&
318 IncrementStat(causeOfDeath.
Killer, character.
IsHuman ? AchievementStat.HumansKilled : AchievementStat.MonstersKilled , 1);
321 if (character != causeOfDeath.
Killer && causeOfDeath.
Killer !=
null)
323 IncrementStat(causeOfDeath.
Killer, character.
IsHuman ? AchievementStat.HumansKilled : AchievementStat.MonstersKilled , 1);
327 UnlockAchievement(causeOfDeath.
Killer, $
"kill{character.SpeciesName}".ToIdentifier());
330 UnlockAchievement(causeOfDeath.
Killer, $
"kill{character.SpeciesName}indoors".ToIdentifier());
334 UnlockAchievement(causeOfDeath.
Killer, $
"kill{character.SpeciesName.Replace("boss
", "")}".ToIdentifier());
337 UnlockAchievement(causeOfDeath.
Killer, $
"kill{character.SpeciesName.Replace("boss
", "")}indoors".ToIdentifier());
342 UnlockAchievement(causeOfDeath.
Killer, $
"kill{character.SpeciesName.Replace("_m
", "")}".ToIdentifier());
345 UnlockAchievement(causeOfDeath.
Killer, $
"kill{character.SpeciesName.Replace("_m
", "")}indoors".ToIdentifier());
351 GameMain.Server?.ServerSettings is { IronmanModeActive: true })
354 identifier:
"europasfinest".ToIdentifier(),
356 characterConditions:
static c => c is { IsDead:
false });
362 causeOfDeath.
Killer != character)
364 UnlockAchievement(causeOfDeath.
Killer,
"killclown".ToIdentifier());
370 UnlockAchievement(causeOfDeath.
Killer,
"whatsmirksbelow".ToIdentifier());
376 UnlockAchievement(causeOfDeath.
Killer,
"killpoison".ToIdentifier());
381 if (item.HasTag(Tags.ToolItem))
383 UnlockAchievement(causeOfDeath.
Killer,
"killtool".ToIdentifier());
388 if (item.Prefab.Identifier ==
"morbusine")
390 UnlockAchievement(causeOfDeath.
Killer,
"killpoison".ToIdentifier());
392 else if (item.Prefab.Identifier ==
"nuclearshell" ||
393 item.Prefab.Identifier ==
"nucleardepthcharge")
395 UnlockAchievement(causeOfDeath.
Killer,
"killnuke".ToIdentifier());
403 UnlockAchievement(character,
"abyssbeckons".ToIdentifier());
406 if (
GameMain.Server?.TraitorManager !=
null)
408 if (
GameMain.Server.TraitorManager.IsTraitor(character))
410 UnlockAchievement(causeOfDeath.
Killer,
"killtraitor".ToIdentifier());
416 public static void OnTraitorWin(
Character character)
422 UnlockAchievement(character,
"traitorwin".ToIdentifier());
425 public static void OnRoundEnded(
GameSession gameSession)
427 if (CheatsEnabled) {
return; }
432 float levelLengthMeters = Physics.DisplayToRealWorldRatio *
Level.
Loaded.
Size.X;
433 float levelLengthKilometers = levelLengthMeters / 1000.0f;
439 if (myCharacter !=
null &&
443 IncrementStat(AchievementStat.KMsTraveled, levelLengthKilometers);
450 IncrementStat(AchievementStat.KMsTraveled, levelLengthKilometers);
455 SteamManager.StoreStats();
466 $
"{mission.Prefab.AchievementIdentifier}{(int) GameMain.GameSession.WinningTeam}"
468 UnlockAchievement(achvIdentifier,
true,
492 bool noDamageRun = !roundData.SubWasDamaged && !gameSession.
Casualties.Any();
498 UnlockAchievement(
"survivereactormeltdown".ToIdentifier(),
true, c => c !=
null && !c.
IsDead && roundData.ReactorMeltdown.Contains(c));
501 UnlockAchievement(
"nodamagerun".ToIdentifier(),
true, c => c !=
null && !c.
IsDead);
506 if (noDamageRun) { UnlockAchievement(
"nodamagerun".ToIdentifier()); }
507 if (roundData.ReactorMeltdown.Any())
509 UnlockAchievement(
"survivereactormeltdown".ToIdentifier());
518 if (charactersInSub.Count == 1)
523 UnlockAchievement(charactersInSub[0],
"lastmanstanding".ToIdentifier());
528 UnlockAchievement(charactersInSub[0],
"lonesailor".ToIdentifier());
533 c != charactersInSub[0] &&
534 c.
TeamID == charactersInSub[0].TeamID &&
537 UnlockAchievement(charactersInSub[0],
"lonesailor".ToIdentifier());
542 foreach (
Character character
in charactersInSub)
544 if (roundData.EnteredCrushDepth.Contains(character))
546 UnlockAchievement(character,
"survivecrushdepth".ToIdentifier());
548 if (character.
Info.
Job ==
null) {
continue; }
549 UnlockAchievement(character, $
"{character.Info.Job.Prefab.Identifier}round".ToIdentifier());
557 private static void UnlockAchievement(
Character recipient, Identifier identifier)
559 if (CheatsEnabled || recipient ==
null) {
return; }
563 UnlockAchievement(identifier);
566 GameMain.Server?.GiveAchievement(recipient, identifier);
570 private static void IncrementStat(
Character recipient, AchievementStat stat,
int amount)
572 if (CheatsEnabled || recipient ==
null) {
return; }
576 IncrementStat(stat, amount);
579 GameMain.Server?.IncrementStat(recipient, stat, amount);
583 public static void UnlockAchievement(Identifier identifier,
bool unlockClients =
false, Func<Character, bool> characterConditions =
null, Func<Client, bool> clientConditions =
null)
585 if (CheatsEnabled) {
return; }
591 if (unlockClients &&
GameMain.Server !=
null)
595 if (clientConditions !=
null && !clientConditions(client)) {
continue; }
596 if (characterConditions !=
null && !characterConditions(client.
Character)) {
continue; }
597 GameMain.Server.GiveAchievement(client, identifier);
606 UnlockAchievementsOnPlatforms(identifier);
609 private static void UnlockAchievementsOnPlatforms(Identifier identifier)
611 if (unlockedAchievements.Contains(identifier)) {
return; }
613 if (SteamManager.IsInitialized)
615 if (SteamManager.UnlockAchievement(identifier))
617 unlockedAchievements.Add(identifier);
621 if (EosInterface.Core.IsInitialized)
623 TaskPool.Add(
"Eos.UnlockAchievementsOnPlatforms", EosInterface.Achievements.UnlockAchievements(identifier), t =>
625 if (!t.TryGetResult(out Result<uint, EosInterface.AchievementUnlockError> result)) { return; }
626 if (result.IsSuccess) { unlockedAchievements.Add(identifier); }
631 public static void IncrementStat(AchievementStat stat,
float amount)
633 if (CheatsEnabled) {
return; }
635 IncrementStatOnPlatforms(stat, amount);
638 private static void IncrementStatOnPlatforms(AchievementStat stat,
float amount)
640 if (SteamManager.IsInitialized)
642 SteamManager.IncrementStats(stat.ToSteam(amount));
645 if (EosInterface.Core.IsInitialized)
647 TaskPool.Add(
"Eos.IncrementStat", EosInterface.Achievements.IngestStats(stat.ToEos(amount)), TaskPool.IgnoredCallback);
651 public static void SyncBetweenPlatforms()
653 if (!SteamManager.IsInitialized || !EosInterface.Core.IsInitialized) {
return; }
655 var steamStats = SteamManager.GetAllStats();
657 TaskPool.AddWithResult(
"Eos.SyncBetweenPlatforms.QueryStats", EosInterface.Achievements.QueryStats(AchievementStatExtension.EosStats), result =>
660 success: stats => SyncStats(stats, steamStats),
661 failure: static error => DebugConsole.ThrowError($
"Failed to query stats from EOS: {error}"));
664 static void SyncStats(ImmutableDictionary<AchievementStat, int> eosStats,
665 ImmutableDictionary<AchievementStat, float> steamStats)
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);
670 static int GetStatValue(AchievementStat stat, ImmutableDictionary<AchievementStat, int> stats) => stats.TryGetValue(stat, out
int value) ? value : 0;
672 var highestStats = AchievementStatExtension.EosStats.ToDictionary(
676 GetStatValue(value, steamStatsConverted),
677 GetStatValue(value, eosStatsConverted)));
679 List<(AchievementStat Stat,
int Value)> eosStatsToIngest =
new(),
680 steamStatsToIncrement =
new();
682 foreach (var (stat, value) in highestStats)
684 int steamDiff = value - GetStatValue(stat, steamStatsConverted),
685 eosDiff = value - GetStatValue(stat, eosStatsConverted);
687 if (steamDiff > 0) { steamStatsToIncrement.Add((stat, steamDiff)); }
688 if (eosDiff > 0) { eosStatsToIngest.Add((stat, eosDiff)); }
691 if (steamStatsToIncrement.Any())
693 SteamManager.IncrementStats(steamStatsToIncrement.Select(
static s => s.Stat.ToSteam(s.Value)).ToArray());
694 SteamManager.StoreStats();
697 if (eosStatsToIngest.Any())
699 TaskPool.Add(
"Eos.SyncBetweenPlatforms.IngestStats", EosInterface.Achievements.IngestStats(eosStatsToIngest.ToArray()), TaskPool.IgnoredCallback);
703 if (!SteamManager.TryGetUnlockedAchievements(out List<Steamworks.Data.Achievement> steamUnlockedAchievements))
705 DebugConsole.ThrowError(
"Failed to query unlocked achievements from Steam");
709 TaskPool.AddWithResult(
"Eos.SyncBetweenPlatforms.QueryPlayerAchievements", EosInterface.Achievements.QueryPlayerAchievements(), t =>
712 success: eosAchievements => SyncAchievements(eosAchievements, steamUnlockedAchievements),
713 failure: static error => DebugConsole.ThrowError($
"Failed to query achievements from EOS: {error}"));
716 static void SyncAchievements(
717 ImmutableDictionary<Identifier, double> eosAchievements,
718 List<Steamworks.Data.Achievement> steamUnlockedAchievements)
720 foreach (var (identifier, progress) in eosAchievements)
722 if (!IsUnlocked(progress)) {
continue; }
724 if (steamUnlockedAchievements.Any(a => a.Identifier.ToIdentifier() == identifier)) {
continue; }
726 SteamManager.UnlockAchievement(identifier);
729 List<Identifier> eosAchievementsToUnlock =
new();
730 foreach (var achievement
in steamUnlockedAchievements)
732 Identifier identifier = achievement.Identifier.ToIdentifier();
733 if (eosAchievements.TryGetValue(identifier, out
double progress) && IsUnlocked(progress)) {
continue; }
735 eosAchievementsToUnlock.Add(achievement.Identifier.ToIdentifier());
738 if (eosAchievementsToUnlock.Any())
740 TaskPool.Add(
"Eos.SyncBetweenPlatforms.UnlockAchievements", EosInterface.Achievements.UnlockAchievements(eosAchievementsToUnlock.ToArray()), TaskPool.IgnoredCallback);
743 static bool IsUnlocked(
double progress) => progress >= 100.0d;
readonly AfflictionPrefab Prefab
readonly Identifier AchievementOnReceived
readonly Identifier AchievementOnRemoved
Steam achievement given when the affliction is removed from the controlled character.
readonly Character Killer
readonly Entity DamageSource
Affliction GetAffliction(string identifier, bool allowLimbAfflictions=true)
CharacterHealth CharacterHealth
virtual AIController AIController
bool HasEquippedItem(Item item, InvSlotType? slotType=null, Func< InvSlotType, bool > predicate=null)
static readonly List< Character > CharacterList
static Character? Controlled
static readonly Identifier HumanSpeciesName
static bool IsInWinningTeam(Character character)
IEnumerable< Character > GetCharacters()
virtual Vector2 WorldPosition
static GameSession?? GameSession
static GameScreen GameScreen
static NetworkMember NetworkMember
CharacterTeamType? WinningTeam
IEnumerable< Mission > Missions
int PermadeathCountForAccount(Option< AccountId > accountId)
IEnumerable< Character > Casualties
readonly EventManager EventManager
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)...
readonly MissionPrefab Prefab
readonly Identifier AchievementIdentifier
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)
readonly Identifier Identifier
bool SectionIsLeaking(int sectionIndex)
Sections that are leaking have a gap placed on them
static List< Structure > WallList
override Vector2? WorldPosition
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,...
static List< Submarine > Loaded
float? RealWorldDepth
How deep down the sub is from the surface of Europa in meters (affected by level type,...
static List< WayPoint > WayPointList