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()
47 roundData =
new RoundData();
52 if (reactor !=
null && reactor.
Item.
Condition > 0.0f) { roundData.Reactors.Add(reactor); }
55 cachedDistances.Clear();
58 public static void Update(
float deltaTime)
65 updateTimer -= deltaTime;
66 if (updateTimer > 0.0f) {
return; }
67 updateTimer = UpdateInterval;
74 identifier:
"maxintensity".ToIdentifier(),
76 conditions:
static c => c is { IsDead:
false, IsUnconscious:
false });
81 if (c.
IsDead) {
continue; }
87 roundData.EnteredCrushDepth.Add(c);
92 if (roundData.EnteredCrushDepth.Contains(c)) { UnlockAchievement(c,
"survivecrushdepth".ToIdentifier()); }
99 foreach (
Reactor reactor
in roundData.Reactors)
107 if (!c.
IsDead && c.
Submarine == sub) { roundData.ReactorMeltdown.Add(c); }
113 Vector2 submarineVel = Physics.DisplayToRealWorldRatio * ConvertUnits.ToDisplayUnits(sub.
Velocity) * 3.6f;
115 if (Math.Abs(submarineVel.X) > 100.0f)
130 if (!roundData.SubWasDamaged)
148 CheckMidRoundAchievements(client.
Character);
155 private static void CheckMidRoundAchievements(
Character c)
157 if (c ==
null || c.
Removed) {
return; }
162 UnlockAchievement(c,
"clowncostume".ToIdentifier());
167 float requiredDist = 500 / Physics.DisplayToRealWorldRatio;
169 if (cachedDistances.TryGetValue(c, out var cachedDistance))
173 cachedDistances.Remove(c);
174 cachedDistance = CalculateNewCachedDistance(c);
175 if (cachedDistance !=
null)
177 cachedDistances.Add(c, cachedDistance);
183 cachedDistance = CalculateNewCachedDistance(c);
184 if (cachedDistance !=
null)
186 cachedDistances.Add(c, cachedDistance);
189 if (cachedDistance !=
null)
191 distSquared = Math.Max(distSquared, cachedDistance.Distance * cachedDistance.Distance);
193 if (distSquared > requiredDist * requiredDist)
195 UnlockAchievement(c,
"crewaway".ToIdentifier());
202 if (path.Unreachable) {
return null; }
208 private static bool SubWallsDamaged(
Submarine sub)
224 public static void OnBiomeDiscovered(
Biome biome)
226 UnlockAchievement($
"discover{biome.Identifier.Value.Replace(" ", "")}".ToIdentifier());
229 public static void OnCampaignMetadataSet(Identifier identifier,
object value,
bool unlockClients =
false)
231 if (identifier.IsEmpty || value is
null) {
return; }
232 UnlockAchievement($
"campaignmetadata_{identifier}_{value}".ToIdentifier(), unlockClients);
235 public static void OnItemRepaired(
Item item,
Character fixer)
240 if (fixer ==
null) {
return; }
242 UnlockAchievement(fixer,
"repairdevice".ToIdentifier());
243 UnlockAchievement(fixer, $
"repair{item.Prefab.Identifier}".ToIdentifier());
270 if (reviver ==
null) {
return; }
271 UnlockAchievement(reviver,
"healcrit".ToIdentifier());
280 causeOfDeath.
Killer !=
null &&
283 IncrementStat(causeOfDeath.
Killer, character.
IsHuman ? AchievementStat.HumansKilled : AchievementStat.MonstersKilled , 1);
286 if (character != causeOfDeath.
Killer && causeOfDeath.
Killer !=
null)
288 IncrementStat(causeOfDeath.
Killer, character.
IsHuman ? AchievementStat.HumansKilled : AchievementStat.MonstersKilled , 1);
292 UnlockAchievement(causeOfDeath.
Killer, $
"kill{character.SpeciesName}".ToIdentifier());
295 UnlockAchievement(causeOfDeath.
Killer, $
"kill{character.SpeciesName}indoors".ToIdentifier());
299 UnlockAchievement(causeOfDeath.
Killer, $
"kill{character.SpeciesName.Replace("boss
", "")}".ToIdentifier());
302 UnlockAchievement(causeOfDeath.
Killer, $
"kill{character.SpeciesName.Replace("boss
", "")}indoors".ToIdentifier());
307 UnlockAchievement(causeOfDeath.
Killer, $
"kill{character.SpeciesName.Replace("_m
", "")}".ToIdentifier());
310 UnlockAchievement(causeOfDeath.
Killer, $
"kill{character.SpeciesName.Replace("_m
", "")}indoors".ToIdentifier());
316 causeOfDeath.
Killer != character)
318 UnlockAchievement(causeOfDeath.
Killer,
"killclown".ToIdentifier());
324 UnlockAchievement(causeOfDeath.
Killer,
"killpoison".ToIdentifier());
329 if (item.HasTag(Tags.ToolItem))
331 UnlockAchievement(causeOfDeath.
Killer,
"killtool".ToIdentifier());
336 if (item.Prefab.Identifier ==
"morbusine")
338 UnlockAchievement(causeOfDeath.
Killer,
"killpoison".ToIdentifier());
340 else if (item.Prefab.Identifier ==
"nuclearshell" ||
341 item.Prefab.Identifier ==
"nucleardepthcharge")
343 UnlockAchievement(causeOfDeath.
Killer,
"killnuke".ToIdentifier());
349 if (
GameMain.Server?.TraitorManager !=
null)
351 if (
GameMain.Server.TraitorManager.IsTraitor(character))
353 UnlockAchievement(causeOfDeath.
Killer,
"killtraitor".ToIdentifier());
359 public static void OnTraitorWin(
Character character)
364 UnlockAchievement(character,
"traitorwin".ToIdentifier());
367 public static void OnRoundEnded(
GameSession gameSession)
369 if (CheatsEnabled) {
return; }
374 float levelLengthMeters = Physics.DisplayToRealWorldRatio *
Level.
Loaded.
Size.X;
375 float levelLengthKilometers = levelLengthMeters / 1000.0f;
381 if (myCharacter !=
null &&
385 IncrementStat(AchievementStat.KMsTraveled, levelLengthKilometers);
392 IncrementStat(AchievementStat.KMsTraveled, levelLengthKilometers);
397 SteamManager.StoreStats();
407 $
"{mission.Prefab.AchievementIdentifier}{(int) GameMain.GameSession.WinningTeam}"
409 UnlockAchievement(achvIdentifier,
true,
429 bool noDamageRun = !roundData.SubWasDamaged && !gameSession.
Casualties.Any();
435 UnlockAchievement(
"survivereactormeltdown".ToIdentifier(),
true, c => c !=
null && !c.
IsDead && roundData.ReactorMeltdown.Contains(c));
438 UnlockAchievement(
"nodamagerun".ToIdentifier(),
true, c => c !=
null && !c.
IsDead);
443 if (noDamageRun) { UnlockAchievement(
"nodamagerun".ToIdentifier()); }
444 if (roundData.ReactorMeltdown.Any())
446 UnlockAchievement(
"survivereactormeltdown".ToIdentifier());
455 if (charactersInSub.Count == 1)
460 UnlockAchievement(charactersInSub[0],
"lastmanstanding".ToIdentifier());
465 UnlockAchievement(charactersInSub[0],
"lonesailor".ToIdentifier());
470 c != charactersInSub[0] &&
471 c.
TeamID == charactersInSub[0].TeamID &&
474 UnlockAchievement(charactersInSub[0],
"lonesailor".ToIdentifier());
479 foreach (
Character character
in charactersInSub)
481 if (roundData.EnteredCrushDepth.Contains(character))
483 UnlockAchievement(character,
"survivecrushdepth".ToIdentifier());
485 if (character.
Info.
Job ==
null) {
continue; }
486 UnlockAchievement(character, $
"{character.Info.Job.Prefab.Identifier}round".ToIdentifier());
494 private static void UnlockAchievement(
Character recipient, Identifier identifier)
496 if (CheatsEnabled || recipient ==
null) {
return; }
500 UnlockAchievement(identifier);
503 GameMain.Server?.GiveAchievement(recipient, identifier);
507 private static void IncrementStat(
Character recipient, AchievementStat stat,
int amount)
509 if (CheatsEnabled || recipient ==
null) {
return; }
513 IncrementStat(stat, amount);
516 GameMain.Server?.IncrementStat(recipient, stat, amount);
520 public static void UnlockAchievement(Identifier identifier,
bool unlockClients =
false, Func<Character, bool> conditions =
null)
522 if (CheatsEnabled) {
return; }
528 if (unlockClients &&
GameMain.Server !=
null)
532 if (conditions !=
null && !conditions(c.
Character)) {
continue; }
533 GameMain.Server.GiveAchievement(c, identifier);
542 UnlockAchievementsOnPlatforms(identifier);
545 private static void UnlockAchievementsOnPlatforms(Identifier identifier)
547 if (unlockedAchievements.Contains(identifier)) {
return; }
549 if (SteamManager.IsInitialized)
551 if (SteamManager.UnlockAchievement(identifier))
553 unlockedAchievements.Add(identifier);
557 if (EosInterface.Core.IsInitialized)
559 TaskPool.Add(
"Eos.UnlockAchievementsOnPlatforms", EosInterface.Achievements.UnlockAchievements(identifier), t =>
561 if (!t.TryGetResult(out Result<uint, EosInterface.AchievementUnlockError> result)) { return; }
562 if (result.IsSuccess) { unlockedAchievements.Add(identifier); }
567 public static void IncrementStat(AchievementStat stat,
float amount)
569 if (CheatsEnabled) {
return; }
571 IncrementStatOnPlatforms(stat, amount);
574 private static void IncrementStatOnPlatforms(AchievementStat stat,
float amount)
576 if (SteamManager.IsInitialized)
578 SteamManager.IncrementStats(stat.ToSteam(amount));
581 if (EosInterface.Core.IsInitialized)
583 TaskPool.Add(
"Eos.IncrementStat", EosInterface.Achievements.IngestStats(stat.ToEos(amount)), TaskPool.IgnoredCallback);
587 public static void SyncBetweenPlatforms()
589 if (!SteamManager.IsInitialized || !EosInterface.Core.IsInitialized) {
return; }
591 var steamStats = SteamManager.GetAllStats();
593 TaskPool.AddWithResult(
"Eos.SyncBetweenPlatforms.QueryStats", EosInterface.Achievements.QueryStats(AchievementStatExtension.EosStats), result =>
596 success: stats => SyncStats(stats, steamStats),
597 failure: static error => DebugConsole.ThrowError($
"Failed to query stats from EOS: {error}"));
600 static void SyncStats(ImmutableDictionary<AchievementStat, int> eosStats,
601 ImmutableDictionary<AchievementStat, float> steamStats)
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);
606 static int GetStatValue(AchievementStat stat, ImmutableDictionary<AchievementStat, int> stats) => stats.TryGetValue(stat, out
int value) ? value : 0;
608 var highestStats = AchievementStatExtension.EosStats.ToDictionary(
612 GetStatValue(value, steamStatsConverted),
613 GetStatValue(value, eosStatsConverted)));
615 List<(AchievementStat Stat,
int Value)> eosStatsToIngest =
new(),
616 steamStatsToIncrement =
new();
618 foreach (var (stat, value) in highestStats)
620 int steamDiff = value - GetStatValue(stat, steamStatsConverted),
621 eosDiff = value - GetStatValue(stat, eosStatsConverted);
623 if (steamDiff > 0) { steamStatsToIncrement.Add((stat, steamDiff)); }
624 if (eosDiff > 0) { eosStatsToIngest.Add((stat, eosDiff)); }
627 if (steamStatsToIncrement.Any())
629 SteamManager.IncrementStats(steamStatsToIncrement.Select(
static s => s.Stat.ToSteam(s.Value)).ToArray());
630 SteamManager.StoreStats();
633 if (eosStatsToIngest.Any())
635 TaskPool.Add(
"Eos.SyncBetweenPlatforms.IngestStats", EosInterface.Achievements.IngestStats(eosStatsToIngest.ToArray()), TaskPool.IgnoredCallback);
639 if (!SteamManager.TryGetUnlockedAchievements(out List<Steamworks.Data.Achievement> steamUnlockedAchievements))
641 DebugConsole.ThrowError(
"Failed to query unlocked achievements from Steam");
645 TaskPool.AddWithResult(
"Eos.SyncBetweenPlatforms.QueryPlayerAchievements", EosInterface.Achievements.QueryPlayerAchievements(), t =>
648 success: eosAchievements => SyncAchievements(eosAchievements, steamUnlockedAchievements),
649 failure: static error => DebugConsole.ThrowError($
"Failed to query achievements from EOS: {error}"));
652 static void SyncAchievements(
653 ImmutableDictionary<Identifier, double> eosAchievements,
654 List<Steamworks.Data.Achievement> steamUnlockedAchievements)
656 foreach (var (identifier, progress) in eosAchievements)
658 if (!IsUnlocked(progress)) {
continue; }
660 if (steamUnlockedAchievements.Any(a => a.Identifier.ToIdentifier() == identifier)) {
continue; }
662 SteamManager.UnlockAchievement(identifier);
665 List<Identifier> eosAchievementsToUnlock =
new();
666 foreach (var achievement
in steamUnlockedAchievements)
668 Identifier identifier = achievement.Identifier.ToIdentifier();
669 if (eosAchievements.TryGetValue(identifier, out
double progress) && IsUnlocked(progress)) {
continue; }
671 eosAchievementsToUnlock.Add(achievement.Identifier.ToIdentifier());
674 if (eosAchievementsToUnlock.Any())
676 TaskPool.Add(
"Eos.SyncBetweenPlatforms.UnlockAchievements", EosInterface.Achievements.UnlockAchievements(eosAchievementsToUnlock.ToArray()), TaskPool.IgnoredCallback);
679 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
IEnumerable< Character > GetCharacters()
virtual Vector2 WorldPosition
static GameSession?? GameSession
static GameScreen GameScreen
static NetworkMember NetworkMember
CharacterTeamType? WinningTeam
IEnumerable< Mission > Missions
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)
bool SectionIsLeaking(int sectionIndex)
Sections that are leaking have a gap placed on them
static List< Structure > WallList
override Vector2? WorldPosition
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